2.1 Undo Log
2.1.1 Undo Log与原子性
事务的持久性是交由Redo Log来保证,原子性则是交由Undo Log来保证。如果事务中的SQL执行到一半出现错误,需要把前面已经执行过的SQL撤销以达到原子性的目的,这个过程也叫做"回滚",所以Undo Log也叫回滚日志。
Undo Log记录了数据在每个操作前的状态,这些记录包括旧的数据值和事务的 ID。如果事务执行过程中需要回滚,就可以根据Undo Log进行回滚操作。
每当我们要对一条记录做改动时(这里的改动可以指INSERT、DELETE、UPDATE),把回滚时所需的东西记下来。比如:
- 当执行了一条insert语句时:至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。对于事务中的每个insert语句,在事务回滚时InnoDB都会完成一个delete操作。
- 当执行了一条delete语句时:至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入。对于事务中的每个delete语句,在事务回滚时InnoDB都会完成一个insert操作。
- 当执行了一条update语句时:至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值。对于事务中的每个update语句,在事务回滚时InnoDB都会完成一个反向的update操作。
另外,Undo Log 也是实现多版本并发控制的基础,通过保存旧版本的数据,InnoDB 可以在并发事务中提供隔离级别,如读已提交(Read Committed)、可重复读(Repeatable Read)等。
Tips:Undo Log主要保证事务的原子性,即通过记录修改前的状态,以提供回滚功能,其次Undo Log用于提供MVCC的快照读。
2.1.2 Undo的存储格式
Redo属于物理日志,即记录了数据库页的物理修改操作,比如页上的哪些字节被更改,具体到物理结构上。Undo属于逻辑日志,即记录了从逻辑角度如何撤销已经发生的变更的信息,第一步第二步该如何做等,通常包括了反向操作所需要的数据。
Tips:"Redo属于物理日志"意味着它详细记录了物理层面的数据页是如何被改变的;而"Undo属于逻辑日志"则表示它更多是从逻辑角度出发,记录了为实现某种目的(如回滚或访问历史版本)所需执行的操作步骤。这两者共同保障了MySQL数据库中事务处理的ACID特性。
在InnoDB中,所有表中都会有三个隐藏的列,分别为:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR。
- DB_TRX_ID:数据行版本号 ,也叫事务ID。当有新的数据修改或插入时的事务ID号,用于记录修改这条记录的事务ID和创建这条记录的事务ID。(记录这条数据是哪个事务修改的,哪个事务创建的)
- DB_ROLL_PTR:删除行版本号,也叫回滚指针。指向Undo Log中这条记录的上一个版本,删除记录,记录当前事务ID。(记录这条数据是哪个事务删除的)
- DB_ROW_ID:聚集索引。如果数据表没有主键,InnoDB会创建一个DB_ROW_ID作为聚集索引。
其中事务ID与回滚指针和Undo Log日志密切相关,另外在InnoDB中,Undo Log分为Insert Undo Log和update Undo Log两种类型,其中删除操作也是借助update Undo Log来完成的。
1)insert类型Undo Log
执行如下insert语句:
sql
insert into user values(1,"小灰",20);
当事务开启后,执行的insert语句都会以"insert类型的undo log"记录在undo log日志中,本次事务插入的所有的数据行的版本号字段都为当前事务的ID。
如果需要进行事务回滚,根据undo log中记录的主键进行delete操作即可。
2)delete类型Undo Log
执行如下delete语句:
sql
delete from user where id=1;
当事务开启后,InnoDB对于delete语句的流程是:
- 1)将被删除的行以"update类型的undo log"记录到undo log日志中。
- 2)将该行的事务ID设置为当前事务ID,回滚指针设置为被删除前那条记录的事务ID。
- 3)将删除标记设置为1,表示该记录是被删除掉的记录。
- 4)更改表空间。
3)update类型Undo Log
执行如下update语句:
sql
update user set name="小绿" where id=1;
当事务开启后,InnoDB对于update语句的流程是:
- 1)将被修改的行以"update类型的undo log"记录到undo log日志中。
- 2)将该行的事务ID设置为当前事务ID,回滚指针设置为被删除前那条记录的事务ID。
- 3)更改表空间。
当同一条数据被修改多次,那么Undo Log将通过数据的事务ID和回滚指针能够形成一个非常好的修改链路:
2.1.3 Undo Log的工作原理
InnoDB
在MySQL
启动时,会在内存中构建一个BufferPool
,而这个缓冲池主要存放两类东西,一类是数据相关的缓冲,如索引、锁、表数据等,另一类则是各种日志的缓冲,如Undo、Redo....
等日志。当一条写SQL
执行时,MySQL并不会直接去往磁盘中的ibd文件写数据,而是先修改内存中的Buffer Pool,这样性能就能得到极大的提升。
与Redo Log一样,Undo Log也存在内存缓冲区,即Undo Log Buffer。当一条写SQL
执行时,不会直接去往磁盘中的xx.ibdata
文件中的Undo Log写数据,而是会写在undo_log_buffer
缓冲区中,因为工作线程直接去写磁盘太影响效率了,写进缓冲区后会由后台线程去刷写磁盘。
- Undo Log完整的工作原理如下:
首先在操作表时,会将表数据从磁盘(.idb)加载到内存中(Buffer),对表的update/delete等操作InnoDB都会事先将修改前的数据备份到Undo Buffer中 ,这样当事务进行回滚时可以根据Undo Buffer中的内容进行事务的回滚操作,除此之外,Undo Buffer提供了数据的快照读取,在事务未提交之前,Undo 日志可以作为并发读写时的读快照,来保证事务的可重复读;
事务做到一半了,失败了,那就要将数据还原到未提交之前的状态,undo 就是记录这些事务步骤的。当然了redo 也记录了,但是redo里面东西太繁杂,不可能什么事都找它(主要是Redo和Undo的格式不一样,应用场景也不一样),于是就将事务步骤写入另外一个地方:undo,以后遇到回滚了就去查找因此在每一步操作时都会写入磁盘中的Undo Log;
引入Undo Log Buffer是来提升Undo Log的性能的,比较操作内存要比操作磁盘快多了,但由此也引入了另外一个问题,那就是既然内存中记录了Undo Log的值,为什么还要在磁盘中也记录Undo Log的值呢?难道Undo Log也要保证持久性?
并不是,在InnoDB中持久性由Redo保证。Undo之所以要写人磁盘是因为InnoDB对数据进行了多版本的控制(MVCC)。
观察如下案例:
session-01 | session-02 |
---|---|
begin; | |
begin; | |
select * from user; | |
update user set name='小蓝' where id=1; | |
commit; | |
select * from user; -- 能否查询到修改的数据? |
如果当前的事务隔离级别为RR(可重复读),那么在session-02事务中修改的数据是不能被查询出来。那数据库表中id=1的这一行数据的name值有没有被改为"小蓝"呢?答案是肯定的,因为事务都已经提交了,磁盘表中的name已经修改为了"小蓝"。这就引入了一个问题,并不是提交的数据就一定能被查询出来的,有时候需要查询旧数据。Undo Log正是保存那些旧版本的数据,让其在其他事务中可见。
2.1.4 Undo Log的系统参数
InnoDB
对undo log
的管理采用段的方式,也就是回滚段(rollback segment
) 。每个回滚段记录了 1024 个 undo log segment
,每个事务只会使用一个回滚段,当一个事务开始的时候,会制定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段。
在MySQL5.5的时候,只有一个回滚段,那么最大同时支持的事务数量为1024个。在MySQL 5.6开始,InnoDB
支持最大 128个回滚段,故其支持同时在线的事务限制提高到了 128*1024 。
我们可以查看InnoDB中Undo Log的有关系统参数,在MySQL5.5
之前没有太多参数,如下:
sql
mysql> show variables like 'innodb_max_undo_log_size';
+--------------------------+------------+
| Variable_name | Value |
+--------------------------+------------+
| innodb_max_undo_log_size | 1073741824 |
+--------------------------+------------+
1 row in set (0.00 sec)
mysql> show variables like 'innodb_rollback_segments';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_rollback_segments | 128 |
+--------------------------+-------+
1 row in set (0.00 sec)
innodb_max_undo_log_size
:本地磁盘文件中,Undo-log
的最大值,默认1GB
。innodb_rollback_segments
:指定回滚段的数量,默认为1
个。
除开上述两个参数外,其他参数基本上是在MySQL5.6
才有的,如下:
sql
mysql> show variables like '%innodb_undo%';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_undo_directory | ./ |
| innodb_undo_log_truncate | OFF |
| innodb_undo_logs | 128 |
| innodb_undo_tablespaces | 0 |
+--------------------------+-------+
4 rows in set (0.00 sec)
innodb_undo_directory
: 设置rollback segment文件所在的路径。这意味着rollback segment
可以存放在共享表空间以外的位置,即可以设置为独立表空间。该参数的默认值为"/",即MySQL的数据文件夹。innodb_undo_logs
: 设置rollback segment
的个数,默认值为128,也就是之前的innodb_rollback_segments
。innodb_undo_tablespaces
: 设置构成rollback segment
文件的数量,这样rollback segment
可以较为平均地分布在多个文件中。设置该参数后,会在路径innodb_undo_directory
看到undo
为前缀的文件,该文件就代表rollback segment
文件。innodb_undo_log_truncate
:是否开启Undo-log
的在线压缩功能,即日志文件超过大小一半时自动压缩,默认OFF
关闭。
在MySQL5.5
版本以后,Undo-log
日志支持单独存放,并且多出了几个参数可以调整Undo-log
的区域。
2.1.5 Undo Log与Purge线程
前面提到,事务提交后Undo Log日志并不会马上删除,因为其他事务很可能需要用到该数据。直接移除可能会导致其他事务读不到数据。那么对于废弃的undo log日志在什么时候删除呢?另外磁盘表中的被标记为删除的记录(数据空洞),也需要进行空间释放。这些数据都是由MySQL内部的线程------Purge线程来执行后台删除。
- 针对于
insert undo log
,因为insert
操作的记录,只对事务本身可见,对其他事务不可见。故该undo log
在事务提交后就没有用,就会直接删除。 - 针对于
update undo log
,该undo log
需要支持MVCC
机制,因此不能在事务提交时就进行删除。提交时放入undo log
链表,有专门的purge
线程进行删除。
有关于Purge线程的参数:
sql
mysql> show variables like '%purge%';
+--------------------------------------+-------+
| Variable_name | Value |
+--------------------------------------+-------+
| gtid_purged | |
| innodb_max_purge_lag | 0 |
| innodb_max_purge_lag_delay | 0 |
| innodb_purge_batch_size | 300 |
| innodb_purge_rseg_truncate_frequency | 128 |
| innodb_purge_threads | 4 |
| relay_log_purge | ON |
+--------------------------------------+-------+
7 rows in set (0.01 sec)
innodb_max_purge_lag
:当InnoDB存储引擎压力非常大时,Purge线程可能并不会工作,此时是否要延缓DML的操作,innodb_max_purge_lag控制Undo Log的数量,如果数量大于该值,就延缓DML的操作,默认为0,代表不延缓;innodb_max_purge_lag_delay
:表示当上面innodb_max_purge_lag的delay超时时间太大,超过这个参数时,将delay设置为该参数值,防止purge线程操作缓慢导致其他SQL线程长期处于等待状态。默认为0,一般不用修改。innodb_purge_batch_size
:用来设置每次purge操作需要清理的Undo Log page的数量。innodb_purge_threads
:Purge线程的数量(默认为4,最大为32)