6、MySQL之MVCC

时间:2024-06-01 14:40:17

6、MySQL之MVCC

  • MVCC
    • undo日志版本链
    • read view快照
    • 比对规则
    • MVCC示例演示
    • 总结

MVCC

  mvcc(Multi-Version Concurrency Control)多版本并发控制,是通过多个修改的历史版本来代替锁,实现事务之间的隔离效果,保证非阻塞读。mvcc的实现主要是通过undo日志版本链read view快照。Mysql在读已提交可重复读隔离级别下都实现了MVCC机制。

undo日志版本链

  为了实现事务的原子性,InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。一般每对一条记录做一次改动,就对应着一条undo日志,但在某些更新记录的操作中,也可能会对应着2条undo日志。
  一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的undo日志,这些undo日志会被从0开始编号,也就是说根据生成的顺序分别被称为第0号undo日志、第1号undo日志、…、第n号undo日志等,这个编号也被称之为undo no。
  begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个写操作InnoDB表的语句,事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。
  undo日志主要中主要包含数据库数据、undo no、trx_id和roll_pointer等信息;
在这里插入图片描述

read view快照

  read view快照主要包含未提交事务的事务id数组和已生成最大的事务id。
  执行select语句的时候,会生成一个read view快照,从undo日志版本链中拿第一条修改记录中的事务id和read view中未提交事务的id数组去比较,如果数组中包含这个事务id,说明这个事务还没提交而且也不是当前事务修改的,这个修改记录是不显示的,再去拿下一个版本去比较,直到找到事务id不在当前未提交事务id的数组中版本,
mysql在读已提交和可重复从读两个隔离级别下都有实现mvcc机制,主要的区别read view生成的时机不一样,读已提交是在每次执行select语句的时候都会生成一份read view,可重复读是在每次开启事务后在第一次执行select语句的时候才会生成。

比对规则

  1. 如果 undo版本链中的 trx_id > read view中最大事务id,表示这个版本是在read view创建之后生成的,是不可见的;
  2. 如果 undo版本链中的 trx_id <= read view中最大事务id,此时判断read view中未提交的事务id数组是否包含trx_id ;
  3. read view中未提交的事务id数组包含trx_id,对于当前read view来说是未提交事务,不显示;
  4. read view中未提交的事务id数组不包含trx_id,对于当前read view来说是已提交事务,显示;

MVCC示例演示

1、准备测试数据

CREATE TABLE `account`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `balance` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

INSERT INTO `account` (`name`, `balance`) VALUES ('lilei', '400');
INSERT INTO  `account` (`name`, `balance`) VALUES ('hanmei', '16000');
INSERT INTO `account` (`name`, `balance`) VALUES ('lucy', '2400');

2、打开四个客户端,连接数据库,确认数据库隔离级别(本次演示采用可重复读隔离级别),确认示例数据;

-- 版本 5.7
show variables like 'tx_isolation';
-- 版本 8.0
show variables like '%isolation%';

-- 5.7版本设置 可重复读
set tx_isolation='repeatable-read';
-- 8.0版本设置 可重复读
set transaction_isolation='repeatable-read';

-- 5.7版本设置 读已提交
set tx_isolation='read-committed';
-- 8.0版本设置 读已提交
set transaction_isolation='read-committed';

-- 确认示例数据;
select * from account;

在这里插入图片描述
3、客户端A执行

begin;
update account set name = 'transaction_A' where id = 4;

4、客户端B执行

begin;
update account set name = 'transaction_B' where id = 5;

5、客户端C执行

begin;
update account set name = 'transaction_C' where id = 6;
commit;

6、客户端D执行

begin;
select * from account where id = 6;

在这里插入图片描述
MVCC查询过程分析
  假设客户端A事务id=100;假设客户端B事务id=200;假设客户端C事务id=300;
  此时客户端D在执行begin操作时,客户端C已经完成事务提交,所以客户端D的read view(一致性视图):未提交的事务id数组=[100,200],最大事务id=300;
  此时id=6这条数据的undo日志如下:
在这里插入图片描述
  客户端D在查询id=6的数据时:
  ① 拿第一条编号是1的undo日志,trx_id=300,发现300 <= read view中最大事务id300,并且不在read view的未提交事务中,展示;

7、客户端A执行

update account set name = 'transaction_A_1' where id = 6;
update account set name = 'transaction_A_2' where id = 6;

8、客户端D执行

select * from account where id = 6;

在这里插入图片描述
MVCC查询过程分析
  客户端D的read view(一致性视图):未提交的事务id数组=[100,200],最大事务id=300;
  此时id=6这条数据的undo日志如下:
在这里插入图片描述
  客户端D在查询id=6的数据时:
  ① 拿第一条编号是3的undo日志,trx_id=100,发现100 <= read view中最大事务id300,但在read view的未提交事务中,不展示;
  ② 拿第二条编号是2的undo日志,trx_id=100,发现100 <= read view中最大事务id300,但在read view的未提交事务中,不展示;
  ③ 拿第三条编号是1的undo日志,trx_id=300,发现300 <= read view中最大事务id300,并且不在read view的未提交事务中,展示;
9、客户端A执行

commit;

10、客户端B执行

update account set name = 'transaction_B_1' where id = 6;
update account set name = 'transaction_B_2' where id = 6;

11、客户端D执行

select * from account where id = 6;

在这里插入图片描述
MVCC查询过程分析
  客户端D的read view(一致性视图):未提交的事务id数组=[100,200],最大事务id=300;
  此时id=6这条数据的undo日志如下:
在这里插入图片描述

  客户端D在查询id=6的数据时:
  ① 拿第一条编号是5的undo日志,trx_id=200,发现200 <= read view中最大事务id300,但在read view的未提交事务中,不展示;
  ② 拿第二条编号是4的undo日志,trx_id=200,发现200 <= read view中最大事务id300,但在read view的未提交事务中,不展示;
  ③ 拿第三条编号是3的undo日志,trx_id=100,发现100 <= read view中最大事务id300,但在read view的未提交事务中,不展示;
  ④ 拿第四条编号是2的undo日志,trx_id=100,发现100 <= read view中最大事务id300,但在read view的未提交事务中,不展示;
  ⑤ 拿第五条编号是1的undo日志,trx_id=300,发现300 <= read view中最大事务id300,并且不在read view的未提交事务中,展示;

12、新建客户端E,首先确认事务隔离级别(参考第2步),执行

begin;
select * from account where id = 6;

在这里插入图片描述

MVCC查询过程分析
  客户端E在执行begin时,客户端A和客户端C的事务已提交,客户端E的read view(一致性视图):未提交的事务id数组=[200],最大事务id=300;
  此时id=6这条数据的undo日志如下:
在这里插入图片描述

  客户端E在查询id=6的数据时:
  ① 拿第一条编号是5的undo日志,trx_id=200,发现200 <= read view中最大事务id 300,但在read view的未提交事务中,不展示;
  ② 拿第二条编号是4的undo日志,trx_id=200,发现200 <= read view中最大事务id 300,但在read view的未提交事务中,不展示;
  ③ 拿第三条编号是3的undo日志,trx_id=100,发现100 <= read view中最大事务id 300,并且100不在read view的未提交事务中,展示;

13、客户端B执行

commit;

14、客户端E执行

update account set name = 'transaction_E_1' where id = 6;
update account set name = 'transaction_E_2' where id = 6;
commit;

11、客户端D执行

select * from account where id = 6;

在这里插入图片描述
MVCC查询过程分析
  假设客户端E事务id=400;
  客户端D的read view(一致性视图):未提交的事务id数组=[100,200],最大事务id=300;
  此时id=6这条数据的undo日志如下:
在这里插入图片描述
  客户端D在查询id=6的数据时:
  ① 拿第一条编号是7的undo日志,trx_id=400,发现400 > read view中最大事务id 300,不展示;
  ② 拿第二条编号是6的undo日志,trx_id=400,发现400 > read view中最大事务id 300,不展示;
  ③ 拿第三条编号是5的undo日志,trx_id=200,发现200 <= read view中最大事务id 300,但在read view的未提交事务中,不展示;
  ④ 拿第四条编号是4的undo日志,trx_id=200,发现200 <= read view中最大事务id 300,但在read view的未提交事务中,不展示;
  ⑤ 拿第五条编号是3的undo日志,trx_id=100,发现100 <= read view中最大事务id 300,但在read view的未提交事务中,不展示;
  ⑥ 拿第六条编号是2的undo日志,trx_id=100,发现100 <= read view中最大事务id 300,但在read view的未提交事务中,不展示;
  ⑦ 拿第七条编号是1的undo日志,trx_id=300,发现300 <= read view中最大事务id 300,并且不在read view的未提交事务中,展示;

总结

  MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。