Mysql InnoDB多版本并发控制MVCC

时间:2022-11-13 16:07:48
参考书籍《mysql是怎样运行的》

系列文章目录和关于我

一丶为什么需要事务隔离级别

mysql是一个客户端/服务断软件,对于同一个服务器来说,可以有多个客户端进行连接,每一个客户端进行连接之后就形成一个会话,每一个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某一个事务的一部分,服务器可以同时处理多个事务。

如果事务时一个接着一个进行,那么下一个事务是在上一个事务的一致性前提下进行的,就没用一致性的问题,但是事务是并发进行且可能访问到相同的数据这时候就会出现如下问题

Mysql InnoDB多版本并发控制MVCC

可以看到AB最开始总和13元,最后AB总和18元,银行血亏五元,这显然违背了一致性——钱的总量不变。这就是并发情况下两个事务的影响,所以需要事务隔离让事务隔离的进行,互不干涉。

1.实现事务隔离的方式:串行执行

最简单直接的方式,同一时间只能有一个事务运行,这样必然不会有上述不一致的情况,但是大大降低了吞吐率并增加了事务的等待时间

2.实现事务隔离的方式:可串行执行

并发事务之所以出现不一致的情况,就是由于多个事务访问相同的数据,需要实现多个事务在访问相同数据的时候进行限制,比方说上图中事务2想访问A账户的值需要等待事务提交事务之后,这样可以让并发事务的执行如同串行执行的效果一样。

二丶并发事务执行的问题:脏写,脏读,不可重复读,幻读

1.脏写

一个事务修改了另外一个未提交事务修改过的数据

  • 脏写导致一致性无法保证

    Mysql InnoDB多版本并发控制MVCC

    上图事务A和事务B都更新紫色数据,其中事务A首先更新为A,然后事务B过来更新为B,这时候事务A回滚后更新为Null,事务 B 明明正常写了一行数据,但是写完之后发现值变了,有点丢失更新的意思。(比如A表示余额,这时候在将余额A判断是否足以支付,判断得到可以,事务B执行扣费写入A-5,商家收到5元,结果这时候回滚了,A变成Null,事务A中转钱的一方钱变为A,钱的总额变为A+5了)

  • 脏写导致原子性受到破坏

    假如上述的事务B还操作了另外的数据,比如插入一条数据C,并且更改为B写入C是在一个事务下面的,需要具备原子性,但是脏写让B的更改需要部分回滚为Null,这样插入C和更改B就不具备原子性(比如A表示余额,这时候在将余额A判断是否足以支付,判断得到可以,事务B执行扣费写入A-5,商家收到5元,结果这时候回滚了,A变成Null,这时候部分回滚,商家的5元没用回滚,商家的库存也没用回滚,原子性被破坏)

2.脏读

如果一个事务读取到另外一个事务未提交的数据,意味着发生了脏读

Mysql InnoDB多版本并发控制MVCC

比如事务A先写数据A,然后事务B督导数据A后在内存中使用A进行一系列操作(比如A表示余额,这时候在将余额A判断是否足以支付,判断得到可以)但是事务A这时候回滚了,事务B再次读取数据发现为null,这就是脏读。

脏读可能引发一致性的问题:比如事务操作时修改x和y的值,并且二者总是相等的,A修改x为1,还没来得及修改y也没用提交事务,这时候事务B读取x=1,y=0,二者不等,事务B读取到了数据库不一致的状态,读取到未提交事务的值

3.不可重复读

假如一个事务修改了另外一个事务未提交的数据,意味发生了不可重复读

Mysql InnoDB多版本并发控制MVCC

比如事务A第一次读取到值为A,接着事务B修改为B,并且提交了事务B,然后事务A再次读取得到的数据是B,同一行数据多次读取值并不相同,这称作不可重复读。它是指在同一个事务里面查询同一行数据,每次查到的数据都不一样。和脏读区别在于脏读是由于别的事务回滚导致,而不可重复读读到的其实是已经提交的数据。

事务A读到事务B提交后的数据似乎很合理,但是我们想象这样一种场景:你有一个流水表和用户余额,其中记录用户每天的流水,你在月初0点的时候核对流水和库存,但是流水很多,你的程序选择一个一个用户的进行核对,核对用户甲,甲没做任何消费,但是当你核对B的时候,你将B的流水load到内存中,但是B这时候(0点30分,这一笔数据新的一个余额)进行了扣除余额的操作,导致B余额和流水对不上了。

4.幻读

如果一个事务A先根据没用搜索条件查询到一些记录,在该事务未提交前,另外一个事务写入(delete,update,insert)了符合搜索条件的记录,这时候事务A再次读取,发现数据条数和第一次读取的不同,如同出现了幻觉,称之为幻读

Mysql InnoDB多版本并发控制MVCC

事务A读到事务B提交后的数据似乎很合理,但是我们想象这样一种场景:你有一个需求将会公司的男性员工了女性员工查询进行展示,你先查询了总数为100人,然后查询男性的总数50人,后查询女性人数准备在页面展示共100人,其中男50人,女50人,结果这是管理信息的人发现有一位员工性别错误录入了,将其从男修改为女,这时候你读取事务就是女51人了,你在主页显示了共100人,其中男50人,女51人

三丶隔离级别

1.Read UnCommitted 读未提交

在此隔离级别下,会发生脏读,不可重复读,和幻读

2.Read Committed 读已提交

在此隔离级别下,会发生不可重复读,和幻读

3.Repeatable Read 可重复读

在此隔离级别下,可能发生幻读

4.Serializable 可串行化

在此隔离级别下,不会发生脏读,不可重复读,和幻读

其中脏写是对一致性影响最严重的,无论是何种隔离级别,都不允许脏写发生,innodb使用锁保证不会出现脏写现象,第一个事务更新某条记录的时候,会给这条记录加锁,另外一个事务在此更新的时候,需要等待第一个事务提交释放锁后更新。隔离级别越高,其并发能力越低。

四丶Mysql设置隔离级别

默认隔离级别可重复读

1.设置全局隔离级别

SET GLOBAL TRANSACTION ISOLATION LEVEL 期望的隔离级别(可选READ UNCOMMITED,READ COMMITED,REPEATABLE READ,SERIALIZABLE),此命令只对执行语句后新产生的会话有效,对当前已经存在的会话无效

2.设置会话隔离级别

SET SESSION TRANSACTION ISOLATION LEVEL 期望的隔离级别(可选READ UNCOMMITED,READ COMMITED,REPEATABLE READ,SERIALIZABLE),对当前会话后续事务有效,该语句可以在已开启的事务中执行,但是不会影响当前正在执行的事务,如果在事务之间执行,只会对后续的事务有效

3.设置下一个事务的隔离级别

SET TRANSACTION ISOLATION LEVEL 期望的隔离级别(可选READ UNCOMMITED,READ COMMITED,REPEATABLE READ,SERIALIZABLE) 只对当前会话的下一个即将开启的事务有效,下一个事务执行完后,后续事务将恢复到之前的隔离级别,该语句不能再已经开启的事务中执行,否则会报错。

4.指定服务器的隔离级别

在启动的时候使用--transaction-isolation=xxx即可执行默认隔离级别

五丶MVCC原理

下面讨论记录对当前事务是否可见都是基于当前事务中执行的查询是快照读(普通查询),对于当前读(select xxx for update,select xxx lock in share mode)是不通用的

1.版本链

对于InnoDB存储引擎来说,其聚簇索引记录中包含两个隐藏列:

  • trx_id:一个事务每次对聚簇索引记录做出改动的时候,都会把该事务的事务id复制给此列
  • roll_point:每次对某条聚簇索引记录进行改动的时,都会把旧的版本写入到undo 日志中,此列相当于一个指针,指向修改前的信息

Mysql InnoDB多版本并发控制MVCC

每次修改都会形成Undo 日志,所有版本的数据会通过roll_point串联成一个链表,称之为版本链,头节点是当前记录的最新值。利用版本链控制多个并发事务访问相同记录时的行为称为MVCC多版本并发控制。

其实在undo日志中,只记录被更新列的信息,而不是记录全部的信息,对于没有记录的列,会通过版本链找少一个版本中的对应列的信息,直到找到聚簇索引叶子节点中的内容

2.Read View

对于使用Read Uncommitted隔离级别的事务,可以读取到没提交的数据,那么直接读取最新的版本即可。对于Serializable隔离级别,innodb直接通过加锁来访问记录。对于read committed 和 repeatable read隔离级别的事务,都必须保证督导的数据是已经提交事务修改过的记录,那么如何判断版本链中的哪个版本的数据是当前事务可见的昵?

innodb 使用的Read View

2.1 read view 的结构

Mysql InnoDB多版本并发控制MVCC

  • m_ids:在生成read view时,当前系统中活跃的读写事务id列表
  • min_trx_id:生成read view时,当前系统中活跃的读写事务中最西澳的事务id,也就是m_ids中的最小值
  • max_trx_id:生成read view时,系统应该分配给下一个事务的事务id值
  • creator_trx_id:生成该read view的事务的事务id

2.2 read view 判断某个版本当前事务释放可见的步骤

  1. 如果被访问版本的trx_idcreator_trx_id相同,意味着当前事务在访问自己修改的记录,自然可见
  2. 如果访问版本的trx_id属性值小于read view中的min_trx_id 表明此版本是生成read view之前已经提交的事务,那么自然可见
  3. 如果访问版本的trx_id,大于等于read view中的max_trx_id说明,当前版本数据是生成read view后开启事务产生的,那么自然不可见
  4. 如果访问版本的trx_id 介于min_trx_idmax_trx_id之间,需要判断trx_id是否位于m_ids列表中,如果在说明创建read view时生成该版本的事务还是活跃的,那么该版本,不可被访问,如果不在说明创建read view 时生成该版本的事务已经提交,可以被访问到

如果某个版本数据对当前事务不可见那么需要一直顺着版本链找上一个版本的数据,并通过上述步骤判断是否可见,直到找到可见的版本,如果一直找不到说明该条记录对当前事务不可见,查询结果将不包含该记录。

2.3 Read Committed和 Repeatable Read的不同

  • Read Committed——每次读取数据前都生成一个Read View

    这样可以保证生成Read view 中的m_ids是实时活跃事务id集合,也许第一次读取的时候事务A没提交,其id位于m_ids中,但是第二次读取的时候事务A提交了,事务A将不位于m_ids中,这样在第二次读取的时候,通过m_ids判断事务A是否提交的时候,可以得到事务A已经提交了,然后让事务A版本产生的数据可见(见2.2.4中的内容)。

  • Repeatable Read——如果使用begin开启事务那么在第一次查询的时候生成Read view,如果使用start transaction with consistent snapshot 那么执行的时候就会生成read view

    这样可以保证当前事务从头到尾都是read view中记录的内容是一致的,第一次读取的时候事务A没有提交,那么不可见,但是第二次读取的时候事务A提交了,但是read view的m_idsmax_trx_id可以判断事务A不可见,比如事务A事务id小于max_trx_id意味着生成read view是事务A启动但是没提交,即使第二次读事务A提交了,但是m_ids中还是包含事务A,那么不可见。如果事务A事务id大于max_trx_id,那么自然第二次还是大于max_trx_id,也是不可见的,从而实现了可重复读。

2.4 二级索引与MVCC

上面我们提到,innodb聚簇索引组织的记录才具备trx_idroll_point,那么我们使用二级索引进行查询的时候,如何判断数据是否可见昵?

  1. 二级索引页面的page header中存在page_max_trx_id属性,每当有事务对其中的记录进行增删改查操作的时候,如果事务的事务id,大于page_max_trx_id,那么会更新page_max_trx_id属性值为其事务id,这意味着page_max_trx_id记录了修改该二级索引页面最大的事务id是多少。当select通过二级索引首先看下对于read view的min_trx_id是否大于该页面的page_max_trx_id,如果大于那么页面中所有记录都对该read view可见,否则就进行下面的第二步
  2. 利用二级索引中的主键值,进行回标,得到对应的聚簇索引记录然后进行回表,然后通过2.2中步骤拿到第一个可见版本的数据,然后比对此纪录和通过二级索引查询得到记录的值是否相同,如果相同那么发送给客户端,否则跳过该记录。