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_<space_id>_trunc.log,记录现在正在truncate 某个undo log文件,然后开始初始化undo log file到10M,操作结束后,删除表示truncate动作的 undo_<space_id>_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的值更新之后,该行即产生一个新版本和一个旧版本,假设之前插入该行的事务 ID100,事务 AID200,该行的隐藏主键为1
 

事务 A 的操作过程为:

  1. DB_ROW_ID = 1 的这行记录加排他锁;
  2. 把该行原本的值拷贝到 undo log 中,DB_TRX_IDDB_ROLL_PTR 都不动;
  3. 修改该行的值这时产生一个新版本,更新 DATA_TRX_ID 为修改记录的事务 ID,将 DATA_ROLL_PTR 指向刚刚拷贝到 undo log 链中的旧版本记录,这样就能通过 DB_ROLL_PTR 找到这条记录的历史版本。如果对同一行记录执行连续的 UPDATEUndo Log 会组成一个链表,遍历这个链表可以看到这条记录的变迁;
  4. 记录 redo log,包括 undo log 中的修改;
那么 INSERTDELETE 会怎么做呢?其实相比 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_flagtrue,说明这条记录已被删除,不返回。否则说明此记录可以安全返回给客户端。
 
 
 
 
 
 
 
 
 
 
 
参考:
posted @ 2020-11-25 19:52  如果的事  阅读(517)  评论(0编辑  收藏  举报