深度了解mysql事务mvcc实现原理

    技术2022-07-11  98

    一:事务

    概念:一组原子性的sql查询语句,也可以看作是一个工作单元 特点:要么全部执行成功,要么全部执行失败

    一个有效的事务需满足的条件(ACID)

    原子性(Atomicity)

    一个事务必须被视为一个单独的内部最小的,”不可分“的工作单元,以确保事务要么全部执行,要么全部执行失败,当一个事务具有原子性的时候,该事务绝对不会部分执行,要么全部执行,要么不执行。

    一致性(Consistency)

    数据库总是从一种一致性状态转换到另一种一致性状态,例如银行转账业务中的俩者金额总和的是不变的。这种一致性包括:实体的完整性(字段的类型、大小等),外键约束

    隔离性(Isolation)

    各个事务中的是互不干扰的,但是严格的隔离性,只有事务隔离级别中的Serializable(可串行化)可以达到

    持久性(Durability)

    一但一个事务提交,事务所造成的数据改变是永久的,即使系统的崩溃数据也不会因此丢失

    二:事务的类别

    1. 读未提交(幻读,不可重复读,脏读都可能发生)

    定义:可以读到另一个事务未提交的数据

    2. 可重复读(mysql默认的隔离级别,可能发生幻读)

    注意:

    不可重复读和幻读的区别是:前者是指读到了已经提交的事务的更改数据(修改或删除),后者是指读到了其他已经提交事务的新增数据。

    定义:

    同一个事务的多个实例在并发处理数据的时候,会看到同样的数据行,但是确可能出现幻读(幻读和不可重复读的区别在于,幻读的注重点在于insert,不可重复读的重点在于select) 那为什么就能这样呢,看到同样的数据行,但是确可能出现幻读,可重复读的内在实现机制是怎样的呢?那这一切就和mvcc机制有关了,以下的所有叙述就拿innodb来讲

    可重复读基于mvcc,readview实现的前提:

    行数据中的影藏属性(当然不止这几个) innodb通过为每个数据行增加俩个隐藏值的方式实现了mvcc,这俩个影藏值记录了行的创建时间及结束时间,同时每一行都有一个系统版本号来替代事件的创建时间,实际操作中存的系统版本号就是事务的id,每开启一个事务,这个系统版本号都会递增。

    mysql高性能中的原话: 在聚集索引中,每一条数据(每个叶子节点)中都包含主键值、事务ID(tid)、用于事务和MVCC的回滚指针(RB)及剩下的列叶子节点通过RB连接

    视图(consistent read view) 这个视图不是查询语句定义的虚拟表。这个一致性视图是为了用于支持隔离级别为《可重复读(repeatable read)和读已提交 (read committed)的实现》

    隔离级别为 可重复读 (repeatable read)只有在第一次select的时候才会生成readview,而读已提交(read committed)在每一次的select的时候都会生成一个readview

    mysql高性能中的原话:

    mvcc的每一次select操作中干了2件事,1:innodb只查找版本号早于当前事务版本的数据行(也就是只查找这个版本,或者比这个版本更早之前的数据)。2:数据行的删除版本必须是未定义,或者大于事务版本的(保证了读取的行在事务开起前时是未被删除的)

    mysql高性能中的原话:mvcc是通过及时保存在某些时刻的数据”快照“实现的(也就是及时保存对应的readview,一个事务内,拿update操作说,innodb会为每个需要更新的行建立一个新的行拷贝,为新的行拷贝记录版本号的同时,修改旧数据的系统版本号,新的行拷贝通过RB与旧数据连接起来形成了一个版本链,之后在select的时候通过比较版本号从而达到readview复用的目的)。

    readview遵循的可见性算法**,参考说白了在每次select的时候,会从最新的这条数据开始遍历readview中的每一条数据,直至找到对当前事务可见的这条数据,然后返回

    算法过程如下: 1:拿到readview最新的这条数据id,与系统当前活跃事务的id逐个进行比较,如果<最小的活跃事务的id,则这条数据对当前系统所有的活跃事务都具有可见性。 2:如果>=,会接着与**出现过的最大事务id+1**进行比较,如果>=这个版本的数据不可见(说明生成readview之前是没有这个版本的数据的),如果<的话,判断readview中最新的这条数据有无commit(readview中维护着一个活跃事务id的数组),commit了的就是可见,没提交就是不可见,这里窃取[@呵呵一笑](https://www.zhihu.com/question/66320138/answer/241418502)的一张源码图

    故每一条数据大概长这样 每一次的修改版本号的操作(update insert delete),就拿updated来说,当有多个事务先后对同一条数据进行修改的时候,通过上面的第6点就得到了这样一个版本链(readview越往上的数据越新)

    下面来实操加深理解可重复读的实现原理 在隔离级别为 可重复读 (repeatable read)下,我分别用黄色,紫色,蓝色按照先后顺序开启了三个事务1、2、3。红色为代码的执行顺序。认真阅读后大家会发现这样一个有趣的现象,事务1、3读取到的数据不是一样的,这是为什么呢?下面开始分析 因为每开启一个事务,版本号都是递增的,转换成流程图就是这样的 事务1,根据隔离级别为 可重复读 (repeatable read)只有在第一次select的时候才会生成readview,故事务1第一次select后的readview永远是只有A对应的这个版本的数据这一条。当事务2提交之后,读取到的数据永远是b=666这条。

    同理,事务3在select后生成的readview包含了A、B对应的这俩条数据,根据readview可见性算法: 取出readview中最新数据的事务id也就是对应的事务2的tid2=2, tid=2与活跃事务中id最小的比较,也就是事务1且tid1=1,tid2>=tid1, 接着让tid2=2与出现过的最大事务的id+1比较,也就是tid2与tid3+1比较,tid2=2<tid3=3+1,且tid=2对应的这条是commit的,所以tid=2的这条数据是可见的 故事务3读到的数据一直是b=1.。(所以避免了不可重复读,其实就是对应事务1、2生成的readview不同,加上readview算法实现的)

    3:读已提交

    定义:一个事务只能读取到另一个事务已经提交的内容 思考:为什么只能读取到另一个事务已经提交的内容,那为什么不能读取到未提交的内容呢,其实这还是和mvcc及readview可见性算法有关

    下面看一个请求流程 由于和可重复读的分析过程差不多,就简述以下吧, 这里就和上面提到的mvcc前提3有关(读已提交 在每一次的select的时候都会生成一个readview), A读到的数据满足readview可见性算法中的第1点 B读不到未提交的数据,是因为不符合readview可见性算法中的第2点中的,未commit的数据读不到 A读得到数据满足readview可见性算法,且readview每select就生成

    4:可串行化

    定义:其实就是利用悲观锁得原理实现得,数据安全,并发能力差

    END总结

    以上的所有读都是快照读 简述以下快照读与当前读吧(快照读就是普通的select 语句,当前读就是加select lock in share 或者 for update 这种,读到的数据是最新的数据)当前读在可重复读级别下演示 说白了mvcc就是根据乐观锁实现得,通过加版本号,在每一次select的时候去快照中获取可见性最新的数据,只不过是mysql内部帮我们封装好了这些个判断逻辑。

    而串行化呢,就是加悲观锁保证并发安全问题,像我们写秒杀系统中不也是可以通过悲观锁的方式来保证并发嘛

    Processed: 0.008, SQL: 9