深入理解Mysql原理-MVCC详细原理剖析(四)

引言

在上面的章节我们说到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的记录了? 是可以的,因为这时候就使用当前读了,能查询到正确记录。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值