事务隔离级别和MVCC
1. 事务隔离级别和MVCC
1.1 事务并发执行时的一致性问题
(1)脏写
事务修改了另一个未提交事务修改过的数据;
这里的一致性是:假设每个事务都遵守将变量 x 和 y 始终设置为相同值,操作序列如下所示:
T1(x = 1)、T2(x = 2)、T2(y = 2)、T1(y = 1)、C1、C2
事务提交后,x = 2,y = 1 并不满足一致性要求
在发生回滚时,也会影响原子性和持久性,例如,假设 x 和 y 初始值为 0
T1(x = 1)、T2(x = 2)、T2(y = 2)、C2、A1
T1 修改 x 时会记录旧值 0,T1 进行回滚时会将 x 恢复为 0,但 T2 中也修改了 x ,就会造成部分回滚的情况(不回滚 y),违背原子性;如果都回滚的话, T2 已经提交,违背持久性的要求。
(2)脏读
事务读取了另一个未提交事务修改过的数据;
(3)不可重复读
一个事务修改了另一个事务读取过的数据;
(4)幻读
一个事务先根据某些搜索条件查询一些记录,在该事务未提交时,另一个事务写入了一些符合前述搜索条件的数据(增删改操作);
1.2 MySQL 设置隔离级别
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL level;
不写 GLOBAL
或者 SESSION
只对执行 SET 语句后的下一个事务产生影响,下一个事务执行完后,后续事务恢复到之前的隔离级别,不能在已经开启的事务中执行,会报错。
(1)可重复读应用场景:事务启动时可以认为表是静态的
1.3 ReadView
对 READ UNCOMMITTED 直接读取记录最新的记录即可;
对 SERIALIZABLE 使用加锁的方式来访问记录;
对 READ COMMITTED 和 REPEATABLE READ 来说,需要保证读取的记录都是已经提交事务修改的记录,通过使用 ReadView 保证。
- m_ids:生成 ReadView 时,当前系统中活跃的读写事务的事务 id 列表;
- min_trx_id:生成 ReadView 时,当前系统中活跃的读写事务的中 最小的事务 id (m_ids 中的最小值);
- max_trx_id:生成 ReadView 时,系统应该分配给下一个事务的事务 id 值;
注意,max_trx_id 并非 m_ids 中的最大值,因为可能事务 id 较大的事务已经提交了(id 为 3 的事务提交了,当前活跃的为 1 和 2)。 - creator_trx_id:生成该 ReadView 的事务的事务 id;
如果被访问版本的 trx_id 与 ReadView 中的 creator_trx_id 相同,则表明当前事务正在访问它自己修改过的记录,可见;
如果被访问版本的 trx_id 小于 ReadView 中的 min_trx_id ,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,可见;
如果被访问版本的 trx_id 大于等于 ReadView 中的 max_trx_id,表明生成该版本的事务在当前事务生成 ReadView 后才开启,不可见;
如果被访问版本的 trx_id 大于等于 min_trx_id 并小于 max_trx_id,判断 trx_id 是否在 m_ids 列表中,如果在,不可见;否则,可见。
(1)READ COMMITTED 隔离级别下,每次读取数据前都会生成一个 ReadView;
(2)在 REPEATABLE READ 隔离级别下,第一次读取数据时生成一个 ReadView;
START TRANSACTION WITH CONSISTENT SNAPSHOT 语句开启事务,会立即生成一个 ReadView。
1.4 二级索引与 MVCC
-
二级索引页面的 Page Header 部分有一个 PAGE_MAX_TRX_ID 的属性,记录了修改该二级索引页面的最大事务 id;当 SELECT 语句访问某个二级索引时,首先判断 ReadView 的 min_trx_id 是否大于 PAGE_MAX_TRX_ID,若大于,则该页面的所有记录都对该 ReadView 可见,否则需要进行回表后再判断可见性。
-
利用二级索引的主键进行回表,找到对该 ReadView 可见的第一个版本,判断该版本中相应的二级索引列的值是否与二级索引查询时的值相同,若是,返回(WHERE 中若有其他条件还需继续判断其他条件),否则接着沿着版本链判断。
1.5 关于 purge
(1)当一个事务提交后,会把这个事务执行过程中产生的一组 update undo log 插入到 History 链表头部。
每个回滚段都有一个 History 链表
(2)为了支持 MVCC ,delete mark 仅仅设置一个标志位,并没有真正删除;
(3)何时进行 purge
-
在事务提交时,会为该事务生成一个名为 trx_no 的值,表示事务提交的顺序;
一组 undo log 中对应的 Undo Log Header 部分有一个属性 TRX_UNDO_TRX_NO,当事务提交时,记录了事务的 trx_no;
History 链表是根据事务提交顺序来存放各组 undo log 的; -
生成 ReadView 时,还会包含一个 trx_no 属性,表示当前系统中最大的 trx_no + 1 赋给该值。 (用来表明生成该 ReadView 时,哪些事务已经提交)
InnoDB 把当前系统中所有的 ReadView 按照创建时间连成一个链表,当执行 purge 操作时,就把系统中最早生成的 ReadView 取出来(如果没有则新创建一个 ReadView ),然后从各回滚段的 History 链表取出 trx_no 较小的各组 undo log,如果该组日志的 trx_no 小于 ReadView 中的 trx_no,就会释放该组日志,如果该组日志包含因 delete mark 操作产生的 undo log,也要把相应记录真正删除。
(4)因此尽量少用长事务,因为这会保留很老的 ReadView,从而导致 undo log 变得特别大。
2. 锁
(1)锁结构
trx 信息:该锁结构与哪个事务关联;
is_waiting:当前事务是否在等待;
获取锁成功或者加锁成功,在内存中生成了与该记录对应的锁结构,而且该锁结构的 is_waiting 属性为 false;
加锁失败,在内存中生成了与该记录对应的锁结构,而且该锁结构的 is_waiting 属性为 true;
不加锁,不需要在内存中生成对应的锁结构,可以直接执行操作;
2.1 写操作
(1)DELETE:先在 B+ 树中定位这条记录的位置,获取记录的 X 锁(获取 X 锁的锁定读),执行 delete mark 操作;
(2)INSERT:一般情况下,新插入的一条记录受隐式锁保护,不需要在内存中生成对应的锁结构;
(3)UPDATE:
不修改主键,且被更新的各个列占用存储空间与之前相同
未修改主键,且至少有一个被更新的列占用存储空间与之前不同,
修改主键,相当于在原纪录上执行 DELETE 操作后再来一次 INSERT 操作,加锁操作需要按照 DELETE 和 INSERT 的规则进行。
2.2 MySQL 中的行锁和表锁
MyISAM、MEMROY 这些存储引擎一般支支持表级锁,而且一般都是针对当前会话来说的,因此最好用在只读场景下。