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 | +--------------------------------------+-------+
参数说明:
- innodb_max_undo_log_size,控制最大undo tablespace文件的大小,该值默认大小为1G,truncate后的大小默认为10M;
- innodb_undo_tablespaces,设置undo独立表空间个数,范围为0-128, 默认为0(表示不开启独立undo表空间,即undo日志存储在ibdata文件中),如果需要设置独立表空间,需要在初始化数据库实例的时候,指定独立表空间的数量;
- innodb_undo_log_truncate,该参数生效的前提是,已设置独立表空间且独立表空间个数大于等于2个;
- 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的值更新之后,该行即产生一个新版本和一个旧版本,假设之前插入该行的事务
ID
为 100
,事务 A
的 ID
为 200
,该行的隐藏主键为1事务 A
的操作过程为:
- 对
DB_ROW_ID = 1
的这行记录加排他锁; - 把该行原本的值拷贝到
undo log
中,DB_TRX_ID
和DB_ROLL_PTR
都不动; - 修改该行的值这时产生一个新版本,更新
DATA_TRX_ID
为修改记录的事务ID
,将DATA_ROLL_PTR
指向刚刚拷贝到undo log
链中的旧版本记录,这样就能通过DB_ROLL_PTR
找到这条记录的历史版本。如果对同一行记录执行连续的UPDATE
,Undo Log
会组成一个链表,遍历这个链表可以看到这条记录的变迁; - 记录
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
的大小关系来决定版本记录的可见性,具体判断流程如下:
- 如果被访问版本的
trx_id
小于m_ids
中的最小值up_limit_id
,说明生成该版本的事务在ReadView
生成前就已经提交了,所以该版本可以被当前事务访问。 - 如果被访问版本的
trx_id
大于m_ids
列表中的最大值low_limit_id
,说明生成该版本的事务在生成ReadView
后才生成,所以该版本不可以被当前事务访问。需要根据Undo Log
链找到前一个版本,然后根据该版本的 DB_TRX_ID 重新判断可见性。 - 如果被访问版本的
trx_id
属性值在m_ids
列表中最大值和最小值之间(包含),那就需要判断一下trx_id
的值是不是在m_ids
列表中。如果在,说明创建ReadView
时生成该版本所属事务还是活跃的,因此该版本不可以被访问,需要查找 Undo Log 链得到上一个版本,然后根据该版本的DB_TRX_ID
再从头计算一次可见性;如果不在,说明创建ReadView
时生成该版本的事务已经被提交,该版本可以被访问。 - 此时经过一系列判断我们已经得到了这条记录相对
ReadView
来说的可见结果。此时,如果这条记录的delete_flag
为true
,说明这条记录已被删除,不返回。否则说明此记录可以安全返回给客户端。
参考:
======专注高性能web服务器架构和开发=====