说明:
上面的数字都是源代码中的关于各种锁的位图值:
LOCK_TABLE:16
LOCK_IX:1
LOCK_REC_NOT_GAP:1024
LOCK_WAIT:256
LOCK_REC:32
所以锁@6表示的是
LOCK_REC |
LOCK_REC_NOT_GAP |
LOCK_X |
LOCK_WAIT = 1315
依次类推
这里检查死锁的算法大体上说一下,无非是检查有没有形成等待环
事务B的锁@8等待事务C的锁@6,事务C的锁@6在等待事务B的锁@3,此时发现又绕回来了,那么产生死锁。
到这里,死锁的现象如何产生已经解释清楚,但是,这是为什么呢?
这里的疑问是:
在事务A提交之后,将事务B唤醒了,此时事务B的锁@4为REC NOTGAP X(1059),那么此时这个事务又去检查锁的情况,看看自己事务的锁有没有GRANT成功的,如果有则直接使用并且继续执行,如果没有则再加锁,做这个检查的函数为lock_rec_has_expl,它做的事情是下面的检查:
===========================================================
lock = lock_rec_get_first(block, heap_no);
while (lock) {
if
(lock->trx == trx
&& !lock_is_wait_not_by_other(lock->type_mode)
&& lock_mode_stronger_or_eq(lock_get_mode(lock),
precise_mode & LOCK_MODE_MASK)
&& (!lock_rec_get_rec_not_gap(lock)
|| (precise_mode & LOCK_REC_NOT_GAP)
|| heap_no == PAGE_HEAP_NO_SUPREMUM)
&& (!lock_rec_get_gap(lock)
|| (precise_mode & LOCK_GAP)
|| heap_no == PAGE_HEAP_NO_SUPREMUM)
&& (!lock_rec_get_insert_intention(lock))) {
return(lock);
}
lock = lock_rec_get_next(heap_no, lock);
}
=============================================================
这里需要满足6个条件:
- 首先这个锁是自己事务的
- 这个锁不是处于等待状态
- 当前锁的类型与precise_mode是兼容的,precise_mode值是X锁,因为这里是要做删除
- 当前锁不是NOT GAP类型,或者要加的锁类型是NOTGAP类型的,或者heapno为1
- 当前锁不是GAP类型,或者要加的锁类型是GAP类型的,或者heapno为1
- 当前锁不是意向插入锁
但此时发现1059(锁@4)根本不满足第4点啊,因为它首先是NOTGAP锁,同时heapno不是1,所以没有找到,所以在外面又重新创建一个锁,因为此时这行已经有锁了,那么它会创建一个REC WAIT X锁(291),也就是锁@8。
所以即使锁@4不是处于等待状态了,此时也不能直接执行呢,而是重新创建了一个锁。此时导致了死锁。
那么现在问题又来了,从上图可以看到,这个时间序列没有什么特别的,或者特殊的一个交叉过程,从而是不是我们可以很容易的重现呢?仅仅通过开启三个会话,都设置为not autocommit的,因为需要将第一个事务A的提交放在事务B C的后面。
那么开始了,创建相同的表,删除同一行记录。
事务A |
事务B |
事务C |
begin |
|
|
delete
删除行数返回为1
|
|
|
|
begin |
|
|
delete 阻塞 |
|
|
|
begin |
|
|
阻塞 |
commit |
|
|
|
观察有没有死锁 其实并没有死锁 删除行数返回为0 |
|
|
|
删除行数返回为0 |
图2
按说,上面这个图与图1没有什么区别,但没有死锁?为什么?
其实没有死锁是正常的,如果这样就死锁了,那mysql简直不能用了!!!
看来还是有区别的
正常模式下再做一次log分析,从log中看出了大问题......
再将上面详细的加锁图在无死锁模式下的情况贴出来:
事务A |
事务B |
事务C |
开始 |
|
|
表的IX锁 17 @1 |
|
|
二级索引行锁X REC NOTGAP 1059 @2
检查死锁 没事
|
|
|
聚簇索引行锁X REC NOTGAP 1059 @7
检查死锁 没事
|
|
|
|
表IX锁 17 @3 |
|
|
二级索引记录行锁 REC X WAIT 291 @4
检查死锁,没事
|
|
|
|
表IX锁 17 @5 |
|
|
二级索引记录行锁 REC X WAIT
291 @6
检查死锁 没事
|
|
wait.... suspend.... |
wait.... suspend.... |
commit |
|
|
|
wakeup this trx 将@4的WAIT去掉,成为35 |
|
|
执行完成,提交 |
|
|
|
执行完成 |
图3
此时发现,图3其实与图1是一样的,那为什么图3可以正常执行完成,而图1死锁了呢?
但认真仔细看了之后,发现有很小的地方是不同的,图3中的锁@4加上的锁是291(REC & X & WAIT),而图1中加的锁比它多了一个NOTGAP的锁,锁@6也是一样的,图3的事务A在提交并且唤醒了锁@4之后,它的锁类型为REC+X(35),而图1中的值也是比它多了一个NOTGAP锁。
现在已经基本定位了问题所在,应该是NOTGAP搞的鬼。但是为什么会有差别呢?
此时还需要回到代码中查看,通过日志分析,发现2个在执行下面代码时走了不同的路:
=======================================
if (prebuilt->select_lock_type != LOCK_NONE) {
ulint lock_type;
if (!set_also_gap_locks
|| srv_locks_unsafe_for_binlog
|| trx->isolation_level <= TRX_ISO_READ_COMMITTED
|| (unique_search
&& !UNIV_UNLIKELY(rec_get_deleted_flag(rec, comp)))) {
goto
no_gap_lock;//直接路到下面
lock_typ
e = LOCK_REC_NOT_GAP;处
} else {
lock_type = LOCK_ORDINARY;
}
、
if (index == clust_index
&& mode == PAGE_CUR_GE
&& direction == 0
&& dtuple_get_n_fields_cmp(search_tuple)
== dict_index_get_n_unique(index)
&& 0 == cmp_dtuple_rec(search_tuple, rec, offsets)) {
no_gap_lock://标记
lock_type = LOCK_REC_NOT_GAP;
}
=======================================
这里关键的分叉口就是在上面红色字体部分,死锁的时候走了
goto
no_gap_lock,而没有出现死锁的时候走的是
lock_type = LOCK_ORDINARY;,而
LOCK_ORDINARY表示的是0,什么都没有,所以这2条路的不同就是差1024(NOTGAP锁)。
那么从日志中发现,走了第一条路是因为条件
(unique_search
&& !UNIV_UNLIKELY(rec_get_deleted_flag(rec, comp))是符合的。
rec_get_deleted_flag函数的作用是判断这条记录是不是已经打了删除标志。
现在豁然明白了,如果当前这条要加锁的记录还没有打删除标志,则加的锁是NOTGAP类型的锁,否则就不设置类型,那说明上面的图1中事务A还是有一个细节没有画出来,正因为这个细节与事务B发生了交叉,导致了事务B在做的时候还没有打了删除标记,所以就加了NOTGAP锁,所以导致后面的死锁。
而正常情况下,也就是图2的测试,因为事务A已经完成了所有的操作,只等待提交,此时肯定已经打了删除标志,则在加锁时不会加NOTGAP锁,所以就不会出现死锁。
哎,用一句同事常说的话:我这下真的了然了,原来问题这么复杂,mysql中的猫腻太多了。
那现在分析一下原因吧:
现在已经确定问题就是出现在上面代码的判断中,在上面代码的上面还有一段注释:
/* Try to place a lock on the index record; note that delete
marked records are a special case in a unique search. If there
is a non-delete marked record, then it is enough to lock its
existence with LOCK_REC_NOT_GAP. */
这说明了加NOTGAP锁的意图,说明上面代码的判断是专门做的,具体原因就无从查起了,但是注释中说这是一种特殊情况,为什么呢?解决方式是把那2行直接去掉就可以了(测试过不会出现死锁了),但这个会不会是解决问题的根本原因,还要等待官方人员的处理。
所以到这里,把完整的死锁图贴上来: