redo log、undo log、bin log之间的关系

90 阅读18分钟

1. 原子性与undo log

原子性(A):一个事务所有的操作,要么全部执行,要么就一个都不执行,即 all-or-nothing。它可以让事务在出现故障等原因,导致不能全部执行成功时,将已经执行的部分操作,回滚到事务前的状态。

MySQL 的 InnoDB 存储引擎使用“Write-Ahead Log”日志方案实现本地事务的持久性

  • “提前写入”(Write-Ahead),就是在事务提交之前,允许将变动数据写入磁盘。与“提前写入”对应的就是,在事务提交之前,不允许将变动数据写入磁盘,而是等到事务提交之后再写入。
  • “提前写入”的好处是:有利于利用空闲 I/O 资源。但“提前写入”同时也引入了新的问题:在事务提交之前就有部分变动数据被写入磁盘,那么如果事务要回滚,或者发生了崩溃,这些提前写入的变动数据就都成了错误。“Write-Ahead Log”日志方案给出的解决办法是:增加了一种被称为 Undo Log 的日志,用于进行事务回滚。

变动数据写入磁盘前,必须先记录 Undo Log,Undo Log 中存储了回滚需要的数据。在事务回滚或者崩溃恢复时,根据 Undo Log 中的信息对提前写入的数据变动进行擦除。

  • 在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;

  • 在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;

  • 在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。

如果某个事务执行过程中对某个表执行了增、删、改操作,那么 InnoDB 存储引擎就会给它分配一个独一无二的 事务id ,分配方式如下:

  • 对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一 个 事务id ,否则的话是不分配 事务id 的。
  • 对于读写事务来说,只有在它第一次对某个表(包括用户创建的临时表)执行增、删、改操作时才会为这个 事务分配一个 事务id ,否则的话也是不分配 事务id 的。 有的时候虽然我们开启了一个读写事务,但是在这个事务中全是查询语句,并没有执行增、删、改的语句, 那也就意味着这个事务并不会被分配一个 事务id 。
  • START TRANSACTION READ ONLY 语句开启一个只读事务。 在只读事务中不可以对普通的表(其他事务也能访问到的表)进行增、删、改操作,但可以对临时表做增、 删、改操作。
  • START TRANSACTION READ WRITE 语句开启一个读写事务,或者使用 BEGIN 、 START TRANSACTION 语句开启的事务默认也算是读写事务。

事务id 本质上就是一个数字,它的分配策略和隐藏列 row_id (当用户没有为表创建主键 和 UNIQUE 键时 InnoDB 自动创建的列)的分配策略大抵相同,具体策略如下:

  • 服务器会在内存中维护一个全局变量,每当需要为某个事务分配一个 事务id 时,就会把该变量的值当作 事 务id 分配给该事务,并且把该变量自增1。
  • 每当这个变量的值为 256 的倍数时,就会将该变量的值刷新到系统表空间的页号为 5 的页面中一个称之为 Max Trx ID 的属性处,这个属性占用 8 个字节的存储空间。
  • 当系统下一次重新启动时,会将上边提到的 Max Trx ID 属性加载到内存中,将该值加上256之后赋值给我们 前边提到的全局变量(因为在上次关机时该全局变量的值可能大于 Max Trx ID 属性值)。

这样就可以保证整个系统中分配的 事务id 值是一个递增的数字。先被分配 id 的事务得到的是较小的 事务id , 后被分配 id 的事务得到的是较大的 事务id 。

InnoDB中的行格式:聚簇索引的记录除了会保存完整的用户数据以外,而且还会自动添加名为trx_id、roll_pointer的隐藏列,如果用户没有在表中定义主键以及UNIQUE键,还会自动添加一个名为row_id的隐藏列。所以一条记录在页面中的真实结构看起来就是这样的:

  • trx_id:某个对这个聚簇索引记录做改动的语句所在的事务对应的事务id而已(此处的改动可以是INSERT、DELETE、UPDATE操作)

  • roll_pointer:一个指向记录对应的undo日志的一个指针

对于只读事务来说,在它第一次对某个表执行增、删、改操作时会为这个事务分配一个事务id,若不涉及到增、删、改操作的话是不分配事务id的。

在事务对表中的记录做改动时才会为这个事务分配一个唯一的事务id

为了实现事务的原子性,InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。一般每对一条记录做一次改动,就对应着一条undo日志,但在某些更新记录的操作中,也可能会对应着2条undo日志。一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的undo日志

undo log 提供了回滚和多个行版本控制(MVCC),在数据库修改操作时,不仅记录了 redo log,还记录了 undo log,如果因为某些原因导致事务执行失败回滚了,可以借助 undo log 进行回滚。

虽然 undo log 和 redo log 都是InnoDB 特有的,但 undo log 记录的是 逻辑日志,redo log 记录的是物理日志。对记录做变更操作时不仅会产生 redo 记录,也会产生 undo 记录(insert,update,delete),undo log 日志用于存放数据被修改前的值,比如 update T set c=c+1 where ID=2; 这条 SQL,undo log 中记录的是 c 在 +1 前的值,如果这个 update 出现异常需要回滚,可以使用 undo log 实现回滚,保证事务一致性。

而多版本并发控制(MVCC) ,也用到了 undo log ,当读取的某一行被其他事务锁定时,它可以从 undo log 中获取该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。

undo 记录默认被记录到系统表空间(ibdata1)中,但是从 MySQL5.6 开始,就可以使用独立的 undo 表空间了。不用担心 undo 会把 ibdata1 文件弄大。

  • undo log 是采用段 (segment)的方式来记录的,每个 undo 操作在记录的时候占用一个 undo log segment

  • rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment,在以前的版本中,只支持一个 rollback segment,也就是只能记录 1024 个 undo log segment,MySQL 5.5 以后,可以支持 128 个 rollback segment,即支持 128*1024 个 undo 操作,还可以通过变量 innodb_undo_logs自定义 rollback segment 数量,默认是 128

一条数据可能被修改多次,每修改一次都会产生一条undo log。

1.1 undo log与redo log比较

redo log:重做日志。实现崩溃恢复,防止数据更新丢失,保证事务的持久性。也就是说,在机器故障恢复后,系统仍然能够通过 Redo Log 中的信息,持久化已经提交的事务的操作结果。

undo log:撤销日志、回滚日志。

undo Log 的作用 / 功能:

  • 事务回滚:可以对提前写入的数据变动进行擦除,实现事务回滚,保证事务的原子性。

  • 实现 MVCC 机制:Undo Log 也用于实现 MVCC 机制,存储记录的多个版本的 undo log,形成版本链。

  • undo log 中存储了回滚需要的数据。在事务回滚或者崩溃恢复时,根据 undo log 中的信息对提前写入的数据变动进行擦除。

undo log不是redo log的逆向过程,其实它们都算是用来恢复的日志:

  1. redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。

  2. undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。

1.2 undo log和redo log的特点

  1. 为了保证持久性,必须在事务提交时将Redo Log持久化。

  2. 数据不需要在事务提交前写入磁盘,而是缓存在内存中。

  3. Undo Log 保证事务的原子性。

  4. 有一个隐含的特点,数据必须要晚于redo log写入持久存储。这是因为Recovery要依赖redo log. 如果redo log丢失了,系统需要保持事务的数据也没有被更新。

1.3 IO性能

undo log redo log的设计主要考虑的是提升IO性能,

为了保证Redo Log能够有比较好的IO性能,InnoDB 的 Redo Log的设计有以下几个特点:

  1. 尽量保持Redo Log存储在一段连续的空间上。因此在系统第一次启动时就会将日志文件的空间完全分配。 以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。
  2. 批量写入日志。日志并不是直接写入文件,而是先写入redo log buffer.当需要将日志刷新到磁盘时 (如事务提交),将许多日志一起写入磁盘.
  3. 并发的事务共享Redo Log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起,以减少日志占用的空间。例如,Redo Log中的记录内容可能是这样的:
记录1: <trx1, insert …>
记录2: <trx2, update …>
记录3: <trx1, delete …>
记录4: <trx3, update …>
记录5: <trx2, insert …>
  1. 因为步骤2的原因,当一个事务将Redo Log写入磁盘时,也会将其他未提交的事务的日志写入磁盘。

  2. Redo Log上只进行顺序追加的操作,当一个事务需要回滚时,它的Redo Log记录也不会从Redo Log中删除掉。

2. undo log redo log binlog

2.1 undo log和redo log

假设有A、B两个数据,值分别为A=1,B=1。

2.2 redolog 和binlog

  • binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制;
  • redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复;

undo log 和 redo log 这两个日志都是 Innodb 存储引擎生成的。

MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。

binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。

2.2.1 redo log 和 binlog 有什么区别?

这两个日志有四个区别。

1、适用对象不同:

  • binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
  • redo log 是 Innodb 存储引擎实现的日志;

2、文件格式不同:

  • binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下:

    • STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
    • ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
    • MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;
  • redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;

3、写入方式不同:

  • binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
  • redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。

4、用途不同:

  • binlog 用于备份恢复、主从复制;

  • redo log 用于掉电等故障恢复。

update T set c=c+1 where ID=2;

这个简单的 update 语句是怎么执行的

  1. 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  2. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。
  4. 告知执行器执行完成了,随时可以提交事务。
  5. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。(非必需,如果没有配置的话,可以跳过此操作)
  6. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

说白了就是执行完更新语句后,先将redo log状态设置为prepared,等更新内容写入binlog后,在将redo log状态设置为commited。

“写 binlog”当成一个动作。但实际上,写 binlog 是分成两步的:

  1. 先把 binlog 从 binlog cache 中写到文件系统的 page cache;
  2. 调用 fsync 持久化。

先将redo log状态设置为prepared,等更新内容写入binlog后,在将redo log状态设置为commited,这种方式叫做两阶段提交

2.3 两阶段提交

所谓的两阶段就是把一个事物分成两个阶段来提交。

两个阶段分别为:

  • prepare阶段
  • commit阶段

2.3.1 流程

将数据A=1 修改成A=2的流程

MySQL准备事务的时候会将写redolog、binlog分成两个阶段。

  • 第一阶段 (prepare阶段):写redo log 并将其标记为prepare状态。紧接着写binlog
  • 第二阶段(commit阶段):写binlog 并将其标记为commit状态。

"将A更新为A=2":更新的BufferPool中的页,此时该页成为脏页

2.3.2 在两阶段提交的情况下,是怎么实现崩溃恢复的呢?

首先比较重要的一点是,在写入redo log时,会顺便记录XID,即当前事务id。在写入binlog时,也会写入XID。

  • 如果在写入redo log之前崩溃,那么此时redo log与binlog中都没有,是一致的情况,崩溃也无所谓。
  • 如果在写入redo log prepare阶段后立马崩溃,之后会在崩恢复时,由于redo log没有被标记为commit。于是拿着redo log中的XID去binlog中查找,此时肯定是找不到的,那么执行回滚操作。
  • 如果在写入binlog后立马崩溃,在恢复时,由redo log中的XID可以找到对应的binlog,这个时候直接提交即可。

总的来说,在崩溃恢复后,只要redo log不是处于commit阶段,那么就拿着redo log中的XID去binlog中寻找,找得到就提交,否则就回滚。

在这样的机制下,两阶段提交能在崩溃恢复时,能够对提交中断的事务进行补偿,来确保redo log与binlog的数据一致性。

所以说,两阶段提交的主要用意是:为了保证redolog和binlog数据的安全一致性。只有在这两个日志文件逻辑上高度一致了。你才能放心的使用redolog帮你将数据库中的状态恢复成crash之前的状态,使用binlog实现数据备份、恢复、以及主从复制。而两阶段提交的机制可以保证这两个日志文件的逻辑是高度一致的。没有错误、没有冲突

你有没有想过这样一件事,binlog默认都是不开启的。

也就是说,如果你根本不需要binlog带给你的特性(比如数据备份恢复、搭建MySQL主从集群),那你根本就用不着让MySQL写binlog,也用不着什么两阶段提交。

只用一个redolog就够了。无论你的数据库如何crash,redolog中记录的内容总能让你MySQL内存中的数据恢复成crash之前的状态。

3. 故障恢复

前面说到未提交的事务和回滚了的事务也会记录Redo Log,因此在进行恢复时,这些事务要进行特殊的的处理。有2种不同的恢复策略:

  1. 进行恢复时,只重做已经提交了的事务。
  2. 进行恢复时,重做所有事务包括未提交的事务和回滚了的事务。然后通过Undo Log回滚那些未提交的事务。

MySQL数据库InnoDB存储引擎使用了第2种策略, InnoDB存储引擎中的恢复机制有几个特点:

  1. 在重做Redo Log时,并不关心事务性。 恢复时,没有BEGIN,也没有COMMIT,ROLLBACK的行为。也不关心每个日志是哪个事务的。尽管事务ID等事务相关的内容会记入Redo Log,这些内容只是被当作要操作的数据的一部分。
  2. 使用第2种策略就必须要将Undo Log持久化,而且必须要在写Redo Log之前将对应的Undo Log写入磁盘。Undo和Redo Log的这种关联,使得持久化变得复杂起来。为了降低复杂度,InnoDB将Undo Log看作数据,因此记录Undo Log的操作也会记录到redo log中。这样undo log就可以象数据一样缓存起来,而不用在redo log之前写入磁盘了。

包含Undo Log操作的Redo Log,看起来是这样的:

记录1: <trx1, Undo log insert <undo_insert …>>
记录2: <trx1, insert …>
记录3: <trx2, Undo log insert <undo_update …>>
记录4: <trx2, update …>
记录5: <trx3, Undo log insert <undo_delete …>>
记录6: <trx3, delete …>
  1. 到这里,还有一个问题没有弄清楚。既然Redo没有事务性,那岂不是会重新执行被回滚了的事务?
    确实是这样。同时Innodb也会将事务回滚时的操作也记录到redo log中。回滚操作本质上也是对数据进行修改,因此回滚时对数据的操作也会记录到Redo Log中。

一个回滚了的事务的Redo Log,看起来是这样的:

记录1: <trx1, Undo log insert <undo_insert …>>
记录2: <trx1, insert A…>
记录3: <trx1, Undo log insert <undo_update …>>
记录4: <trx1, update B…>
记录5: <trx1, Undo log insert <undo_delete …>>
记录6: <trx1, delete C…>
记录7: <trx1, insert C>
记录8: <trx1, update B to old value>
记录9: <trx1, delete A>

一个被回滚了的事务在恢复时的操作就是先redo再undo,因此不会破坏数据的一致性。

  • undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。

  • redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于崩溃恢复。

  • binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制;