MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。
MySQL InnoDB 引擎通过 锁机制、MVCC 等手段来保证事务的隔离性( 默认支持的隔离级别是 可重复读 )。
保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。
ACID以及事务隔离性参考另外一篇文章:MySQL-ACID、事务隔离级别
MVCC 全称Multi-Version Concurrency Control,MVCC是一种通过增加版本冗余数据来实现并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
InnoDB中实现了MVCC主要是为了提高数据库的并发性能,在无锁的情况下也能处理读写并发,大大提高数据库的并发度。
MVCC 的实现依赖于:隐藏字段(DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID)、Read View、undo log。
在内部实现中,InnoDB 通过数据行的 DB_TRX_ID 和 Read View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改。
在内部,InnoDB 存储引擎为每行数据添加了三个 隐藏字段 :
ReadView 是事务快照读的时候产生的数据读视图,在该事务执行快照读的那一刻,会生成一个数据系统当前的快照,记录并维护系统当前活跃事务的id,事务的id值是递增的。
ReadView 的最大作用就是判断数据的可见性,当某个事务执行快照读的时候,会对此记录创建一个ReadView 的视图,在整个事务期间根据某些条件判断该事务能够看到的版本链上的哪条历史数据。
Read View主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”
主要有以下字段:
访问记录的版本事务ID即前面提到的隐藏DB_TRX_ID。
根据访问记录的版本DB_TRX_ID分为数据行的DB_TRX_ID和undo log中数据行的DB_TRX_ID。
当访问undo log中数据行d时,即使用d中的DB_TRX_ID与ReadView 的数据对比判断当前数据行是否可见。
如果某个版本对当前事务不可见,那么顺着版本链找到下个版本记录,然后继续上面的对比规则,直到找到版本链中的最后一个版本,如果最后一个版本都不可见,那么该条记录对此事务完全不可见,也就查不到这个记录。
undo log 主要有两个作用:
在 InnoDB 存储引擎中 undo log 分为两种: insert undo log 和 update undo log:
insert undo log :指在 insert 操作中产生的 undo log。因为 insert 操作的记录只对事务本身可见,对其他事务不可见,故该 undo log 可以在事务提交后直接删除。不需要进行 purge 操作。
update undo log :update 或 delete 操作中产生的 undo log。该 undo log可能需要提供 MVCC 机制,因此不能在事务提交时就进行删除。提交时放入 undo log 链表,等待 purge线程 进行最后的删除。
当前读获取的数据是最新数据,而且在读取时不能被其他修改的,所以会对读取的记录加锁来控制。如下
select * from user where id > 1 lock in share mode;
select * from user where id >1 for update;
快照读,顾名思义读取的是一份快照数据,所以读到的并不一定是最新数据,可能是历史数据。
简单的select查询就是快照读,不加锁非阻塞读,降低数据库的开销。如下:
select * from user where id >1
但是快照读在隔离级别是串行化级别是没有意义的,因为串行化的sql都是排队执行的,不存在并发,所以就会变成当前读。
虽然 RC(读已提交) 和 RR(可重复读) 都通过 MVCC 来读取快照数据,但由于 生成 Read View 时机不同,从而在 RR 级别下实现可重复读。
Read View生成时机:
假设时间线来到 T4 ,那么此时数据行 id = 1 的版本链为:
由于 RC 级别下每次查询都会生成Read View ,并且事务 101、102 并未提交,此时 103 事务生成的 Read View 中活跃的事务
m_ids 为:[101,102] ,
m_low_limit_id为:104,
m_up_limit_id为:101,
m_creator_trx_id 为:103
时间线来到 T6 ,数据的版本链为: 因为在 RC 级别下,重新生成 Read View,这时事务 101 已经提交,102 并未提交,所以此时 Read View 中活跃的事务
m_ids:[102] ,
m_low_limit_id为:104,
m_up_limit_id为:102,
m_creator_trx_id为:103
时间线来到 T9 ,数据的版本链为: 重新生成 Read View, 这时事务 101 和 102 都已经提交,所以 m_ids 为空,则 m_up_limit_id = m_low_limit_id = 104,最新版本事务 ID 为 102,满足 102 < m_low_limit_id,可见,查询结果为 name = 赵六 。
总结: 在 RC 隔离级别下,事务在每次查询开始时都会生成并设置新的 Read View,所以导致不可重复读
在可重复读级别下,只会在事务开始后第一次读取数据时生成一个 Read View(m_ids 列表)。
1.在 T4 情况下的版本链为: 在当前执行 select 语句时生成一个 Read View,此时
m_ids:[101,102] ,
m_low_limit_id为:104,
m_up_limit_id为:101,
m_creator_trx_id 为:103。
此时和 RC 级别下一样:
2.时间点 T6 情况下: 在 RR 级别下只会生成一次Read View,所以此时依然沿用
m_ids :[101,102] ,
m_low_limit_id为:104,
m_up_limit_id为:101,
m_creator_trx_id 为:103。
时间点 T9 情况下: 此时情况跟 T6 完全一样,由于已经生成了 Read View,此时依然沿用 m_ids :[101,102] ,所以查询结果依然是 name = 菜花。
表级锁和行级锁对比:
参考:
https://javaguide.cn/database/mysql/innodb-implementation-of-mvcc/#锁定读
MySQL进阶系列:多版本并发控制mvcc的实现