引言

本文为社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

经过《MySQL锁机制》、《MySQL-MVCC机制》两篇后,咱们现已大致了解MySQL中处理并发业务的手法,不过关于锁机制、MVCC机制都并未与之前说到的《MySQL业务机制》产生相关联系,一同关于MySQL锁机制的完成原理也未曾分析,因而本篇作为业务、锁、MVCC这三者的汇总篇,会在本章中补全之前空缺的一些细节,一同也会将锁、MVCC机制与业务机制之间的联系完全理清楚。

一、MySQL中的死锁现象

还记得咱们在《MySQL锁机制》这篇文章中,描绘业务、衔接、线程三者联系的那段话嘛?

(十)全解MySQL之死锁问题分析、事务隔离与锁机制的底层原理剖析

所谓的并发业务,本质上便是MySQL内部多条作业线程并行履行的状况,也正因为MySQL是多线程应用,所以需求具有完善的锁机制来防止线程不安全问题的问题产生,但熟悉多线程编程的小伙伴应该都清楚一点,关于多线程与锁而言,存在一个100%会呈现的偶发问题,即死锁问题

1.1、死锁问题概述(Dead Lock)

关于死锁的界说,这儿就不展开叙说了,因为在之前《并发编程-死锁、活锁、锁饥饿》中曾详细描绘过,如下:

(十)全解MySQL之死锁问题分析、事务隔离与锁机制的底层原理剖析

一句话来概述死锁:死锁是指两个或两个以上的线程(或进程)在运转进程中,因为资源竞赛而形成彼此等候、彼此相持的现象,一般当程序中呈现死锁问题后,若无外力介入,则不会免除“相持”状况,它们之间会一向彼此等候下去,直到天荒地老、海枯石烂~

当然,为了照顾一些不想看并发编程文章的小伙伴,这儿也把之前的死锁栗子搬过来~

某一天竹子和熊猫在森林里捡到一把玩具弓箭,竹子和熊猫都想玩,本来说好一人玩一次的来,可是后边竹子耍赖,想再玩一次,所以就把弓一向拿在自己手上,而本应该轮到熊猫玩的,所以熊猫跑去捡起了竹子前面刚刚射出去的箭,然后跑回来之后便产生了如下状况:
熊猫道:竹子,快把你手里的弓给我,该轮到我玩了….
竹子说:不,你先把你手里的箭给我,我再玩一次就给你….
终究导致熊猫等着竹子的弓,竹子等着熊猫的箭,双方都不肯让步,成果堕入僵局场面…..

比方上述这个栗子中,「竹子、熊猫」能够了解成两条线程,而「弓、箭」则能够了解成运转时所需的资源,因为双方各自占有对方所需的资源,因而就造就了死锁现象产生,此刻想要处理这个问题,就必须第三者外力介入,把“违背约定”的竹子手中的弓拿过去给熊猫……,然后等熊猫玩了之后,再给竹子,恢复之前原有的“履行次序”。

1.2、MySQL中的死锁现象

MySQLRedis、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会自动检测并介入,强制回滚完毕一个“死锁的参与者(业务)”,然后打破死锁的僵局,让另一个业务能继续履行。

看到这儿有小伙伴会问了,为啥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,然后其间就会有一个业务自动退出竞赛,开释持有的锁资源,这好像听起来蛮不错呀,但实际业务中,仅依靠超时机制去免除死锁是不行的,究竟高并发状况下,50s时刻太长了,会导致越来越多的业务堵塞。

那么咱们能不能把这个参数调小一点呢?比方调到1s,能够吗?当然能够,确实也能保证死锁产生后,在很短的时刻内能够自动免除,但改掉了这个参数之后,也会影响正常业务等候锁的时刻,也便是大部分未产生死锁,但需求等候锁资源的业务,在等候1s之后,就会立马报错并回来,这明显并不合理,究竟简略误伤“友军”。

也正是因为依靠锁超时机制,稍微有些不靠谱,因而InnoDB也专门针关于死锁问题,研发了一种检测算法,名为wait-for graph算法。

1.3.2、死锁检测算法 – wait-for graph

这种算法是专门用于检测死锁问题的,在该算法中会关于现在库中一切活泼的业务生成等候图,啥意思呢?以上述的死锁事例来看,在MySQL内部会生成一张这样的等候图:

(十)全解MySQL之死锁问题分析、事务隔离与锁机制的底层原理剖析

也便是T1持有着「竹子」这条数据的锁,正在等候获取「熊猫」这条数据的锁,而T2业务持有「熊猫」这条数据的锁,正在等候获取「竹子」这条数据的锁,终究T1、T2两个业务之间就呈现了等候闭环,因而当MySQL发现了这种等候闭环时,就会强制介入,回滚完毕其间一个业务,强制打破该闭环,然后免除死锁问题。

但这个“等候图”仅仅为了便利了解画出来的,内部的完成其实存在些许差异,一同来聊一聊。

wait-for graph算法被启用后,会要求MySQL收集两个信息:

  • 锁的信息链表:现在持有每个锁的业务是谁。
  • 业务等候链表:堵塞的业务要等候的锁是谁。

每当一个业务需求堵塞等候某个锁时,就会触发一次wait-for graph算法,该算法会以当时业务作为起点,然后从「锁的信息链表」中找到对应中锁信息,再去依据锁的持有者(业务),在「业务等候链表」中进行查找,看看持有锁的业务是否在等候获取其他锁,假如是,则再去看看另一个持有锁的业务,是否在等候其他锁…..,经过一系列的判别后,再看看是否会呈现闭环,呈现的话则介入损坏。

上面这个算法的进程,听起来好像有些晕乎乎的,但实际上并不难,套个比方来了解,比如现在库中有T1、T2、T3三个业务、有X1、X2、X3三个锁,业务与锁的联系如下:

(十)全解MySQL之死锁问题分析、事务隔离与锁机制的底层原理剖析

此刻当T3业务需求堵塞等候获取X1锁时,就会触发一次wait-for graph算法,流程如下:

  • ①先依据T3要获取的X1锁,在「锁的信息链表」中找到X1锁的持有者T1
  • ②再在「业务等候链表」中查找,看看T1是否在等候获取其他锁,此刻会得知T1等候X2
  • ③再去「锁的信息链表」中找到X2锁的持有者T2,再看看T2是否在堵塞等候获取其他锁。
  • ④再在「业务等候链表」中查找T2,发现T2正在等候获取X3锁,再找X3锁的持有者。

经过上述一系列算法进程后,终究会发现X3锁的持有者为T3,而本次算法又正是T3业务触发的,此刻又回到了T3业务,也就代表着产生了“闭环”,因而也能够证明这儿呈现了死锁现象,所以MySQL会强制回滚其间的一个业务,来抵达免除死锁的目的。

但呈现死锁问题时,MySQL会挑选哪个业务回滚呢?之前分析过,当一个业务在履行SQL更改数据时,都会记载在Undo-log日志中,Undo量越小的业务,代表它对数据的更改越少,一同回滚的代价最低,因而会挑选Undo量最小的业务回滚(如若两个业务的Undo量相同,会挑选回滚触发死锁的业务)。

一同,能够经过innodb_deadlock_detect=on|off这个参数,来操控是否敞开死锁检测机制。

死锁检测机制在MySQL后续的高版别中是默许敞开的,但实际上死锁检测的开支不小,上面三个并发业务堵塞时,会对「业务等候链表、锁的信息链表」合计检索六次,那当堵塞的并发业务越来越多时,检测的效率也会呈线性增长。

1.3.3、怎样防止死锁产生?

因为死锁的检测进程较为耗时,所以尽量不要等死锁呈现后再去免除,而是尽量调整业务防止死锁的产生,一般来说能够从如下方面考虑:

  • 合理的规划索引结构,使业务SQL在履行时能经过索引定位到详细的几行数据,减小锁的粒度。
  • 业务答应的状况下,也能够将阻隔等级调低,因为等级越低,锁的约束会越小。
  • 调整业务SQL的逻辑次序,较大、耗时较长的业务尽量放在特定时刻去履行(如清晨对账…)。
  • 尽可能的拆分业务的粒度,一个业务组成的大业务,尽量拆成多个小业务,缩短一个业务持有锁的时刻。
  • 假如没有强制性要求,就尽量不要手动在业务中获取排他锁,否则会形成一些不必要的锁呈现,增大产生死锁的几率。
  • ……..

其实简略来说,也便是在业务答应的状况下,尽量缩短一个业务持有锁的时刻、减小锁的粒度以及锁的数量。

一同也要记住:当MySQL运转进程中产生了死锁问题,那这个死锁问题以后肯定会再次呈现,当死锁被MySQL自己免除后,必定要记住去排除业务SQL的履行逻辑,找到产生死锁的业务,然后调整业务SQL的履行次序,这样才干从根源上防止死锁产生。

二、锁机制的底层完成原理

关于MySQL的锁机制究竟是怎样完成的呢?关于这点其实很少有资料去讲到,一般都是停留在锁机制的表层阐述,比方锁粒度、锁类型的划分,但已然咱们讲了锁机制,那也就趁便聊一下它的底层完成。

2.1、锁的内存结构

Java中,Synchronized锁是依据Monitor完成的,而ReetrantLock又是依据AQS完成的,那MySQL的锁是依据啥完成的呢?想要搞清楚这点,得先弄理解锁的内存结构,先看图:

(十)全解MySQL之死锁问题分析、事务隔离与锁机制的底层原理剖析

InnoDB引擎中,每个锁方针在内存中的结构如上,其间记载的信息也比较多,先悉数理清楚后再聊聊锁的完成。

2.1.1、锁的业务信息

其间记载着当时的锁结构是由哪个业务生成的,记载的是指针,指向一个详细的业务。

2.1.2、索引的信息

这个是行锁的特有信息,关于行锁来说,需求记载一下加锁的行数据归于哪个索引、哪个节点,记载的也是指针。

2.1.3、锁粒度信息

这个稍微有些杂乱,关于不同粒度的锁,其间存储的信息也并不同,假如是表锁,其间就记载了一下是对哪张表加的锁,以及表的一些其他信息。

但假如锁粒度是行锁,其间记载的信息更多,有三个较为重要的:

  • Space ID:加锁的行数据,地点的表空间ID
  • Page Number:加锁的行数据,地点的页号。
  • n_bits:运用的比特位,关于一页数据中,加了多少个锁(后续结合讲)。

2.1.4、锁类型信息

关于锁结构的类型,在内部完成了复用,选用一个32bittype_mode来表明,这个32bit的值能够拆为lock_mode、lock_type、rec_lock_type三部分,如下:

(十)全解MySQL之死锁问题分析、事务隔离与锁机制的底层原理剖析

  • lock_mode:表明锁的模式,运用低四位。
    • 0000/0:表明当时锁结构是同享意向锁,即IS锁。
    • 0001/1:表明当时锁结构是排他意向锁,即IX锁。
    • 0010/2:表明当时锁结构是同享锁,即S锁。
    • 0011/3:表明当时锁结构是排他锁,即X锁。
    • 0100/4:表明当时锁结构是自增锁,即AUTO-INC锁。
  • lock_type:表明锁的类型,运用低位中的5~8位。
    • LOCK_TABLE:当第5个比特位是1时,表明现在是表级锁。
    • LOCK_REC:当第6个比特位是1时,表明现在是行级锁。
  • rec_lock_type:表明行锁的详细类型,运用其余位。
    • LOCK_ORDINARY:当高23位全零时,表明现在是临键锁。
    • LOCK_GAP:当第10位是1时,表明现在是空隙锁。
    • LOCK_REC_NOT_GAP:当第11位是1时,表明现在是记载锁。
    • LOCK_INSERT_INTENTION:当第12位是1时,表明现在是刺进意向锁。
    • .....:内部还有一些其他的锁类型,会运用其他位。
  • is_waiting:表明现在锁处于等候状况仍是持有状况,运用低位中的第9位。
    • 0:表明is_waiting=false,即当时锁无需堵塞等候,是持有状况。
    • 1:表明is_waiting=true,即当时锁需求堵塞,是等候状况。

OK~,上面分析了这一堆之后,看起来难免有些晕乎乎的,上个比方来了解一下:

00000000000000000000000100100011

比方上面给出的这组bit,锁粒度、锁类型、锁状况是什么状况呢?如下:

(十)全解MySQL之死锁问题分析、事务隔离与锁机制的底层原理剖析

从上图中可得知,现在这组bit代表一个堵塞等候的行级排他临键锁结构。

2.1.5、其他信息

这个所谓的其他信息,也便是指一些用于辅助锁机制的信息,比方之前死锁检测机制中的「业务等候链表、锁的信息链表」,每一个业务和锁的持有、等候联系,都会在这儿存储,将一切的业务、锁衔接起来,就形成了上述的两个链表。

2.1.6、锁的比特位

与其说是锁的比特位,不如说是数据的比特位,比如举个比方:

SELECT * FROM `zz_student`;
+------------+--------+------+--------+
| student_id | name   | sex  | height |
+------------+--------+------+--------+
|          1 | 竹子   || 185cm  |
|          2 | 熊猫   || 170cm  |
|          3 | 子竹   || 182cm  |
|          4 | 棕熊   || 187cm  |
|          5 | 黑豹   || 177cm  |
|          6 | 脑斧   || 178cm  |
|          7 | 兔纸   || 165cm  |
+------------+--------+------+--------+

学生表中有七条数据,此刻就会形成一个比特数组:000000000,等等,好像不对!分明只要七条数据,为啥会有9个比特位呢?因为行锁中,空隙锁能够锁定无穷小、无穷大这两个空隙,因而这组比特中,首位和末位即表明无穷小、无穷大两个空隙。

比如此刻T1业务,对ID=2、3、6这三条数据加锁了,此刻这个比特数组就会变为001100100,表明T1业务一同锁定了三条数据。而之前聊到的n_bits,它就会记载一下在这组比特中,多少条记载被上锁了。

2.2、InnoDB的锁完成

上面现已分析了MySQL的锁方针结构,接着来想象一个问题:

假如一个业务一同需求对表中的1000条数据加锁,会生成1000个锁结构吗?

假如这儿是SQL Server数据库,那肯定会生成1000个锁结构,因为它的行锁是加在行记载上的,但MySQL锁机制并不相同,因为MySQL是依据业务完成的锁,啥意思呢?来看看:

  • ①现在对表中不同行记载加锁的业务是同一个。
  • ②需求加锁的记载在同一个页面中。
  • ③现在业务加锁的类型都是相同的。
  • ④锁的等候状况也是相同的。

当上述四点条件被满意时,符合条件的行记载会被放入到同一个锁结构中,比如以上面的问题为例:

假设加锁的1000条数据分布在3个页面中,一同表中没有其他业务在操作,加的都是同一类型的锁。

此刻依据上述的前提条件,那在内存中仅会生成三个锁结构,能够很大程度上削减锁结构的数量。总归状况再杂乱,也不会像SQL Server般生成1000个锁方针,那样开支太大了!

2.3、MySQL获取锁的进程

当一个业务需求获取某个行锁时,首先会看一下内存中是否存在这条数据的锁结构,假如存在则生成一个锁结构,将其is_waiting对应的比特位改为1,表明现在业务在堵塞等候获取该锁,当其他业务开释锁后,会唤醒当时堵塞的业务,然后会将其is_waiting改为0,接着履行SQL

实际上会发现这个进程并不杂乱,唯一有些难了解的点就在于:业务获取锁时,是怎样在内存中,判别是否现已存在相同记载的锁结构呢?还记得锁结构中会记载的一个信息嘛?也便是「锁粒度信息」,假如是表锁,会记载表信息,假如是行锁,会记载表空间、页号等信息。在业务获取锁时,便是去看内存中,已存在的锁结构的这个信息,来判别是否存在其他业务获取了锁。

拿表锁来说,当业务要获取一张表的锁时,就会依据表名看一下其他锁结构,有没有获取当时这张表的锁,假如现已获取,看一下现已存在的表锁和现在要加的表锁,是否会存在抵触,抵触的话is_waiting=1,反之is_waiting=0,而行锁也是差不多的进程。

开释锁的进程也比较简略,这个作业一般是由MySQL自己完成的,当业务完毕后会自动开释,开释的时候会去看一下,内存中是否有锁结构,正在等候获取现在开释的锁,假如有则唤醒对应的线程/业务。

其实看下来之后咱们会发现,MySQL的锁机制完成,与常规的锁完成有些不相同,一般的锁机制都是依据持有标识+等候行列完成的,而MySQL则是稍微有些不相同。

三、业务阻隔机制的底层完成

关于业务阻隔机制的底层完成,其实在前面的章节中简略聊到过,关于并发业务形成的各类问题,在不同的阻隔等级实际上,是经过不同粒度、类型的锁以及MVCC机制来处理的,也便是调整了并发业务的履行次序,然后防止了这些问题产生,详细是怎样做的呢?先来看看DBMS中对各阻隔等级的要求。

  • RU/读未提交等级:要求该阻隔等级下处理脏写问题。
  • RC/读已提交等级:要求该阻隔等级下处理脏读问题。
  • RR/可重复读等级:要求该阻隔等级下处理不可重复读问题。
  • Serializable/序列化等级:要求在该阻隔等级下处理幻读问题。

虽然DBMS中要求在序列化等级再处理幻读问题,但在MySQL中,RR等级中就现已处理了幻读问题,因而MySQL中能够将RR等级视为最高等级,而Serializable等级几乎用不到,因为序列化等级中处理的问题,在RR等级中基本上现已处理了,再将MySQL调到Serializable等级反而会降低功能。

当然,RR等级下有些极点的状况,仍旧会呈现幻读问题,但线上100%不会呈现,这点后续聊,先来看看各大阻隔等级在MySQL中是怎样完成的。

3.1、RU(Read Uncommitted)读未提交等级的完成

关于RU等级而言,从它姓名上就能够看出来,该阻隔等级下,一个业务能够读到其他业务未提交的数据,但一同要求处理脏写(更新掩盖)问题,那考虑一下该怎样满意这个需求呢?先来看看不加锁的状况:

SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+
-- ----------- 请依照标出的序号阅读代码!!! --------------
-- ①敞开一个业务T1
begin;
-- ③修正 ID=1 的姓名为 竹子
UPDATE `zz_users` SET user_name = "竹子" WHERE user_id = 1;
-- ⑥提交T1
commit;
-- ②敞开另一个业务T2
begin;
-- ④这儿能够读取到T1中还未提交的 竹子 记载
SELECT * FROM `zz_users` WHERE user_id = 1;
-- ⑤T2中再次修正姓名为 黑熊
UPDATE `zz_users` SET user_name = "黑熊" WHERE user_id = 1;
-- ⑦提交T2
commit;

假设上述两个业务并发履行时,都不加锁,T2天然能够读取到T1修正后但还未提交的数据,但当T2再次修正ID=1的数据后,两个业务一同提交,此刻就会呈现T2掩盖T1的问题,这也便是脏写问题,而这个问题是不答应存在的,所以需求处理,咋处理呢?

写操作加排他锁,读操作不加锁!

仍是上述的比方,当写操作加上排他锁后,T1在修正数据时,当T2再次尝试修正相同的数据,也要获取排他锁,因而T1、T2两个业务的写操作会彼此排挤,T2就需求堵塞等候。但因为读操作不会加锁,因而当T2尝试读取这条数据时,天然能够读到数据。

来分析一下,因为写-写会排挤,但写-读不会排挤,因而也满意了RU等级的要求,即能够读到未提交的数据,可是不答应呈现脏写问题。

终究经过这一系列的解说后,能够得知MySQL-RU等级的完成原理,即写操作加排他锁,读操作不加锁!

3.2、RC(Read Committed)读已提交等级的完成

了解了RU等级的完成后,再来看看RCRC等级要求处理脏读问题,也便是一个业务中,不答应读另一个业务还未提交的数据,咋完成呢?

写操作加排他锁,读操作加同享锁!

这样一想,好像好像没问题,仍是以之前的比方来说,因为T1在修正数据,所以会对ID=1的数据加上排他锁,此刻T2想要获取同享锁读数据时,T1的排他锁就会排挤T2,因而T2需求比及T1业务完毕后才干读数据。

因为T2需求等候T1完毕后才干读,已然T1都完毕了,那也就代表着T1业务要么回滚了,T2读上一个业务提交的数据;要么T1提交了,T2T1提交的数据,总归T2读到的数据肯定是提交过的数据。

这种方法确实能处理脏读问题,但好像也会将一切并发业务串行化,会导致MySQL整体功能下降,因而MySQL引入了一种技能,也便是上篇聊到的《MVCC机制》,在每次select查询数据时,都会生成一个ReadView快照,然后依据这个快照去挑选一个可读的数据版别。

因而关于RC等级的底层完成,关于写操作会加排他锁,而读操作会运用MVCC机制。

但因为每次select时都会生成ReadView快照,此刻就会呈现下述问题:

-- ①T1业务中先读取一次 ID=1 的数据
SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+
-- ②T2业务中修正 ID=1 的姓名为 竹子 并提交
UPDATE `zz_users` SET user_name = "竹子" WHERE user_id = 1;
commit;
-- ③T1业务中再读取一次 ID=1 的数据
SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 竹子      || 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+

此刻调查这个事例,分明是在一个业务中查询同一条数据,成果两次查询的成果并不一致,这也是所谓的不可重复读的问题。

3.3、RR(Repeatable Read)可重复读等级的完成

RC等级中,虽然处理了脏读问题,但仍旧存在不可重复读问题,而RR等级中,便是要保证一个业务中的屡次读取成果一致,即处理不可重复读问题,咋处理呢?两种计划:

  • ①查询时,对方针数据加上临键锁,即读操作履行时,不答应其他业务改动数据。
  • MVCC机制的优化版:一个业务中只生成一次ReadView快照。

相较于第一种计划,第二种计划明显功能会更好,因为第一种计划不答应读-写、写-读业务共存,而第二种计划则支持读写业务并行履行,咋做到的呢?其实也比较简略:

写操作加排他锁,对读操作仍旧选用MVCC机制,但RR等级中,一个业务中只要初次select会生成ReadView快照。

-- ①T1业务中先读取一次 ID=1 的数据
SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+
-- ②T2业务中修正 ID=1 的姓名为 竹子 并提交
UPDATE `zz_users` SET user_name = "竹子" WHERE user_id = 1;
commit;
-- ③T1业务中再读取一次 ID=1 的数据
SELECT * FROM `zz_users` WHERE user_id = 1;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 竹子      || 6666     | 2022-08-14 15:22:01 |
+---------+-----------+----------+----------+---------------------+

仍是以这个场景为例,在RC等级中,会关于T1业务的每次SELECT都生成快照,因而当T1第2次查询时,生成的快照中就能看到T2修正后提交的数据。但在RR等级中,只要初次SELECT会生成快照,当第2次SELECT操作呈现时,仍旧会依据第一次生成的快照查询,所以就能保证同一个业务中,每次看到的数据都是相同的。

也正是因为RR等级中,一个业务仅有初次select会生成快照,所以不仅仅处理了不可重复读问题,还处理了幻读问题,举个比方:

-- 先查询一次用户表,看看整张表的数据
SELECT * FROM `zz_users`;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       1 | 熊猫      || 6666     | 2022-08-14 15:22:01 |
|       2 | 竹子      || 1234     | 2022-09-14 16:17:44 |
|       3 | 子竹      || 4321     | 2022-09-16 07:42:21 |
|       4 | 猫熊      || 8888     | 2022-09-27 17:22:59 |
|       9 | 黑竹      || 9999     | 2022-09-28 22:31:44 |
+---------+-----------+----------+----------+---------------------+
-- ①T1业务中,先查询一切 ID>=4 的用户信息
SELECT * FROM `zz_users` WHERE user_id >= 4;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       4 | 猫熊      || 8888     | 2022-09-27 17:22:59 |
|       9 | 黑竹      || 9999     | 2022-09-28 22:31:44 |
+---------+-----------+----------+----------+---------------------+
-- ②T1业务中,再将一切 ID>=4 的用户暗码重置为 1111
UPDATE `zz_users` SET password = "1111" WHERE user_id >= 4;
-- ③T2业务中,刺进一条 ID=6 的用户数据
INSERT INTO `zz_users` VALUES(6,"棕熊","男","7777","2022-10-02 16:21:33");
-- ④提交业务T2
commit;
-- ⑤T1业务中,再次查询一切 ID>=4 的用户信息
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|       4 | 猫熊      || 1111     | 2022-09-27 17:22:59 |
|       6 | 棕熊      || 7777     | 2022-10-02 16:21:33 |
|       9 | 黑竹      || 1111     | 2022-09-28 22:31:44 |
+---------+-----------+----------+----------+---------------------+

此刻会发现,分明T1中现已将一切ID>=4的用户暗码重置为1111了,成果改完再次查询会发现,表中仍旧存在一条ID>=4的数据:棕熊,并且暗码未被重置,这好像产生了错觉相同。

假如是RC等级,因为每次select都会生成快照,因而会呈现这个幻读问题,但RR等级中因为只要初次查询会生成ReadView快照,因而上述事例放在RR等级的MySQL中,T1看不到T2新增的数据,因而MySQL-RR等级也处理了幻读问题。

小争议:MVCC机制是否完全处理了幻读问题呢?

先上定论,MVCC并没有完全处理幻读问题,在一种奇葩的状况下仍旧会呈现问题,先来看比方:

-- 敞开一个业务T1
begin;
-- 查询表中 ID>10 的数据
SELECT * FROM `zz_users` where user_id > 10;
Empty set (0.01 sec)

因为用户表中不存在ID>10的数据,所以T1查询时没有成果,再继续往下看。

-- 再敞开一个业务T2
begin;
-- 向表中刺进一条 ID=11 的数据
INSERT INTO `zz_users` VALUES(11,"墨竹","男","2222","2022-10-07 23:24:36");
-- 提交业务T2
commit;

此刻T2业务刺进一条ID=11的数据并提交,此刻再回到T1业务中:

-- 在T1业务中,再次查询表中 ID>10 的数据
SELECT * FROM `zz_users` where user_id > 10;
Empty set (0.01 sec)

成果很明显,仍旧未查询到ID>10的数据,因为这儿是经过第一次生成的快照文件在读,所以读不到T2新增的“幻影数据”,好像没问题对嘛?接着往下看:

-- 在T1业务中,对 ID=11 的数据进行修正
UPDATE `zz_users` SET `password` = "1111" where `user_id` = 11;
-- 在T1业务中,再次查询表中 ID>10 的数据
SELECT * FROM `zz_users` where user_id > 10;
+---------+-----------+----------+----------+---------------------+
| user_id | user_name | user_sex | password | register_time       |
+---------+-----------+----------+----------+---------------------+
|      11 | 墨竹      || 1111     | 2022-10-07 23:24:36 |
+---------+-----------+----------+----------+---------------------+

嗯?!??此刻会发现,T1业务中又能查询到ID=11的这条幻影记载了,这是啥原因导致的呢?因为咱们在T1中修正了ID=11的数据,在《MVCC机制原理分析》中曾讲过MVCC经过快照检索数据的进程,这儿T1依据本来的快照文件检索数据时,因为发现ID=11这条数据上的躲藏列trx_id是自己,因而就能看到这条幻影数据了。

实际上这个问题有点怪样子,能够了解成幻读问题,也能够了解成是不可重复读问题,总归不管怎样说,便是MVCC机制存在些许问题!但这种状况线下一般不会产生,究竟不同业务之间都是互不相知的,在一个业务中,不可能会去自动修正一条“不存在”的记载。

但如若你实在不放心,想要完全杜绝任何风险的呈现,那就直接将业务阻隔等级调整到Serializable即可。

3.4、Serializable序列化等级的完成

前面现已将RU、RC、RR三个等级的完成原理弄懂了,最终再来看看最高的Serializable等级,在这个等级中,要求处理一切可能会因并发业务引发的问题,那怎样做呢?比较简略:

一切写操作加临键锁(具有互斥特性),一切读操作加同享锁。

因为一切写操作在履行时,都会获取临键锁,所以写-写、读-写、写-读这类并发场景都会互斥,而因为读操作加的是同享锁,因而在Serializable等级中,只要读-读场景能够并发履行。

四、业务与锁机制原理篇总结

在本章中,实则更多的是对《MySQL业务篇》、《MySQL锁机制》、《MySQL-MVCC机制》的弥补以及汇总,在本篇中补齐了MySQL死锁分析、锁完成原理、业务阻隔机制原理等内容,也结合业务、锁、MVCC机制三者的知识点,完全理清楚了MySQL不同阻隔等级下的完成,最终做个简略的小总结:

  • RU等级:读操作不加锁,写操作加排他锁。
  • RC等级:读操作运用MVCC机制,每次SELECT生成快照,写操作加排他锁。
  • RR等级:读操作运用MVCC机制,初次SELECT生成快照,写操作加临键锁。
  • 序列化等级:读操作加同享锁,写操作加临键锁。
等级/场景 读-读 读-写/写-读 写-写
RU等级 并行履行 并行履行 串行履行
RC等级 并行履行 并行履行 串行履行
RR等级 并行履行 并行履行 串行履行
序列化等级 并行履行 串行履行 串行履行

到这儿,MySQL业务机制、锁机制、MVCC机制、阻隔机制就完全分析完毕啦~