innodb行锁在高并发抽奖中是防超发的刚性依赖,但update不走索引会退化为全表行锁;user_id必须建索引且类型严格匹配,复合条件需联合索引;rr级别间隙锁会阻塞合法插入,可降级为rc规避;死锁高频,须统一访问顺序并自动重试。

高并发抽奖场景下,InnoDB行锁不是“锦上添花”,而是防止超发、数据错乱的刚性依赖——但前提是你的SQL真能触发行级锁;否则它和MyISAM一样会锁表,甚至更糟。
UPDATE不走索引时,InnoDB行锁直接退化为表锁
抽奖核心逻辑常是 UPDATE lottery SET status = 1 WHERE user_id = ? AND status = 0。如果 user_id 没建索引,InnoDB无法定位目标行,只能全表扫描并逐行加锁(Record Lock + Gap Lock 组合),效果等同于锁整张表。此时哪怕只有两个用户同时抽奖,也会互相阻塞,TPS断崖下跌。
- 用
EXPLAIN看执行计划:若type是ALL或index,说明没走有效索引 -
user_id字段类型要和查询参数严格一致(比如 PHP 传字符串'123'而字段是INT,会触发隐式转换导致索引失效) - 复合条件如
WHERE user_id = ? AND draw_time > ?,需建联合索引INDEX idx_user_time (user_id, draw_time),而非单列索引
间隙锁在RR隔离级别下会意外阻塞合法插入
抽奖常伴随“中奖记录写入”动作,比如向 win_log 表插入一行。若事务A执行了 SELECT * FROM lottery WHERE user_id = 123 FOR UPDATE(命中索引),在 RR 隔离级别下,InnoDB不仅锁住匹配行,还会锁住 user_id = 123 对应的索引间隙。此时事务B尝试插入 user_id = 123 的新抽奖记录(即使该记录还不存在),会被阻塞。
- 这不是 bug,是 RR 级别防幻读的机制,但业务上往往不需要这种强一致性
- 解决方案之一是降级为
READ COMMITTED隔离级别:SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED,它关闭间隙锁,只保留 Record Lock - 注意:RC 下不可重复读问题会出现,但抽奖类场景通常可接受(只要不超发)
死锁不是小概率事件,而是高频风险点
抽奖服务常分两步:先扣库存(lottery 表),再写中奖(win_log 表)。若两个请求以相反顺序访问这两张表(A 先 lot 后 log,B 先 log 后 lot),InnoDB 死锁检测器会在 10ms 内主动回滚其中一个事务,抛出 Deadlock found when trying to get lock。
- 死锁日志在错误日志里可查,关键看
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:和*** (2) HOLDS THE LOCK(S):两段 - 统一访问顺序是最简单有效的规避方式:所有业务逻辑强制先操作
lottery,再操作win_log - 应用层捕获死锁异常后应自动重试(建议 ≤ 3 次),而不是向上抛错
真正危险的不是“没用InnoDB”,而是用了InnoDB却误以为“只要引擎对就万事大吉”。一个没索引的 WHERE、一次跨表的乱序更新、一个没意识到的间隙锁,都可能让抽奖系统在峰值时刻瞬间雪崩。











