引言
在上面的章节我们说到mysql的事务隔离级别,提到用MVCC来实现,这章我们就来说说MVCC的实现原理。
1. 隔离级别
首先我们先回顾事务的隔离级别和他们要解决的问题
事务隔离级别:
读未提交(READ_UNCOMMITTED):事务中可以读取到其他事务未提交数据。
读已提交(READ_COMMITTED):事务中可以读取到其他事务已提交数据。
重复读(REPETABLE_READ):多事务操作同一数据,但单事务中对该数据的多次查询结果(不论数据改变前还是改变后)都不变。
可串行化(SERIALIZABLE):事务串行执行
对应要解决的问题:
隔离级别 | 脏读 | 非重复读 | 幻读(Phantom read) |
READ_UNCOMMITTED | allowed | allowed | allowed |
READ_COMMITTED | prevented | allowed | allowed |
REPETABLE_READ | prevented | prevented | allowed |
SERIALIZABLE | prevented | prevented | prevented |
脏读:事务可以读取到其他事务未提交的数据
不可重复读:多个事务操作相同数据时,某一事务内对该数据的读取随着其他事务的操作发生改变,也就是说同一事务内对某条数据的多次读取不能保证都一样。
幻读:当事务2插入一行记录,在插入数据的前后,事务1查询了应该包含这个新纪录的数据,查询结果中均无事务2的新增数据,但此时在事务1中执行包含修改删除在内的更新或加锁操作时会用到该数据,没读到,但用到,这时幻读发生了。
通过上面对概念的理解,我们发现隔离级别从上往下解决的问题是包含关系,解决不可重复读问题的级别也就不存在脏读的问题,当然对应的执行效率也是逐步降低的。
我们在日常工作中一般都不会使用“读未提交”和“可串行化”这俩个级别,原因很明显:
“读未提交”带来的脏读问题不能接受;
“可串行化”这种串行执行带来的效率问题在大部分场景也是无法接受的;
而且这两种的实现逻辑上也相对简单,我们就不多介绍。
“重复读”和“读已提交”是我们用的比较多的,相比“读已提交”,“重复读”的隔离级别保证我们在某一时刻事务操作内部一致性,而“读已提交”的实时查询展示满足大部分场景。那这些在mysql中怎么实现的了?这就要说一下本章主角:MVCC
2. MVCC原理介绍
MVCC,即Multi-Version Concurrency Control (多版本并发控制)。它是一种并发控制的方法,它实现读取数据不用加锁,可以让读取数据同时修改。修改数据时同时可读取。
重点就在于不加锁,我们都知道为了保障事务的隔离性,各个事务不影响,最直接的方式就是加锁,比如串行化隔离级别就是加锁实现的。但是频繁的加锁,导致读数据时,没办法修改,修改数据时,没办法读取,大大降低了数据库性能,所以在mysql中采取MVCC这种方式来解决这个问题,实现“重复读”和“读已提交”这两种隔离级别。
从名字上我们大致也能理解,它是基于版本来进行并发控制,也就是说通过把一条记录各个阶段的数据按版本进行记录,在根据id关联、事务版本号和指向关系组成版本链(就是之前提过的undolog),辅助一套判断规则来实现并发控制。
2.1.引出一下几个概念:
1. 事务版本号
又叫事务ID,事务每次开启前,都会从数据库获得一个自增长的事务ID,这就是事务版本号,可以从事务ID判断事务的执行先后顺序.
2.隐藏逻辑字段(trx_id、roll_pointer、row_id)
InnoDB存储引擎的数据记录里有很多隐藏字段,我们主要介绍一下trx_id(上面介绍的事务版本号)、roll_pointer(历史数据位置指针),如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列row_id,为方便理解我们在本文中就取row_id和主键同义吧。
3. 版本链
根据undolog这些我们可以形成版本链。
4. 快照读
又叫读视图(read-view),一个事务在进行 select 操作(快照读)的时候会创建一个 read-view ,这个read-view 其实只是三个字段:trx_list(当前活跃事务)、up_limit_id(最小活跃事务id)、low_limit_id(下一待分配事务id)。
事务中普通select操作会产生快照读。简单理解就是记录当前活跃事务的快照。
5. 当前读
当前读,顾名思义就是读取当前最新的数据,并且对读取的数据加锁,阻止其他事务同时修改相同的记录,避免出现安全问题,一般在有锁的情况下会发生当前读:
update、delete、insert
select … lock in share mode (主动加共享锁)
select … for update (主动加排他锁)
2.2. 实现逻辑(可见性判断)
先定义几个参数:
trx_id:当前事务id
trx_list:当前活跃事务id
up_limit_id:最小活跃事务id
low_limit_id:下一待分配事务id
db_trx_id: 用来判断已提交事务id,从当前数据生效的事务id开始。
可见性判断逻辑如下:
1.首先比较这条记录的 db_trx_id是否是小于up_limit_id 或者等于trx_id(当前事务id)。如果满足,那么说明当前事务能看到这条记录。如果大于则进入下一轮判断
2.然后判断这条记录的 db_trx_id是否大于等于low_limit_id。如果大于等于则说明此事务无法看见该条记录,不然就进入下一轮判断。
3.判断该条记录的 db_trx_id是否在活跃事务的数组中,如果在则说明这条记录还未提交对于当前操作的事务是不可见的,如果不在则说明已经提交,那么就是可见的。
4.如果此条记录对于该事务不可见且roll_pointer不为空那么就会指向回滚指针的地址,重新获取db_trx_id,重复上述步骤,直到找到可见记录或遍历完成吴可见数据。
是不是看这挺麻烦的,我们举个例子:
图1-1
T3查询的是修改之后的结果age=2,因为db_trx_id=2(事务2)满足条件3,所以对事务1事务2的操作是可见的,所以查询结果是age=2。
是不是看这挺麻烦的,不用强行记,换个简单的理解方式:
RC:查询时实时使用当前生效数据。
RR:查询时使用本事务修改后数据,或使用当前生效数据(好像没法做到重复读是吧)。
所以这是个问题,我在举个例子(RR隔离级别):
图1-2
从上图可以看到在RR隔离级别下事务1中的两次查询的结果都是“age=19”,没有受事务2的影响,但是读快照2和图1-1中的读快照信息一致,查询结果却不一致。这里我们需要记住一点,在RR隔离级别下事务中同一条数据记录的读快照会使用首次生成的,所以两次读取结果一致,也就是说T4时刻的快照读信息2是不对的,实际上使用的是T2时刻的快照读1,T4时刻没有生成新的读快照。
如果在事务1中的T4时刻使用update age等于2的数据是否能更新到id=1的记录了? 是可以的,因为这时候就使用当前读了,能查询到正确记录。