关于 race-conditions
面向开发者的并发问题排查与编码指导技能,聚焦时序影响正确性的竞态条件。它归纳检测线索,并介绍原子操作、事务、锁和幂等键等处理方式。
主要功能
- 01识别检查后操作等竞态模式
- 02说明原子更新与数据库事务
- 03介绍乐观锁和分布式锁
- 04使用幂等键防止重复处理
- 05提供并发测试思路
适用场景
- 01排查偶发且难以复现的故障
- 02处理数据库读取后写入逻辑
- 03审查共享状态和并发 API 请求
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yanko-belov/ code-craft --skill race-conditions安装后在 Claude Code 中输入/race-conditions确认已加载。
详细介绍
竞态条件
概述
当结果取决于时机时,就存在竞态。竞态是随时可能发生的 bug。
当正确性取决于事件发生的相对时机时,就会出现竞态条件。它们很隐蔽,因为大多数时候都能正常运行,却会随机失败,而且几乎不可能复现。
适用场景
- 多个异步操作访问共享状态
- 数据库先读取再写入的模式
- 并发 API 请求修改同一资源
- “在开发环境正常,到了生产环境就失败”
- 无法复现的间歇性 bug
铁律
NEVER read-then-write without atomicity guarantees.
没有例外:
- 不能因为“速度很快,时机无关紧要”
- 不能因为“同一时间只有一个用户”
- 不能因为“出了问题再修”
- 不能因为“测试时能正常运行”
如果时机可能影响结果,就存在竞态条件。
检测:TOCTOU 模式
检查时刻到使用时刻:先检查某件事,再据此采取行动。
// ❌ VIOLATION: Classic race condition
async function withdrawMoney(accountId: string, amount: number): Promise<void> {
// Time-of-check
const account = await db.accounts.findById(accountId);
if (account.balance >= amount) {
// Time-of-use (gap where another transaction can happen!)
await db.accounts.update(accountId, {
balance: account.balance - amount // Uses stale value!
});
}
}
// Two simultaneous $80 withdrawals from $100 account:
// T1: Reads balance = $100 ✓
// T2: Reads balance = $100 ✓ (race!)
// T1: balance >= 80? Yes. Update to $20
// T2: balance >= 80? Yes. Update to $20 (should have been denied!)
// Result: Two $80 withdrawals, final balance $20 (should be overdraft error)
正确模式
1. 原子操作
// ✅ CORRECT: Atomic update with condition
async function withdrawMoney(accountId: string, amount: number): Promise<boolean> {
const result = await db.accounts.updateOne(
{
_id: accountId,
balance: { $gte: amount } // Check AND update atomically
},
{
$inc: { balance: -amount }
}
);
if (result.modifiedCount === 0) {
throw new InsufficientFundsError(accountId, amount);
}
return true;
}
2. 数据库事务
// ✅ CORRECT: Transaction with proper isolation
async function transferMoney(
fromId: string,
toId: string,
amount: number
): Promise<void> {
await db.transaction(async (tx) => {
// Lock rows with FOR UPDATE
const from = await tx.accounts
.findById(fromId)
.forUpdate(); // Locks row until commit
const to = await tx.accounts
.findById(toId)
.forUpdate();
if (from.balance < amount) {
throw new InsufficientFundsError(fromId, amount);
}
await tx.accounts.update(fromId, { balance: from.balance - amount });
await tx.accounts.update(toId, { balance: to.balance + amount });
// Commit releases locks
});
}
3. 乐观锁
// ✅ CORRECT: Optimistic locking with version
async function updateDocument(
id: string,
updates: Partial<Document>
): Promise<Document> {
const maxRetries = 3;
for (let attempt = 0; attempt < maxRetries; attempt++) {
const doc = await db.documents.findById(id);
const result = await db.documents.updateOne(
{
_id: id,
version: doc.version // Only update if version matches
},
{
$set: updates,
$inc: { version: 1 } // Increment version
}
);
if (result.modifiedCount > 0) {
return { ...doc, ...updates, version: doc.version + 1 };
}
// Version mismatch - someone else updated, retry
await sleep(Math.random() * 100); // Jitter
}
throw new ConcurrentModificationError(id);
}
4. 分布式锁
// ✅ CORRECT: Distributed lock for complex operations
async function processOrder(orderId: string): Promise<void> {
const lockKey = `order:${orderId}:lock`;
const lockTTL = 30000; // 30 seconds
const lock = await redis.acquireLock(lockKey, lockTTL);
if (!lock) {
throw new OrderAlreadyProcessingError(orderId);
}
try {
// Safe - only one process can be here for this order
await doExpensiveOrderProcessing(orderId);
} finally {
await redis.releaseLock(lockKey, lock);
}
}
5. 幂等键
// ✅ CORRECT: Idempotency prevents duplicate processing
async function createPayment(
idempotencyKey: string,
data: PaymentData
): Promise<Payment> {
// Check if already processed
const existing = await db.payments.findByIdempotencyKey(idempotencyKey);
if (existing) {
return existing; // Return previous result
}
// Try to claim the idempotency key atomically
try {
await db.idempotencyKeys.insert({
key: idempotencyKey,
status: 'processing',
createdAt: new Date(),
});
} catch (error) {
if (isDuplicateKeyError(error)) {
// Another request claimed it - fetch and return
const existing = await db.payments.findByIdempotencyKey(idempotencyKey);
if (existing) return existing;
throw new PaymentProcessingError('Payment in progress');
}
throw error;
}
// Safe to process - we own the idempotency key
const payment = await processPayment(data);
await db.idempotencyKeys.update(idempotencyKey, {
status: 'completed',
result: payment.id,
});
return payment;
}
常见竞态条件模式
| 模式 | 问题 | 解决方案 |
|---|---|---|
| 检查后执行 | 检查与执行之间状态发生变化 | 原子地检查并执行 |
| 读取-修改-写入 | 读取后值发生变化 | 原子更新或加锁 |
| 延迟初始化 | 多个线程同时初始化 | 双重检查锁定或原子初始化 |
| 计数器递增 | 更新丢失 | 原子递增 |
| 先到先得 | 多个请求都声称“第一个” | 使用唯一约束进行原子声明 |
特定语言的模式
JavaScript/Node.js
// ❌ VIOLATION: Shared state in closure
let requestCount = 0;
async function handleRequest() {
requestCount++; // Race! Read-modify-write is NOT atomic
// ...
}
// ✅ CORRECT: Atomic counter
import { createClient } from 'redis';
const redis = createClient();
async function handleRequest() {
const count = await redis.incr('request_count'); // Atomic
// ...
}
Python
# ❌ VIOLATION: Check-then-act
def get_or_create(key: str, factory: Callable) -> Any:
if key not in cache: # Check
cache[key] = factory() # Act - race!
return cache[key]
# ✅ CORRECT: Atomic with lock
from threading import Lock
lock = Lock()
def get_or_create(key: str, factory: Callable) -> Any:
with lock:
if key not in cache:
cache[key] = factory()
return cache[key]
抗压应对方案
1. “速度很快,时机无关紧要”
施压:“这个操作只需要几微秒”
**回应:**生产环境的负载会造成操作重叠。在高负载下,“快速”操作也会频繁重叠。流量越大,竞态条件越容易出现。
**行动:**使用原子操作。速度快并不能避免竞态。
2. “同一时间只有一个用户”
施压:“流量很低,不会有并发请求”
**回应:**用户会双击、刷新标签页。机器人会猛烈请求。移动设备会在超时后重试。“低流量”也会出现突发。
**行动:**即使预期没有并发,也要按并发场景设计。
3. “出了问题再修”
施压:“先发布,以后再修”
**回应:**竞态条件几乎不可能复现。你可能要花数周调试“随机”故障。
**行动:**现在就正确实现。这样比以后调试更省成本。
4. “测试时能正常运行”
施压:“所有测试都通过了”
**回应:**测试按顺序运行,生产环境则是并发运行。竞态条件会隐藏在串行执行中。
**行动:**编写并发测试。进行负载测试。假定竞态确实存在。
警示信号——停止并重新考虑
如果发现以下任意模式,很可能存在竞态:
if (condition) { update based on condition }read(); compute(); write(computed);check availability; book;get count; increment; save count;- 全局或共享的可变状态
- “大多数时候都能正常运行”
- “在开发环境无法复现”
- 用来“修复”间歇性 bug 的超时设置
这些情况都意味着:添加原子性保障。
测试竞态条件
// Concurrent test to expose races
describe('withdraw', () => {
it('handles concurrent withdrawals correctly', async () => {
await db.accounts.create({ id: 'test', balance: 100 });
// Simulate 10 concurrent $20 withdrawals
const withdrawals = Array(10).fill(null).map(() =>
withdrawMoney('test', 20).catch(() => 'failed')
);
const results = await Promise.all(withdrawals);
const successful = results.filter(r => r !== 'failed').length;
// Only 5 should succeed (5 × $20 = $100)
expect(successful).toBe(5);
const account = await db.accounts.findById('test');
expect(account.balance).toBe(0);
});
});
常见自我安慰(全部无效)
| 借口 | 事实 |
|---|---|
| “速度足够快” | 即使操作很快,高负载下仍会重叠。 |
| “流量很低” | 重试、双击和机器人都会造成并发。 |
| “开发环境能正常运行” | 开发环境是串行的,生产环境是并行的。 |
| “出了问题再修” | 竞态 bug 几乎无法定位。现在就修复。 |
| “加个 sleep 就行” | sleep 无法修复竞态,只会掩盖问题。 |
| “用户不会那样做” | 用户什么意料之外的事都做得出来。 |
快速参考
| 场景 | 解决方案 |
|---|---|
| 更新前检查余额 | 原子条件更新 |
| 递增计数器 | 原子递增(Redis INCR、SQL += 1) |
| 复杂的多步操作 | 使用锁的数据库事务 |
| 跨服务操作 | 分布式锁 |
| 防止重复请求 | 幂等键 |
| 版本冲突 | 乐观锁 |
总结
如果正确性取决于时机,那你就有一个 bug。
先读后写会产生竞态。先检查后操作会产生竞态。观察状态与据此操作之间的任何间隙都会产生竞态。请使用原子操作、事务、锁或幂等键。切勿假设“速度足够快”或“流量很低”。