数据库死锁
2020-03-06 本文已影响0人
爱读书的夏夏
文章来自公司某位DBA大神。
在工作中,经常有同学问道,mysql怎么又死锁了,而最常出现的情况就是把查询与插入的语句放在一个事务中,先通过查询(for update),如果没有发现,则再做插入操作,而这样的操作又是并发的,这样很容易出现某一个事务报死锁的错误,下面就来解释一下这为什么会产生死锁:
测试表结构:
+--------------------------------+----------+
| Field | Type | Null | Key | Default | Extra |
+--------------------------------+----------+
| sno | int(11) | YES | | NULL | |
| name | varchar(100) | YES | | NULL | |
+--------------------------------+----------+
现在有2个会话,分别执行如下语句,便会死锁:
会话1中执行语句select * from myinfo where sno=1 for update;时,事务1会加以下锁:
- 表的IX锁,修改数据时都会加这个意向锁。@1
- 因为这个表现在还没有任何数据,由于读一致的隔离级别,所以它会上一个GAP锁,对当前最大的heap_no(1)加一个GAP锁,为LOCK_X锁,其实也是一个记录锁。@2
加这2个锁之后,会话1的查询for update已经执行完成。
会话2中执行语句select * from myinfo where sno=1 for update;时,事务2会加以下锁: - 同样的,加IX的表锁。@3
- 由于事务1中已经对heap_no为1的页面上加了GAP锁,相当于是把整个表都锁了,而在事务2中,同样也要加一个GAP锁,同样是X锁,按理来说,这个是互斥的,因为事务1中已经加了记录的X锁了,但在innodb中,如果2个锁都不是意向插入锁,则GAP锁是互不排斥的,所以事务2也同样加了一个GAP锁,heap_no为1,也是REC记录锁,因为这2个都不是意向插入锁的。
到此会话2的查询for update也执行完成。@4
此时会话1继续执行插入操作:
首先,它会加一个GAP锁,防止别人插入,而自己来完成插入操作,那么它还会在表中加入一个插入意向排它锁,同样是在heap_no为1的位置,那么此时这个事务需要看它是不是有要等待的锁,因为发现事务2中已经加了一个GAP排它记录锁@4,而这2个锁是互斥的,所以这个锁需要等待。@5
然后在会话2上面执行同样的插入操作:
同样的,事务2也会加意向插入排它锁@6,它同样会在所有事务的锁中查找会不会有冲突的锁,正好锁@2就是已经在同样位置加了排它锁,那么此时会等待,这看上去是要死锁了,因为事务1已经在等事务2了,现在事务2又要等事务1,那么innodb在这个地方做了一个死锁检查的,发现确实死锁了,然后通过判断哪个事务作为牺牲品,来决定哪个回滚,判断依据为当前事务产生的回滚记录个数及锁个数,谁的更小谁就牺牲,然后把牺牲者回滚,也就是会话2的会报发现死锁,并且已经回滚,此时发现会话1也插入完成了。
死锁检查算法:lock_deadlock_recursive(事务2, 事务2, lock(@6), &cost, 0);lock表示正在加的锁,前面2个参数为,谁在检查死锁,传入的就是谁。
从当前事务2开始,@6锁就是参数中的lock,事务2新加的这个锁,这个函数会顺序遍历所有的锁,看它是不是要等待某一个锁,因为遍历中发现要等待锁@2,而@2对应的事务trx为事务1,当前事务与事务1不同,所以此时还没有判断为死锁,而此时发现事务1正在等待一个锁,等待锁的锁是@5,此时再调用函数lock_deadlock_recursive(事务2, 事务1, @5, &cost, 0);
此时再遍历所有锁(在heap_no为1上面的锁),发现@5等待事务2,而此时发现函数的第一个参数与@5等待的事务是同一个事务,则说明有死锁,因为已经转了一圈了。
而此时如果还没有发现,则继续扫描,直到扫描完或者发现死锁为止。当然如果事务太多,扫描会太慢,系统中也设置了最大递归上限200,如果超过这个值,则不会再继续扫描。当前事务也是被做为了牺牲者回滚了。
总结:在mysql中死锁是很容易出现的,因为上面的表中是没有主键的,所以任何时候新插入数据,都是追加在最后的,所以都会产生页面最大值的GAP锁,所以就会出现上面的锁,因为锁了最大值GAP锁,其它人就不会插入成功了,因而导致了死锁。同样,如果在一个有主键的表中,只要表中没有任何数据,情况还是一样的,因为现在页面中的数据范围还是最大与最小,所以为了防止 别人插入成功,只能将最大GAP上锁,而如果表中已经有数据了,如果新插入数据比已经存在的数据靠前,就不会出现这个问题,因为此时锁的位置是原来数据与最小值之间的GAP,而如果在最大值后面插入的话,情况还是一样的。正因为上面的道理,很多应用程序为了保证一个表中值的唯一性,先通过for update查出来,如果没有再插入,这样线程并发执行就报了死锁的情况。