前言
之前文章中我们介绍过MySQL事务隔离级别及事务并发问题,当时说到InnoDB存储引擎通过多版本并发控制(Multiversion Concurrency Control 简称MVCC)机制解决了幻读问题,那我们今天就来看看到底是怎么解决的。
读这篇文章之前可以先了解一下MySQL中InnoDB数据结构
一、InnoDB引擎对隔离级别的支持
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
读未提交(read-uncommitted) | 可能 | 可能 | 可能 |
不可重复读(read-committed) | 不可能 | 可能 | 可能 |
可重复读(repeatable-read) | 不可能 | 不可能 | InnoDB不可能 |
串行化(serializable) | 不可能 | 不可能 | 不可能 |
隔离级别到底如何实现?
二、什么是 MVCC(多版本并发控制)
英文全称:
Multiversion concurrency control
白话解释:
并发访问(读或写)数据库时,对正在事务内处理的数据做 多版本的管理。以达到用来避免写操作的堵塞,从而引发读操 作的并发问题。
版本号管理
mvcc版本号是在行数据中记录(隐藏字段),DB_TRX_ID(数据行的版本号)、DB_ROLL_PT(删除版本号)
例如:
三、mysql中MVCC处理逻辑
1、数据新增
在事务开始的时候,会拿到一个全局事务ID(自增),然后在新增数据的时候,会在DB_TRX_ID(数据行的版本号)字段记录当前事务ID号。
执行:
//例如全局事务id从1开始自增;
begin; //拿到事务id=1
insert into user(name, age) VALUE('bob',20);
insert into user(name, age) VALUE('jack',22);
commit;
运行结果:
我们的表中DB_TRX_ID字段会记录当前事务ID号
2、删除数据
在事务开始的时候,会拿到一个全局事务ID(自增),然后在删除数据的时候,会在DB_ROLL_PT(删除版本号)字段记录当前事务ID号。
执行:
//例如目前全局事务id为3;
begin; //拿到事务id=3
delete user where id=2;
commit;
运行结果:
我们的表中DB_ROLL_PT字段会记录当前事务ID号
3、修改数据
在事务开始的时候,会拿到一个全局事务ID(自增),先把要修改的数据行进行copy操作,copy后的数据行中DB_TRX_ID字段设置成当前事务ID号,再把copy后的数据行中需要修改的字段进行修改,然后原来数据行中DB_ROLL_PT字段设置成当前事务ID号。
执行:
//例如目前全局事务id为5;
begin; //拿到事务id=5
update user set age=18 where id=1;
commit;
运行结果:
4、数据查询
1)查找数据行版本号(DB_TRX_ID)小于或等于当前事务版本号的数据行,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身的数据,这样才能确保取出来的数据,事务开始时没有修改和插入过。
2)查找删除版本号(DB_ROLL_PT)要么为NULL要么是大于当前事务ID的数据行,这样才能确保取出来的数据,是在事务开始时没有被删除过。
执行:
//例如目前全局事务id为8;
begin; //拿到事务id=8
select * from user;
commit;
运行结果:
四、MVCC版本控制案例解析
数据准备
//例如全局事务id从1开始自增;
begin; //拿到事务id=1
insert into user(name, age) VALUE('bob',20);
insert into user(name, age) VALUE('jack',22);
commit;
执行结果:
1、案例一
执行以下操作:
时间轴 | 事务A | 事务B |
1 | begin; | - |
2 | select * from user ; | - |
3 | - | begin; |
4 | - | update user set age =28 where id =1; |
5 | select * from user ; | - |
执行结果:
例如目前事务A拿到全局事务id为2,事务B拿到全局事务id为3;
时间轴1、2执行后得到结果为:
(1,bob,20;2,jack,22)
时间轴3,4执行结果:
时间轴5执行结果:
(1,bob,20;2,jack,22)
分析:
数据查询条件是,查找数据行版本号(DB_TRX_ID)小于或等于当前事务版本号的数据行,所以两次查询结果一致。
2、案例二
执行以下操作:
时间轴 | 事务A | 事务B |
1 | begin; | - |
2 | update user set age =28 where id =1; | - |
3 | - | begin; |
4 | - | select * from user ; |
执行结果:
例如目前事务A拿到全局事务id为2,事务B拿到全局事务id为3;
时间轴1、2执行后得到结果为:
时间轴4执行结果:
(1,bob,20;2,jack,22)
分析:
数据查询条件是,查找数据行版本号(DB_TRX_ID)小于或等于当前事务版本号的数据行,但是现在看到这事务ID是大于的,为什么还是这个结果呢?那我们就要了解Undo log是做什么的了。
五、MVCC版本控制中Undo Log、Redo Log
1、Undo Log
- Undo Log是什么:
undo意为取消,以撤销操作为目的,返回指定某个状态的操作;
undo log指事务开始之前,在操作任何数据之前,首先将需操作的数据备份到一 个地方 (Undo Log); - UndoLog是为了实现事务的原子性而出现的产物
事务处理过程中如果出现了错误或者用户执行了 ROLLBACK语句,Mysql可以利用Undo Log中的备份将数据恢复到事务开始之前的状态 - UndoLog在Mysql innodb存储引擎中用来实现多版本并发控制
事务未提交之前,Undo保存了未提交之前的版本数据,Undo 中的数据可作为数据旧版本快照供其他并发事务进行快照读
UndoLog在案例二中的作用
在时间轴4执行中,对Undo中的数据进行读取返回(快照读),确保读取到的数据是事务开始前提交的数据
当前读、快照读
- 快照读:
SQL读取的数据是快照版本,也就是历史版本,普通的SELECT就是快照读
innodb快照读,数据的读取将由 cache(原本数据) + undo(事务修改过的数据) 两部分组成 - 当前读:
SQL读取的数据是最新版本。通过锁机制来保证读取的数据无法通过其他事务进行修改
UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE都是当前读
2、Redo Log
- Redo Log 是什么:
Redo,顾名思义就是重做。以恢复操作为目的,重现操作;
Redo log指事务中操作的任何数据,将最新的数据备份到一个地方 (Redo Log) - Redo log的持久:
不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo 中。具体的落盘策略可以进行配置 - RedoLog是为了实现事务的持久性而出现的产物
防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。
Redo log补充知识点
- 指定Redo log 记录在{datadir}/ib_logfile1&ib_logfile2 可通过innodb_log_group_home_dir 配置指定目录存储
- 一旦事务成功提交且数据持久化落盘之后,此时Redo log中的对应事务数据记录就失去了意义,所以Redo log的写入是日志文件循环写入的
1、指定Redo log日志文件组中的数量 innodb_log_files_in_group 默认为2
2、指定Redo log每一个日志文件最大存储量innodb_log_file_size 默认48M
3、指定Redo log在cache/buffer中的buffer池大小innodb_log_buffer_size 默认16M - Redo buffer 持久化Redo log的策略, Innodb_flush_log_at_trx_commit:
1、取值 0 每秒提交 Redo buffer --> Redo log OS cache -->flush cache to disk[可能丢失一秒内的事务数据]
2、取值 1 默认值,每次事务提交执行Redo buffer --> Redo log OS cache -->flush cache to disk[最安全,性能最差的方式]
3、取值 2 每次事务提交执行Redo buffer --> Redo log OS cache 再每一秒执行 ->flush cache to disk操作