MySQL 一致性视图(MVCC)

2023-04-22,,

更多内容,前往IT-BLOG

MySQL中支持的四种隔离级别


提到事务,你肯定会想到 ACIDAtomicityConsistencyIsolationDurability,即原子性一致性隔离性持久性),来说说其中I,也就是“隔离性”。当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,所以下面我们来说说隔离级别。

SQL标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)、串行化(serializable)。
【1】读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到;
【2】读已提交指,一个事务提交之后,它做的变更才会被其他事务看到;
【3】可重复读指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据时一致的。当然可重复读隔离级别下,未提交变更对其他事务也是不可见的;
【4】串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

MySQL在 REPEATABLE READ隔离级别下,是可以禁止幻读问题的发生的。我们可以通过下面命令来设置隔离级别。

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL [REPEATABLE READ|READ COMMITTED|READ UNCOMMITTED|SERIALIZABLE];

MVCC原理


对于使用 InnoDB存储引擎的表来说,它的聚簇索引记录中都包含必要的隐藏列:【trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给 trx_id隐藏列】

ReadView

ReadView所解决的问题是使用 READ COMMITTEDREPEATABLE READ隔离级别的事务中,不能读到未提交的记录,这需要判断一下版本链中的哪个版本是当前事务可见的。ReadView中主要包含4个比较重要的内容:
【1】m_ids:表示在生成 ReadView时当前系统中活跃的读写事务的事务id列表。
【2】min_trx_id:表示在生成 ReadView时当前系统中活跃的读写事务中最小的事务id,也就是 m_ids中的最小值。
【3】max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的 id值。
【4】creator_trx_id:表示生成该 ReadView事务的事务id。

ReadView是如何工作的?


有了这些信息,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
【1】如果被访问版本的 trx_id属性值与 ReadView中的 creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
【2】如果被访问版本的 trx_id属性值小于 ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成 ReadView前已经提交,所以该版本可以被当前事务访问。
【3】如果被访问版本的 trx_id属性值大于 ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成 ReadView后才开启,所以该版本不可以被当前事务访问。
【4】如果被访问版本的 trx_id属性值在 ReadView的 min_trx_id和 max_trx_id之间,那就需要判断一下 trx_id属性值是不是在 m_ids列表中,如果在,说明创建 ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView时生成该版本的事务已经被提交,该版本可以被访问。

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

在 MySQL中,READ COMMITTEDREPEATABLE READ隔离级别的的一个非常大的区别就是它们生成 ReadView的时机不同。

我们这里使用一个示例来解释:

1 CREATE TABLE `t` (
2 `id` int(11) NOT NULL,
3 `k` int(11) DEFAULT NULL,
4 PRIMARY KEY (`id`)
5 ) ENGINE=InnoDB;
6 insert into t(id, k) values(1,1) ;
事务A 事务B
begin  
  begin
  update t set k= k+1 where id=1;
  commit;
update t set k = k+1 where id=1;  
select k from t where id =1;  
commit;  

在这个例子中,我们做如下假设:
【1】事务A、B的版本号分别是100、200,且当前系统里只有这3个事务;
【2】三个事务开始前,(1,1)这一行数据的 row trx_id是90;

READ COMMITTED


每次读取数据前都生成一个ReadView

继续上面的例子,假设现在有一个使用 READ COMMITTED隔离级别的事务开始执行:

1 # 使用READ COMMITTED隔离级别的事务
2 BEGIN;
3
4 # SELECT1:Transaction 100、200未提交
5 select k from t where id=1 ; # 得到值为1

这个 SELECT的执行过程如下:
【1】在执行 SELECT语句时会先生成一个ReadView,ReadView的 m_ids列表的内容就是[100, 200],min_trx_id为100,max_trx_id为201,creator_trx_id为0;
【2】然后从版本链中挑选可见的记录,最新的版本 trx_id值为200,在 m_ids列表内,所以不符合可见性要求;
【3】下一个版本的 trx_id值也为100,也在 m_ids列表内,所以也不符合要求,继续跳到下一个版本;
【4】下一个版本的 trx_id值为90,小于 ReadView中的 min_trx_id值100,所以这个版本是符合要求的;

之后,我们把事务B的事务提交一下,然后再到刚才使用 READ COMMITTED隔离级别的事务中继续查找,如下:

# 使用READ COMMITTED隔离级别的事务
BEGIN; # SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到值为1 # SELECT2:Transaction 200提交,Transaction 100未提交
SELECT * FROM hero WHERE number = 1; # 得到值为2

这个 SELECT2的执行过程如下:
【1】在执行 SELECT语句时又会单独生成一个 ReadView,该 ReadView的 m_ids列表的内容就是[100](事务id为 200的那个事务已经提交了,所以再次生成快照时就没有它了),min_trx_id为100,max_trx_id为201,creator_trx_id为0;
【2】然后从版本链中挑选可见的记录,从图中可以看出,最新版本 trx_id值为100,在 m_ids列表内,所以不符合可见性要求;
【3】下一个版本的 trx_id值为200,小于max_trx_id,并且不在 m_ids列表中,所以可见,返回的值为2;

REPEATABLE READ


在第一次读取数据时生成一个ReadView

假设现在有一个使用 REPEATABLE READ隔离级别的事务开始执行:

1 # 使用REPEATABLE READ隔离级别的事务
2 BEGIN;
3
4 # SELECT1:Transaction 100、200未提交
5 SELECT * FROM hero WHERE number = 1; # 得到值为1

这个SELECT1的执行过程如下:
【1】在执行 SELECT语句时会先生成一个 ReadView,ReadView的 m_ids列表的内容就是[100, 200],min_trx_id为100,max_trx_id为201,creator_trx_id为0;
【2】然后从版本链中挑选可见的记录,该版本的 trx_id值为100,在m_ids列表内,所以不符合可见性要求;
【3】下一个版本该版本的 trx_id值为200,也在 m_ids列表内,所以也不符合要求,继续跳到下一个版本;
【4】下一个版本的 trx_id值为90,小于 ReadView中的 min_trx_id值100,所以这个版本是符合要求的;
之后,我们把事务B 的事务提交一下,然后再到刚才使用 REPEATABLE READ隔离级别的事务中继续查找:

1 # 使用REPEATABLE READ隔离级别的事务
2 BEGIN;
3
4 # SELECT1:Transaction 100、200均未提交
5 SELECT * FROM hero WHERE number = 1; # 得到值为1
6
7 # SELECT2:Transaction 200提交,Transaction 100未提交
8 SELECT * FROM hero WHERE number = 1; # 得到值为1

这个SELECT2的执行过程如下:
【1】因为当前事务的隔离级别为 REPEATABLE READ,而之前在执行 SELECT1时已经生成过 ReadView了,所以此时直接复用之前的ReadView,之前的 ReadView的 m_ids列表的内容就是[100, 200],min_trx_id为100,max_trx_id为201,creator_trx_id为0。
【2】然后从版本链中挑选可见的记录,该版本的 trx_id值为100,在 m_ids列表内,所以不符合可见性要求;
【3】下一个版本该版本的 trx_id值为200,也在 m_ids列表内,所以也不符合要求,继续跳到下一个版本;
【4】下一个版本的 trx_id值为90,小于 ReadView中的 min_trx_id值100,所以这个版本是符合要求的;

MySQL事务原子性保证


事务原子性要求事务中的一系列操作要么全部完成,要么不做任何操作,不能只做一半。原子性对于原子操作很容易实现,就像HBase中行级事务的原子性实现就比较简单。但对于多条语句组成的事务来说,如果事务执行过程中发生异常,需要保证原子性就只能回滚,回滚到事务开始前的状态,就像这个事务根本没有发生过一样。如何实现呢?

MySQL实现回滚操作完全依赖于undo log,多说一句,undo log在 MySQL除了用来实现原子性保证之外,还用来实现MVCC,下文也会涉及到。使用 undo实现原子性在操作任何数据之前,首先会将修改前的数据记录到 undo log中,再进行实际修改。如果出现异常需要回滚,系统可以利用 undo中的备份将数据恢复到事务开始之前的状态。下图是 MySQL中表示事务的基本数据结构,其中与 undo相关的字段为 insert_undo和 update_undo,分别指向本次事务中所产生的 undo log。

事务回归根据 update_undo(或者indert_undo)找到对应的 undo log,做逆向操作即可。对于已经标记删除的数据清理删除标记,对于更新数据直接回滚更新;插入操作稍微复杂一些,不仅需要删除数据,还需要删除相关的聚集索引以及更新二级索引记录。

MySQL 一致性视图(MVCC)的相关教程结束。

《MySQL 一致性视图(MVCC).doc》

下载本文的Word格式文档,以方便收藏与打印。