You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rails Sidekiq后台任务操作User关联Book表时出现MySQL死锁问题求助

死锁产生原因

核心逻辑问题

InnoDB的行级排他锁是逐行获取的,不会一次性锁定所有目标行,当多个事务以不同顺序请求同一组行的锁时,就会形成循环等待,触发死锁。
你的后台任务包含两处更新操作,天然存在加锁顺序不一致的问题:

  • 第一处 user.books.find_by(genre_id: XXX).update(status: "done") 会先定位到符合genre_id条件的单条book记录,优先给这行记录加排他锁
  • 第二处 user.books.update_all(status: "undone") 会扫描该用户所有book记录,默认按照主键ID升序的顺序逐行加锁再更新

如果符合genre_id条件的book主键ID较大,就会出现:先给大ID行加锁,再按从小到大的顺序给其他行加锁的情况,和其他任务的加锁顺序形成冲突。

日志验证

从你提供的InnoDB状态日志可以直接确认循环等待的存在:

  • 事务1(TRANSACTION 4613489)已经持有了主键ID较小的行锁(heap no 2对应的主键0x0000000000000001),正在等待获取主键ID较大的行锁(heap no 187对应的主键0x000000000000040b)
  • 事务2(TRANSACTION 4613488)已经持有了上述主键ID较大的行锁,正在反向等待获取主键ID较小的行锁,循环等待形成,死锁触发。

额外诱因

你books表的user_id字段大概率未添加索引:执行user.books.update_all时,InnoDB无法通过索引直接定位到对应用户的所有book记录,只能走全表扫描逐行校验user_id,过程中会给所有扫描过的行加排他锁,哪怕不属于当前用户,大幅提升了不同用户任务之间的锁冲突概率。

解决方案

  1. 固定加锁顺序:所有更新操作前,先按主键升序锁定当前用户的所有book记录,避免顺序混乱:
def perform
  # 先按主键升序锁定当前用户所有book,统一加锁顺序
  user.books.order(:id).lock!
  user.books.find_by(genre_id: XXX).update(status: "done")
  # 其他业务逻辑
  user.books.update_all(status: "undone")
end
  1. 添加索引避免全表锁:给books表的user_id字段添加索引,避免update_all时扫描锁定无关行:
# 数据库迁移文件内容
add_index :books, :user_id
  1. 缩小锁持有范围:如果两处更新不需要在同一个事务内完成,可以拆分事务减少锁持有时间;也可以合并两处更新逻辑,减少锁操作次数。
  2. 避免同用户任务并发:通过Sidekiq唯一任务插件,保证同一时间同一个用户的该类任务只执行一个,从根源消除并发冲突。

内容的提问来源于stack exchange,提问作者user16012143

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.06 02:51:03