【MySQL】MVCC详解, 图文并茂简单易懂

欢迎来到的学习小屋

祝读本文的朋友都天天开心呀

目录

MVCC简介

MVCC也称: 多版本并发控制. 顾名思义, MVCC是通过数据行的多个版本管理来实现数据库的并发控制. MVCC使得在InnoDB的事务隔离级别下, 执行一致性读操作有了保证. 简单来说就是: 在需要读取一些正在被另一个事务更新的行数据时, 读取之前的历史版本数据(旧数据); 而不需要等待另一个事务释放锁.

并不是所有的存储引擎都支持MVCC技术, 本文讲解的是MySQLInnoDB存储引擎下的MVCC机制.

快照读与当前读

MVCC机制主要解决的是读--写冲突问题, 提高数据库的并发性能.

当发生读写冲突时:

采用快照读, 不加锁 非阻塞并发读

采用当前读, 加锁

MVCC机制本质上是采用了乐观锁思想

快照读

又名一致性读, 读取到的是快照数据. 不加锁的简单SELECT都是快照读.

例如:

复制代码
SELECT * FROM student WHERE...

快照读可能读到的并不一定是数据的最新版本, 而有可能是之前的历史版本.

快照读可以形象的理解为我们生活中的照片, 拍摄到的画面都是过去式.

当前读

当前读读取到的是 最新版本数据.

读取时需要保证其他并发事务不能修改当前记录, 因此需要加锁.

例如:

复制代码
SELECT * FROM student LOCK IN SHARE MODE;    # 共享锁


SELECT * FROM student FOR UPDATE;    # 排他锁


-- 修改操作对应的排他锁
INSERT INTO student VALUES ...    
DELETE FROM student WHERE ...
UPDATE student SET ...

当前读可以形象的理解为生活中的直播, 看到到的画面是实时的, 最新的.

隔离级别

事务有四种隔离级别: 读未提交, 读已提交, 可重复读, 串行化.

  1. 读未提交: 其他事务可以看见未提交事务做出的数据改变. 也就是会发生脏读.
  2. 读已提交: 其他事务只能看到已经提交事务做出的数据改变. 若另一个事务不断的更新某一行数据并提交, 那么读取出来的数据前后不一致, 即不可重复读.
  3. 可重复读: 解决了不可重复读问题, 确保同一事务多次读取结果一致. 但是依然会发生幻读问题--另一个事务插入新的数据行, 那么读取该范围内的数据时, 会发现新的"幻影"行.
  4. 串行化: 最严格的隔离级别, 通过加锁避免了脏读, 不可重复读和幻读, 但是性能较差.

MySQL的默认隔离级别是可重复读
可重复读隔离级别解决了脏读不可重复读问题, 未解决幻读问题.

但是, MySQL中的MVCC机制解决了幻读问题! 它可以在大多数情况下代替行级锁, 提高数据库的事务并发能力.

隐藏字段和Undo Log版本链

对于使用InnoDB存储引擎的表来说, 其聚簇索引中的行数据包含了三个隐藏字段: row_id, trx_id, roll_pointer.

其中trx_idroll_pointer使得用户记录生成一条Undo Log版本链.

  • trx_id: 这个字段记录了最后修改行记录的事务ID.
  • roll_pointer: 这个字段是一个指针, 指向该行记录的回滚段, 在发生事务回滚时用来撤销行记录的修改.

例如: students的表数据如下

复制代码
mysql> SELECT * FROM students WHERE id=1;
+------+----------+----------+
| id   | stu_name | major    |
+------+----------+----------+
|    1 | 张三     | 软件工程 |
+------+----------+----------+
1 row in set (0.00 sec)

假设插入该记录的事务ID等于8, 则该条记录的示意图如下所示:

??

注意: insert undo只在事务回滚起作用, 当事务提交后, 该类型的undo日志就没有用了, 它占用的UndoLog Segment也会被系统回收(也就是该undo日志占用的Undo页面链表要么被重用, 要么被释放).

假设之后两个事务ID分别为10, 20的事务对该条数据进行UPDATE操作, 操作流程如下:

??

注意: 不能两个事务交叉更新同一条数据! 即: 一个事务修改另一个未提交的事务修改过的数据 (脏写).

InnoDB使用锁来保证不会有脏写的情况发生: 当一个事务更新了某条记录后, 会给这条记录加锁; 另一个事务需要等待锁被释放后才可以更新.

每次修改, 都会记录一条undo日志, 每一条undo日志也都会有一个roll_pointer字段, 将这些undo日志串成一个链表--Undo Log 版本链

Undo Log 版本链的头结点就是当前的最新记录. 所有版本依靠roll_ptr字段连接成一个链表.

MVCC原理--ReadView

MVCC的实现依赖于: 两个隐藏字段, Undo Log, ReadView.

ReadView简介

ReadView就是事务在使用MVCC机制进行快照读操作时产生的读视图.

当事务读取数据时, 会数据库系统生成当前的一个快照, InnoDb会为事务构造一个数组, 用于记录并维护系统中当前的活跃事务ID组(活跃是指: 开启了但是还没有进行提交).

设计思路

适用隔离级别

ReadView仅仅适用于读已提交可重复读隔离级别, 对于这两种隔离级别, 都必须保证读到的是已经提交的事务 修改过的记录. 假如另一个事务已经修改但是还没有提交, 是不能直接读取到的. 核心问题是判断版本链中哪些版本记录是当前事务可见的 ,这是ReadView要解决的主要问题.

对于读未提交: 读到的就是最新版本数据.

对于串行化: 事务排队执行, InnoDB使用机制来避免读写冲突.

重要内容
  1. creator_trx_id: 创建这个ReadView事务ID.
  2. trx_ids: 在生成ReadView时, 当前系统中活跃的读写事务的ID列表.
  3. up_limit_id: 活跃的事务中最小的事务ID.
  4. low_limit_id: 表示在生成ReadView时系统应该分配给下一个事务的ID, 也就是系统中最大的事务ID值.

??

注意: 对于只读事务, creator_trx_id 默认为0

举例:

若现有id为1, 2, 3这三个事务, 之后id=3的事务提交了;

那么一个新的读事务在生成ReadView的时, trx_ids=1,2, up_limit_id=1, low_limit_id的值为4

ReadView规则

当需要读取某条记录的时候, 只需要按照以下步骤就可以判断该记录的某个版本是否可见.

  1. 当读取版本的trx_id=creator_trx_id, 也就是当前事务修改过的记录--可见.
  2. trx_id<up_limit_id, 该版本的事务已经在生成ReadView之前就提交了--可见.
  3. trx_id>=low_limit_id, 该版本的事务是在生成ReadView之后才开启的--不可见.
  4. up_limit_id<=trx_id<low_limit_id, 则需要分情况讨论
    • 不在trx_ids列表中, 说明该事务已经提交--可见.
    • 在列表中, 该事务在生成ReadView时处于活跃状态--不可见.

如图所示:

MVCC整体流程

  1. 获取自己的事务版本号:creator_trx_id
  2. 生成ReadView
  3. 将查询到的数据, 与ReadView中的事务版本号进行对比
  4. 若可见, 则从Undo Log中获取历史快照, 否则顺着版本链找到下一个数据, 重复3,4.
  5. 若最后一个版本也不可见, 则意味着这条记录对该事务是完全不可见的, 查询结果就不包含该记录.

??

说明: MVCC是通过隐藏字段生成的Undo Log版本链, 加上ReadView规则帮我们判断当前版本的数据是否可见.

不同隔离级别下的MVCC

读已提交

在隔离级别为读已提交时,一个事务中的每一次 SELECT 查询都会重新获取一次Read View

??

注意: 此时同样的查询语句都会重新获取一次Read View这时如果Read View不同,就可能产生不可重复读或者幻读的情况

可重复读

当隔离级别为可重复读的时候,就避免了不可重复读,这是因为一个事务只在第一次 SELECT 的时候会获取一次 ReadView,而后面所有的 SELECT 都会复用这个ReadView

总结

本文介绍了MVCCREAD COMMITTDREPEATABLE READ这两种隔离级别下事务在执行快照读操作时访问记录的版本链的过程。这样使不同事务的读-写操作并发执行,从而提升系统性能。

核心点在于 ReadView 的原理,READ COMMITTDREPEATABLE READ这两个隔离级别的一个很大不同就是生成ReadView的时机不同:

  • READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView
  • REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。

??

说明: 执行DELETE语句或者更新主键的UPDATE语句并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的delete mark操作; 相当于只是对记录打上了一个删除标志位,这主要就是为MVCC服务的

通过MVCC可以解决:

  1. 读写冲突问题: 通过MVCC可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升数据库并发处理能力

  2. 降低了死锁的概率: 这是因为MVCC采用了乐观锁的方式. 读取数据时并不需要加锁,对于写操作,也只锁定必要的行

  3. 解决快照读的问题: 当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果

如果本篇文章对你有所帮助的话, 不要忘了给我点个赞哦~ 笔芯??

相关推荐
杉氧1 小时前
Navigation Compose 深度实践:如何优雅地串联起你的全栈 App?
android·架构·android jetpack
这个DBA有点耶2 小时前
AI写的SQL跑崩了生产库,这锅谁背?
数据库·人工智能·程序员
镜舟科技2 小时前
Databricks 再提 LTAP,AI 时代的数据底座为何重回大一统叙事?
数据库·架构·agent
Databend3 小时前
从湖仓升级为 Agent 时代的数据控制面,Snowflake 和 Databricks 有哪些布局
大数据·数据库·agent
雨白4 小时前
指针与数组的核心机制
android
ClouGence6 小时前
SQL Server CDC 能放到 Always On 备库读吗?一文讲透原理与实践
数据库·sql server
黄林晴9 小时前
Room 3.0 正式发布!包名彻底重构,KMP 成为核心主线
android·android jetpack
三少爷的鞋10 小时前
Kotlin 协程环境下的 DCL 懒加载:别把线程时代的经验直接搬过来
android
plainGeekDev10 小时前
Gson → kotlinx.serialization
android·java·kotlin