系列文章目录
第1章 MySQL系列文章之子查询
第2章 MySQL系列文章之表的操作和约束
第3章 MySQL系列文章之表的视图和存储过程
第4章 MySQL系列文章之逻辑架构
第5章 MySQL系列文章之存储引擎
第6章 MySQL系列文章之索引的数据结构
第7章 MySQL系列文章之索引的创建与设计原则
第8章 MySQL系列文章之索引的性能分析工具的使用
第9章 MySQL系列文章之索引优化与查询优化
第10章 MySQL系列文章之数据库其它调优策略
第11章 MySQL事务和事务日志
第12章 MySQL的锁
第13章 MySQL的多版本并发控制
第14章 MySQL日志和主从复制
第15章 基于Docker的MySQL备份
目录
一、MVCC概述
MVCC (Multiversion Concurrency Control),多版本并发控制。 是通过数据行的多个版本来实现数据库的并发控制。
它使得在InnoDB的事务隔离级别下执行一致性读操作有了保证。换言之,就是为了查询一些正在被另一个事务更新的行,并且可以看到它们被更新之前的值,这样在做查询的时候就不用等待另一个事务释放锁。是采用乐观锁思想的一种方式。
二、快照读与当前读
2.1、快照读
快照读又叫一致性读,读取的是快照数据。不加锁的简单的 SELECT 都属于快照读,即不加锁的非阻塞读。读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
优点:快照读是基于MVCC实现的,避免了加锁操作,降低了开销。
2.2、当前读
当前读的是记录的最新版本数据,会加锁,保证其他并发事务不能修改当前记录。加锁的 SELECT、增删改都会进行当前读。
三、MVCC原理
MVCC 的实现依赖于:隐藏字段、Undo Log、Read View。

3.1、ReadView概述
3.2、ReadView设计思路
4个隔离级别中,第1种READ UNCOMMITTED和第4中SERIALIZABLE读到的数据都是最新的版本,不使用MVCC。
而第2种READ COMMITTED和第3种REPEATABLE READ必须保证读到的是已经提交的事务修改的记录。假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的,这是ReadView要解决的主要问题。
ReadView中4个参数:
参数名 | 说明 |
---|---|
creator_trx_id | 创建这个 ReadView 的事务 ID,新开事务读操作时为0,当前事务读操作时为当前事务ID |
trx_ids | 生成ReadView时,瞬时状态下活跃的事务id列表 |
up_limit_id | trx_ids中的最小值 |
low_limit_id | 生成ReadView时,系统中要分配给下一个事务的id值,是系统最大的事务id值 |
3.3、ReadView的规则
假如一个事务访问数据库记录,记数据库中最新的记录的事务ID为trx_id。
下边的步骤判断记录的某个版本是否可见:
- 若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_id属性值是不是等于trx_ids 列表中某个值
如果等于,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
如果不等于,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
3.4、MVCC操作流程
- 首先获取事务自己的版本号,也就是事务 ID;
- 获取 ReadView;
- 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
- 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
- 最后返回符合规则的数据。
注意:
1、隔离级别在读已提交时,每次查询都会重新生成Read View。Read View不同时,可能产生不可重复读和幻读。
2、隔离级别可重复读时,每次查询都使用该事务第一次查询生成的Read View。
四、举例
4.1、READ COMMITTED隔离级别下
现在有两个事务id 分别为10 、20 的事务在执行:
# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
此刻,表student 中id 为1 的记录得到的版本链表如下所示:
假设现在有一个使用READ COMMITTED 隔离级别的事务开始执行:
# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'
之后,我们把事务id 为10 的事务提交一下:
# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
COMMIT;
然后再到事务id 为20 的事务中更新一下表student 中id 为1 的记录:
# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
UPDATE student SET name="钱七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;
此刻,表student中id 为1 的记录的版本链就长这样:
然后再到刚才使用READ COMMITTED 隔离级别的事务中继续查找这个id 为1 的记录,如下:
# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'
# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'王五'
4.2、REPEATABLE READ隔离级别下
例子同上,只产生一个ReadView
# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1:Transaction 10、20均未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值为'张三'
# SELECT2:Transaction 10提交,Transaction 20未提交
SELECT * FROM student WHERE id = 1; # 得到的列name的值仍为'张三'
参考:MySQL高级篇-宋红康