MVCC 机制(Multiversion Concurrency Control)
1、背景
InnoDB 相比 MyISAM 有两大特点,一是支持事务而是支持行级锁,事务的引入带来了一些新的挑战。相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率
,提高数据库系统的事务吞吐量,从而可以支持可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况:
- 1、
更新丢失(Lost Update)
:当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 ——最后的更新覆盖了其他事务所做的更新。如何避免这个问题呢,最好在一个事务对数据进行更改但还未提交时,其他事务不能访问修改同一个数据。 - 2、
脏读(Dirty Reads)
:一个事务正在对一条记录做修改,在这个事务并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些尚未提交的脏数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。 - 3、
不可重复读(Non-Repeatable Reads)
:在事务A中先后两次读取同一个数据,但是两次读取的结果不一样。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。 - 4、
幻读(Phantom Reads)
:在事务A中按照某个条件先后两次查询数据库,两次查询结果的行数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。
以上是并发事务过程中
会存在的问题,解决更新丢失可以交给应用,但是后三者需要数据库提供事务间的隔离机制
来解决。实现隔离机制的方法主要有两种:
- 加读写锁
- 一致性快照读,即 MVCC
2、定义和特征
定义
MVCC (Multiversion Concurrency Control)
叫多版本并发控制
,是在并发访问数据库时,通过对数据做多版本管理,避免因为写锁的阻塞而造成读数据的并发阻塞问题。(MVCC 是无锁操作的一种实现方式
)
通俗的讲就是MVCC通过保存数据的历史版本,根据比较版本号来处理数据的是否显示,从而达到读取数据的时候不需要加锁就可以保证事务隔离性的效果
特征
- 1、MySQL 中 InnoDB 引擎支持 MVCC
- 2、应对高并发事务, MVCC 比单纯的加行锁更有效, 开销更小
- 3、MVCC 在读已提交(Read Committed)和可重复读(Repeatable Read)隔离级别下起作用
- 4、MVCC 既可以基于乐观锁又可以基于悲观锁来实现
3、实现原理
MVCC实现的核心知识点
- 1、事务版本号
- 每次事务开启前都会从数据库获得一个
自增长的事务ID
,可以从事务ID判断事务的执行先后顺序。
- 每次事务开启前都会从数据库获得一个
- 2、表的隐藏列
- 3、undo log (回滚日志)
- 4、 read view(可读视图)
3.1、表格的隐藏列
-
1、
DB_TRX_ID
: 记录操作该数据事务的事务ID,大小为 6 个字节; -
2、
DB_ROLL_PTR
:指向上一个版本
数据在undo log
里的位置指针
,大小为 7 个字节;(在下一节具体说明如何组织 undo Log 链) -
3、
DB_ROW_ID
:行标识(隐藏单调自增 ID),大小为 6 字节,如果表没有主键,InnoDB 会自动生成一个隐藏主键,因此会出现这个列。每条记录的头信息(record header)里都有一个专门的bit(deleted_flag)来表示当前记录是否已经被删除。
3.2、undo log(回滚日志)
- 1、undo log:实现原子性的关键,是当事务回滚时能够
撤销所有已经成功执行的sql语句
。- 当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
- 2、undo log:属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo
log的内容做与之前相反的工作:- 对于每个insert,回滚时会执行delete;
- 对于每个delete,回滚时会执行insert;
- 对于每个update,回滚时会执行一个相反的update,把数据改回去。
以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
实例:
3.2.1、如何组织 undo log 链表
模拟一次数据修改的过程来了解下事务版本号、表格隐藏的列和undo log他们之间的使用关系
。
(1)首先准备一张原始原始数据表
ID | Name | DB_TRX_ID | DB_ROLL_PTR |
---|---|---|---|
1 | zhnagsan | 103 | 0xxxxxxx |
(2)开启一个事务A: 对user_info
表执行 update user_info set name =“李四”where id=1
会进行如下流程操作
start transaction;
update user_info set name =“lisi” where id=1
commit;
- 1、对
DB_ROW_ID = 103
的这行记录加排他锁
- 2、把
user_info
表修改前的数据
拷贝到undo log
,DB_TRX_ID 和 DB_ROLL_PTR
都不动 - 3、修改
user_info表 id=1
的数据,这时产生一个新版本,更新 DATA_TRX_ID
为修改记录的事务 ID(104)
,将DATA_ROLL_PTR
指向刚刚拷贝到undo log
链中的旧版本记录,这样就能通过DB_ROLL_PTR
找到这条记录的历史版本。- 如果对同一行记录执行连续的
UPDATE
,Undo Log
会组成一个链表,遍历这个链表可以看到这条记录的变迁
- 如果对同一行记录执行连续的
- 4、记录
redo log
,包括undo log
中的修改
那么 INSERT 和 DELETE 会怎么做呢?
INSERT
会产生一条新纪录,它的DATA_TRX_ID
为当前插入记录的事务 ID
;DELETE
某条记录时可看成是一种特殊的UPDATE
,其实是软删,真正执行删除操作会在commit
时,DATA_TRX_ID
则记录下删除该记录的事务 ID
。
3.3、read view(可读视图)
3.3.1、如何实现一致性读 —— ReadView
- 在
RU 隔离级别(读未提交)
下,直接读取版本的最新记录就 OK,对于SERIALIZABLE
隔离级别,则是通过加锁互斥来访问数据,因此不需要MVCC 的帮助。 - MVCC 运行在
RC(读已提交)
和RR(可重复读)
这两个隔离级别下,当 InnoDB 隔离级别设置为二者其一时,在 SELECT 数据时就会用到版本链
核心问题是版本链中哪些版本对当前事务可见?
InnoDB 为了解决这个问题,设计了 readview(可读视图)的概念。在InnoDB中每个SQL语句执行前都会得到一个readview。副本主要保存了当前数据库系统中正处于活跃(没有commit)的事务的ID号,其实简单的说这个副本中保存的是系统中当前不应该被本事务看到的其他事务id列表。
3.3.2、Read view 的重要参数
- 1、
trx_ids
: 当前系统活跃(未提交)事务版本号集合。 - 2、
low_limit_id
: 创建当前read view 时“当前系统最大事务版本号+1
”。 - 3、
up_limit_id
: 创建当前read view 时“系统正处于活跃事务最小版本号
” - 4、
creator_trx_id
: 创建当前
read view的事务版本号;
3.3.3、Read view 匹配条件(事务可见范围)
-
(1)数据事务ID <up_limit_id 则显示
如果数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。 -
(2)数据事务ID>=low_limit_id 则不显示
如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不予显示。 -
(3) up_limit_id <数据事务ID<low_limit_id 则与活跃事务集合trx_ids里匹配
如果数据的事务ID大于最小的活跃事务ID,同时又小于等于系统最大的事务ID,这种情况就说明这个数据有可能是在当前事务开始的时候还没有提交的。所以这时候我们需要把数据的事务ID与当前read view 中的活跃事务集合trx_ids 匹配:- 情况1: 如果事务ID不存在于trx_ids 集合(则说明read view产生的时候事务已经commit了),这种情况数据则可以显示。
- 情况2: 如果事务ID存在trx_ids则说明read view产生的时候数据还没有提交,但是如果数据的事务ID等于creator_trx_id,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。
- 情况3: 如果事务ID既存在trx_ids而且又不等于creator_trx_id那就说明read view产生的时候数据还没有提交,又不是自己生成的,所以这种情况下此数据不能显示。
-
(4)不满足read view条件时候,从undo log里面获取数据
当数据的事务ID不满足read view条件时候,从undo log里面获取数据的历史版本,然后数据历史版本事务号回头再来和read view 条件匹配 ,直到找到一条满足条件的历史数据,或者找不到则返回空结果;
3.4、模拟MVCC实现流程
下面我们通过开启两个同时进行的事务来模拟MVCC的工作流程。
(1)创建user_info表,插入一条初始化数据
(2)事务A和事务B同时对user_info进行修改和查询操作
事务A:update user_info set name =”李四”
事务B:select * fom user_info where id=1
问题: 先开启事务A ,在事务A修改数据后但未进行commit,此时执行事B。最后返回结果如何。
执行流程如下图:
执行流程说明:
-
(1)事务A:开启事务,首先得到一个事务编号102;
-
(2)事务B:开启事务,得到事务编号103;
-
(3)事务A:进行修改操作,首先把原数据拷贝到undolog,然后对数据进行修改,标记事务编号和上一个数据版本在undo log的地址。
-
(4)事务B: 此时事务B获得一个read view ,read view对应的值如下
-
(5)事务B: 执行查询语句,此时得到的是事务A修改后的数据
-
(6)事务B: 把数据与read view进行匹配,
- 1、数据事务ID为102 大于up_limit_id
- 2、数据事务ID为102 小于low_limit_id
- 3、数据事务ID为102存在于 trx_ids,
- 4、数据事务ID为102不等于creator_trx_id
发现不满足read view显示条件,所以从undo lo获取历史版本的数据再和read view进行匹配,最后返回数据如下。
3.5、各种事务隔离级别下的Read view 工作方式
3.5.1、READ_UNCOMMITTED(读未提交)
- 该隔离级别不会使用 MVCC。它只要执行 select,那么就会获取 B+ 树上最新的记录。而不管该记录的事务是否已经提交。
- 不会获取read view 副本。
3.5.2、READ COMMITTED(读已提交)
在 READ COMMITTED 隔离级别下,会使用 MVCC。在开启一个读取事务之后,它会在每一个 select 操作
之前都生成一个 Read View
。
- 这样就可能造成同一个事务里前后读取数据可能不一致的问题(重复读)
具体更详细说明:请参考
3.5.3、REPEATABLE READ(可重复读)
REPEATABLE READ 与 READ COMMITTED 的区别只有在生成 Read View 的时机
上。
- READ COMMITTED 是在每次执行 select 操作时,都会生成一个新的 Read View。
- REPEATABLE READ
只会在第一次执行 select 操作时生成一个 Read View
,直到该事务提交之前,所有的select 操作都是使用第一次生成的 Read View。
RR(重复读)级别下的一个事务里只会获取一次read view副本,从而保证每次查询的数据都是一样的。
具体更详细说明:请参考
3.5.4、SERIALIZABLE(串行化读)
该隔离级别不会使用 MVCC
。如果使用的是普通的 select 语句,它会在该语句后面加上 lock in share mode,变为一致性锁定读
。假设一个事务读取一条记录,其他事务对该记录的更改都会被阻塞
。
- 在该隔离级别下,
读写操作变为了串行操作
。
参考
1、https://zhuanlan.zhihu.com/p/52977862
2、https://zhuanlan.zhihu.com/p/64576887
3、https://zhuanlan.zhihu.com/p/149640067
4、https://baijiahao.baidu.com/s?id=1629409989970483292&wfr=spider&for=pc
5、https://zhuanlan.zhihu.com/p/115912936