MySQL 死锁示例
1、什么是死锁
死锁是指两个或两个以上的事务在执行的过程中,因争夺资源而造成的一种互相等待的现象。
2、死锁示例
以下示例是基于RR隔离级别的基础下进行的。
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`name` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
## insert 两条数据
mysql> select * from t;
+----+-------+
| id | name |
+----+-------+
| 1 | james |
| 2 | kobe |
+----+-------+
2.1、经典死锁场景: 加锁顺序不一致导致的死锁
在聚集索引上加锁(行锁)顺序不一样导致产生的死锁:两事务相互竞争对方已占有的资源,导致死锁。
序号 | 事务1 | 事务2 |
---|---|---|
0 | begin; | begin; |
1 | select * from t where id = 1 for update; | select * from t where id = 2; for update; |
此时事务1持有id=1的行锁 | 此时事务1持有id=2的行锁 | |
2 | select * from t where id = 2 for update; | |
此时在等待id=2的行锁释放 | ||
3 | select * from t where id = 1 for update; | |
此时事务2在等待id=1的行锁,与事务1形成循环等待,所以会导致死锁。 ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction 这是mysql返回的,此时事务2会释放id=2的锁和取消id=1的竞争 |
这只是其中一种场景,比如还会遇到根据非聚集索引进行加锁时,可能会锁住多行。所以如果多个事务加锁顺序不一样导致有竞争就可能会导致死锁
另外提醒下:Select * from xxx where id in (xx,xx,xx) for update
在in里面的列表值mysql是会自动从小到大排序,加锁也是一条条从小到大加的锁
2.2、插入意向锁和间隙锁发生死锁
当对未存在的行进行锁的时候(即使条件为主键),mysql是会锁住一段范围(有gap锁)
序号 | 事务1 | 事务2 |
---|---|---|
0 | begin; | begin; |
1 | select * from t where id = 11 for update; | select * from t where id = 22; for update; |
由于11这条记录不存在,所以行锁转变为间隙锁,锁住的范围为(2, +∞) | 同理,由于22这条记录不存在,所以行锁转变为间隙锁,锁住的范围为(2, +∞) | |
所以这两个事务对同一个区间加了间隙锁,由于间隙锁是允许共存的,所以可以继续执行 | ||
2 | insert t values(11, 'oven'); | |
此时事务1需要对id=11加插入意向锁,但由于插入意向锁与事务2中的间隙锁冲突,发生阻塞 | ||
3 | insert t values(22, 'steven'); | |
此时事务2需要对id=22加插入意向锁,这时MySQL检测到死锁,回滚。 |
2.3 行锁与间隙锁发生死锁
准备如下数据:
select * from t order by id asc;
+----+--------+
| id | name |
+----+--------+
| 1 | james |
| 2 | kobe |
| 3 | wade |
| 5 | davis |
| 6 | paul |
| 7 | jordan |
| 10 | maddie |
+----+--------+
7 rows in set (0.00 sec)
序号 | 事务1 | 事务2 |
---|---|---|
0 | begin; | begin; |
1 | select * from t where id = 5 for update; | |
事务1对id=1这条记录加上行锁 | ||
2 | select * from t where id < 20 for update; | |
此时事务2加的是间隙锁,整体区间为(-∞, 20),由于事务1持有此区间中的一个行锁, 所以需要等待事务1所持有的行锁释放。此时事务2持有的真正的锁区间仅为(-∞, 5) | ||
3 | insert t values(4, 'test'); | |
事务1此时再去获取id=4的行锁, 而id=4在事务2中的间隙锁范围内, 这时MySQL检测到死锁,回滚。 |
总结下:
Session2在等待Session1的id=5的锁,session2又持了1到4的锁,最后,session1在插入新行时又得等待session2,故死锁发生了。
这种一般是在业务需求中基本不会出现,因为你锁住了id=5,却又想插入id=4的行,这就有点跳了,当然肯定也有解决的方法,那就是重理业务需求,避免这样的写法。
此时提个问题哈:
第三步事务1插入的记录是:insert t values(11, 'test'); 会导致死锁吗?
3、如何解决死锁
1、最简单的方法就是设置超时,即当两个事务互相等待时,当一个等待时间超过设置的某一阈值时,其中一个事务进行回滚,另一个等待的事务就能继续执行。
在innodb存储引擎中,参数innodb_lock_wait_timeout用来设置超时的时间。
2、另外一种主动的方案:采用wait-for graph的方式来进行死锁检测,如果检测到死锁,会选择回滚undo量最小的事务;
4、如何预防死锁
最直接的方法就是破坏产生死锁的条件:如互斥条件、循环等待等;
比如:
1、不同事务中的加锁顺序尽量保持统一;
2、尽量避免大事务,占有的资源锁越多,越容易出现死锁。建议拆成小事务
3、尽量避免间隙锁。建议将间隙所转化为行锁