undo log 是 innodb 实现,总的来说提供两个作用:回滚和多版本控制(MVCC)。是事务特性的重要组成部分,在数据发生更新操作时候(INSERT、DELETE、UPDATE)时会产生 undo 记录。 先于 redo log 被记录。
与 redo log 不同的是 undo log 是逻辑日志,可以勉强理解为 redo log 的相反操作。例如 redo log 的 insert 对应着 undo log delete 操作。
不同于之前章节将参数部分放到小节最后,本节需要先了解部分相关参数后会对后面的内容有一定的理解帮助。
与之相关的参数大致如下所示(5.6 之前版本有些差异,不在本文讨论范围了):
mysql> show variables like '%undo%';
+--------------------------+----------+
| Variable_name | Value |
+--------------------------+----------+
| innodb_max_undo_log_size | 10485760 |
| innodb_undo_directory | ./ |
| innodb_undo_log_truncate | OFF |
| innodb_undo_logs | 128 |
| innodb_undo_tablespaces | 0 |
+--------------------------+----------+
mysql> show global variables like '%truncate%';
+--------------------------------------+-------+
| Variable_name | Value |
+--------------------------------------+-------+
| innodb_purge_rseg_truncate_frequency | 128 |
+--------------------------------------+-------+
参数解答:
purge 线程进行 truncate undo log file 时,会检查该文件上是否还有活动事务。
如果没有,将 undo log file 标记为不可分配状态,此时新产生的 undo log 将记录到其他文件上,所以至少需要2个独立表空间文件,才能进行 truncate 操作。
当标注不可分配后,会创建一个独立的文件 undo_
然后开始初始化 undo log file 到10MB,truncate 完成后删除表示 truncate 动作的 undo_
该文件保证了即使在 truncate 过程中发生了故障宕机重启数据库服务,重启后数据库会继续完成 truncate 操作。
完成 truncate 后删除文件,标识该 undo log file 可分配。
和 redo log 类似,undo log 也是分布在 buffer 和 磁盘文件中。磁盘中 undo log 默认存放在共享表空间中,但是开启独立表空间(innodb_file_per_table)则会放到对应的各个独立表空间中。
当然也可以使用 innodb_undo_tablespaces 将 undo log 从数据文件中抽离出来,单独存放于 undo 独立表空间,当然 mysql 不建议这样操作。
数据的变更分为两类:1. INSERT 操作在事务提交前只对当前事务可见,所以产生的Undo日志可以在事务提交后直接删除;2. UPDATE、DELETE 操作需要维护多版本信息,两着被归成一类,即 update_undo。
insert 操作对应的 undo 日志类型,进行 insert 操作时,InnoDB会向聚簇索引和所有二级索引都插入一条记录。但是 undo 日志只会记录一条针对聚簇索引的日志。因为聚簇索引记录和二级索引记录是一样的,所以在进行回滚时会根据主键值把聚簇索引和二级索引中相应的记录都删掉。
delete 操作对应的 undo 日志类型,删除记录需要进行两个阶段:
在两个阶段完成后,一条记录才算真正删除,且占用的空间将可能在之后被重新使用。事务被提交后就不需要回滚了,所以回滚只会存在 delete mark 阶段。
TRX_UNDO_DEL_MARK_REC 类型 undo log 结构如下:
从上图中得出:和 TRX_UNDO_INSERT_REC 类型不同点中多了一个 索引列各列信息的内容(len of index_col_info),即某个列被包含在某个索引中,则相关信息就会被记录到这个索引列各列的信息部分。
此处的信息即为:列在记录中的位置(pos)、列占用的存储空间大小(len)、列实际值(value),用于在事务提交后,purge 阶段进行数据删除。
update 操作需要根据是否更新主键分为不同的情况处理,两种方式的实现是不一样的。
1 不更新主键
就地更新(in-place update)
就地更新即被更新的记录在更新前后占用的存储空间一样大,则可以在原记录上修改。只要有一列更新前后不一样大都不能进行就地更新。
先删再增
当不满足就地更新条件就需要先将该记录删掉,然后在由更新后的值创建一条新的记录。当然主键值是不变的。
需要注意的是:这里的删除不是上面 TRX_UNDO_DEL_MARK_REC 情况,没有两个阶段。而是在更新当前线程进行真正删除。
若更新后的记录大小不超过删掉的原有记录大小,则直接复用原有记录的存储空间。
若更新后的记录大小超过删掉的原有记录大小,则需要申请一块新的空间。若本数据页没有可用的空间,则进行页分裂,在插入更新后的记录。
TRX_UNDO_UPD_EXIST_REC 类型 undo log 结构如下:
图 7.4:
n_updated:代表被更新的列信息,<pos,old_len,old_value> 列表中的 pos、old_len 和 old_value 分别表示被更新列在记录中的位置、更新前该列占用的存储空间大小、更新前该列的真实值。
len of index_col_info:若被更新的列包含索引列,则会被添加到此部分。
2 更新主键
如更新语句涉及到主键,则将会进行两个阶段:
需要注意的是: 更新主键的情况会记录两条 undo log,即 delete mark 操作时记录一条类型为 TRX_UNDO_DEL_MARK_REC 的 undo log;插入新记录时记录一条类型为 TRX_UNDO_INSERT_REC 的 undo log。
INSERT、DELETE 对二级索引的影响和对聚簇索引是相似的,UPDATE 则不同,若没有涉及二级索引的列,那么就不需要对二级索引执行任何操作。
UPDATE 若涉及到二级索引的列,则和更新主键类似:
为了保证事务并发操作时,不同事务产生的 undo log 不发生冲突,InnoDB 采用回滚段的方式来维护 undo log 的并发写入和持久化。
undo log 采用分段(segment) 形式记录,rollback segment 称为回滚段,每个回滚段中有1024个 undo log segment。
可通过 innodb_undo_logs 设置回滚段个数,默认 128个。即默认支持同时 128 * 1024 个 undo 操作。
默认 128 个,从 resg slot0 到 resg slot127,其中:
除去 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。说明并发的事务太多了,需要考虑下是否要业务分流, 如常见的分库、分表等等。
根据上面分配规则的描述,可用下图进行展现回滚段基本布局结构:
简单来说:当数据库实例从崩溃中恢复时,需要将活跃的事务从 undo log 中提取出来,对于 ACTIVE 状态的事务直接回滚,对于 Prepare 状态的事务,如果该事务对应的 binlog 已经记录,则提交,否则回滚事务。 这里可以看第六节两阶段提交部分。