• 【Mysql面试加分项】——Innodb的死锁检测机制【文末送书】


    起因

    从胖虎午后遛弯说起

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

    不一会儿,两个人就来到了小胡同的中间,面面相觑,不知所措。

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

    番外:翌日,大熊看胖虎一天没回家,感觉他很可怜,发挥了谦让的精神,主动向后退,让胖虎先通过。

    Innodb下的死锁示例

    注:本文所用Mysql事物隔离级别为,可重复读(REPEATABLE-READ)可用SELECT @@transaction_isolation语句查询当前数据库事物隔离级别。

    1. 首先我们先创建一个表,里面有id和score(分数)两个字段,都是Int类型。
    CREATE TABLE dead_lock_demo(
             id INT,
             score INT
    ) CHARSET=ascii ROW_FORMAT=Compact;#设置行格式为Compact
    
    • 1
    • 2
    • 3
    • 4

    番外: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);
    
    • 1
    • 2

    此时表中的数据是这样的。

    接下来我们开启两个事务会话,并且两个事务分别执行下面几条SQL。

    序号事物A事物B
    1BEGIN
    2UPDATE dead_lock_demo SET score = 99 WHERE id = 1; 将id为1的分数修改为90分,语句将获取id=1那条数据的排它锁BEGIN
    3UPDATE dead_lock_demo SET score = 100 WHERE id = 2;将id为2的分数修改为100分,语句将获取id=2那条数据的排它锁
    4UPDATE dead_lock_demo SET score = 80 WHERE id = 2; 将id为2的分数修改为80分,id为2的数据已被事务B获取排它锁,所以这条语句将会阻塞等待锁释放
    5UPDATE 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对死锁的处理

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    利用超时时间

    如果我们要是停用了上面的死锁检测,那么我们还有一个方法可以解决死锁问题,就是设置一个获取锁的超时时间,Innodb可以通过参数 Innodb_lock_wait_timeout来设置超时时间。当一个事务获取锁超时,Innodb会将超时的事务进行回滚
    利用超时时间回滚虽然简单方便,但是有以下两点坏处:

    1. 如果当前事务已经进行了非常多的更新操作,这个时候做回滚,会对undo_log日志进行大量的操作,但是innodb直接操作的是内存缓存区,如果不是超级多的数据更改其实问题也不大。

    2. innodb_lock_wait_timeout默认值是50s,所以如果真的发生死锁,需要等待50秒才能解锁,这时如果50s内有大量的请求打到数据库上,会导致数据库异常甚至不可用,这是我们不能容忍的。

    番外:Innodb在对数据进行增、删、改操作时,先操作的是缓存区(buffer pool),然后由系统内核控制具体什么时候刷盘(异步持久化),我们的undo_log日志也对应了一块缓存区,在写undo_log日志时,也是先写到缓存区,再进行刷盘。这里留一个思考:既然Innodb是先写到缓存区,再由系统内核控制异步刷盘,那万一数据还没有刷盘就断电了怎么办?会导致数据丢失吗?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);
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    我们以事务A和事务B同时对ID=1这条记录进行加锁为例:

    1. 事务A对Id=1这条记录进行加锁,会生成一个锁结构,与记录进行关联。并且之前没有别的事务对Id=1这条记录加锁,所以is_waiting方法返回false。事务A获取锁成功。
    2. 这时事务B也想对Id=1这条记录记性加锁,那事务B就会查当前记录有没有已生成的锁结构,发现有锁结构之后,事务B自己也会再生成一个锁结构与这条记录关联,不过锁结构的is_waiting()会返回true,标识当前事务在等待锁。
    3. 在事务A提交后,就会把自己的锁结构释放掉,然后查询还有没有别的事务在等待锁,发现事务B在等待,就将事务B的锁结构的LOCK_WAIT设置为true,也就是会导致is_waiting()会返回true。事务B获取锁成功,继续执行。

    最后

    对于所有的程序员也好,架构师也罢,都要对数据库的原理有一定了解。这样才能写好或设计出一个好的系统。

    所以小编想免费送一本Mysql技术内幕-innodb存储引擎(当当自营下单)。

    抽奖链接:大家可以在 公众号 云下风澜 对话框内回复666自助获取

  • 相关阅读:
    为什么说,企业数字化转型归根到底是人的转型?
    springweb层控制类的使用
    Vue源码学习(四):<templete>渲染第三步,将ast语法树转换为渲染函数
    学习笔记:物理渲染-间接光照
    29.Java中的文件操作【20220804】
    CMU15445 (Fall 2019) 之 Project#1 - Buffer Pool 详解
    R 语言nutrient数据集的可视化
    Redis 只会用缓存?20种妙用让同事直呼牛X
    带修主席树—简介
    实战回忆录:从Webshell开始突破边界
  • 原文地址:https://blog.csdn.net/weixin_37152234/article/details/125916869