前言
众所周知,事务的一大特点是原子性,即同一事务的SQL要同时成功或者失败。那大家有没有想过在MySQL的innoDB
存储引擎中是如何保证这样的原子性操作的?实际上它是利用事务执行过程中生成的日志undo log
来实现的,那么undo log
究竟是怎么一回事呢?
undo log介绍
大家不妨先思考下,如果事务中的SQL执行到一半,遇到报错,需要把前面已经执行过的SQL撤销以达到原子性的目的,这个过程也叫做"回滚",该怎么实现呢?
每当我们要对一条记录做改动时(这里的改动可以指INSERT、DELETE、UPDATE),把回滚时所需的东西记下来。比如:
- 你插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删
掉就好了
- 你删除了一条记录,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入
到表中就好了
- 你修改了一条记录,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值
MySQL把这些为了回滚而记录的这些内容称之为撤销日志或者回滚日志, 即undo log
。
undo log存储形式
undo log
的日志内容是逻辑日志,非物理日志。
- 物理日志的意思是指具体的把具体某个数据页上的某个偏移量的指改为什么,是具体到物理结构上了,比如
redo log
就是物理日志。 - 而逻辑日志只是记录了某条数据的信息是怎么样的,没有到具体的物理磁盘上
InnoDB
对undo log
的管理采用段的方式,也就是回滚段(rollback segment
) 。每个回滚段记录了 1024 个 undo log segment
,每个事务只会使用一个回滚段,当一个事务开始的时候,会制定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段。
在MySQL5.5的时候,只有一个回滚段,那么最大同时支持的事务数量为1024个。在MySQL 5.6开始,InnoDB
支持最大 128个回滚段,故其支持同时在线的事务限制提高到了 128*1024 。
- insert undo log格式
记录的是insert
语句对应的undo log
,它生成的undo log
记录格式如下图:
- update undo log格式
记录的是update、delete
语句对应的undo log
,它生成的undo log
记录格式如下图:
那么上面这些生成undo log日志文件最终是存储在哪的呢?
这些回滚段是存储于共享表空间ibdata
中。从MySQL5.6版本开始,可通过参数对rollback segment
做进一步的设置。这些参数包括:
innodb_undo_directory
: 设置rollback segment文件所在的路径。这意味着rollback segment
可以存放在共享表空间以外的位置,即可以设置为独立表空间。该参数的默认值为“/”,表示当前noDB存储引擎的目录。innodb_undo_logs
: 设置rollback segment
的个数,默认值为128。innodb_undo_tablespaces
: 设置构成rollback segment
文件的数量,这样rollback segment
可以较为平均地分布在多个文件中。设置该参数后,会在路径innodb_undo_directory
看到undo
为前缀的文件,该文件就代表rollback segment
文件。
事务回滚机制
对于InnoDB
引擎来说,每个行记录除了记录本身的数据之外,还有几个隐藏的列:
DB_ROW_ID
:如果没有为表显式的定义主键,并且表中也没有定义唯一索引,那么InnoDB会自动为表添加一个row_id的隐藏列作为主键。DB_TRX_ID
:每个事务都会分配一个事务ID,当对某条记录发生变更时,就会将这个事务的事务ID写入tx_id
中。
DB_ROLL_PTR
: 回滚指针,本质上就是指向undo log
的指针。
- insert数据:
insert into user (name, sex) values('旭阳', '女')
插入的数据都会生成一条insert undo log
,并且数据的回滚指针会指向它。undo log
会记录undo log
的序号、插入主键的列和值…,那么在进行rollback
的时候,通过主键直接把对应的数据删除即可。
- update数据
update user set sex = '男' where id = 1;
update user set name = 'alvin' where id = 1;
这时会把老的记录写入新的undo log
,让回滚指针指向新的undo log
,它的undo no
是1,并且新的undo log
会指向老的undo log(undo no=0)
,最终形成undo log
版本链,如下图所示:
可以发现每次对数据的变更都会产生一个undo log
,当一条记录被变更多次时,那么就会产生多条undo log
,undo log
记录的是变更前的日志,并且每个undo lo
g的序号是递增的,那么当要回滚的时候,按照序号依次向前推,就可以找到我们的原始数据了。
那么按照上面的例子,事务要进行回滚,最终得到下面的执行流程:
-
通过
undo no=2
的日志把id=1的数据的name还原成“旭阳" -
通过
undo no=1
的日志把id=1的数据的sex还原成"女" -
通过
undo no=0
的日志把id=1的数据根据主键信息删除
undo log生命周期
生成过程
MySQL处于性能考虑,数据会优先从磁盘加载到Buffer Pool
中,在更新Buffer Pool
中数据之前,会优先将数据记录到undo log
中。
记录undo log
日志,MySQL不会直接去往磁盘中的xx.ibdata
文件写数据,而是会写在undo_log_buffer
缓冲区中,因为工作线程直接去写磁盘太影响效率了,写进缓冲区后会由后台线程去刷写磁盘。
删除过程
现在我们已经明白了undo log
日志是如何生成,并且作用于事务回滚的,那这些数据是什么时候删除呢?
- 针对于
insert undo log
,因为insert
操作的记录,只对事务本身可见,对其他事务不可见。故该undo log
在事务提交后就没有用,就会直接删除。
● 针对于update undo log
,该undo log
需要支持MVCC
机制,因此不能在事务提交时就进行删除。提交时放入undo log
链表,有专门的purge
线程进行删除。
总结
本文详细讲解了MySQL中redo log
日志的用处,以及它是如何保证事务的原子性。实际上,redo log
也还有一个很重要的左右就是还对事务的隔离性实现起到作用,具体的在后面的MVCC机制中详细说明。如果本文对你有帮助的话,请留下一个赞吧。