并发与竞态审阅
假设两个请求同时到达,逐条走共享状态
审查下面这段代码的并发安全。方法:假设有两个请求在同一毫秒到达,逐个走一遍共享状态。请:1)列出所有共享可变状态(内存变量、缓存、数据库行、文件);2)指出哪几处存在检查与使用之间的间隙(先查后写);3)说明在什么并发量下才会暴露,以及暴露时的现象;4)给出最小改法(锁、唯一约束、原子操作、改为幂等),并说明各自的代价。 ``` [粘贴代码] ```
使用建议
低并发项目也值得跑一遍,先查后写的坑往往要等流量涨上来才炸。
适合什么场景
- 存在「先查再写」的逻辑:库存扣减、优惠券领取、幂等下单。
- 单机测试永远通过,多实例部署或压测后开始出现重复数据。
不适合的场景
- 单进程、无共享状态、纯函数:没有并发问题可审。
- 已经确认问题在数据库隔离级别或锁等待:直接看锁和事务,不必通读代码。
示例
输入
def claim_coupon(user_id, coupon_id):
if db.exists("select 1 from claims where user_id=? and coupon_id=?", user_id, coupon_id):
return "already"
if db.count("select count(*) from claims where coupon_id=?", coupon_id) >= 100:
return "sold_out"
db.insert("claims", user_id=user_id, coupon_id=coupon_id)
return "ok"你会得到
共享可变状态是 claims 表和库存计数;两处 check-then-act 间隙:同一用户并发两次都能通过 exists,库存剩 99 时两个请求都能通过 count<100。暴露条件是双实例加约 10 QPS 的抢券,现象是重复领取和超发。最小改法:`(user_id, coupon_id)` 唯一约束兜底,再用一条 `INSERT ... SELECT ... WHERE count < 100` 原子写入,代价是冲突时需要重试。
常见误用
- 只说「看看有没有并发问题」,不交代部署形态(单实例还是多实例、有没有 Redis 锁):它会把单机安全的代码判成危险。
- 收到「加锁」建议就加进程内锁,多实例部署下等于没加。
可以这样改
- 加「假设 3 个实例、无共享缓存」,它才会把进程内变量也算作共享状态。
- 要求「给出能稳定复现的压测脚本思路(并发数、请求顺序)」,抽象风险就变成可验证实验。