你一定要知道的MySQL之MVCC多版本并发控制

2,169 阅读11分钟

提到MVCC,那么首先还是要说一下什么是事务和事务隔离级别

MySQL事务

在关系型数据库中,一个逻辑工作单元要成为事务,必须满足四个特性,ACID,即原子性(Atomicity),一致性(Consistency),隔离性(Isolation)和持久性(Durability)

原子性

事务是一个原子操作单元,其对数据的修改,要么全部提交,要么全部回滚。

一致性

指的是事务开始之前和事务结束之后,数据库的完整性限制未被破坏。一致性包括两个方面的内容,分别是约束一致性和数据一致性。

  • 约束一致性:创建表结构时所指定的外键,Check,唯一索引等约束,在MySQL中不支持Check
  • 数据一致性:是一个综合性的规定,因为它是由原子性,持久性,隔离性共同保证的结果,而不是单单依赖于某一种技术。

隔离性

指的是事务之间相互不能干扰,即一个事务内部的操作以及使用的数据对其他并发事务是隔离的。

持久性

指的是一个事务一但提交了,它对数据库中的数据的改变就应该是永久性的,后续的操作和故障不应该对其有任何影响,不会丢失。

并发事务

当事务进行并发处理的时候,就可能会带来一些问题,比如:更新丢失,脏读,不可重复读,幻读等。

  • 更新丢失:当两个或多个事务更新同一行记录,会产生更新丢失现象。可以分为回滚覆盖和提交覆盖。
    • 回滚覆盖:一个事务回滚操作,把其他事务已提交的数据给覆盖了
    • 提交覆盖:一个事务提交操作,把其他事务已提交的数据给覆盖了
  • 脏读:一个事务读取到了另一个事务修改但未提交的数据。
  • 不可重复读:一个事务中多次读取同一行记录不一致,后面读取的跟前面读取的不一致
  • 幻读:一个事务中多次相同条件查询,结果不一致。后续查询的结果和前面查询结果不同,多了或少了几行记录

不可重复读总的来说就是一个事务中多次数据到的记录内容不同,幻读总的来说就是就是一个事务中多次查询总数不同。

事务隔离级别

前面提到的"更新丢失","脏读","幻读"等并发事务问题,其实都是数据库一致性问题,为了解决这些问题,MySQL数据库是通过事务隔离级别来解决的,数据库系统提供了以下4种事务隔离级别供用户选择。

  • 读未提交:解决了回滚覆盖类型的更新丢失,但可能会发生脏读现象,也就是可能读取到其他会话中未提交事务修改的数据。
  • 读已提交:只能读取到其他会话中已提交的数据,解决了脏读。但可能发生不可重复读现象,也就是可能在一个事务中两次查询结果不一致。
  • 可重复读:解决了不可重复读,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上会出现幻读,简单的说幻读指的是当用户读取某一范围的数据行时,另一个事务又在该范围插入了新行,当用户读取该范围的数据时会发现新的幻影行。
  • 串行化:所有的增删改查串行执行,它通过强制事务排序,解决相互冲突,从而解决幻读的问题,这个级别可能导致大量的超时现象和锁竞争,效率低下。

数据库的事务隔离级别越高,并发问题就越小,但是并发处理能力越差(代价)。读未提交隔离级别最低,并发问题多,但是并发处理能力好。以后使用时,可以根据系统特点来选择一个合适的隔离级别,比如对不可重复读和幻读并不敏感,更多关心数据库并发处理能力,此时可以使用Read Commited隔离级别。

下面用一幅图让大家理解一下上面说到的隔离级别。假设数据表T中只有一列,其中一行的值是1,下面是按照时间顺序执行两个事物的行为 我们来看看在不同的隔离级别下,事物A会有哪些不同的返回结果,也就是图里面V1,V2,V3的返回值分别是什么。

若隔离级别是"读未提交",则V1的值就是2。这时候事物B虽然还没有提交,但是结果已经被A看到了。因此V2,V3也都是2。

若隔离级别是"读已提交",则V1是1,V2是的值是2。事物B的更新在提交后才能被A看到,所以,V3的值也是2

若隔离级别是"可重复读",则V1,V2是1,V3是2。之所有V2还是1,遵循的就是这个要求:事物在执行期间看到的数据前后必须是一致的。

若隔离级别是"串行化",则在事务B执行"将1改成2"的时候,会被锁住。直到事务A提交后,事务B才可以继续执行。所以从A的角度看,V1,V2值是1,V3的值是2。

在MySQL中,默认的事务隔离级别是可重复读,为什么不是串行化呢?上面说到了,串行化就当相当于加了一把锁,变成同步串行执行了,效率会低的可怜,所以,MySQL中默认的事务隔离级别是可重复读

select @@TRANSACTION_ISOLATION用这个命令查看事务的隔离级别,结果

注意:事务隔离级别,针对InnoDB引擎,支持事务功能,MyISAM引擎没有事务。

Redo和Undo日志

在讲MVCC之前,先讲一下Redo和Undo日志

Undo Log

Undo顾名思义,撤销,取消的意思,所以这里的Undo Log其实就是为了事务的回滚。

数据库事务开始之前,会将要修改的记录存放到Undo日志中,当事务回滚时或者数据库崩溃时,可以利用Undo日志,撤销未提交事务对数据库产生影响,Undo日志在事务提交时,并不会立即删除,innodb会将该事务对应的undo log放入到删除列表中,后面会通过后台线程purge thread进行回收处理。Undo log属于逻辑日志,记录一个变化过程,例如,执行一个delete,undo log会记录一个insert,执行一个update,undo log会记录一个反的update。

Undo log存储:undo log采用段的方式管理和记录。在innodb数据文件中包含一种rollback segment回滚段,内部包含1024个undo log segment。可以通过下面一组参数来控制undo log存储。

show variables like '%innodb_undo%';

Redo Log

Redo国名思议就是重做。以恢复操作为目的,在数据库发生意外时重现操作。

Redo log指事务中修改任何数据,将最新的数据备份存储的位置(Redo log),被称为重做日志。

Redo log随着事务操作的执行,就会生成Redo log。在事务提交时会将产生Redo log写入Log buffer,并不是随着事务的提交就立刻写入磁盘文件。等事务操作的脏页写入到磁盘之后,Redo log的使命也完成了,Redo log占用的空间就可以重用(被覆盖写入)。

MVCC多版本并发控制

什么是MVCC?

MVCC-多版本并发控制(Multi-Version Concurrency Control)主要是MySQL当中InnoDB存储引擎用来实现隔离级别的一种具体实现,用于实现读已提交和可重复读这两种隔离级别。在代码中,读已提交和可重复读是两个接口,MVCC就是具体实现。

MVCC对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁来实现的。

undo日志版本链与read view机制详情

undo日志版本链是指一行数据被多个事务依次修改过后,在事务修改完后,MySQL会保留修改之前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链 trx_id是事务id,roll_pointer是用来指向上一个事务地址的回滚指针,简单来说就是,原本数据库里面有一条信息,这时候有个事务来修改这行了,事务id是300,那么undo就会新增一条行,记录id,name和事务id300还有就是roll_pointer用来记录上一个事务记录的位置

在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已提交的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果

版本链比对规则:

  1. 如果row的trx_id落在绿色部分(trx_id<min_id),表示这个版本是已提交的事务生成的,这个数据是可见的。
  2. 如果row的trx_id落在红色部分(trx_id>min_id),表示这个版本是由将来启动的事务生成的,是不可见的(若row的trx_id就是当前自己的事务是可见的)
  3. 如果row的trx_id落在黄色部分(min_id<=trx_id<=max_id),那就包括两种情况
    1. 若row的trx_id在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若row的trx_id就是当前自己的事务是可见的)
    2. 若row的trx_id不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。

举例说明一下: 这里我们有5个事务 看上面的图,首先几个事务同时begin,事务1进行update,紧接着,事务2和3也进行update操作,之后事务3进行commit事务,紧接着事务4进行select查询,首先按照上面说的,会生成一个当前事务的一致性视图[100,200], 300 假设生成是这样的视图,括号里面的是所有未提交事务id数组,和一个已经提交的最大事务id。这里事务4第一次select,查询结果,相信大家应该已经猜到了,没错就是lilei300,这里首先判断数组里面的事务编号是100的,跟trx_id进行比较发现他其实是在上图中未提交与已提交事务中,这时候发现,trx_id在视图数组中,是不可见的,然后200进行比较,还是和100一样,然后用300进行比较,发现300的事务id也在未提交与已提交事务中,但是不在视图数组中,表示这个班班是已经提交的事务生成的,所以最后返回事务id为300的name,lilei300,由于生成的视图是不变的,在可重复读隔离级别下,所以第二次select和第三次select的结果都是一样的。但是最后一个事务,是在第一个事务commit之后查询所以,第一个视图查询结果就是lilei2

对于删除的情况可以认为是update的特殊情况,会将版本链最新的数据复制一份,然后将trx_id修改成删除操作trx_id,同时在该记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经删除,在查询时候按照上面的规则查到对应的记录,如果delete_flag标记位是true,意味着记录已被删除,则不返回数据。

注意:begin/start transaction命令并不是一个事务的起点,在执行到他们之后的第一个修改操作InnoDB表的语句,事务才真正启动,才会向MySQL申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。

总结:MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。