记录一次死锁分析-Deadlock found when trying to get lock; try restarting transaction
场景描述
Laravel 中使用 MySQL 作为队列驱动时出现的。
版本:
MySQL 5.×
Laravel 5.×
stackoverflow 上相关的问题
Laravel Queue deadlock when deleting from jobs table [duplicate]
GitHub 相关的 issue
Laravel 5. PDOException. QUEUE_DRIVER=database 1213 Deadlock
报错信息
截图:
1 |
|
分析前复习
InnoDB 的锁相关基础知识
InnoDB 实现了两种标准的行级锁
- S Lock,允许事务读一行数据。
- X Lock,允许事务删除或更新一行数据。
other
一致性的非锁定读(consistent nonlocking read)是 InnoDB 通过行多版本控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。
如果读取的行正在执行 DELETE 或 UPDATE 操作,这时读取操作不会因此去等待行上锁的释放。相反的,InnoDB 引擎会去读取行的一个快照数据。
但是在某些情况下,用户需要显示地对数据库读取操作进行加锁以保证数据逻辑地一致性。而这需要数据库支持加锁语句,即使是对于 SELECT 的只读操作。
InnoDB 对于 SELECT 语句支持两种一致性的锁定读(locking read)操作:
1 |
|
行锁三个的算法
Record Lock
Gap Lock
Next-Key Lock
死锁概念
两个或两个以上的事务在执行的过程中,因争夺锁的资源而造成的一种互相等待的现象。
思索底层原理-设计算法,没有看
数据库普遍采用 wait-for graph(等待图)的方式来进行死锁的检测,InnoDB 存储引擎也采用这种方式。通常使用深度优先的算法实现(新版本的使用非递归的方式)。
死锁Status
执行 show engine innodb status; 查看:
1 |
|
表结构
1 |
|
表结构中,主键索引:id
,辅助索引:queue
。
死锁信息分析
事务 245747603
状态:
- LOCK WAIT 280 lock struct(s)
- 2711 row lock(s)
- thread id 25494
sql:1
2
3
4
5
6
7
8
9
10
11select
*
from `jobs`
where
`queue` = 'default'
and (
(`reserved_at` is null and `available_at` <= 1627523126) or (`reserved_at` <= 1627523036)
)
order by `id` asc
limit 1
for update1
2
3-- 表示锁住的资源, waiting:等待加 Record Lock(试图锁定一行记录)
-- locks rec but not gap 表示锁住的是一个索引而不是范围
RECORD LOCKS space id 0 page no 2517233 n bits 80 index PRIMARY of table `j_mrplus`.`jobs` trx id 245747603 lock_mode X locks rec but not gap waiting
事务 245747602
状态:
- 3 lock struct(s)
- 2 row lock(s)
- thread id 25491
sql:
1 |
|
1 |
|
等待的锁:
1 |
|
分析-重点
死锁出现在队列的消费执行中,相关的查询和删除的重要 sql 分别如下:
1 |
|
从下面的代码可知,删除前也会 进行 X 锁查询:
1 |
|
事务 245747603
的查询条件中有辅助索引 jobs_queue_index
,列 queue
,(下面的分析没有直接在日志里体现,经由代码分析)所以,这个事务InnoDB 会给使用 Next-Key Lock,不仅锁定范围,还会锁定记录本身,查询的排序是升序,所以每次新的查询都会类似锁定 (0, 3184]
这些记录
事务 245747602
,查询的列为,主键索引(唯一索引),应用的锁算法为 Record Lock
,只锁定了 id = 3183
的记录,由 InnoDB 实现的两种锁 得知,加的行级锁为 X 锁。
结论
两个事务之间形成了 AB-BA 死锁。
即 A 在等待 B,B 在等待 A。
两个事务中不仅仅各有一条 sql,除了日志里体现的,由队列逻辑代码分析,其实还有其它的 sql
过程大概如下
队列系统正循环查询数据库中的记录进行消费和删除
队列逻辑:前一个事务 A,查到了 id=3183 进行了消费
消费完后进行删除,开启事务 245747602
(delete from jobs
where id
= 3183, 删除操作也会尝试加 X 锁),在这个删除操作前,还有一个尝试加 X 锁的查询,但是队列的下一次查询也开始进行了,查询条件中包含了辅助索引,所以事务要进行 Next-Key Lock,也就是类似范围为 (0, 3184)
的加锁,进入等待
两个事务的资源在相互等待,导致了报错 1213 的报错。
事务重新执行
事务的重新执行是 Laravel 处理的,没有影响业务。
处理办法
避免使用 MySQL 作为队列的驱动,尽量使用 Redis。