MVCC的理解
脏读、幻读、不可重复读
脏读:读取未提交(未提交的数据被回滚)的数据
幻读:一个事务相同查询条件,前后查询的数据量不一致问题(指新插入的行)。
不可重复读:同一事务前后多次读取,数据内容不一致。事务为可重复读可解决。
为什么要有MVCC?
解决了在可重复读、读提交的事务隔离级别下读写的事务并发。
在没有MVCC存在的情况下如何保证不会出现脏读?
隔离级别
隔离级别 | 脏读 | 幻读 | 不可重复读 | 概念 |
---|---|---|---|---|
READ UNCOMMITTED | √ | √ | √ | 事务能够看到其他事物没有提交的修改 |
READ COMMITTED | × | √ | √ | 事务能看到其他事物提交后的修改,前后两次读取可能会出现数据不一致 |
REPEATABLE READ(默认隔离级别) | × | √ (INNODB不可能) | × | 事务在多次读取的数据一致 |
SERIALIZABLE | × | × | × | 事务串行 |
实现原理
使用三个隐藏字段、undo log 和 read view一起实现
三个字段
ROW_ID 6字节,数据表没有主键,将自动生成一个row_id;
TRX_ID 6字节,最近修改事务id,记录创建这条记录或者最后一次修改该记录的事务id
ROLL_PTR 7字节,回滚指针,指向这条记录上一个版本,用于配合undolog,指向上一个旧版本
当前记录
name | age | ROW_ID | TRX_ID | ROLL_PTR |
---|---|---|---|---|
Jone | 18 | 1 | 3 | 0x10000001 |
undo log
回滚日志,用于记录数据被修改前的信息,用于保障未提交事务的原子性。主要记录的是数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。
name | age | ROW_ID | TRX_ID | ROLL_PTR |
---|---|---|---|---|
Jone | 17 | 1 | 2 | 0x10000000 |
name | age | ROW_ID | TRX_ID | ROLL_PTR |
---|---|---|---|---|
Jone | 16 | 1 | 1 | null |
Read View
可见性判断,事务进行快照读操作的时候产生的读视图,在执行读的时候生成当前数据系统的快照,记录并维护系统当前活跃事务的id(事务id是递增的),把它当做条件去判断当前事务能够看到哪个版本的数据。有可能读取最新的数据,也有可能读取当前航记录的undo log中的某个版本数据。
属性:
trx_list: 数值列表,用来维护ReadView生成时刻系统正在活跃的事务ID;
up_limit_id: 记录trx_list列表中事务最小的ID;
low_limit_id: ReadView生成时刻,系统即将分配的下一个事务ID
规则:
-
TRX_ID < up_limit_id
⇒ 当前事务能看到TRX_ID的记录TRX_ID >= up_limit_id
⇒ 看条件2 -
TRX_ID ≥ low_limit_id
⇒ 代表TRX_ID所在的记录在ReadView生成后才出现的,当前事务不可能见TRX_ID < low_limit_id
⇒ 看条件3 -
判断TRX_ID是否在活跃事务中,如果在代表ReadView生成时,这个事务是活跃状态,修改的数据当前事务看不到。如果不在代表这个事务在ReadView生成之前就已经提交,那么是可见的。
举个例子
数据:
name | age | ROW_ID | TRX_ID | ROLL_PTR |
---|---|---|---|---|
Jone | 20 | 1 | 5 | 0x10000000 |
undo log
name | age | ROW_ID | TRX_ID | ROLL_PTR |
---|---|---|---|---|
Jone | 19 | 1 | 1 | null |
可重复读的事务隔离级别下:
事务1 | 事务2 | 事务3 | 事务4 | 事务5 |
---|---|---|---|---|
BEGIN; | BEGIN; | BEGIN; | BEGIN; | BEGIN; |
快照读 | UPDATE; | |||
COMMIT; | ||||
... | 快照读 | 快照读 | ... |
此时事务2的Read View
up_limit_id | trx_list | low_limit_id |
---|---|---|
1 | 1,2,3,4,5 | 6 |
up_limit_id | trx_list | low_limit_id |
---|---|---|
1 | 1,2,3,4,5 | 6 |
此时事务3的Read View
up_limit_id | trx_list | low_limit_id |
---|---|---|
1 | 1,2,3,4 | 6 |
读提交的事务隔离级别下:
事务1 | 事务2 | 事务3 | 事务4 | 事务5 |
---|---|---|---|---|
BEGIN; | BEGIN; | BEGIN; | BEGIN; | BEGIN; |
快照读 | UPDATE; | |||
COMMIT; | ||||
... | 快照读 | 快照读 | ... |
-- 查看当前事务id
SELECT TRX_ID FROM information_schema.INNODB_TRX WHERE TRX_MYSQL_THREAD_ID = CONNECTION_ID();
-- 设置当前对话事务隔离级别
set session TRANSACTION ISOLATION level REPEATABLE read;
set session TRANSACTION ISOLATION level read committed;
可重复隔离级别,事务执行情况
T1 | T2 |
---|---|
begin; | |
select; == null | |
begin; | |
insert; | |
commit; | |
select; ==null | |
commit; |
T1 | T2 |
---|---|
begin; | |
begin; | |
insert; | |
commit; | |
select; == insert | |
commit; |
说明可重复读隔离性下,第一次select获取到read view后,第二次不会再次获取read view
读已提交隔离级别,事务执行情况
T1 | T2 |
---|---|
begin; | |
select; == null | |
begin; | |
insert; | |
commit; | |
select; ==insert | |
commit; |
T1 | T2 |
---|---|
begin; | |
begin; | |
insert; | |
commit; | |
select; ==insert | |
commit; |
说明读已提交隔离性下,第一次select获取到read view后,第二次再次获取read view。可以获取到其他事务提交的数据。
关于spring应用下的一些问题
mybatis-plus通过配置关闭缓存
# 解决两次相同的select,后面一次select不会查询数据库问题;
# 原因在于mybatis有一级缓存,再一次sqlsession里,如果执行相同的select语句,mybatis不会重新插叙那数据库。
mybatis-plus:
configuration:
local-cache-scope: statement
Spring通过清除SqlSession缓存
@Resource
private SqlSessionFactory sqlSessionFactory;
SqlSessionUtils.getSqlSession(sqlSessionFactory).clearCache();
扩展阅读
ACID
原子性:undo log保证
一致性: 由其他三者一起保证
隔离性: MVCC + undo log
持久性:redo log 保证