Undo Log在MySQL事务的实现中也起着至关重要的作用,MySQL中事务的一致性是由Undo Log实现的。本节对MySQL中的Undo Log进行介绍,主要包括Undo Log文件的基本概念、存储方式、基本原理、MVCC机制和Undo Log文件的常见参数配置。
Undo Log在MySQL事务的实现中主要起到两方面的作用:回滚事务和多版本并发事务,也就是常说的MVCC机制。
在MySQL启动事务之前,会先将要修改的数据记录存储到Undo Log中。如果数据库的事务回滚或者MySQL数据库崩溃,可以利用Undo Log对数据库中未提交的事务进行回滚操作,从而保证数据库中数据的一致性。
Undo Log会在事务开始前产生,当事务提交时,并不会立刻删除相应的Undo Log。此时,InnoDB存储引擎会将当前事务对应的Undo Log放入待删除的列表,接下来,通过一个后台线程purge thread进行删除处理。
Undo Log与Redo Log不同,Undo Log记录的是逻辑日志,可以这样理解:当数据库执行一条insert语句时,Undo Log会记录一条对应的delete语句;当数据库执行一条delete语句时,Undo Log会记录一条对应的insert语句;当数据库执行一条update语句时,Undo Log会记录一条相反的update语句。
当数据库崩溃重启或者执行回滚事务时,可以从Undo Log中读取相应的数据记录进行回滚操作。
MySQL中的多版本并发控制也是通过Undo Log实现的,当select语句查询的数据被其他事务锁定时,可以从Undo Log中分析出当前数据之前的版本,从而向客户端返回之前版本的数据。
需要注意的是,因为MySQL事务执行过程中产生的Undo Log也需要进行持久化操作,所以Undo Log也会产生Redo Log。由于Undo Log的完整性和可靠性需要Redo Log来保证,因此数据库崩溃时需要先做Redo Log数据恢复,然后做Undo Log回滚。
在MySQL中,InnoDB存储引擎对于Undo Log的存储采用段的方式进行管理,在InnoDB存储引擎的数据文件中存在一种叫作rollback segment的回滚段,这个回滚段内部有1024个undo log segment段。
Undo Log默认存放在共享数据表空间中,默认为ibdata1文件中。如果开启了innodb_file_per_table参数,就会将Undo Log存放在每张数据表的.ibd文件中。
默认情况下,InnoDB存储引擎会将回滚段全部写在同一个文件中,也可以通过innodb_undo_tablespaces变量将回滚段平均分配到多个文件中。innodb_undo_tablespaces变量的默认值为0,表示将rollback segment回滚段全部写到同一个文件中。
需要注意的是,innodb_undo_tablespaces变量只能在停止MySQL服务的情况下修改,重启MySQL服务后生效,但是不建议修改这个变量的值。
Undo Log写入磁盘时和Redo Log一样,默认情况下都需要经过内核空间的OS Buffer,如图2-5所示。
同样,如果在打开日志文件时设置了O_DIRECT标志位,就可以不经过操作系统内核空间的OS Buffer,直接向磁盘写入数据,这点和Redo Log也是一样的。
这里依然以商城系统的下单业务为例来简单说明Undo Log的基本原理,如图2-6所示。
从图2-6中可以看出,MySQL数据库事务提交之前,InnoDB存储引擎会将数据表中修改前的数据保存到Undo Log Buffer。Undo Log Buffer中的数据会持久化到磁盘的Undo Log文件中。当数据库发生故障重启或者事务回滚时,InnoDB存储引擎会读取Undo Log中的数据,将事务还未提交的数据回滚到最初的状态。同时,系统可以根据需要查询并加载订单表中的数据,也就是加载order.ibd文件中的数据,也可以向订单表写入数据,也就是持久化数据到order.ibd文件中。
在MySQL中,Undo Log除了实现事务的回滚操作外,另一个重要的作用就是实现多版本并发控制,也就是MVCC机制。在事务提交之前,向Undo Log保存事务当前的数据,这些保存到Undo Log中的旧版本数据可以作为快照供其他并发事务进行快照读。
图2-5 Undo Log Buffer写日志到Undo Log文件的示意图
图2-6 商城业务用户下单时MySQL内部Undo Log的基本原理
Undo Log的回滚段中,undo logs分为insert undo log和update undo log。
1)insert undo log:事务对插入新记录产生的Undo Log,只在事务回滚时需要,在事务提交后可以立即丢弃。
2)update undo log:事务对记录进行删除和更新操作时产生的Undo Log,不仅在事务回滚时需要,在一致性读时也需要,因此不能随便删除,只有当数据库所使用的快照不涉及该日志记录时,对应的回滚日志才会被purge线程删除。
关于InnoDB实现MVCC机制,简单点理解就是InnoDB存储引擎在数据表的每行记录后面保存了两个隐藏列,一个隐藏列保存行的创建版本,另一个隐藏列保存行的删除版本。每开始一个新的事务,这些版本号就会递增。
在可重复读隔离级别下,MVCC机制在增删改查操作下分别按照如下方式实现。
1)当前操作是select操作时,InnoDB存储引擎只会查找版本号小于或者等于当前事务版本号的数据行,这样可以保证事务读取的数据行要么之前就已经存在,要么是当前事务自身插入或者修改的记录。另外,行的删除版本号要么未定义,要么大于当前事务的版本号,这样可以保证事务读取的行在事务开始之前没有被删除。
2)当前操作是insert操作时,将当前事务的版本号保存为当前行的创建版本号。
3)当前操作是delete操作时,将当前事务的版本号保存为删除的数据行的删除版本号,作为行删除标识。
4)当前操作是update操作时,InnoDB存储引擎会将待修改的行复制为新的行,将当前事务的版本号保存为新数据行的创建版本号,同时保存当前事务的版本号为原来数据行的删除版本号。
需要注意的是,将当前事务的版本号保存为行删除版本号时,相应的数据行并不会被真正删除,当事务提交时,会将这些行记录放入一个待删除列表,因此需要根据一定的策略对这些标识为删除的行进行清理。为此,InnoDB存储引擎会开启一个后台线程进行清理工作,是否可以清理需要后台线程来判断。
为便于读者理解Undo Log实现MVCC机制的原理,上面介绍的实现过程经过了简化。从本质上说,为实现MVCC机制,InnoDB存储引擎在数据库每行数据的后面添加了3个字段:6字节的事务id(DB_TRX_ID)字段、7字节的回滚指针(DB_ROLL_PTR)字段、6字节的DB_ROW_ID字段。每个字段的作用如下所示。
1)6字节的事务id(DB_TRX_ID)字段。
用来标识最近一次对本行记录做修改(insert、update)的事务的标识符,即最后一次修改本行记录的事务id。如果是delete操作,在InnoDB存储引擎内部也属于一次update操作,即更新行中的一个特殊位,将行标识为已删除,并非真正删除。
2)7字节的回滚指针(DB_ROLL_PTR)字段。
主要指向上一个版本的行记录,能够从最新版本的行记录逐级向上,找到要查找的行版本记录。
3)6字节的DB_ROW_ID字段。
这个字段包含一个随着新数据行的插入操作而单调递增的行id,当由InnoDB存储引擎自动产生聚集索引时,聚集索引会包含这个行id,否则这个行id不会出现在任何索引中。
为了方便读者理解,这里举一个简单的示例。假设有事务A和事务B两个事务,事务A对商品数据表中的库存字段进行更新,同时事务B读取商品的信息。Undo Log实现的MVCC机制流程如图2-7所示。
图2-7 Undo Log实现的MVCC机制流程
手动开启事务A后,更新商品数据表中id为1的数据,首先会把更新命令中的数据写入Undo Buffer中。在事务A提交之前,事务B手动开启事务,查询商品数据表中id为1的数据,此时的事务B会读取Undo Log中的数据并返回给客户端。
在MySQL命令行输入如下命令可以查看Undo Log相关的参数。
show variables like "%undo%";
其中几个重要的参数说明如下所示。
1)innodb_max_undo_log_size:表示Undo Log空间的最大值,当超过这个阈值(默认是1GB),会触发truncate回收(收缩)操作,回收操作后,Undo Log空间缩小到10MB。
2)innodb_undo_directory:表示Undo Log的存储目录。
3)innodb_undo_log_encrypt:MySQL 8中新增的参数,表示Undo Log是否加密,OFF表示不加密,ON表示加密,默认为OFF。
4)innodb_undo_log_truncate:表示是否开启在线回收Undo Log文件操作,支持动态设置,ON表示开启,OFF表示关闭,默认为OFF。
5)innodb_undo_tablespaces:此参数必须大于或等于2,即回收一个Undo Log时,要保证另一个Undo Log是可用的。
6)innodb_undo_logs:表示Undo Log的回滚段数量,此参数的值至少大于或等于35,默认为128。
7)innodb_purge_rseg_truncate_frequency:用于控制回收Undo Log的频率。Undo Log空间在回滚段释放之前是不会回收的,要想增加释放回滚区间的频率,就要降低innodb_purge_rseg_truncate_frequency参数的值。