数据库事务系列3 undo

Posted 小耶哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据库事务系列3 undo相关的知识,希望对你有一定的参考价值。

5.undo

5.1 基本概念

undo用于数据库的回滚操作,如果用户执行的事务或者语句由于某些原因失败了,又或者用户通过rollback请求回滚,就可以利用undo信息将数据回滚到修改之前的样子。

Undo存放在数据库内部的一个特殊段(segment)中, 这个段称为undo段。Undo段位于共享表空间内。

用户通常对undo有这样的误解:

undo用于将数据库物理地恢复到执行语句或者事务之前的样子。

但是事实并发如此,undo 是逻辑日志,因此只是将数据库逻辑地恢复到原来的样子。

所有的修改都被逻辑的取消了,但是数据结构和页本身在回滚之后可能与之前的样子大不相同。这是因为在多用户并发系统中,可能会有很多并发事务,一个事务在修改当前页的某些记录,还有其他事务在修改当前页的其他记录,因此不能将一个页回滚到事务开始的样子,因为会影响其他事务的正在工作。

 

例如,用户执行一个插入10万条记录的事务,这个事务会导致分配一个新的段,即表空间会增大。在用户执行回滚时,会将插入的事务进行回滚,但是表空间大小并不会收缩。因此,innodb存储引擎回滚时,只是做了与先前相反的工作。对于每一个Insert执行一个delete, 对于每一个delete执行一个insert,对于每个update执行一个相反的update.

Undo另一个作痛是MVCC, 以此实现非锁定读。

最重要的一点,undo log 会产生redo log, undo log的产生会伴随着redo log的产生,因为undo log也需要持久性的维护。


5.2 undo 存储管理

InnoDB存储引擎对undo的管理同样采用段的方式。

但是这个段和之前介绍的段不同。

首先,InnoDB存储引擎有rollback segment, 每个rollback segment中记录了1024个undo log segment, 而在每个undo log segment中进行undo页的申请。

共享表空间偏移量为5的页(0, 5)记录了所有rollback segment header所在的页,这个页的类型为FIL_PAGE_TYPE_SYS。

InnoDB1.1版本之前(不包括1.1版本),只有一个rollback segment,因此支持同时在线的事务限制为1024.虽然对绝大多数的应用都已经够用,但是终究是个瓶颈。从1.1版本开始,支持最大128个rollback segment, 支持的事务并发数增加到128*1024个。

但是这些rollback segment都存储于共享表空间中。从1.2版本开始,可通过参数对rollback segment做进一步的设置。

1. innodb_undo_directory: 用于设置rollback segment 文件所在的路径。默认值是”.”, 表示当前InnoDB存储引擎的目录。这也意味着,rollback segment可以放在共享表空间以外的位置。

2. innodb_undo_logs: 用来设置rollback segment的个数,默认是128个。在1.2版本中,该参数用来替换之前的innodb_rollback_segments.

3. innodb_undo_tablespaces: 用来设置构成rollback segments文件的数量,这样rollback segments可以较为平均的分布在多个文件中。设置该参数之后,会在路劲innodb_undo_directory看到多个undo为前缀的文件,该文件就代表rollback segment文件。

 

事务在undo log segment分配页并写入undo log的这个过程同样需要重做日志。当事务提交时,InnoDB存储引擎会做两件事:

1. undo log 放入列表中,以供之后的purge操作。

2. 判断undo log所在的页是否需要重用,若可以,则给下个事务使用。

 

事务提交后,并不能马上删除undo log和undo log所在的页,因为可能还有其他事务需要通过undo log 来得到行记录之前的版本。

所以事务提交时,将undo log放入一个链表中,是否可以最红删除undo log及其所在的页,由purge线程来判断。

 

若为每一个事务分配一个单独的undo页,会非常浪费空间,特别是对于OLTP的应用类型。因为在事务提交时,并不能马上释放页。

因此,在InnoDB存储引擎设计中,对undo页可以进行重用。具体来说,当事务提交时,首先将undo log放入列表。然后,判断undo页的使用空间是否小于3/4,如果是,则表示undo 页可以被重用,之后新的undo log记录在当前undo log的后面。

由于存放undo log的列表是以记录进行组织的,而undo 页可能存放着不同事物的undo log , 因此purge操作需要涉及磁盘的离散读取操作,是一个比较缓慢的过程。


5.3 undo log格式

innoDB存储引擎中,undo log 分为:

1. insert undo log;

2. update undo log;

 

Insert undo log是指在insert操作中产生的undo log。因为insert操作的记录,只对事物本身可见,故该undo log可以在事务提交后直接删除,不需要进行purge操作。

Update undo log记录的是对delete和update操作产生的undo log.该undo log可能需要MVCC机制,因此不能再事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。

 

3. purge操作

delete和update可能并不直接删除原有数据。例如,对于一个表t执行下面的sql:

delete  from t where a=1;

t上列a有聚集索引,列b上有辅助索引。对于delete操作,仅是将主键列等于1的记录delete flag设置为1,记录并没有被删除。其次,对于辅助索引上a=1, b=1的记录同样没有做任何处理,甚至没有产生undo log。而真正删除这行记录的操作被”延迟”了,最终在purge操作中完成。若该行记录已经不被其他事务引用,那么就可以进行真正的删除。

苏州天平山初冬美景 宋老师摄于2019年冬

 

4. group commit

若事务为非只读事务,则每次事务提交时需要进行一次fsync操作,以此保证重做日志都已经写入磁盘。但是,磁盘的fsync的性能是有限的,为了提高其效率,当前数据库都提供了group commit操作,即一次fsync可以刷新确保多个事务日志写入文件。

对于innodb存储引擎来说,事务提交时分两个阶段:

1. 修改内存中事务对应的信息,并且将日志写入到重做日志缓存。

2. 调用fsync将确保日志都从重做日志缓冲写入磁盘。

 

然而,在InnoDB1.2版本之前,在开启二进制日志之后,InnoDB存储引擎的group commit功能会失效,从而导致性能的下降。并且在线环境多使用replication环境,因此二进制日志的选项基本都是开启状态,因此这个问题尤其明显。

导致这个问题的主要原因是,在开启二进制日志之后,为了保证存储引擎层中的事务和二进制日志的一致性,二者之间使用了两阶段事务,其步骤如下:

1. 当事务提交时InnoDB存储引擎进行prepare操作;

2. mysql数据库上层写入二进制日志;

3. InnoDB存储引擎层将日志写入重做日志文件;

(a) 修改内存中事务对应的信息,并且将日志写入重做日志缓存;

(b) 调用fsync将确保日志都从重做日志缓存写入磁盘;

 

一旦步骤2中的操作完成,就确保了事务的提交,即使在步骤3的时候数据库发生了宕机。

此外,每个步骤都需要进行一次fsync操作才能保证上下两层数据的一致性。

步骤2的fsync由参数sync_binlog控制,步骤3的fsync操作由innodb_flush_log_at_trx_commit控制。整个过程如下图所示:

 

为了保证MySQL数据库上层二进制日志的写入顺序和InnoDB层的事务提交顺序一致,Mysql数据库内部使用了prepare_commit_mutex这个锁。但是在启用这个锁之后,步骤3的a步不可以在其他事务执行步骤b时进行,从而导致了group commit失效。

为什么需要保证MySQL数据库上层二进制日志的写入顺序和InnoDB层的事务提交顺序一致呢?这是因为备份及恢复的需要。如下图所示:

 

InnoDB存储引擎层事务提交的顺序与Mysql数据库上层的二进制日志不同

 

我司感恩节小礼物


可以看到若通过在线备份进行数据库恢复来重新建立replication,  事务T1的数据会产生丢失。因为在InnoDB存储引擎层会检测事务T3在上下两层都完成了提交,不需要再进行恢复。因此通过锁prepare_commit_mutex以串行的方式来保证顺序性,然而这会使group commit无法生效。如下图所示:

 

通过锁prepare_commit_mutex保证InnoDB存储引擎层事务提交与Mysql数据库上层的二进制日志写入的顺序性

这个问题最早在2010年Mysql数据库大会中提出,最终有了完美解决方案。不但Mysql数据库上层的二进制日志写入是group commit的,InnoDB存储引擎层也是group commit的。此外,还移除了原先的锁prapre_commit_mutex,从而大大提高了数据库的整体性。Mysql5.6采用了类型的实现方式,并将其称为Binary Log group Commit(BLGC).


Mysql5.6 BLGC 的实现方式是将事务提交的过程分为几个步骤完成,在数据库上层进行提交时首先按顺序将其放入一个队列中,队列中的第一个事务称为leader, 其他事务称为follower ,  leader控制着follower的行为。

BLDC的步骤分为三个阶段:

1. flush阶段:将每个事务的二进制日志写入内存中。

2. sync阶段:将内存中的二进制日志刷新到磁盘,若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的写入,这就是BLGC。

3. Commit阶段:leader根据顺序调用存储引擎层事务的提交,Innodb存储引擎本就支持group commit,因此修复了原先由于锁prapre_commit_mutex导致group commit失效的问题。

 

当有一组事务在进行commit阶段,其他事务可以进行flush阶段,从而使group commit不断生效。当提交的事务越多,group commit效果越明显,数据库性能的提高就越大。

参数binlog_max_flush_queue_time用来控制flush阶段中等待的时间,即使前一组的事务提交完成,当前组的事务也不会马上进入sync阶段,而是至少需要等待一段时间。这样做的好处是group commit的事务数量更多,然而,这也可能会导致事务的响应时间变慢。默认值是0.


往期回顾

InnoDB存储引擎系列








75道BAJT高级Java面试题专题

















运维专家专题





经典回顾

















微信支付专题




参考资料

1 | CSDN


责编 | 小耶哥

本期作者 | 才高7缸

平台建设及技术支持 | 小耶哥


以上是关于数据库事务系列3 undo的主要内容,如果未能解决你的问题,请参考以下文章

mysql8学习笔记30--InnoDB内核3

事务--04---MySQL事务日志----Undo日志

数据库事务系列1 事务概述 事务分类

undolog实现事务原子性,redolog实现事务的持久性

Oracle 通过undo块查看事务信息(转)

redo日志undo日志与事务隔离性