竞态用例是什么
竞态条件出现在「检查后使用」(check-then-act)的时间窗内:余额校验刚刚通过,扣减前另一个请求已把余额用掉。竞态用例的设计重点不是穷举线程时序,而是放大时间窗、制造交错、观察最终一致性——余额与流水是否对平、库存是否为负、幂等键是否唯一。这类缺陷通常只在并发或不稳定网络下暴露。
基本信息
| 本质 | 检查与使用之间的窗口、非原子序列、缓存与数据库不一致 |
|---|---|
| 触发手段 | 并发同时发起、关键接口注入延迟、客户端重试与重发 |
| 观察点 | 最终余额与流水对平、幂等键唯一、库存不为负 |
| 常见误判 | 单线程手工测试通过就认为没问题,必须用并发工具压 |
| 修复方向 | 数据库锁、唯一约束、幂等设计、乐观锁版本号 |
典型使用场景
- 支付与退款:同账户并行扣款、退款与扣款交错执行
- 库存与秒杀:同商品并行下单,检查超卖与负库存
- 重复提交:快速双击、网络重试、客户端自动重发
- 优惠券与积分:同一张券或同一笔积分被并发核销
- 状态流转:订单取消与发货并发时的最终状态归属
- 对账补偿:失败重试与补偿任务和主流程并发时的账目一致性
并发触发与核对
并发触发与核对
# 仅测试环境:用同一幂等键并发发起 20 个请求
for i in $(seq 1 20); do curl -s -X POST https://test.local/api/pay \
-H 'Idempotency-Key: order-42-pay' -d 'amount=100' & done; wait
# 核对三件事:最终余额、流水条数、是否只生成一条扣款记录
# 放大窗口:在检查与使用之间通过代理注入延迟,提高交错概率
# 复查 SQL:SELECT balance FROM account WHERE id=1;竞态用例设计要素
| 要素 | 做法 | 断言 |
|---|---|---|
| 放大窗口 | 在检查与使用之间注入延迟 | 交错概率提升且可复现 |
| 触发手段 | 并发同时发起、重试与重发 | 重复请求被幂等处理 |
| 关键断言 | 余额与流水对平、幂等键唯一、库存不为负 | 数据最终一致有据可查 |
| 失败恢复 | 重试与对账补偿路径 | 补偿后账目仍对平 |
| 副作用检查 | 通知、积分、日志只产生一次 | 无重复副作用 |
手工点击的并发度太低,几乎命中不了竞态窗口;把并发发起与故障注入纳入例行测试项,比指望「碰巧复现」可靠得多。
延迟注入与并发放大只能在测试环境做:生产环境注入延迟会直接伤害真实用户,还可能触发风控与限流,把测试事故放大成生产事故。