优惠券领用易触发行锁冲突,因“先查库存再插入明细”存在时间窗口,多请求并发读到相同剩余量后争抢插入,引发插入意向锁、排他锁甚至间隙锁;需确保索引覆盖查询、显式事务控制、乐观锁替代强锁、拆分热点行。

为什么优惠券领用容易触发行锁冲突
因为典型的领券逻辑是「先查库存,再插入明细」,而 SELECT ... FROM coupon_activity WHERE act_code = ? 和 INSERT INTO coupon_detail 之间存在时间窗口,多个并发请求会同时读到相同剩余数量,接着都尝试插入——InnoDB 在插入时会对 coupon_detail 的唯一索引 coup_code_idx 或二级索引 act_code_idx 加插入意向锁(Insert Intention Lock),再升级为排他锁(X Lock)。若未命中索引或扫描范围过大,甚至可能升级为间隙锁(Gap Lock),进一步扩大阻塞面。
避免全表扫描和间隙锁的关键:索引必须覆盖查询条件
常见错误是只在 act_code 上建了普通索引,但查询语句没走索引,或优化器误判导致全表扫描。这会让 InnoDB 对整个索引范围加间隙锁,所有后续插入都被阻塞。
-
coupon_detail表上必须有KEY act_code_idx (act_code),且类型为 BTREE(默认);如果业务还按用户查,coup_user_id和act_code组合建联合索引更稳妥 - 执行
EXPLAIN SELECT COUNT(*) FROM coupon_detail WHERE act_code = '000000',确认type是ref或range,不是ALL - 禁用
autocommit=0下的隐式事务裸跑;所有领券操作必须显式包裹BEGIN; ... COMMIT;,否则锁生命周期不可控
用乐观锁替代 SELECT FOR UPDATE 的真实收益
在优惠券场景中,「发行总量」和「用户限领数」都是可预判的弱一致性指标,没必要全程强锁。直接用带版本号或原子计数的更新语句,把锁从「事务级」压缩到「语句级」。
- 给
coupon_activity表加version INT DEFAULT 0字段,更新时用:UPDATE coupon_activity SET coup_issue_num = coup_issue_num - 1, version = version + 1 WHERE act_code = ? AND version = ? - 检查
ROW_COUNT()是否为 1;为 0 说明已被其他事务扣减,直接返回“已发完” - 不再需要
SELECT ... FOR UPDATE,也就规避了因锁顺序不一致引发的死锁风险 - 注意:该方式要求应用层实现失败重试,且不能用于强一致性校验(如实时显示剩余张数)
拆分热点行比调参数更治本
单个 act_code 对应全部 1000 张券,就是典型热点行。哪怕加了索引、用了乐观锁,所有更新仍集中在同一聚簇索引页上,CPU cache line 和 buffer pool latch 都会争抢。
- 把一张活动拆成 N 个子活动码(如
'000000_01'到'000000_10'),每份配额 100 张,路由逻辑由应用层哈希或轮询决定 - 或者改用「分片计数器」:新建
coupon_activity_shard表,每行记录一个活动+分片的已发数量,用INSERT ... ON DUPLICATE KEY UPDATE原子更新 -
innodb_lock_wait_timeout调小(比如 3 秒)只是让失败更快暴露,不能减少冲突本身;盲目增大max_connections反而加剧锁队列堆积
真正卡住系统的往往不是锁机制本身,而是把「业务规则校验」和「数据写入」混在同一事务里,又没对齐索引与查询模式。拆行、压锁粒度、让失败可预期——这三件事做完,SHOW ENGINE INNODB STATUS 里就很难再看到大量 lock wait 了。











