引言
本文为社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!
经过《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
,然后其间就会有一个业务自动退出竞赛,开释持有的锁资源,这好像听起来蛮不错呀,但实际业务中,仅依靠超时机制去免除死锁是不行的,究竟高并发状况下,50s
时刻太长了,会导致越来越多的业务堵塞。
那么咱们能不能把这个参数调小一点呢?比方调到
1s
,能够吗?当然能够,确实也能保证死锁产生后,在很短的时刻内能够自动免除,但改掉了这个参数之后,也会影响正常业务等候锁的时刻,也便是大部分未产生死锁,但需求等候锁资源的业务,在等候1s
之后,就会立马报错并回来,这明显并不合理,究竟简略误伤“友军”。
也正是因为依靠锁超时机制,稍微有些不靠谱,因而InnoDB
也专门针关于死锁问题,研发了一种检测算法,名为wait-for graph
算法。
1.3.2、死锁检测算法 – wait-for graph
这种算法是专门用于检测死锁问题的,在该算法中会关于现在库中一切活泼的业务生成等候图,啥意思呢?以上述的死锁事例来看,在MySQL
内部会生成一张这样的等候图:
也便是T1
持有着「竹子」这条数据的锁,正在等候获取「熊猫」这条数据的锁,而T2
业务持有「熊猫」这条数据的锁,正在等候获取「竹子」这条数据的锁,终究T1、T2
两个业务之间就呈现了等候闭环,因而当MySQL
发现了这种等候闭环时,就会强制介入,回滚完毕其间一个业务,强制打破该闭环,然后免除死锁问题。
但这个“等候图”仅仅为了便利了解画出来的,内部的完成其实存在些许差异,一同来聊一聊。
wait-for graph
算法被启用后,会要求MySQL
收集两个信息:
- 锁的信息链表:现在持有每个锁的业务是谁。
- 业务等候链表:堵塞的业务要等候的锁是谁。
每当一个业务需求堵塞等候某个锁时,就会触发一次wait-for graph
算法,该算法会以当时业务作为起点,然后从「锁的信息链表」中找到对应中锁信息,再去依据锁的持有者(业务),在「业务等候链表」中进行查找,看看持有锁的业务是否在等候获取其他锁,假如是,则再去看看另一个持有锁的业务,是否在等候其他锁…..,经过一系列的判别后,再看看是否会呈现闭环,呈现的话则介入损坏。
上面这个算法的进程,听起来好像有些晕乎乎的,但实际上并不难,套个比方来了解,比如现在库中有
T1、T2、T3
三个业务、有X1、X2、X3
三个锁,业务与锁的联系如下:
此刻当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
的锁是依据啥完成的呢?想要搞清楚这点,得先弄理解锁的内存结构,先看图:
InnoDB
引擎中,每个锁方针在内存中的结构如上,其间记载的信息也比较多,先悉数理清楚后再聊聊锁的完成。
2.1.1、锁的业务信息
其间记载着当时的锁结构是由哪个业务生成的,记载的是指针,指向一个详细的业务。
2.1.2、索引的信息
这个是行锁的特有信息,关于行锁来说,需求记载一下加锁的行数据归于哪个索引、哪个节点,记载的也是指针。
2.1.3、锁粒度信息
这个稍微有些杂乱,关于不同粒度的锁,其间存储的信息也并不同,假如是表锁,其间就记载了一下是对哪张表加的锁,以及表的一些其他信息。
但假如锁粒度是行锁,其间记载的信息更多,有三个较为重要的:
-
Space ID
:加锁的行数据,地点的表空间ID
。 -
Page Number
:加锁的行数据,地点的页号。 -
n_bits
:运用的比特位,关于一页数据中,加了多少个锁(后续结合讲)。
2.1.4、锁类型信息
关于锁结构的类型,在内部完成了复用,选用一个32bit
的type_mode
来表明,这个32bit
的值能够拆为lock_mode、lock_type、rec_lock_type
三部分,如下:
-
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
,锁粒度、锁类型、锁状况是什么状况呢?如下:
从上图中可得知,现在这组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
等级的完成后,再来看看RC
,RC
等级要求处理脏读问题,也便是一个业务中,不答应读另一个业务还未提交的数据,咋完成呢?
写操作加排他锁,读操作加同享锁!
这样一想,好像好像没问题,仍是以之前的比方来说,因为T1
在修正数据,所以会对ID=1
的数据加上排他锁,此刻T2
想要获取同享锁读数据时,T1
的排他锁就会排挤T2
,因而T2
需求比及T1
业务完毕后才干读数据。
因为
T2
需求等候T1
完毕后才干读,已然T1
都完毕了,那也就代表着T1
业务要么回滚了,T2
读上一个业务提交的数据;要么T1
提交了,T2
读T1
提交的数据,总归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
机制、阻隔机制就完全分析完毕啦~