mcpField
R
race-conditions

race-conditions

github.com/yanko-belov开发1346 次安装40 周前更新
代码仓库

关于 race-conditions

面向开发者的并发问题排查与编码指导技能,聚焦时序影响正确性的竞态条件。它归纳检测线索,并介绍原子操作、事务、锁和幂等键等处理方式。

主要功能

  1. 01识别检查后操作等竞态模式
  2. 02说明原子更新与数据库事务
  3. 03介绍乐观锁和分布式锁
  4. 04使用幂等键防止重复处理
  5. 05提供并发测试思路

适用场景

  1. 01排查偶发且难以复现的故障
  2. 02处理数据库读取后写入逻辑
  3. 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。

先读后写会产生竞态。先检查后操作会产生竞态。观察状态与据此操作之间的任何间隙都会产生竞态。请使用原子操作、事务、锁或幂等键。切勿假设“速度足够快”或“流量很低”。

评论

支持 Markdown