本文主要简单记录一下关于mysql innodb引擎中关于死锁检测与处理的代码的阅读。
1. 概述
innodb中检测与处理死锁的代码入口在:
storage/innobase/lock/lock0lock.c 的lock_deadlock_occurs函数 (大约3484行)。
这个函数调用了lock_deadlock_recursive函数 迭代地检查死锁。
当事务尝试获取(请求)加一个锁,并且需要等待时,innodb会开始进行死锁检测。
innodb中对于死锁的判断主要有3个情况:
1. 事务间形成锁的循环等待
2. 在检测死锁过程中检测的事务数超过了 LOCK_MAX_N_STEPS
3. 在检测死锁的过程中迭代的层数超过了 LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK
关于2和3会在后面具体介绍。
检测到死锁后有两种可能的处理:
1. 牺牲当前锁的事务
2. 牺牲检测到死锁时的另一个锁的事务(这个锁等待当前锁)
牺牲的选择是基于事务的weight。 (关于事务weight的计算会在后面介绍)
2. 背景知识
1. 事务的状态
一个事务的状态有4大类:
TRX_STATE_NOT_STARTED,
TRX_STATE_ACTIVE,
TRX_STATE_PREPARED,
TRX_STATE_COMMITTED_IN_MEMORY
意思很明显, 当事务处于激活(ACTIVE)的状态时,它可能有4种子状态:
TRX_QUE_RUNNING,
TRX_QUE_LOCK_WAIT,
TRX_QUE_ROLLING_BACK,
TRX_QUE_COMMITTING
TRX_QUE_LOCK_WAIT在死锁中用于判断循环等待。
2. innodb中锁的类型标识位
锁的类型标识位保存在锁结构 “lock_struct” 中的 “ulint type_mode” 属性中, 在innodb中常叫它bitmap。
它主要使用了12位的字节来表示锁的类型
这12个字节主要包含了4个部分的内容 mode, type, 等待标识 ,和 具体的行锁类型。
上表为mode和type的掩码。 0xFUL是15(1111),用于获取mode。0xF0UL是240(11110000),用于获取type,目前已经定义的type有表锁和行锁2类
前4位为mode mode主要有5个: IS IX S X AI (用于自增字段)
上表为这5个mode的兼容性关系。
类型之后就是一个bit位的等待标识:
掩码为256(100000000),第9位。
之后为行锁具体的锁类型:
LOCK_ORDINARY 为正常锁,即与gap没有关系的锁。
LOCK_GAP 只锁记录前的gap,阻止对该记录进行修改,包括在该记录前进行插入。
在gap上innodb是允许有冲突的事务。
LOCK_REC_NOT_GAP 锁记录而不锁gap,阻止修改,但允许在该记录前进行插入。
LOCK_INSERT_INTENTION 插入意向锁,一般配合LOCK_GAP使用,当gap上的冲突消失时进行插入。
3. 死锁的检测
lock_deadlock_occurs函数调用lock_deadlock_recursive 来迭代地检测死锁。
lock_deadlock_recursive检测的过程大概是:
1. 首先把待判断的锁设置为初始事务start等待的新锁
2. 如果要待判断的锁是行锁的话,从第一个开始循环取该行锁所在页中的锁为锁B;表锁的话跳到步骤6
3. 如果待判断的锁是否等待这个锁B
4. 如果等待, 并且锁B的事务是初始事务start,说明产生死锁了,要判定是牺牲这个锁B的事务还是牺牲新锁的事务start
5. 如果等待,并且迭代深度或总消耗超过了上限(每次调用lock_deadlock_recursive时消耗加1)时,也判定发生死锁
6. 如果等待,判断这个锁B是否处于等待其他锁的状态
7. 如果处于等待其他锁的状态,那么迭代地调用lock_deadlock_recursive 来判断这个锁是否等待该页上的其他锁,同时迭代层数加1。 这样迭代判断是否最终会形成循环等待。
8. 如果是表锁,那么这次迭代就没有死锁
上图lock_deadlock_recursive是迭代的主函数。 start为初始事务, wait_lock为待判断锁, cost和depth为累积的消耗和迭代深度。
如果是行锁的话把变量lock设置为页上的第一个锁,如果是表锁的话就直接把lock设置为待判断的锁。
开始循环, 首先判断变量lock是否已经等于待判定锁(表锁,或页中其他所有的行锁都判断完了),那么没有死锁发生。
接着判断待判断的锁是否等待lock变量指定的锁。 如果等待,首先判断是否迭代深度或总消耗是否已经达到上限。 接着判断lock的事务是否是初始事务,如果是,进入死锁处理。
如果lock的事务是否不是初始事务, 并且总消耗和迭代深度已经达到上限,也按死锁处理了。
如果lock的事务是否不是初始事务,接着递归调用去判断是否有循环等待。
最后取页中的下一个锁,进入下一个循环。
4. 判断锁1是否等待锁2
对于锁的判断主要在lock_has_to_wait函数(1017行)
如果锁1与锁2 属于同一个事务,或者两个锁的mode之间没有冲突的话,那么锁之间没有等待关系。否则调用lock_rec_has_to_wait函数来判断等待。
判断的主要把所有非等待的情况列出来:
1. 如果锁1类型为gap 并且没有LOCK_INSERT_INTENTION
2. 如果锁1类型不是LOCK_INSERT_INTENTION,而锁2是gap
3. 如果锁1类型是gap,而锁2类型不是gap
4. 如果锁2是LOCK_INSERT_INTENTION
那么锁1 不需要 等待锁2, 否则锁1需要等待锁2
5. 死锁牺牲者的判断
死锁的处理主要在lock_deadlock_occurs中。 即判断牺牲哪个事务。
入口函数为trx_weight_ge (storage/innobase/trx/trx0trx.c 1479行)。 把较轻的那个事务判定为牺牲者。
一个事务的weight的计算是基于修改的行数与锁定的行数来计算的。
6. 总结
innodb中的死锁判断主要作用与一个页内的行锁, 这也与数据库i/o以页为单位进行操作相一致。 所以innodb的行锁更多的是从事务的层面上而言, 具体到底层其实也是锁整个页。