目录与学习目标

  1. 1MVCC多版本并发控制机制介绍
  2. 2undo日志版本链(概念)(先了解)(不要纠结)
  3. 3readview机制 (概念)(先了解)(不要纠结)
  4. 4:实例讲解 前置知识
  5. 5:实例讲解
  6. 6MVCC多版本并发控制机总结

1:MVCC多版本并发控制机制介绍

  1. Mysql在可重复读隔离级别下如何保证事务较高的隔离性,
  2. 同样的sql查询语句在一个事务 里多次执行查询结果相同,就算其它事务对数据有修改也不会影响当前事务sql语句的查询结果。
  3. 这个隔离性就是靠MVCC(Multi-Version Concurrency Control)机制来保证的,
  4. 而写操作是会通过加间隙锁互斥来保证隔离性。
  5. 在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。
  6. Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。

  1. 读锁:全部事务都可读 但全部事务都不可写
  2. 写锁:本事务可读可写 但其他事务不可读 不可写
  3. 间隙锁: 间隙范围内 全部事务都可以读 但只有本事务可以写
  4. 间隙范围:不仅仅是本事务的查询访问 还包括了数据区间访问
  5. 可重复读:
  6. 读操作 MVCC机制保证不可重复的
  7. 写操作 加间隙锁 解决一定程度的幻读
  8. 事务1update tableName set name = `hesuijin` where id =1
  9. 事务2delete from tableName where id =1 (阻塞等待)
  10. tableName id 15 1015 那么它存在数据区间 (1,5] (5,10] (10,15] 15,正无穷
  11. 覆盖区间:6在数据区间 (5,10]上 所以覆盖范围 (1,5] (5,10]
  12. 事务1update tableName set name = `hesuijin` where id <7
  13. 事务2insert into tableName values(8,'HSJ') ;(阻塞等待)
  14. 可串行化:
  15. 读写操作 都通过加间隙锁 保证数据ACID

2:undo日志版本链(概念)(先了解)(不要纠结)

  1. undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志
  2. 并且用两个隐藏字段trx_idroll_pointer把这些undo日志串联起来形成一个历史记录版本链
  3. 不管是否 提交写操作语句的事务 都会把日志记录放进 历史记录版本链
  4. trx_id 为本次的事务ID
  5. roll_pointer 指向上一个版本
  6. 最下方为第一次修改:trx_id = 60
  7. 最上方为最后一次修改:trx_id = 200
  8. 如果整个数据最后提交 那么最终的数据就是 id = 1 name = liei4
  9. begin/start transaction 命令并不是一个事务的起点,
  10. 只有在执行到它们之后的第一个 InnoDB 写操作的语句,
  11. 事务才真正启动,才会向mysql申请事务idmysql内部是严格按照事务的启动顺序来分配事务id的。

image.png

3:readview机制 (概念)(先了解)(不要纠结)

  1. 在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view(活跃视图数组)
  2. 该视图在事务结束之前都不会变化
  3. 注意:如果是读已提交隔离级别在每次执行查询sql时都会重新生成(了解)
  4. select开启事务的时候就已经是确定好了:readview的数据
  5. 这个视图由执行查询时由
  6. 所有未提交事务id数组(数组里最小的idmin_id
  7. 所有已提交的最大事务idmax_id)组成
  8. readview:[100,200], 300
  9. 然后映射出事务视图:
  10. 已提交事务:小于100的一定是已提交 (不包括100)(历史数据)
  11. 未开始事务:大于300的一定是未开始(不包括300)(将来数据)
  12. 另外还有 已提交与未提交 放在同一个区域
  13. 深度理解:开始select的时候 看到的 已经提交的数据和还没有提交 的对应事务Id
  14. 后续进行select出查询数据的时候
  15. 1:可以先定位到 事务ID100,300 历史事务与将来事务的区分
  16. 2:然后再快速的查找 100,200 300 里面的提交与未提交 查找出真正的数据
  17. 注意:已经提交的事务 存放的位置 可能是小于100 也可能是在100300之间

image.png

4:实例讲解 前置知识

image.png
image.png
image.png

  1. EXACL图片讲解:
  2. 1:首先有5个窗口,其中5个窗口有3个执行update语句,其他两个执行select语句。
  3. 2:按照序号顺序执行相应的SQL语句
  4. 3:执行update的时候 undo日志版本链 会新增一条数据
  5. 4:执行select的时候会按照以下规则进行判断
  6. 1:获取的trx_id readview映射的事务视图 绿色 红色 做比较
  7. 2:获取的trx_id readview的数组做比较
  8. 3:不成功则进入下也trx_id
  9. 1:如果 row trx_id 落在绿色部分( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
  10. 2 如果 row trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的, 是不可见的;
  11. 3 如果 row trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况
  12. a. row trx_id 在视图(readview)数组中,表示这个版本是由还没提交的事务生成的,不可见
  13. b. row trx_id 不在视图(readview)数组中,表示这个版本是已经提交了的事务生成的,可见。
  14. 注意:不用去管为什么 先跑一遍下面的逻辑
  15. 注意:不用去管为什么 先跑一遍下面的逻辑
  16. 注意:不用去管为什么 先跑一遍下面的逻辑

5:实例讲解


  1. 步骤1
  2. 123 4 行开启事务 并执行 update语句
  3. 2Transaction 100),3 行(Transaction 200)只是为了有一个事务标志

  1. 步骤2
  2. 注意数据:一开始的数据 Id=1 name='lilei' trx_id=60

  1. 步骤3
  2. 执行行数 5
  3. update account set name ='lilei300' where id =1; (不管有没有提交)
  4. 这个时候产生 Id=1 name='lilei300' trx_id=300 的历史数据
  5. 插入到 undo日志版本链其roll_pointer指向上一条数据

  1. 步骤3
  2. 执行行数 6
  3. 注意此时的执行事务300 commit 不该 undo_log
  4. 但改变后面select readview

  1. 步骤4
  2. 执行行数 7
  3. select 1
  4. select name from account where id = 1;
  5. --readview:[100,200], 300 lilei300
  6. 1:执行查询SQL
  7. 2:生成readview
  8. 未提交的trx_id [100,200] 也就是 [Transaction 100,Transaction 200]
  9. 已提交的trx_id 300
  10. 3:注意此时 id=1的日志undo_log 仅有两条数据 trx_id =300 trx_id =60
  11. 4:先指向300
  12. 5 最后查询出lilei300
  13. 执行select的时候会按照以下规则进行判断
  14. 1:获取的trx_id readview映射的事务视图 绿色 红色 做比较
  15. 2:获取的trx_id readview的数组做比较
  16. 3:不成功则进入一个trx_id

image.png
image.png


  1. 步骤5
  2. 执行行数 8 9
  3. update account set name = 'lilei1' where id = 1;
  4. update account set name = 'lilei2' where id = 1;
  5. undo_log新增两条日志

  1. 步骤6
  2. 执行行数 10
  3. select 1
  4. select name from account where id = 1;
  5. --readview:[100,200], 300 lilei300 最后查询出lilei300
  6. 第一次查找:1 lilei2 100 查找trx_id = 100
  7. 进行版本链对比 落在黄色区域
  8. 按照read-view [100,200], 300 在数组中 因此不可见 然后继续查询
  9. 第二次查找:1 lilei1 100 查找trx_id = 100
  10. 重复上面操作
  11. 进行版本链对比 落在黄色区域
  12. 按照read-view [100,200], 300 在数组中 因此不可见 然后继续查询
  13. 第三次查询:1 lilei300 300 查找trx_id = 300
  14. 进行版本链对比 落在黄色区域
  15. 按照read-view [100,200], 300 在数组外 因此可见 获取结果300

image.png


  1. 步骤7
  2. 执行行数 11
  3. 事务Transaction 100 提交

  1. 步骤8
  2. 执行行数 12 13
  3. update account set name = 'lilei3' where id = 1;
  4. update account set name = 'lilei4' where id = 1;
  5. undo_log新增两条日志

  1. 步骤9
  2. 执行行数 14
  3. select 1
  4. select name from account where id = 1;
  5. --readview:[100,200], 300
  6. 按照步骤6的规则 发现查询的结果 lilei300
  7. 执行行数 14
  8. select 2
  9. select name from account where id = 1;
  10. --readview:[200], 300
  11. 按照步骤6的规则 发现查询的结果 lilei2
  12. 为什么会发生这样的情况:
  13. select 1:的 readview视图是 之前执行第一次select产生的
  14. --readview:[100,200], 300 不变的
  15. select 2:的 readview视图是 现在执行第一次select产生的
  16. --readview:[200], 300

image.png


  1. 注意:
  2. 1:不管是否commit执行写操作都加到版本链上面
  3. 2:事务里的任何sql查询结果需要从对应版本链里的最新数据
  4. 开始逐条跟read-view做比对从而得到最终的快照结果。

6:MVCC多版本并发控制机总结

  1. 1:对数据进行写操作是有undo_log 加入到undo_log链表 那么也有相应的事务ID
  2. 2:假如在多个事务对同一个主键ID的数据进行 写操作的时候 那么就有一个undo_log链表 里面有多个undo_log
  3. 3:当一个新事务对该主键ID 执行一个select操作的时候 会生成相应readview 然后该readview就不变了
  4. 4:该readview主要由 undo_log 中的 已提交事务ID以及未提交事务ID 组成
  5. 5:然后会根据 readview 来执行 undo_log链表的判断 哪个数据应该被查询出来
  6. 1:写操作产生 undo_log
  7. 2undo_log链表组成
  8. 3:读操作产生 readview
  9. 4readview 组成
  10. 5:根据 readview 来执行 undo_log链表的判断