InnoDB MVCC机制


InnoDB MVCC机制

undo log 与 redo log

undo

undo日志用于存放数据修改被修改前的值,假设修改 tba 表中 id=2的行数据,把Name='B' 修改为Name = 'B2' ,那么undo日志就会用来存放Name='B'的记录,如果这个修改出现异常,可以使用undo日志来实现回滚操作,保证事务的一致性。
Id Name
1 A
2 B
3 C
4 D
对数据的变更操作,主要来自 INSERT UPDATE DELETE,而UNDO LOG中分为两种类型:
  • INSERT_UNDO,记录插入的唯一键值,INSERT操作在事务提交前只对当前事务可见,因此产生的Undo日志可以在事务提交后直接删除;
  • UPDATE_UNDO,记录修改的唯一键值以及old column记录,对于UPDATE/DELETE则需要维护多版本信息。
  与undo相关的参数
mysql> show global variables like '%undo%';
+--------------------------+------------+
| Variable_name            | Value      |
+--------------------------+------------+
| innodb_max_undo_log_size | 1073741824 |
| innodb_undo_directory    | ./         |
| innodb_undo_log_truncate | OFF        |
| innodb_undo_logs         | 128        |
| innodb_undo_tablespaces  | 3          |
+--------------------------+------------+

mysql> show global variables like '%truncate%';
+--------------------------------------+-------+
| Variable_name                        | Value |
+--------------------------------------+-------+
| innodb_purge_rseg_truncate_frequency | 128   |
| innodb_undo_log_truncate             | OFF   |
+--------------------------------------+-------+

参数说明:

  1. innodb_max_undo_log_size,控制最大undo tablespace文件的大小,该值默认大小为1G,truncate后的大小默认为10M;
  2. innodb_undo_tablespaces,设置undo独立表空间个数,范围为0-128, 默认为0(表示不开启独立undo表空间,即undo日志存储在ibdata文件中),如果需要设置独立表空间,需要在初始化数据库实例的时候,指定独立表空间的数量;
  3. innodb_undo_log_truncate,该参数生效的前提是,已设置独立表空间且独立表空间个数大于等于2个;
  4. innodb_purge_rseg_truncate_frequency,用于控制purge回滚段的频度,默认为128。
  InnoDB的purge线程,根据innodb_undo_log_truncate设置开启或关闭、innodb_max_undo_log_size的参数值,以及truncate的频率来进行空间回收和 undo file 的重新初始化。 purge线程在truncate undo log file的过程中,需要检查该文件上是否还有活动事务,如果没有,需要把该undo log file标记为不可分配,这个时候,undo log 都会记录到其他文件上,所以至少需要2个独立表空间文件,才能进行truncate 操作,标注不可分配后,会创建一个独立的文件undo__trunc.log,记录现在正在truncate 某个undo log文件,然后开始初始化undo log file到10M,操作结束后,删除表示truncate动作的 undo__trunc.log 文件,这个文件保证了即使在truncate过程中发生了故障重启数据库服务,重启后,服务发现这个文件,也会继续完成truncate操作,删除文件结束后,标识该undo log file可分配。   UNDO内部由128个回滚段(Rollback segment)组成,保存在ibdata系统表空间中,分为rseg slot0 - rseg slot127: 回滚段(rollback segment)分配如下:
  • slot 0 ,预留给系统表空间ibdata;
  • slot 1 - 32,存放于临时表的系统表空间中;
  • slot 33 -127,如果有独立表空间,则预留给UNDO独立表空间;如果没有,则预留给系统表空间;
每一个resg slot内部由1024个undo segment 组成。   回滚段中除去32个提供给临时表事务使用,剩下的 128-32=96个回滚段,可执行 96*1024 个并发事务操作,每个事务占用一个 undo segment slot,注意,如果事务中有临时表事务,还会在临时表空间中的 undo segment slot 再占用一个 undo segment slot,即占用2个undo segment slot。如果错误日志中有:Cannot find a free slot for an undo log。则说明并发的事务太多了,需要考虑下是否要分流业务。 rollback segment 采用 轮询调度的方式来分配使用,如果设置了独立表空间,那么就不会使用系统表空间回滚段中undo segment,而是使用独立表空间的,同时,如果回滚段正在 Truncate操作,则不分配。       Undo记录中存储的是老版本数据,当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要顺着undo链找到满足其可见性的记录。 当版本链很长时,通常可以认为这是个比较耗时的操作。    

redo

当数据库对数据做修改的时候,需要把数据页从磁盘读到buffer pool中,然后在buffer pool中进行修改,那么这个时候buffer pool中的数据页就与磁盘上的数据页内容不一致,称buffer pool的数据页为dirty page 脏数据,如果这个时候发生非正常的DB服务重启,那么这些数据还没在内存,并没有同步到磁盘文件中(注意,同步到磁盘文件是个随机IO),也就是会发生数据丢失,如果这个时候,能够在有一个文件,当buffer pool 中的data page变更结束后,把相应修改记录记录到这个文件(注意,记录日志是顺序IO),那么当DB服务发生crash的情况,恢复DB的时候,也可以根据这个文件的记录内容,重新应用到磁盘文件,数据保持一致。 这个文件就是redo log ,用于记录 数据修改后的记录,顺序记录。它可以带来这些好处:
  • 当 buffer pool 中的dirty page 还没有刷新到磁盘的时候,发生crash,启动服务后,可通过redo log 找到需要重新刷新到磁盘文件的记录;
  • buffer pool  中的数据直接flush到disk file,是一个随机IO,效率较差,而把buffer pool中的数据记录到redo log,是一个顺序IO,可以提高事务提交的速度;
假设修改 tba 表中 id=2的行数据,把Name='B' 修改为Name = 'B2' ,那么redo日志就会用来存放Name='B2'的记录,如果这个修改在flush 到磁盘文件时出现异常,可以使用redo log实现重做操作,保证事务的持久性。
Id Name
1 A
2 B
3 C
4 D
  这里注意下redo log 跟binary log 的区别,redo log 是存储引擎层产生的,而binary log是数据库层产生的。假设一个大事务,对tba做10万行的记录插入,在这个过程中,一直不断的往redo log顺序记录,而binary log不会记录,直到这个事务提交,才会一次写入到binary log文件中。      

MVCC

Undo log是InnoDB MVCC事务特性的重要组成部分。当我们对记录做了变更操作时就会产生undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可以使用独立的Undo 表空间。如果事务因为异常或者被显式的回滚了,那么所有数据变更都要改回去。这里就要借助回滚日志中的数据来进行恢复了。   看一个例子,两个事务A 和 B 分别按如下顺序读、写数据,       可见,事务B在事务A提交前后读到的数据在不同的事务隔离级别下可能不一致,  
事务B的隔离级别

rs1

rs2
Read Uncommited 20  20
Read Commited 10 20
Repatable Read 10 10
Serializable 20  20
      InnoDB 引擎行记录结构如下: 其中,
  • DATA_TRX_ID,6bytes,最近更新该行的事务ID;
  • DATA_ROLL_PTR,7bytes,指向该行回滚段(rollback segment)的指针,通过它找到旧版本的数据(在undo Log 链中);
  • DB_ROW_ID,6bytes,隐藏主键,如果表没有主键,InnoDB会自动生成一个隐藏主键(单调自增ID);
  上文提到,在多个事务并行操作某行数据的情况下,不同事务对该行数据的 UPDATE 会产生多个版本,然后通过回滚指针组织成一条 Undo Log 链, 上面的例子中,事务A对x的值更新之后,该行即产生一个新版本和一个旧版本,假设之前插入该行的事务 ID 为 100,事务 A 的 ID 为 200,该行的隐藏主键为1  

事务 A 的操作过程为:

  1. 对 DB_ROW_ID = 1 的这行记录加排他锁;
  2. 把该行原本的值拷贝到 undo log 中,DB_TRX_ID 和 DB_ROLL_PTR 都不动;
  3. 修改该行的值这时产生一个新版本,更新 DATA_TRX_ID 为修改记录的事务 ID,将 DATA_ROLL_PTR 指向刚刚拷贝到 undo log 链中的旧版本记录,这样就能通过 DB_ROLL_PTR 找到这条记录的历史版本。如果对同一行记录执行连续的 UPDATE,Undo Log 会组成一个链表,遍历这个链表可以看到这条记录的变迁;
  4. 记录 redo log,包括 undo log 中的修改;
那么 INSERT 和 DELETE 会怎么做呢?其实相比 UPDATE 这二者很简单,
  • INSERT 会产生一条新纪录,它的 DATA_TRX_ID 为当前插入记录的事务 ID;
  • DELETE 某条记录时可看成是一种特殊的 UPDATE,其实是软删(每条记录的头部有一个delete_flag位),真正执行删除操作会在 commit 时,DATA_TRX_ID 则记录下删除该记录的事务 ID。
  在 Read Uncommited (RU)隔离级别下,直接读取版本的最新记录就 OK,而对于 SERIALIZABLE 隔离级别,则是通过加锁互斥来访问数据,因此不需要 MVCC 的帮助。 因此 MVCC 运行在 Read Commited (RC)和 Repeatable Read(RR)这两个隔离级别下,当 InnoDB 隔离级别设置为二者其一时,在 SELECT 数据时就会用到版本链,那么问题是版本链中哪些版本对当前事务可见。      

RR级别的ReadView

在 RR 隔离级别下,每个事务 touch first read 时(本质上就是执行第一个 SELECT语句时,后续所有的 SELECT 都是复用这个 ReadView,其它 update, delete, insert 语句和一致性读 snapshot 的建立没有关系),会将当前系统中的所有的活跃事务拷贝到一个列表生成 ReadView。

下图中事务 A 第一条 SELECT 语句在事务 B 更新数据前,因此生成的 ReadView 在事务 A 过程中不发生变化,即使事务 B 在事务 A 之前提交,但是事务 A 第二条查询语句依旧无法读到事务 B 的修改。

      下图中,事务 A 的第一条 SELECT 语句在事务 B 的修改提交之后,因此可以读到事务 B的修改。但是注意,如果事务 A 的第一条 SELECT 语句查询时,事务 B 还未提交,那么事务 A 也查不到事务 B 的修改。    

RC级别的ReadView

在 RC 隔离级别下,每个 SELECT 语句开始时,都会重新将当前系统中的所有的活跃事务拷贝到一个列表生成 ReadView。

二者的区别就在于生成 ReadView 的时间点不同,RR是事务之后第一个 SELECT 语句开始、RC是事务中每条 SELECT 语句开始。

ReadView 中是当前活跃的事务 ID 列表,称之为 m_ids,其中最小值为 up_limit_id,最大值为 low_limit_id,事务 ID 是事务开启时 InnoDB 分配的,其大小决定了事务开启的先后顺序,因此我们可以通过 ID 的大小关系来决定版本记录的可见性,具体判断流程如下:

  1. 如果被访问版本的 trx_id 小于 m_ids 中的最小值 up_limit_id,说明生成该版本的事务在 ReadView 生成前就已经提交了,所以该版本可以被当前事务访问。
  2. 如果被访问版本的 trx_id 大于 m_ids 列表中的最大值 low_limit_id,说明生成该版本的事务在生成 ReadView 后才生成,所以该版本不可以被当前事务访问。需要根据 Undo Log 链找到前一个版本,然后根据该版本的 DB_TRX_ID 重新判断可见性。
  3. 如果被访问版本的 trx_id 属性值在 m_ids 列表中最大值和最小值之间(包含),那就需要判断一下 trx_id 的值是不是在 m_ids 列表中。如果在,说明创建 ReadView 时生成该版本所属事务还是活跃的,因此该版本不可以被访问,需要查找 Undo Log 链得到上一个版本,然后根据该版本的 DB_TRX_ID 再从头计算一次可见性;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
  4. 此时经过一系列判断我们已经得到了这条记录相对 ReadView 来说的可见结果。此时,如果这条记录的 delete_flag 为 true,说明这条记录已被删除,不返回。否则说明此记录可以安全返回给客户端。
                      参考: http://mysql.taobao.org/monthly/2015/04/01/ https://cloud.tencent.com/developer/article/1454636 https://dev.mysql.com/doc/refman/8.0/en/set-transaction.html