一个阳光明媚的午后,胖虎迈着苏格兰碎步,面带微笑的在小胡同里由南向北走。同时大熊也在此时,在小胡同里由北向南走,我们假设这个小胡同只能同时容纳一个人通过。

不一会儿,两个人就来到了小胡同的中间,面面相觑,不知所措。
这时两个人就满足了死锁的条件,胖虎站在南边想往北走,大熊站在北边想往南走,通过上面的假设可知,胡同只可容纳一个人通过,恰巧两个人此时谁也不想往后退,所以胖虎和大熊就耗死在这了,形成了死锁。

番外:翌日,大熊看胖虎一天没回家,感觉他很可怜,发挥了谦让的精神,主动向后退,让胖虎先通过。
注:本文所用Mysql事物隔离级别为,可重复读(REPEATABLE-READ)可用SELECT @@transaction_isolation语句查询当前数据库事物隔离级别。
CREATE TABLE dead_lock_demo(
id INT,
score INT
) CHARSET=ascii ROW_FORMAT=Compact;#设置行格式为Compact
番外:ROW_FORMAT=Compact 是将当前表的行格式(记录结构)设置为 Compact。 Innodb有4种行格式,分别是Compact、Redundant、Dynamic和Compressed行格式,每种格式有不同的对象结构,这里不做详细赘述。
我们再往表里插入两条数据,id为1的88分,id为2的90分。
INSERT INTO dead_lock_demo (id,score)
VALUES (1,88),(2,90);
此时表中的数据是这样的。

接下来我们开启两个事务会话,并且两个事务分别执行下面几条SQL。
| 序号 | 事物A | 事物B |
|---|---|---|
| 1 | BEGIN | |
| 2 | UPDATE dead_lock_demo SET score = 99 WHERE id = 1; 将id为1的分数修改为90分,语句将获取id=1那条数据的排它锁 | BEGIN |
| 3 | UPDATE dead_lock_demo SET score = 100 WHERE id = 2;将id为2的分数修改为100分,语句将获取id=2那条数据的排它锁 | |
| 4 | UPDATE dead_lock_demo SET score = 80 WHERE id = 2; 将id为2的分数修改为80分,id为2的数据已被事务B获取排它锁,所以这条语句将会阻塞等待锁释放 | |
| 5 | UPDATE dead_lock_demo SET score = 70 WHERE id = 1; 将id为1的分数修改为70分,此时id为1的数据排它锁正被事务A持有,并且事务A在等待事务B释放id为2那条数据的排它锁,所以此时就会形成死锁,Innodb将输出错误提示:1213-Deadlock found when trying to get lock;try restarting transaction |

上述操作发生在两个不同的事务中,最终在B事务执行第5条sql时innodb抛出了死锁错误提示,并回滚了B事务,并且无需人工干预。那么innodb是如何发现死锁的呢?
上述内容可见,Innodb可以主动检测到程序发生了死锁,那么它是怎么做到的呢?
以Mysql5.7为例,采用的是对事务等待图(wait-for graph)进行深度优先搜索的方式来检测死锁的
注:Mysql 8.0以上版本对等待图进行了优化,构建了一个 稀疏等待关系图结构。这里因为小编在生产一直用的都是5.7版本,所以就不对8.0以上版本进行赘述了。
通过我们上述的两个事务A和B可以构建出一个简单的等待关系图。

图中存储了锁和事务等待关系。并且事务与事务之间的连线,代表事务在等待另一个事务释放资源:当这个图中两个事务之间出现了环,就代表存在死锁,就会被Innodb检测出来,很明显,上图中事务A和事务B就出现了一个环。
除了上述条件,Innodb还会判断如果一个锁上的等待事务超过200个或锁定线程等待列表上的事务拥有超过1000000个锁,就直接认定是发生了死锁,进行回滚。
当出现死锁时,Innodb会选择回滚消耗资源量最小的那个事务,因为回滚操作会回滚undo_log中的数据,如果事务A操作了非常多的更新和插入,事务B只操作了一条数据更新,那么很显然,回滚事务B对于我们来说更加划算。
接下来,我们一起来看看innodb死锁检测的源码吧,具体判断逻辑小编都写在下面代码的注释上了。(mysql源码目录 storage/innobase/lock/lock0lock.cc文件下DeadlockChecker::search()方法)。方法是经过简化的,删掉了很多代码,要不实在放不下
const trx_t*
DeadlockChecker::search()
{
/* Look at the locks ahead of wait_lock in the lock queue. */
ulint heap_no;
//获取事务中的第一个锁,也就是我们事务A中 id=1的这条数据的锁
const lock_t* lock = get_first_lock(&heap_no);
for (;;) {
if (lock == NULL) {
break;
}
//如果想要获取的锁,在同一个事务内,不会造成死锁,直接走到最后返回return(0);
else if (lock == m_wait_lock) {
/* We can mark this subtree as searched */
ut_ad(lock->trx->lock.deadlock_mark <= m_mark_start);
lock->trx->lock.deadlock_mark = ++s_lock_mark_counter;
ut_ad(s_lock_mark_counter > 0);
/* Backtrack */
lock = NULL;
}
//如果没有发生锁冲突,获取下一个锁
else if (!lock_has_to_wait(m_wait_lock, lock)) {
/* No conflict, next lock */
lock = get_next_lock(lock, heap_no);
}
//检测到死锁发生(事务形成一个循环),调用select_victim()选择要回滚的事务。
else if (lock->trx == m_start) {
/* Found a cycle. */
notify(lock);
return(select_victim());
}
//1.判断等待锁的事务数超过200。 2.如果锁定线程等待列表上的事务拥有的超过1000000个锁。 判断以上2个条件是否成立,一个成立就直接认为是死锁,进行选择性回滚。
else if (is_too_deep()) {
/* Search too deep to continue. */
m_too_deep = true;
return(m_start);
//如果锁对应的事务处于等待状态,
} else if (lock->trx->lock.que_state == TRX_QUE_LOCK_WAIT) {
//这块逻辑稍微复杂点,可以先不看
}
else {
lock = get_next_lock(lock, heap_no);
}
}
ut_a(lock == NULL && m_n_elems == 0);
/* No deadlock found. */
return(0);
}
如果我们要是停用了上面的死锁检测,那么我们还有一个方法可以解决死锁问题,就是设置一个获取锁的超时时间,Innodb可以通过参数 Innodb_lock_wait_timeout来设置超时时间。当一个事务获取锁超时,Innodb会将超时的事务进行回滚。
利用超时时间回滚虽然简单方便,但是有以下两点坏处:
如果当前事务已经进行了非常多的更新操作,这个时候做回滚,会对undo_log日志进行大量的操作,但是innodb直接操作的是内存缓存区,如果不是超级多的数据更改其实问题也不大。
innodb_lock_wait_timeout默认值是50s,所以如果真的发生死锁,需要等待50秒才能解锁,这时如果50s内有大量的请求打到数据库上,会导致数据库异常甚至不可用,这是我们不能容忍的。
番外:Innodb在对数据进行增、删、改操作时,先操作的是缓存区(buffer pool),然后由系统内核控制具体什么时候刷盘(异步持久化),我们的undo_log日志也对应了一块缓存区,在写undo_log日志时,也是先写到缓存区,再进行刷盘。这里留一个思考:既然Innodb是先写到缓存区,再由系统内核控制异步刷盘,那万一数据还没有刷盘就断电了怎么办?会导致数据丢失吗?Innodb是怎么解决这个问题的? 答案关注文末公众号,会在后续文章中更新哦!
当一个事务在对某一条记录进行加锁时,会生成一个锁结构。以下是innodb中锁结构体的定义。这里我删掉了很多字段属性,留下了一些最基础的。
struct lock_t {
//当前这个锁属于哪个事务
trx_t* trx; /*!< transaction owning the
lock */
//通过一个宏,将这个事务拥有的锁用一个链表串起来
UT_LIST_NODE_T(lock_t)
trx_locks; /*!< list of the locks of the
transaction */
//一个表锁和行锁的联合体结构,我们一般用的都是行锁
union {
lock_table_t tab_lock;/*!< table lock */
lock_rec_t rec_lock;/*!< record lock */
} un_member; /*!< lock details */
//返回当前事务是否在等待
bool is_waiting() const
{
return(type_mode & LOCK_WAIT);
}
};

我们以事务A和事务B同时对ID=1这条记录进行加锁为例:
对于所有的程序员也好,架构师也罢,都要对数据库的原理有一定了解。这样才能写好或设计出一个好的系统。
所以小编想免费送一本Mysql技术内幕-innodb存储引擎(当当自营下单)。
抽奖链接:大家可以在 公众号 云下风澜 对话框内回复666自助获取
