专栏系列文章:MySQL系列专栏mysql
事务的第一个特性就是原子性
,原子性就是要保证一个事务中的增删改操做要么都成功,要么都不作。这时就须要 undo log
,在对数据库进行修改前,会先记录对应的 undo log,而后在事务失败或回滚的时候,就能够用这些 undo log 来将数据回滚到修改以前的样子。web
下面先简单介绍下事务ID和行记录中的隐藏列,由于后面的内容都与这两个东西有关系。sql
事务执行过程当中在对某个表执行增、删、改
操做时,InnoDB就会给这个事务分配一个惟一的事务ID
。若是一个事务中没有执行增删改操做,就不会分配事务ID。数据库
InnoDB 在内存维护了一个全局变量来表示事务ID,每当要分配一个事务ID时,就获取这个变量值,而后把这个变量自增1
。每当这个变量的值为256
的倍数时,就会将该变量的值刷新到系统表空间的页号为5
的页面中一个称之为Max Trx ID
的属性处。当系统下一次从新启动时,会将Max Trx ID
属性加载到内存中,并将该值加上256
以后赋值给这个全局变量(这个过程跟主键row_id
的分配是相似的)。markdown
咱们能够经过 information_schema.INNODB_TRX
来查询当前系统中运行的事务信息,这张表的第一个字段trx_id
就是事务ID。数据结构
mysql> SELECT trx_id,trx_state,trx_started,trx_rows_locked,trx_isolation_level,trx_is_read_only FROM information_schema.INNODB_TRX;
+-----------+-----------+---------------------+-----------------+---------------------+------------------+
| trx_id | trx_state | trx_started | trx_rows_locked | trx_isolation_level | trx_is_read_only |
+-----------+-----------+---------------------+-----------------+---------------------+------------------+
| 164531720 | RUNNING | 2021-05-14 16:38:59 | 1 | REPEATABLE READ | 0 |
+-----------+-----------+---------------------+-----------------+---------------------+------------------+
复制代码
在介绍InnoDB行记录格式这篇文章中,咱们了解到行记录中会有三个隐藏列:并发
DB_ROW_ID
:若是没有为表显式的定义主键,而且表中也没有定义惟一索引,那么InnoDB会自动为表添加一个row_id
的隐藏列做为主键。DB_TRX_ID
:事务中对某条记录作增删改时,就会将这个事务的事务ID写入trx_id
中。DB_ROLL_PTR
:回滚指针,本质上就是指向 undo log 的指针。每对一条记录作一次改动,就会产生1
条或者2
条 undo log
。一个事务中可能会有多个增删改SQL语句,一个SQL语句可能会产生多条 undo log,一个事务中的这些 undo log 会被从 0
开始递增编号,这个编号称为 undo no
。高并发
undo log
主要是记录对数据库增删改的撤销日志,下面就分别来看下增删改操做的 undo log 格式是怎样的。url
仍是以以前的 account 表为例来作一些演示。spa
CREATE TABLE `account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`card` varchar(60) NOT NULL COMMENT '卡号',
`balance` int(11) NOT NULL DEFAULT '0' COMMENT '余额',
PRIMARY KEY (`id`),
UNIQUE KEY `account_u1` (`card`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='帐户表';
复制代码
咱们能够经过 information_schema.INNODB_SYS_TABLES
查询获得这张表的表空间ID为 13881
。
mysql> SELECT * FROM information_schema.INNODB_SYS_TABLES WHERE name = 'test/account';
+----------+-------------+------+--------+-------+-------------+------------+---------------+------------+
| TABLE_ID | NAME | FLAG | N_COLS | SPACE | FILE_FORMAT | ROW_FORMAT | ZIP_PAGE_SIZE | SPACE_TYPE |
+----------+-------------+------+--------+-------+-------------+------------+---------------+------------+
| 13881 | test/account | 33 | 6 | 13935 | Barracuda | Dynamic | 0 | Single |
+----------+-------------+------+--------+-------+-------------+------------+---------------+------------+
复制代码
插入一条数据对应的undo
操做其实就是根据主键删除这条数据就好了。因此 insert 对应的 undo log 主要是把这条记录的主键记录上。
INSERT 产生的 undo log 类型为 TRX_UNDO_INSERT_REC
,大体结构以下图所示:
start、end
:指向记录开始和结束的位置。undo type
:undo log 的类型,也就是 TRX_UNDO_INSERT_REC
。undo no
:在当前事务中 undo log 的编号。table id
:表空间ID。主键列信息
:这一块就须要记录INSERT这行数据的主键ID信息,或者惟一列信息。好比咱们开启了一个事务,向 account 中插入两条数据:
BEGIN;
INSERT INTO account(id,card,balance) VALUES (1, 'AA', 0),(2, 'BB', 0);
复制代码
假设这个事务的事务ID为100
,这条INSERT语句会插入两条数据,就会产生两个 undo log。插入记录的时候,会在行记录的隐藏列事务ID中写入当前事务ID,并产生 undo log,记录中的回滚指针会保存 undo log 的地址。而同一个页中的多条记录会经过next_record
链接起来造成一个单链表,这块能够参考前面的行记录格式和数据页结构相关的文章。
删除一条数据大体能够分为两个阶段:
首先是用户线程执行删除时,会先将记录头信息中的 delete_mask
标记为 1
,而不是直接从页中删除,由于可能其它并发的事务还须要读取这条数据。(后面讲MVCC的时候就知道为何了)
提交事务后,后台有一个 purge
线程会将数据真正删除。
首先要知道,页中的数据是经过记录头信息中的 netx_record
链接起来的单向链表(假设这个链表称为数据链表
)。页中还有另外一个链表,称为垃圾链表
,记录真正删除后,会从数据链表中移除,而后加入到垃圾链表的头部,以便重用空间。
因此阶段二就是将记录从数据链表移除,加入到垃圾链表的头部。
也就是说,删除操做在事务提交前,只会经历阶段一,就是将记录的 delete_mask
标记为 1
。
DELETE 对应的 undo log 类型为 TRX_UNDO_DEL_MARK_REC
,它的结构大体以下图所示,与 TRX_UNDO_INSERT_REC
类型相比,主要多了三个部分:
old trx_id
:这个属性会保存记录中的隐藏列trx_id
,这个属性在MVCC并发读的时候就会起做用了。old roll_pointer
:这个属性保存记录中的隐藏列roll_pointer
,这样就能够经过这个属性找到以前的 undo log。索引列信息
:这部分主要是在第二阶段事务提交后用来真正删除记录的。此时接着执行一条删除的SQL语句,将id=2的这条数据删除:
BEGIN;
INSERT INTO account(id,card,balance) VALUES (1, 'AA', 0),(2, 'BB', 0);
DELETE FROM account WHERE id = 2;
复制代码
由于是在同一个事务中,因此记录中的隐藏列trx_id
没变,记录头中的delete_mask
则标记为1
了。而后生成了一个新的 undo log,并保存了记录中本来的trx_id
和roll_pointer
,因此这个新的 undo log 就指向了旧的 undo log,而记录中的 roll_pointer 则指向这个新的 undo log。注意 undo log 中的事务编号也在递增。
在执行UPDATE语句时,InnoDB对更新主键和不更新主键这两种状况有大相径庭的处理方案,对应中两种不一样的 undo log 类型。
在不更新主键的状况下,又能够细分为被更新的列占用的存储空间不发生变化和发生变化的状况。
更新记录时,对于被更新的每一个列
来讲,若是更新后的列和更新前的列占用的字节数
都同样大,那么就能够进行就地更新,也就是直接在原记录的基础上修改对应列的值。
若是有任何一个
被更新的列更新前和更新后占用的字节数
大小不一致,那么就会先把这条旧的记录从聚簇索引页面中删除掉,而后再根据更新后列的值建立一条新的记录插入到页面中。注意这里的删除并非将 delete_mask
标记为 1
,而是真正的删除,从数据链表中移除加入到垃圾链表的头部。
若是新的记录占用的存储空间大小不超过旧记录占用的空间,就能够直接重用刚加入垃圾链表头部的那条旧记录所占用的空间,否就会在页面中新申请一段空间来使用。
不更新主键的这两种状况生成的 undo log 类型为 TRX_UNDO_UPD_EXIST_REC
,大体结构以下图所示,与 TRX_UNDO_DEL_MARK_REC
相比主要是多了更新列的信息。
假设此时更新id=1的这条数据,各列占用的字节大小都未变化:
BEGIN;
INSERT INTO account(id,card,balance) VALUES (1, 'AA', 0),(2, 'BB', 0);
DELETE FROM account WHERE id = 2;
UPDATE account SET card = 'CC' WHERE id = 1;
复制代码
这条记录就会执行就地更新
,一样会产生一条新的 undo log,并指向原来的 undo log。
要知道记录是按主键大小连成一个单向链表的,若是更新了某条记录的主键值,这条记录的位置也将发生改变,也许就被更新到其它页中了。
这种状况下的更新分为两步:
delete_mask
改成 1
,尚未真正删除。因此这种状况下,会产生两条 undo log:
TRX_UNDO_DEL_MARK_REC
类型的 undo log。TRX_UNDO_INSERT_REC
类型的 undo log。这两种类型的结构前面已经说过了。
此时再将id=1的主键更新:
BEGIN;
INSERT INTO account(id,card,balance) VALUES (1, 'AA', 0),(2, 'BB', 0);
DELETE FROM account WHERE id = 2;
UPDATE account SET card = 'CC' WHERE id = 1;
UPDATE account SET id = 3 WHERE id = 1;
复制代码
更新主键后,本来的记录就被标记删除了,而后新增了一个 TRX_UNDO_DEL_MARK_REC
的 undo log。接着插入了一条新的id=3的记录,并建立了一个新的 TRX_UNDO_INSERT_REC
类型的 undo log。
前面在一个事务中增删改产生的一系列 undo log,都有 undo no
编号的。在回滚的时候,就能够应用这个事务中的 undo log,根据 undo no
从大到小开始进行撤销操做。
例如上面的例子若是最后回滚了:
delete_mask
改成 0
;card='CC'
还原为原来的card='AA'
;delete_mask
改成 0
;能够看到,回滚时经过执行 undo log 撤销,就将数据还原为原来的样子了。
但须要注意的是,undo log 是逻辑日志
,只是将数据库逻辑地恢复
到原来的样子。全部修改都被逻辑地取消了,可是数据结构和页自己在回滚以后可能大不相同。由于同时可能不少并发事务在对数据库进行修改,所以不能将一个页回滚到事务开始的样子,由于这样会影响其余事务正在进行的工做。
前边介绍了几种类型的 undo log,它们其实被分为两个大类来存储:
TRX_UNDO_INSERT
类型为 TRX_UNDO_INSERT_REC 的 undo log 属于此大类,通常由 INSERT 语句产生,或者在 UPDATE 更新主键的时候也会产生。
TRX_UNDO_UPDATE
除了类型为 TRX_UNDO_INSERT_REC 的 undo log,其余类型的 undo log 都属于这个大类,好比 TRX_UNDO_DEL_MARK_REC 、 TRX_UNDO_UPD_EXIST_REC ,通常由 DELETE、UPDATE 语句产生。
之因此要分红两个大类,是由于不一样大类的 undo log 不能混着存储,由于类型为TRX_UNDO_INSERT_REC
的 undo log 在事务提交后能够直接删除掉,而其余类型的 undo log 还须要提供MVCC
功能,不能直接删除。
undo log
是存放在FIL_PAGE_UNDO_LOG
类型的页中,一个事务中可能会产生不少 undo log,也许就须要申请多个undo页,因此 InnoDB 将其设计为一个链表的结构,将一个事务
中的多个undo页链接起来。
可是前面说了 undo log 分为两大类,不能混着存储,因此若是事务中产生了这两大类型的 undo log,会建立两个链表,一个用来存储 TRX_UNDO_INSERT
类别的 undo log,一个用来存储 TRX_UNDO_UPDATE
类别的 undo log。
若是事务中还修改了临时表,InnoDB规定对普通表和临时表修改产生的 undo log 要分开存储,因此在一个事务中最多可能会有4
个 undo 页面链表。
须要注意的是这些链表并非事务一开始就分配好的,而是在须要某个类型的链表的时候才会去分配。
若是有多个并发事务执行,为了提升 undo log 的写入效率,不一样事务执行过程当中产生的 undo log 会被写入到不一样的 undo 页面链表中。也就是说一个事务最多可能单独分配4个链表,两个事务可能就8个链表。
但其实大部分事务都是一些短事务,产生的 undo log 不多,这些 undo log 只会占用一个页少许的存储空间,这样就会很浪费。因而 InnoDB 设计在事务提交后,在某些状况下能够重用这个事务的 undo 页面链表。
undo 链表能够被重用的条件:
3/4
时才能够被重用。对于TRX_UNDO_INSERT
类型的 insert undo
页面链表,这些 undo log 在事务提交以后就没用了,能够被清除掉。因此在某个事务提交后,重用这个链表时,能够直接覆盖掉以前的 undo log。
对于TRX_UNDO_UPDATE
类型的 update undo
页面链表,这些 undo log 在事务提交后,不能当即删除掉,由于要用于MVCC
。因此重用这个链表时,只能在后面追加 undo log,也就是一个页中可能写入多组 undo log。
redo log
是存放在重作日志文件中的,而 undo log
默认是存放在系统表空间中的一个特殊段(segment)
中,这个段称为回滚段(Rollback Segment
),链表中的页面都是从这个回滚段里边申请的。
为了更好的管理系统中的 undo 页面链表,InnoDB 设计了一个 Rollback Segment Header
的页面,每一个Rollback Segment Header
页面都对应着一个Rollback Segment
。一个 Rollback Segment Header 页面中包含1024
个undo slot
,每一个 undo slot 存放了 undo 链表头部的 undo 页的页号。
一个 Rollback Segment Header 只有 1024
个 undo slot,假设一个事务中只分配了1
个undo链表,那最多也只能支持1024
个并发事务同时执行,在现今高并发状况下,这显然是不够的。
因此InnoDB定义了128
个回滚段,也就有128
个 Rollback Segment Header,就有128*1024=131072
个undo slot
,也就是说最多同时支持131072
个并发事务执行。
在系统表空间的第5
号页面中存储了这128
个Rollback Segment Header
页面地址。
能够经过以下几个参数对回滚段作配置:
innodb_undo_directory
:undo log 默认存放在系统表空间中,也能够配置为独立表空间。能够经过这个参数设置独立表空间的目录,默认是数据目录。
innodb_undo_logs
:设置回滚段的数量,默认是128
。但须要注意的是,针对临时表的回滚段数量固定为32
个,那么针对普通表的回滚段数量就是这个参数值减去32
,若是设置小于32的值,就只有1
个针对普通表的回滚段。
innodb_undo_tablespaces
:设置undo表空间文件的数量,这样回滚段能够较为平均的分布到多个文件中。该参数默认为0
,表示不建立undo独立表空间。
mysql> SHOW VARIABLES LIKE 'innodb_undo%';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_undo_directory | .\ |
| innodb_undo_logs | 128 |
| innodb_undo_tablespaces | 0 |
+--------------------------+-------+
复制代码
undo log 写入 undo 页后,这个页就变成脏页了,也会加入 Flush 链表中,而后在某个时机刷到磁盘中。
事务提交时会将 undo log
放入一个链表中,是否能够最终删除 undo log 及 undo log 所在页,是由后台的一个 purge
线程来完成的。
最后也是最为重要的一点是,undo log
写入 undo 页的时候也会产生 redo log
,由于 undo log
也须要持久性的保护。
这里其实要说的的是前面 redo log 未解决的一个问题。
仍是这张T一、T2并发事务的图,在图中箭头处,若是T1事务执行完成提交事务,此时 redo log 就会刷盘。而T2事务还未执行完成,但它的 mtr_T2_1
已经刷入磁盘了。若是此时数据库宕机了,T2事务实际是执行失败的。在重启数据库后,就会读取 mtr_T2_1
来恢复数据,而T2事务实际是未完成的,因此这里恢复数据就会致使数据有问题。
因此这时 undo log 就派上用场了,redo log 恢复时,一样会对 undo 页重作,mtr_T2_1
这段 redo log 对数据页重作后,因为T2事务未提交,就会用 undo log 来撤销这些操做。就解决了这个问题。