引言
经过《MySQL锁机制》、《MySQL-MVCC机制》两篇后,咱们已经大致了解MySQL中处理并发事务的手段,不过对于锁机制、MVCC机制都并未与之前说到的《MySQL事务机制》产生关联关系,同时对于MySQL锁机制的实现原理也未曾剖析,因此本篇作为事务、锁、MVCC这三者的汇总篇,会在本章中补全之前空缺的一些细节,同时也会将锁、MVCC机制与事务机制之间的关系彻底理清楚。
一、MySQL中的死锁现象
还记得咱们在《MySQL锁机制》这篇文章中,描述事务、连接、线程三者关系的那段话嘛?
所谓的并发事务,本质上就是MySQL内部多条工作线程并行执行的情况,也正由于MySQL是多线程应用,所以需要具备完善的锁机制来避免线程不安全问题的问题产生,但熟悉多线程编程的小伙伴应该都清楚一点,对于多线程与锁而言,存在一个100%会出现的偶发问题,即死锁问题。
1.1、死锁问题概述(Dead Lock)
对于死锁的定义,这里就不展开叙述了,因为在之前《并发编程-死锁、活锁、锁饥饿》中曾详细描述过,如下:
一句话来概述死锁:死锁是指两个或两个以上的线程(或进程)在运行过程中,因为资源竞争而造成相互等待、相互僵持的现象,一般当程序中出现死锁问题后,若无外力介入,则不会解除“僵持”状态,它们之间会一直相互等待下去,直到天荒地老、海枯石烂~
当然,为了照顾一些不想看并发编程文章的小伙伴,这里也把之前的死锁栗子搬过来~
某一天竹子和熊猫在森林里捡到一把玩具弓箭,竹子和熊猫都想玩,原本说好一人玩一次的来,但是后面竹子耍赖,想再玩一次,所以就把弓一直拿在自己手上,而本应该轮到熊猫玩的,所以熊猫跑去捡起了竹子前面刚刚射出去的箭,然后跑回来之后便发生了如下状况: 熊猫道:竹子,快把你手里的弓给我,该轮到我玩了.... 竹子说:不,你先把你手里的箭给我,我再玩一次就给你.... 最终导致熊猫等着竹子的弓,竹子等着熊猫的箭,双方都不肯退步,结果陷入僵局场面.....
比如上述这个栗子中,「竹子、熊猫」可以理解成两条线程,而「弓、箭」则可以理解成运行时所需的资源,由于双方各自占据对方所需的资源,因此就造就了死锁现象发生,此时想要解决这个问题,就必须第三者外力介入,把“违反约定”的竹子手中的弓拿过去给熊猫......,然后等熊猫玩了之后,再给竹子,恢复之前原有的“执行顺序”。
1.2、MySQL中的死锁现象
而MySQL与Redis、Nginx这类单线程工作的程序不同,它属于一种内部采用多线程工作的应用,因而不可避免的就会产生死锁问题,比如举个例子:
SELECT * FROM `zz_account`; +-----------+---------+ | user_name | balance | +-----------+---------+ | 熊猫 | 6666666 | | 竹子 | 8888888 | +-----------+---------+ -- T1事务:竹子向熊猫转账 UPDATE `zz_account` SET balance = balance - 888 WHERE user_name = "竹子"; UPDATE `zz_account` SET balance = balance + 888 WHERE user_name = "熊猫"; -- T2事务:熊猫向竹子转账 UPDATE `zz_account` SET balance = balance - 666 WHERE user_name = "熊猫"; UPDATE `zz_account` SET balance = balance + 666 WHERE user_name = "竹子"; 复制代码
上面有一张很简单的账户表,因为只是为了演示效果,所以其中仅设计了用户名和余额两个字段,紧接着有T1、T2两个事务,T1中竹子向熊猫转账,而T2中则是熊猫向竹子转账,也就是一个相互转账的过程,此时来分析一下:
-
①T1事务会先扣减竹子的账户余额,因此会修改数据,此时会默认加上排他锁。
-
②T2事务也会先扣减熊猫的账户余额,因此同样会对熊猫这条数据加上排他锁。
-
③T1减完了竹子的余额后,准备获取锁把熊猫的余额加888,但由于此时熊猫的锁被T2事务持有,T1会陷入阻塞等待。
-
④T2减完熊猫的余额后,也准备获取锁把竹子的余额加666,但此时竹子的锁被T1持有。
此时就会出现问题,T1等待T2释放锁、T2等待T1释放锁,双方各自等待对方释放锁,一直如此僵持下去,最终就引发了死锁问题,那先来看看具体的SQL执行情况是什么样的呢?如下:
如上图所示,一步步的跟着标出的序号去看,最终会发现:当死锁问题出现时,MySQL会自动检测并介入,强制回滚结束一个“死锁的参与者(事务)”,从而打破死锁的僵局,让另一个事务能继续执行。
看到这里有小伙伴会问了,为啥MySQL能自动检测死锁呀?其实这跟死锁检测机制有关,后续再细说。
但是要牢记一点,如果你也想自己做上述实验,那么千万不要忘了在创建了表后,基于user_name创建一个主键索引:
ALTER TABLE `zz_account` ADD PRIMARY KEY p_index(user_name); 复制代码
如果你不为user_name字段加上主键索引,那是无法模拟出死锁问题的,这是为什么呢?还记得之前在《MySQL锁机制-记录锁》中聊到的一点嘛?在InnoDB中,如果一条SQL语句能命中索引执行,那就会加行锁,但如果无法命中索引加的就是表锁。
在上述给出的案例中,因为表中没有显示指定主键,同时也不存在一个唯一非空的索引,因此InnoDB会隐式定义一个row_id来维护聚簇索引的结构,但因为update语句中无法使用这个隐藏列,所以是走全表方式执行,因此就将整个表数据锁起来了。
而这里的四条update语句都是基于zz_account账户表在操作,因此两个事务竞争的是同一个锁资源,所以自然无法复现死锁现象,也就是T1修改时,T2的第一条SQL也不能执行,会阻塞等待表锁的释放。
而当咱们显示的定义了主键索引后,InnoDB会基于该主键字段去构建聚簇索引,因此后续的update语句可以命中索引,执行时自然获取的也是行级别的排他锁。
1.3、MySQL中死锁如何解决呢?
在之前关于死锁的并发文章中聊到过,对于解决死锁问题可以从多个维度出发,比如预防死锁、避免死锁、解除死锁等,而当死锁问题出现后该如何解决呢?一般只有两种方案:
-
锁超时机制:事务/线程在等待锁时,超出一定时间后自动放弃等待并返回。
-
外力介入打破僵局:第三者介入,将死锁情况中的某个事务/线程强制结束,让其他事务继续执行。
1.3.1、MySQL的锁超时机制
在InnoDB中其实提供了锁的超时机制,也就是一个事务在长时间内无法获取到锁时,就会主动放弃等待,抛出相关的错误码及信息,然后返回给客户端。但这里的时间限制到底是多久呢?可以通过如下命令查询:
show variables like 'innodb_lock_wait_timeout'; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | innodb_lock_wait_timeout | 50 | +--------------------------+-------+ 复制代码
默认的锁超时时间是50s,也就是在50s内未获取到锁的事务,会自动结束并返回。那也就意味着当死锁情况出现时,这个死锁过程最多持续50s,然后其中就会有一个事务主动退出竞争,释放持有的锁资源,这似乎听起来蛮不错呀,但实际业务中&#x