竞争条件怎么出现
竞争条件指并发请求在「检查—使用」的间隙同时通过校验,典型表现是重复领券、超额提现、库存超卖、绕过次数限制。根因是校验与写操作不是原子的:两个请求都读到了「还有余额」的旧状态,于是都执行了扣减。防护方向有三个:把检查与扣减合并成一条原子操作、用唯一约束兜住重复、或者把并发请求串行化。
基本信息
| 典型场景 | 领券与秒杀、提现与转账、库存扣减、短信次数限制、试用与注册奖励 |
|---|---|
| 根因 | 检查与写入分离、缺少唯一约束、缓存与数据库状态不一致 |
| 防护手段 | 条件更新、唯一索引、行锁、队列串行化、幂等键 |
| 验证方式 | 并发请求工具(ab、wrk 或自定义脚本),并发必须真实同时到达 |
| 回归重点 | 冲突异常处理与用户提示,避免漏洞修完变成 500 |
| 关联设计 | 接口幂等性与重试策略 |
手段与适用条件
| 手段 | 适用条件 | 注意点 |
|---|---|---|
| 条件更新 | 单库单行的余额与计数 | 以影响行数判断结果,不依赖前置查询 |
| 唯一索引 | 同一用户同一活动仅允许一次 | 必须捕获重复键异常并转成业务提示 |
| SELECT FOR UPDATE 行锁 | 需要两步读取与写入 | 控制锁范围与事务时长,避免死锁 |
| 队列串行化 | 跨服务或跨库场景 | 引入排队延迟,需幂等键去重 |
| 分布式锁 | 多实例并发写同一资源 | 超时与释放策略要明确,避免锁泄漏 |
条件更新(原子且幂等)
条件更新(原子且幂等)
-- 风险:先查余额再扣减,两步之间可能被并发穿插
-- SELECT balance FROM account WHERE id = 1;
-- UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 修复:条件更新一条语句完成,原子且幂等
UPDATE account SET balance = balance - 100
WHERE id = 1 AND balance >= 100;
-- 影响行数为 0 表示余额不足,直接拒绝本次请求,不再二次查询其他手段
其他手段
唯一索引防重复记录:同一用户同一活动仅一条,重复插入报冲突
SELECT ... FOR UPDATE 行锁:注意锁范围与死锁风险,事务尽量短
分布式场景用队列或幂等服务串行化处理,配幂等键去重
业务层兜底:重复请求返回「已处理」而不是报错,减少用户重复提交典型使用场景
- 营销活动与秒杀上线前的并发评审
- 提现、支付链路的对账与差异排查
- 库存与限额类接口的加固
- 渗透测试报告中竞争条件类问题的整改
- 用并发工具复现与验证修复效果
- 放量前的压测与幂等性回归
复现并发问题需要并发请求工具(ab、wrk 或自定义脚本),手工点击无法暴露。验证时注意请求要真正同时到达,否则会误判为已修复。
常见的修复组合是「条件更新 + 唯一索引 + 幂等键」三层。跨服务场景下只做其中一层,仍可能在网络重试与补偿环节漏掉。