目录
什么是MVCC?
MVCC实现原理
Undo Log 日志
InnoDB行格式
undo日志格式
1. insert undo log格式
2. update undo log格式
事务回滚机制
Read View
MVCC案例分析
案例01-读已提交RC隔离级别下的可见性分析
案例02-可重复读RR隔离级别下的可见性分析
什么是MVCC?
MVCC(Multiversion Concurrency Control)中文名叫多版本并发控制,是现代数据库(包括MySql,Oracle,PostgreSQL等)引擎实现中常用的处理读写冲突的手段。它可以看成行级别锁的一种妥协,它在许多情况下避免了使用锁,同时可以提供更小的开销,目的在于提高数据库高并发场景下的吞吐量。
MVCC实现原理
MVCC实现原理关键在于数据快照,不同的事务访问不同版本的数据快照,从而实现事务下对数据的隔离级别。虽然说具有多个版本的数据快照,但这并不意味着必须拷贝数据,保存多份数据文件(这样会浪费存储空间),InnoDB通过事务的Undo日志巧妙地实现了多版本的数据快照。
MVCC的核心问题就是:判断一下版本链中的哪个版本是当前事务可见的!
- 对于使用 RU 隔离级别的事务来说,直接读取记录的最新版本就好了,不需要Undo log。
- 对于使用 串行化 隔离级别的事务来说,使用加锁的方式来访问记录,不需要Undo log。
- 对于使用 RC 和 RR 隔离级别的事务来说,需要用到undo 日志的版本链。
其核心思想是读不加锁,读写不冲突。
实现依赖是 Undo日志和 Read View。
Undo Log 日志
Undo Log:数据库事务开始之前,会将要修改的记录存放到 Undo 日志里,当事务回滚时或者数据库崩溃时,可以利用 Undo 日志,撤销未提交事务对数据库产生的影响。
InnoDB行格式
我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式。
InnoDB一共有四种行格式:Compact、Redundant、Dynamic和Compressed
行格式,行格式之间存在差异,但总体可以抽象成以下部分:
- row_id:如果没有为表显式的定义主键,并且表中也没有定义唯一索引,那么InnoDB会自动为表添加一个row_id的隐藏列作为主键。
- trx_id:每个事务都会分配一个事务ID,当对某条记录发生变更时,就会将这个事务的事务ID写入
tx_id 中。
- roll_pointer: 回滚指针,本质上就是指向
undo log
的指针。
undo日志格式
undo log
的日志内容是逻辑日志,非物理日志。
- 物理日志的意思是指具体的把具体某个数据页上的某个偏移量的指改为什么,是具体到物理结构上了,比如
redo log
就是物理日志。 - 而逻辑日志只是记录了某条数据的信息是怎么样的,没有到具体的物理磁盘上
根据行为的不同Undo日志分为两种: Insert Undo Log 和 Update Undo Log
1. insert undo log格式
记录的是insert
语句对应的undo log
,Insert 操作的记录只对事务本身可见,对于其它事务此记录是不可见的,所以 Insert Undo Log 可以在 事务提交后直接删除而不需要进行回收操作。
它生成的undo log
记录格式如下图:
2. update undo log格式
记录的是update、delete
语句对应的undo log
,是Update或Delete 操作中产生的Undo日志,Update操作会对已经存在的行记录产生影响,为了实现MVCC多版本并发控制机制,因此Update Undo 日志不能在事务提交时就删除,而是在事务提交时将日志放入指定区域,等待 Purge 线程进行最后的删 除操作。
当确定某个数据的旧版本不再被任何事务需要时(即没有其他事务的read view能够看到这个旧版本的数据),Purge线程就会介入,将这些不再需要的undo日志从链表中删除,并释放相应的空间。
查看purge参数
show variables like '%purge%' ;
它生成的undo log
记录格式如下图:
事务回滚机制
表结构
CREATE TABLE `tab_user` (
`id` int(11) NOT NULL,
`name` varchar(100) DEFAULT NULL,
`age` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
1. insert数据:
INSERT INTO `gorgor_user`.`tab_user`( `name`, `age`) VALUES ('刘备', 18);
插入的数据都会生成一条insert undo log
,并且数据的回滚指针会指向它。undo log
会记录undo log
的序号、插入主键的列和值...,那么在进行rollback
的时候,通过主键直接把对应的数据删除即可。
2. update数据
update tab_user set age = 30 where id = 1;
update tab_user set name = '雄雄' where id = 1;
这时会把老的记录写入新的undo log
,让回滚指针指向新的undo log
,它的undo no
是1,并且新的undo log
会指向老的undo log(undo no=0)
,最终形成undo log
版本链,如下图所示:
可以发现每次对数据的变更都会产生一个undo log
,当一条记录被变更多次时,那么就会产生多条undo log
,undo log
记录的是变更前的日志,并且每个undo lo
g的序号是递增的,那么当要回滚的时候,按照序号依次向前推,就可以找到我们的原始数据了。
那么按照上面的例子,事务要进行回滚,最终得到下面的执行流程:
- 通过
undo no=2
的日志把id=1的数据的name还原成“刘备" - 通过
undo no=1
的日志把id=1的数据的age还原成"18" - 通过
undo no=0
的日志把id=1的数据根据主键信息删除
Read View
什么是ReadView?
ReadView是张存储事务id的表,主要包含当前系统中有哪些活跃的读写事务,把它们的事务id放到一个 列表中。结合Undo日志的默认字段【事务trx_id】来控制那个版本的Undo日志可被其他事务看见。
在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前永远都不会变化。如果是读已提交隔离级别在每次执行查询sql时都会重新生成read-view。
这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
四个列:
- m_ids:表示在生成ReadView时,当前系统中活跃的读写事务id列表
- m_low_limit_id:事务id下限,表示当前系统中活跃的读写事务中最小的事务id,m_ids事务列表 中的最小事务id
- m_up_limit_id:事务id上限,表示生成ReadView时,系统中应该分配给下一个事务的id值
- m_creator_trx_id:表示生成该ReadView的事务的事务id
如何判断可见性?
开启事务执行第一次查询时,首先生成ReadView,然后依据Undo日志和ReadView按照判断可见性, 按照下边步骤判断记录的版本链的某个版本是否可见。
版本链比对规则如下:
- 如果被访问版本的 trx_id 属性值,小于ReadView中的事务下限id,表明生成该版本的事务在生 成 ReadView 前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的 trx_id 属性值,等于ReadView中的 m_creator_trx_id ,可以被访问。
- 如果被访问版本的 trx_id 属性值,大于等于ReadView中的事务上限id,在生成 ReadView 后才产 生的数据,所以该版本不可以被当前事务访问。
- 如果被访问版本的 trx_id 属性值,在事务下限id和事务上限id之间,那就需要判断是不是在 m_ids 列表中。
- 如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;
- 如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
循环判断Undo log中的版本链某一的版本是否对当前事务可见,如果循环到最后一个版本也不可见的 话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录。
当前读与快照读
在MVCC并发控制中,读操作可以分成两类:快照读 (Snapshot Read)与当前读 (Current Read)
- 快照读:读取的是记录的可见版本 (有可能是历史版本),不用加锁。
- 当前读:读取的是记录的最新版本,并且当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。常见的 update/insert/delete、还有 select … for update、select … lock in share mode 都是当前读.
MVCC案例分析
表结构
CREATE TABLE `tab_user` (
`id` int(11) NOT NULL,
`name` varchar(100) DEFAULT NULL,
`age` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
插入一条数据
INSERT INTO `gorgor_user`.`tab_user`( `name`, `age`) VALUES ('刘备', 18);
案例01-读已提交RC隔离级别下的可见性分析
每次读取数据前都生成一个ReadView
在读已提交隔离级别下,T4结果是刘备,T6结果是迪迪,T8结果是张三。
结果分析:
T3时刻,Undo log 日志版本链图如下:
T4时刻,select 查询会生成当前事务的一致性视图 Read-View。
- m_ids:表示在生成ReadView时,当前系统中活跃的读写事务id列表
- m_low_limit_id:事务id下限,表示当前系统中活跃的读写事务中最小的事务id,m_ids事务列表 中的最小事务id
- m_up_limit_id:事务id上限,表示生成ReadView时,系统中应该分配给下一个事务的id值
- m_creator_trx_id:表示生成该ReadView的事务的事务id
从Undo log 日志版本链中,可以根据版本链比对规则比对(规则在上面),trx_id 等于100的事务,是在 m_ids 活跃的读写事务id列表中,不可见. 而 trx_id = 80 的事务比 m_low_limit_id 还小,可见.所以结果为刘备.
T6时刻,select 查询会生成当前事务的一致性视图 Read-View。
从Undo log 日志版本链中,trx_id = 100 比 m_low_limit_id 还小, 所以可见, 结果就为 迪迪.
T8时刻,select 查询会生成当前事务的一致性视图 Read-View。
此时,没有活跃的事务id, 即 m_ids 为空, 意味着再创建 read-view 的那一刻, 没有其他事务是活跃的(即所有其他事务都已提交或未开始). 根据可见性原则,最终结果值为 张三.
案例02-可重复读RR隔离级别下的可见性分析
在事务开始后第一次读取数据时生成一个ReadView。对于使用 RR 隔离级别的事务来说,只会在第一次执行查询语句时生成一个 ReadView ,之后的查询就不会重复生成了。
代码与执行流程与RC案例完全相同,唯一不同的是事务隔离级别。
T4时刻,select 查询会生成当前事务的一致性视图 Read-View。
从Undo log 日志版本链中,trx_id 等于100的事务,是在 m_ids 活跃的读写事务id列表中,不可见. 而 trx_id = 80 的事务比 m_low_limit_id 还小,可见.所以结果为刘备.
T6时刻,在RR 隔离级别的事务中,ReadView只生成一次,此时ReadView和T4时刻一样.
查询结果还是刘备.同理,T8时刻也一样.