玩转MySQL:事务、锁、MVCC的查漏补缺与汇总

本文深入探讨了MySQL中的事务、锁和MVCC机制,包括死锁现象、解决策略以及锁的底层实现原理。通过分析死锁的产生、检测和避免方法,解释了MySQL如何利用wait-for graph算法检测并解决死锁。此外,文章还介绍了锁的内存结构,如事务信息、索引信息、锁粒度和类型信息等。在事务隔离机制部分,文章详述了RU、RC、RR和Serializable四个级别的实现,特别是RR级别如何通过MVCC和一次性ReadView快照解决脏读、不可重复读和幻读问题。最后,总结了不同隔离级别的特点和实现方式。
摘要由CSDN通过智能技术生成

引言

经过《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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值