用途
本技能用于处理并发场景下的竞态条件(Race Conditions)。当多个操作访问共享状态、操作顺序会影响结果,或出现“大多数时候正常、偶尔失败”的间歇性缺陷时,应当使用本技能。竞态条件的本质是:正确性取决于事件的相对时序。它们之所以危险,是因为大多数时候能正常工作、随机失败,并且几乎无法复现。
核心原则
铁律:在没有原子性保证的情况下,绝不要执行“先读后写”。资料明确列出没有例外的情况——不能因为“它很快,时序不重要”“同一时间只有一个用户”“坏了再修”“测试里能通过”就跳过原子性保证。只要时序可能影响结果,就存在竞态条件。
检测:TOCTOU 模式
Time-Of-Check to Time-of-Use(检查时到使用时)指先检查某状态、再据此行动。资料给出的经典示例是取款:先读取账户余额并判断是否充足,再执行更新。两个并发的 80 元取款请求可能都读到 100 元余额,都通过检查,最终都扣款成功,导致本应被拒绝的透支。
正确模式
资料提供了五类正确做法:
- 原子操作:把条件检查与更新合并为一次原子更新(如带条件的 updateOne 配合 $inc)。
- 数据库事务:在事务中使用 FOR UPDATE 锁定行,直到提交才释放锁。
- 乐观锁:使用版本号,仅当版本匹配时才更新并递增版本,失败则带抖动重试,超过重试次数抛出并发修改错误。
- 分布式锁:对复杂操作使用带 TTL 的分布式锁(如 Redis),确保同一订单只有一个进程处理,并在 finally 中释放锁。
- 幂等键:通过幂等键防止重复处理,先原子地占用幂等键,再安全地执行处理,最后标记完成。
常见竞态模式
资料以表格形式列出常见模式及对应方案:检查后行动(用原子检查并行动)、读-改-写(用原子更新或加锁)、惰性初始化(用双重检查锁或原子初始化)、计数器递增(用原子递增)、先到先得(用带唯一约束的原子认领)。
语言特定写法
JavaScript/Node.js:闭包中的共享状态自增不是原子的,应改用 Redis 的原子 incr。Python:先检查再行动的缓存写入存在竞态,应使用 Lock 加锁。
抗压协议
资料针对四种常见借口给出回应:
- “它很快,时序不重要”:生产负载会造成重叠,竞态随流量放大,应使用原子操作。
- “同一时间只有一个用户”:用户会双击、刷新标签页、机器人会高频请求、移动端超时会重试,应按并发设计。
- “坏了再修”:竞态几乎无法复现,会耗费数周调试“随机”失败,应现在就正确构建。
- “测试里能通过”:测试串行执行,生产并发执行,竞态隐藏在串行执行中,应编写并发测试和负载测试。
危险信号
资料列出应停下重新审视的模式:if 条件后基于条件更新、read/compute/write、检查可用性后预订、读取计数后递增再保存、全局或共享可变状态、“大多数时候正常”、“开发环境无法复现”、用超时“修复”间歇性缺陷。出现这些即意味着需要加入原子性保证。
测试竞态
资料给出并发测试示例:创建余额 100 的账户,模拟 10 个并发的 20 元取款,断言只有 5 个成功且最终余额为 0。
常见合理化借口
资料以表格逐条反驳:快操作在负载下仍会重叠;低流量也会因重试、双击、机器人产生并发;开发是串行、生产是并行;竞态缺陷难以定位应现在修复;加 sleep 只是掩盖而非修复;用户会做你意想不到的事。
快速参考
余额检查后更新用原子条件更新;递增计数器用原子递增;复杂多步操作用带锁的数据库事务;跨服务操作用分布式锁;防重复请求用幂等键;版本冲突用乐观锁。
限制
资料未提供该技能的具体安装方式、运行环境要求、收费信息或平台支持范围,这些内容暂未提供。