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、尽量避免间隙锁。建议将间隙所转化为行锁

posted @ 2022-02-21 21:22  玉树临枫  阅读(752)  评论(0编辑  收藏  举报