InnoDB对MVCC的实现

2023-05-26  本文已影响0人  意大利大炮

参考链接:https://zhuanlan.zhihu.com/p/52977862

1、MVCC概念

是什么?

多版本并发控制(Multi-Version Concurrency Control)

有什么用?

怎么做到的

2、InnoDb的MVCC实现

有什么用?

InnoDB MVCC实现的核心知识点

事务版本号

数据表的隐藏列

表里的每一行数据都有以下几个隐藏列

Undo log(回滚日志)

Undo log 的用途

(1)保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。
(2)用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。

一条数据在undo日志里存在多个版本,回滚操作会不会乱掉?

答:不会的

事务版本号、表格的隐藏列、undo log的关系

我们模拟一次数据修改的过程来让我们了解下事务版本号、表格隐藏的列和undo log他们之间的使用关系。

  1. 首先准备一张原始原始数据表


    image.png
  2. 开启一个事务A: 对user_info表执行 update user_info set name =“李四”where id=1 会进行如下流程操作
    1、 首先获得一个事务编号104(当前系统最大事务号为104,事务新建后最大事务号变为105)
    2、把user_info表id=1的行修改前的数据拷贝到undo log
    3、修改user_info表 id=1的数据
    4、把修改后的数据事务版本号改成当前事务版本号,并把DB_ROLL_PTR 地址指向undo log数据地址。
  3. 最后执行完结果如图:
image.png

Read view

Read view 的几个重要属性:

属性 备注
trx_ids 当前系统活跃(未提交)事务版本号集合。注意,trx_ids并不包含当前事务的id
low_limit_id 创建当前read view 时“目前出现过的最大事务id加1”。
up_limit_id 创建当前read view 时“活跃事务列表的最小事务ID,如果活跃事务列表为空,则up_limit_id 为 low_limit_id”
creator_trx_id 创建当前read view的事务版本号;

事务不同隔离级别copy read_view的时机
RC(read commit) 级别下同一个事务里面的每一次查询都会获得一个新的read view副本。这样就可能造成同一个事务里前后读取数据可能不一致的问题(重复读)

image.png
RR(重复读)级别下的一个事务里只会获取一次read view副本,从而保证每次查询的数据都是一样的。
image.png

READ_UNCOMMITTED 级别的事务不会获取read view 副本。

级别 时机 --
RU 不需要
RC 每次Select时都会获取最新的 事务在begin之后,执行每条select(读操作)语句时,快照会被重置,即会重新创建一个快照(read view)。
RR 第一次Select语句执行时 只会创建一次快照(read view),作用于当前数据库的所有表的Select语句,即:即便是Select的表A,后面再SelectB也不会再次copy read_view了
序列化 不需要

Select时,Read view 匹配指定行的版本号流程:

  1. 事务ID=creator_trx_id,显示
    • 数据的事务ID等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见
  2. 数据的DB_TRX_ID(事务ID)<up_limit_id, 显示
    • 事务copy read view前数据已提交(如果未提交,up_limit_id<最小活跃事务id>就是它了)
      显示。
  3. 数据事务ID>=low_limit_id,不显示
    • 事务copy read view后创建的数据(他比copy时的系统最大事务ID要大,不管提没提交都不显示)
  4. up_limit_id<=数据事务ID<low_limit_id, 需要匹配trx_ids(活跃事务集合)
    • 事务大于最小活跃事务id,小于当时系统最大事务id,这种情况需要判断当时此事务有没有提交,如果已经提交了就显示,未提交就不显示(防止脏读),折旧需要匹配trx_ids。
  5. 情况1: 数据事务ID not in trx_ids, 显示
    • 事务copy read view时已经提交了(如果copy时未提交,他应该在活跃(未提交的)的事务列表里),这种情况数据则可以显示。
  6. 情况2:数据事务ID in trx_ids,不显示
    • 如果事务ID存在trx_ids则说明copy read view时,数据还没有提交,为了防止脏读,就不显示。
  7. 当前记录不满足read view条件时候,从undo log里面获取数据,循环以上过程,直到找到可以显示的数据或遍历完所有的历史版本
    • 通过DB_ROLL_PTR找到上一个版本的数据在undo log里的地址,并循环以上过程。

RC不可重复读的原因
综合以上流程,可以看到RC和RR的区别在于:RC每次select语句都会重新构建read view,这就可能导致:两个或多个read view在走匹配流程时,在第2->5这个过程可能不一致。

Innodb实现MCC的原理

image.png

模拟MVCC实现流程

  1. 创建user_info表,插入一条初始化数据


    image.png
  2. 事务A和事务B同时对user_info进行修改和查询操作
    事务A:update user_info set name =”李四”
    事务B:select * fom user_info where id=1
    问题:
    先开启事务A ,在事务A修改数据后但未进行commit,此时执行事务B。最后返回结果如何。
    流程如下:
    image.png
    执行流程说明:
    1. 事务A:开启事务,首先得到一个事务编号102;
    2. 事务B:开启事务,得到事务编号103;
    3. 事务A:进行修改操作,首先把原数据拷贝到undolog,然后对数据进行修改,标记事务编号和上一个数据版本在undo log的地址。


      image.png
    4. 事务B: 此时事务B获得一个read view ,read view对应的值如下


      image.png
    5. 事务B: 执行查询语句,此时得到的是事务A修改后的数据


      image.png
    6. 事务B: 把数据与read view进行匹配
      数据事务ID为102 等于up_limit_id (这里不小于up_limit_id)
      数据事务ID为102 小于low_limit_id
      数据事务ID为102 存在于 trx_ids
      数据事务ID为102 不等于creator_trx_id
      发现不满足read view显示条件,所以从undo lo获取历史版本的数据再和read view进行匹配,最后返回数据如下。


      image.png

快照读和当前读

快照读

快照读是指读取数据时不是读取最新版本的数据,而是基于历史版本读取的一个快照信息(mysql读取undo log历史版本) ,快照读可以使普通的SELECT 读取数据时不用对表数据进行加锁,从而解决了因为对数据库表的加锁而导致的两个如下问题
1、解决了因加锁导致的修改数据时无法对数据读取问题;
2、解决了因加锁导致读取数据时无法对数据进行修改的问题;

当前读

当前读是读取的数据库最新的数据,当前读和快照读不同,因为要读取最新的数据而且要保证事务的隔离性,所以当前读是需要对数据进行加锁的(Update delete insert select ....lock in share mode select for update 为当前读)

MVCC能解决脏读问题?

RR级别下MVCC是否有解决幻读问题?

不完全能

这个例子会出现幻读问题,事务a的UPDATE语句执行之后,会把更新语句的WHERE条件覆盖到的所有数据都进行更新,这就绕过了read view匹配的过程。而当前事务所做的任何更新,对本事务所有SELECT查询都变的可见(此UPDATE语句会把涉及的所有记录原数据拷贝到undolog,然后对数据进行修改,并标记事务编号为事务a的编号。再SELECT时就可以匹配到了),因此最后输出的结果是UPDATE执行后更新的所有记录。

能解幻读的场景

1、开启事务1,获得事务ID为1。
2、事务1执行查询,得到readview。
3、开始事务2。
4、执行insert。
5、提交事务2。
6、执行事务1的第二次查询 (因为这里是RR级别,所以不会再去获得readview,还是使用第一次获得的readview)
7、最后得到的结果是,插入的数据不会显示,因为插入的数据事务ID大于等于 readview里的最大活跃事务ID。

不能解决幻读的场景

1、开启事务1,获得事务ID为1。
2、事务1执行查询,得到readview。
3、开启事务2,执行insert语句。
4、提交事务2。
5、事务1执行第二次查询,此时查不到新插入的数据。
6、事务1执行update语句,此语句的where条件覆盖了事务2insert的数据。比如:'UPDATE user SET score = 0'
7、事务1执行第三次查询,可以查到新插入的数据。

为什么要避免长事务

  1. 影响undo 日志的回收
  2. 锁资源不能得到释放

长事务对undo日志回收的影响。

可以通过语句查询长事务:
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

上一篇 下一篇

猜你喜欢

热点阅读