事务
一、前言
其中原子性,通过 undo log 来保障,持久性通过 redo log 来保障。接下来,我们分析一下隔离性
INFO
在并发场景之下,可能会出现的问题:
- 脏读,是指一个事务,读取到了另一个事务还没有提交的信息
- 不可重复读,在一个事务之中多次读取同一个数据,如果说前后两次读取到的数据不一致。
- 幻读,在一个事务内多次查询某个符合查询条件的记录数量,前后两次查询到的记录数量不一致
为了解决这些问题,就有了隔离级别,在 MySQL 之中,提供了不同的隔离级别,隔离级别越高,安全性就会越高,但是性能就会越低
那么,MySQL 又是如何实现这些隔离级别的呢?
在 undo log 之中,提到了版本链,每条数据的更新都会同时记录一条 undo log,每个事务都会分配一个事务ID
对于读未提交来说,他可以读取到其他事务未提交的数据,所以直接读取记录的最新的版本就好了,对于可串行化,通过锁的方式来控制,如果事务始终对某行记录进行更新操作,其余事务如果想读取,必须等该事务提交之后,释放对应的锁,才能够读取到
而 读已提交 和 可重复读,都必须保证读到已经提交了的事务修改过的记录,假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核心问题就是需要判断一下版本链中那个版本是当前事务可见的。为此,提出 ReadView
。
在 Read View 之中,包含了如下内容:
m_ids
:生成ReadView
时当前系统中活跃的读写事务的事务id列表min_trx_id
:m_ids 中的最小值max_trx_id
:m_ids 中的最大值creator_trx_id
:生成该ReadView
的事务的事务id
当具备了 ReadView 的时候,我们来看一下,读取一条数据对应的流程
- 如果被访问版本的
trx_id
属性值与ReadView
中的creator_trx_id
值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。 - 如果被访问版本的
trx_id
属性值小于当前系统活跃的最小事务ID,说明这个版本事务已经提交了,所以可以进行访问。 - 如果被访问版本的
trx_id
属性值大于或等于当前系统活跃的最大事务ID,表明生成该版本的事务在当前事务生成ReadView
后才开启,所以该版本不可以被当前事务访问。 - 如果被访问版本的
trx_id
属性值在ReadView
的min_trx_id
和max_trx_id
之间,那就需要判断一下trx_id
属性值是不是在m_ids
列表中,如果在,说明创建Rea dView
时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView
时生成该版本的事务已经被提交,该版本可以被访问。
INFO
只读事务不分配trx_id(会分配一个假trx_id),只有涉及到数据更新才会分配事务ID。
那么 read view 是什么时候生成的呢?
read committed
在每次一次进行普通select操作的时候生成一个ReadViewrepeatable read
只在第一次进行普通select操作前生成一个ReadView,后面会复用这个ReadView
,版本链不会发生变化,所以说不会出现重复读的问题。
二、案例
下面以实际的案例分析一下具体的隔离级别
我现在有一张表 tt ,建表语句如下:
CREATE TABLE `tt` (
`id` int(0) NOT NULL AUTO_INCREMENT,
`a` varchar(255),
`b` varchar(255),
PRIMARY KEY (`id`) USING BTREE,
INDEX `index_a_b`(`a`, `b`) USING BTREE
) ENGINE = InnoDB;
2.1 隐式事务
前提:T2 时刻是分配了事务ID,假设事务ID 是 100;表中初始化数据如下:
insert into tt values(1,1,1);
1)T3 , Session2 生成 readView,此时活跃的事务ID为:100,在判断版本链的过程之中,最新的 a = 2,事务ID 为 100,处于活跃的事务ID列表之中,不可见,所以 a = 1
2)T5 ,Session2 生成了readView,此时没有活跃的事务ID,在判断版本链的时候,能够读取到最新的数据
时间 | session1 | session2 |
---|---|---|
T1 | begin | |
T2 | update tt set a = 2 where id = 1; | |
T3 | select * from tt; 此时读取到的数据,a 等于 1 | |
T4 | commit | |
T5 | select * from tt; 此时读取到的数据,a 等于2 |
2.2 读已提交
前提:T2 时刻是分配了事务ID,假设事务ID 是 100;表中初始化数据如下:
insert into tt values(1,1,1);
T5 ,session2 生成了 ReadView,此时活跃的事务ID 为 100,记录不可见,所以 a = 1,
T6 ,session1事务提交了
3)T7,此时 session2 重新生成了 ReadView,此时没有活跃的事务ID了,就可以认为最新的记录是可见的,此时 a = 2
时间 | session1 | session2 |
---|---|---|
T1 | set session transaction isolation level READ COMMITTED; | |
T2 | begin | |
T3 | update tt set a = 2 where id = 1; | |
T4 | begin | |
T5 | select * from tt; 此时读取到的数据,a 等于 1 | |
T6 | commit | |
T7 | select * from tt; 此时读取到的数据,a 等于 2 |
由于每一次都重新生成了 ReadView ,所以在读已提交的隔离级别之下,解决了脏读的问题,但是没有解决不可重复读和幻读的问题
2.3 可重复读
前提:T2 时刻是分配了事务ID,假设事务ID 是 100;表中初始化数据如下:
insert into tt values(1,1,1);
1)T4 ,session2 生成 ReadView,此时活跃的事务ID为 100
2)T5,session1 将事务提交了
3)T6,session2 复用了 T4 时刻生成的 ReadView ,就导致 T6 的时候,还是认为 事务ID 100 是活跃的,记录仍然是不可见
时间 | session1 | session2 |
---|---|---|
T1 | begin | |
T2 | update tt set a = 2 where id = 1; | |
T3 | begin | |
T4 | select * from tt; 此时读取到的数据,a 等于 1 | |
T5 | commit | |
T6 | select * from tt; 此时读取到的数据,a 等于1 |
结论一:在可重复读的隔离级别之下,已经解决了不可重复读的问题。
接下来,看一下幻读的场景,表中的初始数据如下:
时间 | session1 | session2 |
---|---|---|
T1 | begin | begin |
T2 | select * from tt; return:(1,1,1)(3,3,1) | |
T3 | insert into tt values(2,2,2); | |
T4 | select * from tt; return:(1,1,1)(3,3,1) | |
T5 | commit | |
T6 | select * from tt; return:(1,1,1)(3,3,1) |
结论二:在可重复读的场景之下,使用 MVCC ,实际上解决了幻读的问题
这里就有问题了,为什么上文之中提到了,在可重复读的隔离级别之下,还是有可能出现幻读的问题呢?
这里需要区分一下读取的两种方式:
- 快照读:当通过 MVCC 生成的快照读取数据时,不会出现幻读的问题,
- 当前读,读取所有已经提交的记录的最新值,加锁的 SELECT 或者对数据进行增删改的时候都会进行当前读,这种情况是会出现幻读的问题