mysql MVCC 多版本并发控制
在研究 MVCC 之前需要先了解 2 个概念
当前读
查询操作
select * from xx for update (排他锁)或者 select * from xx lock in share mode(共享锁)
我们平时一般不用这个
增删改
增删改都是当前读操作, 修改语句都需要先读取数据, 判断约束是否正确, 比如主键索引 唯一索引
当前读总是读取到最新的数据
快照读
普通的 select * from xx 平时一般我们用这个
快照读读取的是历史版本数据
一. 复习 不可重复读 和 幻读
-
不可重复读
1 个事务第一次查询了某条记录,然后过了一会再次查询的时候发现同样 ID 的记录和第一次查询的不一样
解决方式 隔离级别调整成 可重复读
-
幻读
1 个事务第一次查询列表返回 10 条记录,然后过了一会再次查询的时候发现变成了 11 条记录
解决方式 隔离级别调整成 串行化
- 隔离级别 只针对于快照读
- 注意红色字体, 产生不可重复读 和 幻读的前提 都是查询了 2 次, 也就说说中间的时间点,其他的事务干了 1 些别的事情
二. 隐藏列
我们创建的表,其实有 2-3 个隐藏的列,隐藏主键(如果表有自己的主键,就没有这列),事务 ID,回滚指针
(用户表)
用户名 | 密码 | 邮箱 | 年龄 | 隐藏主键 DB_ROW_ID | 事务 ID DB_TRX_ID | 回滚指针 DB_ROLL_PTR |
---|---|---|---|---|---|---|
张三 | *** | *** | 11 | 1 | ? | ? |
李四 | *** | *** | 12 | 2 | ? | ? |
三. readview
在进行快照读的时候,会生成一个 readview 读视图
readview 有 4 个属性
活跃的事务 ID 集合 (就是没有 commit 的事务) |
列表中的最小事务 ID | 尚未分配的下一个事务 ID | 创建该视图的事务 ID |
---|
PS:为什么用中文 因为网上查到的字段有 2 种, 可能是因为数据库版本问题导致的变量名不一样, 但是意思是一样的
四. undolog
在事务中增删改, 都会生成 undolog
每次修改的回滚指针都会指向上一个版本的记录,如果 rollback,就会找到对应的记录恢复
undolog 不会在 commit 之后立刻删除,而是放入待清理的链表,由 purge 线程判断是否有其他事务在使用.来决定是否删除
如下图:
四. 可见性算法(比较的规则)
- 当前记录的事务 ID是否小于 readview列表中的最小事务 ID,如果小于,代表了这条记录在这个 readview 创建之前就已经 commit 了,所以能看见,否则进行下一步判断
- 当前记录的事务 ID是否大于等于readview 的尚未分配的下一个事务 ID,如是大于等于就代表这条记录是在 readview 之后生成的,所以不能看到,否则进入一下一步判断
- 当前记录的事务 ID是否在活跃的事务 ID 集合中, 如果在,就说明这条记录在 readview 创建的时候还没有 commit,也就是说当前事务不能看到该记录,否则就能看到
五. 案例分析 1
在下面时间线之前已经存在了一条张三的记录
隔离级别: 可重复读
ID | name | age |
---|---|---|
1 | 张三 | 10 |
时间线 | 事务 1 | 事务 2 |
---|---|---|
1 | 开启事务,分配到事务 ID1 | |
2 | 开启事务,分配到事务 ID2 | |
3 | 修改张三的年龄为 20,并且 commit | |
4 | 查询张三 |
结果?
张三的年龄为 20
为何: 因为查询的时候才产生 readview,所以事务 1 查询时在时间线 4 的时候产生的 readview 活跃的事务 ID 只有 1,最小事务 ID 也是 1,下一个分配的事务 ID 是 3
带入到可见性算法 三条全部不成立, 所以能看到
六. 案例分析 2
在下面时间线之前已经存在了一条张三的记录
隔离级别: 可重复读
ID | name | age |
---|---|---|
1 | 张三 | 10 |
时间线 | 事务 1 | 事务 2 |
---|---|---|
1 | 开启事务,分配到事务 ID1 | |
2 | 查询张三的记录,当前年龄为 10 | |
3 | 开启事务,分配到事务 ID2 | |
4 | 修改张三的年龄为 20,并且 commit | |
5 | 再次查询张三 |
结果?
张三的年龄为 10
为何: 因为 隔离级别: 可重复读 的时候, 一个事务中的多次读操作, 重复使用第一次读操作产生的 readview
在时间线 2 的时候 产生的 readview 活跃的事务 ID 为1 和 2,最小事务 ID 是 1,下一个分配的事务 ID 是 3
时间线 5 的查询的 readview 还是时间线 2 的 readview
带入到可见性算法 第三条成立, 所以不能看见 undolog 里新的那条年龄 20 的记录
七. 案例分析 3
在下面时间线之前已经存在了一条张三的记录
隔离级别: 读已提交
ID | name | age |
---|---|---|
1 | 张三 | 10 |
时间线 | 事务 1 | 事务 2 |
---|---|---|
1 | 开启事务,分配到事务 ID1 | |
2 | 查询张三的记录,当前年龄为 10 | |
3 | 开启事务,分配到事务 ID2 | |
4 | 修改张三的年龄为 20,并且 commit | |
5 | 再次查询张三 |
和案例分析 2 唯一的区别就是隔离级别不一样
结果?
张三年龄为 20
为何: 因为隔离级别: 读已提交 和 隔离级别: 可重复读 的区别就在于, 隔离级别: 读已提交 每次查询都会创建 1 个新的 readview,时间线 2 和时间线 5 产生了 2 个 readview
八. 案例分析 4
在下面时间线之前已经存在了一条张三的记录
隔离级别: 可重复读
ID | name | age |
---|---|---|
1 | 张三 | 10 |
时间线 | 事务 1 | 事务 2 |
---|---|---|
1 | 开启事务,分配到事务 ID1 | |
2 | 查询李四(查询不到) | |
3 | 开启事务,分配到事务 ID2 | |
4 | 插入李四的记录,并且 commit | |
5 | 查询李四 |
结果?
查询不到李四
为何: 因为 隔离级别: 可重复读 的时候, 一个事务中的多次读操作, 重复使用第一次读操作产生的 readview
在时间线 2 的时候 产生的 readview 活跃的事务 ID 为1 和 2,最小事务 ID 是 1,下一个分配的事务 ID 是 3
时间线 5 的查询的 readview 还是时间线 2 的 readview
带入到可见性算法 第三条成立, 所以不能看见 undolog 里新的那条李四的记录
目前看起来好像解决了幻读问题
八. 其他注意点
MVCC 并没有完全解决幻读, 只是在快照读的时候解决了幻读
时间线 | 事务 1 | 事务 2 |
---|---|---|
1 | 开启事务,分配到事务 ID1 | |
2 | 查询李四(查询不到) | |
3 | 开启事务,分配到事务 ID2 | |
4 | 插入李四的记录,并且 commit | |
5 | 修改李四的年龄为 30 或者 全表修改年龄为 30 | |
5 | 使用 select * from user for update 语句 | |
6 | 查询李四(时间线 5 发生的话 就能查询到数据) |
注意时间线 5 ,有 2 个时间线 5,当中任何 1 个发生,都会使时间线 6 能查询到结果,当事务中有修改操作 或者 手动使用当前读之后(必须包含该记录,where 条件能覆盖到新插入的数据) 之后的查询会重新生成 readview
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于