欢迎来到啊妮莫的学习小屋 |
祝读本文的朋友都天天开心呀 |
目录
- MVCC简介
- 快照读与当前读
- 快照读
- 当前读
- 隔离级别
- 隐藏字段和Undo Log版本链✨
- MVCC原理--ReadView✨
- ReadView简介
- 设计思路
- 适用隔离级别
- 重要内容
- ReadView规则
- MVCC整体流程
- 不同隔离级别下的MVCC
- 读已提交
- 可重复读
- 总结
MVCC简介
MVCC
也称: 多版本并发控制
. 顾名思义, MVCC是通过数据行的多个版本管理来实现数据库的并发控制. MVCC
使得在InnoDB
的事务隔离级别下, 执行一致性读
操作有了保证. 简单来说就是: 在需要读取一些正在被另一个事务更新的行数据时, 读取之前的历史版本数据(旧数据); 而不需要等待另一个事务释放锁.
并不是所有的存储引擎都支持MVCC
技术, 本文讲解的是MySQL
中InnoDB
存储引擎下的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 ...
当前读可以形象的理解为生活中的直播, 看到到的画面是实时的, 最新的.
隔离级别
事务有四种隔离级别: 读未提交
, 读已提交
, 可重复读
, 串行化
.
读未提交
: 其他事务可以看见未提交事务做出的数据改变. 也就是会发生脏读
.读已提交
: 其他事务只能看到已经提交事务做出的数据改变. 若另一个事务不断的更新某一行数据并提交, 那么读取出来的数据前后不一致, 即不可重复读
.可重复读
: 解决了不可重复读
问题, 确保同一事务多次读取结果一致. 但是依然会发生幻读
问题–另一个事务插入新的数据行, 那么读取该范围内的数据时, 会发现新的"幻影"行.串行化
: 最严格的隔离级别, 通过加锁
避免了脏读, 不可重复读和幻读, 但是性能较差.
MySQL
的默认隔离级别是可重复读
可重复读
隔离级别解决了脏读
和不可重复读
问题, 未解决幻读
问题.
但是, MySQL
中的MVCC
机制解决了幻读
问题! 它可以在大多数情况下代替行级锁, 提高数据库的事务并发能力.
隐藏字段和Undo Log版本链✨
对于使用InnoDB
存储引擎的表来说, 其聚簇索引中的行数据包含了三个隐藏字段: row_id
, trx_id
, roll_pointer
.
其中trx_id
和roll_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
使用锁
机制来避免读写冲突
.
重要内容
creator_trx_id
: 创建这个ReadView
的事务ID
.trx_ids
: 在生成ReadView
时, 当前系统中活跃的读写事务的ID列表
.up_limit_id
: 活跃的事务中最小的事务ID
.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规则
当需要读取某条记录的时候, 只需要按照以下步骤就可以判断该记录的某个版本是否可见.
- 当读取版本的
trx_id
=creator_trx_id
, 也就是当前事务修改过的记录–可见. trx_id
<up_limit_id
, 该版本的事务已经在生成ReadView
之前就提交了–可见.trx_id
>=low_limit_id
, 该版本的事务是在生成ReadView
之后才开启的–不可见.up_limit_id
<=trx_id
<low_limit_id
, 则需要分情况讨论- 不在
trx_ids
列表中, 说明该事务已经提交–可见. - 在列表中, 该事务在生成
ReadView
时处于活跃状态–不可见.
- 不在
如图所示:
MVCC整体流程
- 获取自己的事务版本号:
creator_trx_id
- 生成ReadView
- 将查询到的数据, 与
ReadView
中的事务版本号进行对比 - 若可见, 则从
Undo Log
中获取历史快照, 否则顺着版本链找到下一个数据, 重复3,4. - 若最后一个版本也不可见, 则意味着这条记录对该事务是完全不可见的, 查询结果就不包含该记录.
🌈
说明: MVCC是通过隐藏字段生成的Undo Log版本链, 加上ReadView规则帮我们判断当前版本的数据是否可见.
不同隔离级别下的MVCC
读已提交
在隔离级别为读已提交
时,一个事务中的每一次 SELECT 查询都会重新获取一次Read View
🌈
注意: 此时同样的查询语句都会重新获取一次
Read View
这时如果Read View
不同,就可能产生不可重复读
或者幻读
的情况
可重复读
当隔离级别为可重复读
的时候,就避免了不可重复读
,这是因为一个事务只在第一次 SELECT 的时候会获取一次 ReadView
,而后面所有的 SELECT 都会复用这个ReadView
总结
本文介绍了MVCC
在READ COMMITTD
、REPEATABLE READ
这两种隔离级别下事务在执行快照读
操作时访问记录的版本链的过程。这样使不同事务的读-写操作
并发执行,从而提升系统性能。
核心点在于 ReadView 的原理,READ COMMITTD
、REPEATABLE READ
这两个隔离级别的一个很大不同就是生成ReadView的时机不同:
READ COMMITTD
在每一次进行普通SELECT操作前都会生成一个ReadViewREPEATABLE READ
只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
🌈
说明: 执行DELETE语句或者更新主键的UPDATE语句并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的delete mark操作; 相当于只是对记录打上了一个删除标志位,这主要就是为MVCC服务的
通过MVCC
可以解决:
-
读写冲突问题
: 通过MVCC可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升数据库并发处理能力 -
降低了死锁的概率
: 这是因为MVCC采用了乐观锁的方式. 读取数据时并不需要加锁,对于写操作,也只锁定必要的行 -
解决快照读的问题
: 当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果
如果本篇文章对你有所帮助的话, 不要忘了给我点个赞哦~ 笔芯💞