# 前言

在数据库的四种隔离级别和三种异常中,读已提交可以解决脏读的异常,可重复读可以解决不可重复的的异常,串行化可以解决幻读的问题,但是串行化的方式会将数据库的事务并发程度降到最低。MVCC是一种利用乐观锁的想法,不采用锁机制来解决不可重复读和幻读问题。它在大多数情况下可以替代行级锁同时降低系统的开销。
image.png

# 快照读和当前读

|快照读

快照读读取的是记录的可见版本(可能是历史数据)。InnoDB执行select时,默认使用快照读,也可以说未加锁的SELECT都属于快照读。

  1. SELECT * FROM table ...;

假设事务B执行insert提交后,事务A执行了select,那么返回的数据中就会有事务B添加的那条数据,之后无论其他事务是否提交或者修改数据,select语句查找的数据都不会改变。

再假设事务A的逻辑是:if sex = ‘男’,执行:

  1. UPDATE tb_user SET type = 1 where id = 8;

如果在事务A没有commit之前,事务B修改了性别,因为读取的是快照信息,就仍然会执行上述语句。快照读这时就会出现问题。

|当前读

当前读就是读取最新的数据。MVCC中的增删改查(INSERTUPDATEDELETESELECT)需要加锁操作,都属于当前读。其中SELECT可以加锁或者不加锁。

  1. INSERT;
  2. UPDATE;
  3. DELETE;
  4. /* SELECT可以强制加锁 */
  5. SELECT * FROM table WHERE ? lock in share mode; /* 加S锁 */
  6. SELECT * FROM table WHERE ? for update; /* 加X锁 */

例如使用SELECT * FROM able WHERE ? for update;当前读每次读的数据就可能不相同而出现幻读问题,会查询到其他事务insert的数据。

# 一致性非锁定读和锁定读

|一致性非锁定读

一致性非锁定读指的是如果要读取的行记录被加 X 锁,读取操作不会等待行记录上的锁释放,而是会读取行的最新快照数据。InnoDB在 RC 和 RR 两种隔离级别中会使用一致性非锁定读,但是这两种隔离级别对于读取的快照数据不相同:

  • RC:总是读取行记录的最新版本。如果行被加锁,不会等待锁的释放,而是回去读取行记录的最新快照记录;
  • RR:总是读取事务开始时的快照数据版本;

    |一致性锁定读

    通过加锁保证数据逻辑的一致性。 ```sql / 对于读取的行记录加X锁,其他事务不能对行加任何锁 / SELECT … FOR UPDATE;

/ 对于读取的行记录添加一个 S 共享锁。其它事务可以向被锁定的行加 S 锁,但是不允许添加 X 锁,否则会被阻塞住 / SELECT … LOCK IN SHARE MODE;

  1. <a name="w6aWQ"></a>
  2. ## |区别
  3. - 一致性非锁定读的情况下其他事务仍然可以读取数据,极大地提高了数据库的并发性。
  4. - 一致性锁定读适用于对数据一致性要求比较高的情况,需要我们对读操作进行加锁从而保证数据逻辑的一致性。
  5. <a name="WXC5J"></a>
  6. # # InnoDB中的MVCC
  7. MVCC(Multiversion Concurrency Control)通过数据行的多个版本管理来实现数据库的并发控制,主要思想为保存数据的历史版本,通过事务的ID来判断数据是否显示,读取数据时不需要加锁同时可以保证事务的隔离等级。InnoDB主要通过Undo Log和Read View来实现MVCC。
  8. <a name="nWhaA"></a>
  9. ## |事务ID和隐藏列
  10. 在数据库中每开启一个事务,该事务就会有一个事务版本号。事务版本号是**自增长**的,通过事务ID的大小,我们可以判断事务的时间顺序。
  11. InnoDB的行记录中存储了重要的隐藏字段,其中包括了行ID、事务ID和回滚指针。行记录中存储的事务ID为最后一个对该数据进行修改更新的事务ID。回滚指针就是指向这个记录的Undo Log信息。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2323967/1646809203314-319d2183-5ca1-4a93-8d89-6af67d7a36d0.png#clientId=uefe1fa0d-ebae-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=123&id=uc6f2e26f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=178&originWidth=838&originalType=binary&ratio=1&rotation=0&showTitle=true&size=25373&status=done&style=stroke&taskId=u84609e6c-91b7-44e3-a1ce-f9cbe01df85&title=%E8%A1%8C%E8%AE%B0%E5%BD%95%E4%B8%AD%E7%9A%84%E9%9A%90%E8%97%8F%E5%AD%97%E6%AE%B5&width=578 "行记录中的隐藏字段")
  12. <a name="JEpLo"></a>
  13. ## |Undo Log
  14. InnoDB将行记录保存在了Undo Log中,我们可以通过回滚指针来找到它们,回滚指针将数据行的所有快照记录通过链表的数据结构类型串联起来。同样,每个快照都保存了当时修改该记录的事务ID。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2323967/1646810470838-33da9985-2d46-4c56-b038-68f988a62bac.png#clientId=uefe1fa0d-ebae-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=322&id=u0f260891&margin=%5Bobject%20Object%5D&name=image.png&originHeight=538&originWidth=838&originalType=binary&ratio=1&rotation=0&showTitle=true&size=74543&status=done&style=stroke&taskId=u8279eb20-0304-455b-931d-0618e82c556&title=Undo%20Log%E9%80%9A%E8%BF%87%E5%9B%9E%E6%BB%9A%E6%8C%87%E9%92%88%E5%B0%86%E5%8E%86%E5%8F%B2%E8%A1%8C%E8%AE%B0%E5%BD%95%E4%B8%B2%E8%81%94&width=501 "Undo Log通过回滚指针将历史行记录串联")
  15. <a name="WY3Az"></a>
  16. ## |Read View
  17. Read View中保存了当前事务开始时所有活跃(未提交)的事务列表。Read View包含了四个重要属性:
  18. - **trx_ids:**系统当前正在活跃的事务 ID 集合。
  19. - **up_limit_id:**活跃的事务中最大的事务 ID。
  20. - **low_limit_id:**活跃的事务中最小的事务 ID。
  21. - **creator_trx_idL:**创建这个 Read View 的事务 ID。
  22. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/2323967/1646810049005-e20c4b31-6d4a-4cb6-aeac-6524f5afda67.png#clientId=uefe1fa0d-ebae-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=146&id=u977babb9&margin=%5Bobject%20Object%5D&name=image.png&originHeight=223&originWidth=760&originalType=binary&ratio=1&rotation=0&showTitle=true&size=21917&status=done&style=stroke&taskId=u9860d8e5-91cd-4154-b4ba-a8940a00390&title=Read%20View%E4%B8%AD%E4%BF%9D%E5%AD%98%E4%BA%86%E5%90%84%E7%A7%8D%E4%BA%8B%E5%8A%A1ID%E9%9B%86%E5%90%88&width=498 "Read View中保存了各种事务ID集合")<br />如果当前事务creator_trx_id要读取某个行记录,需要和该行记录的事务ID(trx_id)比较,出现的所有情况如下:
  23. 1. **trx_id < low_limit_id:**说明修改该行记录的事务在当前活跃事务创建之前就已经提交,所以该行记录对事务creator_trx_id是可见。
  24. 1. **trx_id > up_limit_id:**说明修改该行记录的事务在当前活跃事务创建之后才能提交,所以该行记录对事务creator_trx_id不可见。
  25. 1. **low_limit_id < trx_id < up_limit_id:**说明修改该行记录的事务可能在当前事务创建时还处于活跃的状态。此时在trx_ids中遍历,如果tri_id在tri_ids中,说明进行修改的事务还处于活跃的状态,所以该行记录不可见。如果tri_id不在tri_ids中,说明进行修改的事务已经提交,该行记录因此可见。
  26. <a name="UOE6W"></a>
  27. ## |MVCC的流程
  28. 1. 获取当前事务的 ID;
  29. 1. 获取 Read View;
  30. 1. 查询得到的数据,然后与 Read View 中的事务版本号进行比较;
  31. 1. 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
  32. 1. 最后返回符合规则的数据。
  33. **RC:**事务中的一次`select`会获取一次Read View。如果两次`select`的Read View不同时,会发生不可重复读或者幻读的异常。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2323967/1646811699871-ddd8ff79-3c5f-4d29-acbb-6accb66896d2.png#clientId=uefe1fa0d-ebae-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=174&id=udcc5919f&margin=%5Bobject%20Object%5D&name=image.png&originHeight=398&originWidth=1203&originalType=binary&ratio=1&rotation=0&showTitle=true&size=47220&status=done&style=stroke&taskId=u898b532e-8773-4e20-8a4b-a2366ae553d&title=%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E4%B8%BA%E8%AF%BB%E5%B7%B2%E6%8F%90%E4%BA%A4%E6%97%B6%EF%BC%8C%E4%BC%9A%E5%8F%91%E7%94%9F%E4%B8%8D%E5%8F%AF%E9%87%8D%E5%A4%8D%E6%88%96%E5%B9%BB%E8%AF%BB%E7%9A%84%E5%BC%82%E5%B8%B8&width=525 "隔离级别为读已提交时,会发生不可重复或幻读的异常")<br />**RR:**事务只在第一次`select`获取Read View,之后的事务中所有`select`都会重复使用该 Read View。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/2323967/1646811703914-18752bd3-59dc-4d7e-96ca-4eeac1d2110d.png#clientId=uefe1fa0d-ebae-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=181&id=u8c4284b1&margin=%5Bobject%20Object%5D&name=image.png&originHeight=341&originWidth=1012&originalType=binary&ratio=1&rotation=0&showTitle=true&size=35769&status=done&style=stroke&taskId=u3d6d62b8-d2b8-4831-838c-9adb1c47312&title=%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E4%B8%BA%E5%8F%AF%E9%87%8D%E5%A4%8D%E8%AF%BB%E6%97%B6%EF%BC%8C%E5%8F%AA%E4%BC%9A%E5%8F%91%E7%94%9F%E5%B9%BB%E8%AF%BB%E7%9A%84%E5%BC%82%E5%B8%B8&width=538 "隔离级别为可重复读时,只会发生幻读的异常")
  34. <a name="L0nzh"></a>
  35. # # MVCC解决幻读
  36. InnoDB中的行锁有三种方式:
  37. 1. **Record Lock:**记录锁,针对单个行记录添加锁。
  38. 1. **Gap Lock:**间隙锁,可以帮我们锁住一个范围(索引之间的空隙),但不包括记录本身。采用间隙锁的方式可以防止幻读情况的产生。
  39. ```sql
  40. /* 其他事务不能在t.c中插入15 */
  41. SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
  1. Next-Key Lock:帮我们锁住一个范围(左必右开),同时锁定记录本身,相当于间隙锁 + 记录锁。该锁主要用来解决幻读的问题。例如范围中包含10, 11, 13, 20,那么就需要锁定一些区间:
    1. (-∞, 10]
    2. (10, 11]
    3. (11, 13]
    4. (13, 20]
    5. (20, +∞)
    在隔离级别为RR时,InnoDB 会采用 Next-Key 锁机制解决幻读问题。
    image.png
    image.png

    # 总结

    MVCC 的核心就是 Undo Log+ Read View,“MV”就是通过 Undo Log 来保存数据的历史版本,实现多版本的管理,“CC”是通过 Read View 来实现管理,通过 Read View 原则来决定数据是否显示。同时针对不同的隔离级别,Read View 的生成策略不同,也就实现了不同的隔离级别。

MVCC主要解决了三种问题:

  • 读写之间的阻塞问题
  • 降低了死锁的概率
  • 解决一致性问题

    # 参考

  1. 当前读与快照读-简书
  2. 当前读与快照读-掘金
  3. mysql MVCC不能避免幻读
  4. 一致性非锁定锁读和一致性锁定读
  5. Mysql的一致性锁定读和一致性非锁定
  6. Innodb锁机制:Next-Key Lock 浅谈