文章目录
前言
事务
我们知道,事务具有四大特性(ACID
):
- 原子性(
Atomicity
):令一个事务的所有操作不可分割,要么全部执行、要么全不执行。 - 隔离性(
Isolation
):事务并行处理时,不同的事务之间操作数据不相互影响。 - 一致性(
Consistency
):事务满足现实世界的约束,保持正确的状态。 - 持久性(
Durability
):事务一旦提交,对数据的修改就是永久性的,即使数据库出现故障也不会让修改丢失。
隔离性
本文我们深入探究下事务的隔离性,首先通过一个例子看看并行条件下不同事务是如何相互影响的:
案例:数据A值为5,创建两个事务,共同对数据A减1,在串行处理条件下两个事务提交后A的结果应该为3。
对A减一需要分三步操作:读取变量A,将变量A减一,将变量A写回主存,两个事务并行处理可能的结果如下表所示:
由上表可知最终得到的结果是A=4,显然这是由于事务之间读取变量时相互影响造成的,所以为了保证并行处理的一致性要求,我们应该让事务之间按照顺序一个一个单独执行,或者最终执行的效果和单独执行一样,最终结果就是事务之间没有影响,看起来像是被互相隔离了一样,这也就是事务的隔离性。
一、如何保证事务的隔离性?
我们能想到的最简单的一种方式就是加锁,即在当前事务执行的过程中,其他事务阻塞,排队等待当前事务执行完毕提交,数据每次只能被一个事务所读写。这种多个事务的执行方式称之为可串行化执行。
通过可串行化执行,我们可以确保事务的一致性是一定被保证了的,无论是读-写、写-读、还是写-写情况,事务之间都是完全隔离,没有任何影响的。
但是这种执行方式对性能影响太大,尤其是在多核CPU条件下,硬件的优势丝毫没有得到发挥。那么我们能不能微微减弱可串行化执行的隔离性呢?也就是说,可以让事务之间没有那么彻底地隔离,做到兼顾性能与安全。
在研究之前,我们首先应该详细地先了解事务之间是怎么互相影响的,都有哪些影响方式影响了事务的一致性。
二、事务之间相互影响的探究
假设存在两个事务在并发条件且没有任何隔离措施情况下执行任务,两个线程分别为事务A,事务B。事务A的功能是修改数据,事务B的功能是读取数据。
1.脏写
现象:
事务B写了一个数据,过一会发现没写上。
本质原因:
事务A修改了某一数据的值,随后事务B也修改了这个数据并提交了,但是A还没有提交,所以A随时都有回滚的可能,假设A在某一时刻回滚,任务回到了最初的起点(即A修改前的状态),所以就导致了已经提交了的事务B没写上该数据。
2.脏读
现象:
事务B查询到了一个数据,过一会发现读了个“假数据”。
本质:
事务A修改了某一数据的值,随后事务B查询了这个数据并提交了,但是A还没有提交,所以A随时都有回滚的可能,假设A在某一时刻回滚,任务回到了最初的起点(即A修改前的状态),所以就导致了已经提交了的事务B查询到的那个数据是一个根本不存在的值。
在这里小小总结下:
脏读和脏写都是因为其中一个事务A先把数据的值修改了,但没有立刻提交,而另一个事务B在这个空档期读或写了该数据,并在事务A提交之前提交,如果这时事务A发生回滚,那就造成了对应的脏读或脏写。
3.不可重复读
这里将事务B的功能微微改变下:多次查询某一数据。
现象:
事务B多次查询的结果不一致。
本质:
事务B开启了事务查询到某一数据的值并且没有立刻提交事务,随后事务A将该值修改并马上提交,事务B再次查询该值发现两次查询的结果不一致。
可以发现不可重复读相对于脏读和脏写来说影响没有那么大。
4.幻读
现象:
事务B按照搜索条件数次查询,每次查询结果不一致。
本质:
事务B开启了事务,并根据搜索条件查询到了一批数据,且没有立刻提交事务,随后事务A将在这个搜索条件下写入了一批数据,并马上提交,事务B再次查询该搜索条件下对应的数据发现变多了。
本质同不可重复读是类似的,区分细节在于不可重复读针对的是值的不同,幻读指的是数据条数的不同
三、如何平衡安全与性能?
上文我们列举了所有可能出现读写安全的情况,并且我们之前提到尽量避免可串行化执行的方式来保证一致性,因为那样会严重影响性能,所以我们根据上文列举的四种一致性问题,逐一分析,看看有没有什么不加锁的方式来保证一致性。
不同事务对数据的操作方式分为读-读,读-写,写-写,显然在并行条件下,读-读是不会发生任何一致性问题的,只有多事务间存在写事务时才可能引起一致性问题,而写-写毫无疑问如果不加控制的话一定会发生一致性问题,因此我们把优化的重点放到读-写方式上,即脏读、不可重复读、幻读等现象。
1.脏读
我们之前提到脏读的本质在于事务B读取了事务A修改的值并且A发生了回滚,导致事务B读了一个根本不存在的值,那么我们可不可以读取事务A修改之前的值?这样即使A回滚,B读到的值依然是有效的。
读取当前所有正在执行修改数据的事务修改之前的那条数据,可以避免脏读。
2.不可重复读
同理,不可重复读的本质在于事务B重复读取了事务A修改之前和修改之后的数据,导致事务B数次查询结果不一致,那么我们仍然可以让事务B数次读取事务A修改之前的值。
读取当前所有正在执行修改数据的事务修改之前的那条数据,可以避免可重复读。
3.幻读
幻读的一致性问题的严重性最低,只有通过加锁的方式才能保证不幻读。
一致性问题的严重性:脏写 >脏读>可重复读>幻读
四、InnoDB是如何平衡安全与性能的?
1. 隔离级别
InnoDB设置了四种隔离级别,具体的功能如下图所示:
2. 版本链
在介绍版本链之前,先了解两个参数,每个聚簇索引记录中都包含这两个参数列
trx_id
:一个事务每次对某条聚簇索引改动时,都会把该事务的事务id赋值给trx_id列roll_pointer
:每次对某条聚簇索引记录进行改动时,都会把上一个版本的数据写入undo
日志中。(也可以理解为一个指针,指向上一个版本的数据)
有了这两个参数,我们就可以构建一个版本链,具体来说,版本链是一个链表,头节点是该数据当前值,链表通过roll_pointer
指针指向上一个版本的记录,并且每个节点都包含了执行该记录时的事务ID
。
举个例子:
开启一个事务,插入一条数据“西游记”,开启第二个事务,按照顺序依次修改该数据为“孙悟空”、“猪八戒”;开启第三个事务,按照顺序依次修改数据为“沙僧”、“白龙马”。那么对应的版本链如下图所示(省略亿点点细节):
3. 版本链有什么用?
版本链用来控制并发事务访问相同记录时的行为,这种机制也称之为MVCC
。
4. MVCC
由上文可知,我们对脏读、不可重复读、幻读一一列举了我们的想法,即如何通过不加锁的方式还能保证一致性,通过我们最后列举的结论我们可以总结得知核心思想就是控制查询得到的数据的版本。这也就是版本链在MVCC
机制起到大作用的原因。所以现在的核心问题是将版本链中的哪个版本数据分配给读事务?
ReadView
具体来说,InnoDB
通过ReadView
(一致性视图)的方式来确定版本链中的版本。一致性视图包含以下四个参数:
m_ids
:生成一致性视图时,当前系统还未提交的读写事务的事务id
。min_trx_id
:在生成一致性视图时,当前系统中还未提交的读写事务中最小的事务id
;也就是m_ids
的最小值。max_trx_id
:生成一致性视图时,系统应该分配给下一个事务的事务id
(由于事务id
的分配是递增的,所以这个数值就是m_ids
的最大值加1)。creator_trx_id
:生成该一致性视图的事务的事务id
(只有记录被改动才会分配事务id
,查询的事务id
永远为0)。
强调一下,只有进行数据修改的事务才会被分配事务
id
,且分配的id
值是每次自增1的。
有了如上参数后,InnoDB
又制定了一下的规则步骤来定位版本链中的版本,生成ReadView
后,只要将按下面的步骤来判断记录的哪个版本是我们想让查询语句查询到的(下面的trx_id
都代表版本链中的版本的trx_id
属性,其他的表示ReadView
的属性):
trx_id
==creator_trx_id
,表示当前事务在访问自己修改过的记录,所以可以访问trx_id
<min_trx_id
,说明该版本的事务在生成一致性视图之前就已经提交,所以可以访问trx_id
>=max_trx_id
,生成该版本的事务在当前事务生成一致性视图后才开启,不可访问min_trx_id < trx_id
<max_trx_id
,则需要判断trx_id
是否在m_ids
列表中,即判断该版本的事务是否提交,若未提交则不可被访问,若提交则可以被访问- 如果当前版本的数据对当前事务不可见,就顺着版本链找到前一个版本的数据,并继续上面的步骤,直到版本链的最后一个版本。
通过上述参数和规则,我们只要在查询事务前生成
ReadView
,就可以保证在查询前,定位到当前系统中未提交的事务们,这样可以保证在对比trx_id
时,定位到的可见数据记录对应的事务一定是已经提交过的,即不会发生脏读。
而保证可重复读只需要在第一次查询之前生成ReadView
即可,原理同脏读一样。
隔离级别对应的MVCC执行机制
脏读案例
还是以上文中西游记为案例,假设事务1插入完记录后就提交了,事务2和事务3在修改数据后并没有立即提交,此时一个读事务打算读取这条记录,首先它会定位在版本链的头节点中,按照上文步骤开始执行:
- 执行
SELECT
语句前生成ReadView
,m_ids
列表的范围是[2,3],min_trx_id
=2,max_trx_id
=4,creator_trx_id
=0(因为是读事务,所以不分配事务id
) - 读取链表头节点白龙马,
trx_id
=3,在m_ids
列表内,不符合可见性要求,根据roll_pointer
跳到下一个版本。 - 读取链表节点沙僧,
trx_id
=3,在m_ids
列表内,不符合可见性要求,根据roll_pointer
跳到下一个版本。 - 读取链表节点猪八戒,
trx_id
=2,在m_ids
列表内,不符合可见性要求,根据roll_pointer
跳到下一个版本。 - 读取链表节点孙悟空,
trx_id
=2,在m_ids
列表内,不符合可见性要求,根据roll_pointer
跳到下一个版本。 - 读取链表节点西游记,
trx_id
=1,小于min_trx_id
,符合可见性要求,所以返回个用户的版本就是西游记
总结
MVCC
机制在不适用锁的前提下根据不同的隔离级别设置不同的执行机制,通过版本链与记录的trx_id
属性按照规则步骤对比,筛选出当前隔离级别下查询事务的可见性版本,在保证一致性的前提下提高了性能。