谁来保证InnoDB的原子性、一致性与持久性的?

谁来保证InnoDB的原子性、一致性与持久性的?

一、事务的 ACID 特性与日志的角色

InnoDB中的事务完全符合 ACID 特性,即

原子性:原子性是指整个数据库事务是不可分割的工作单位。只有 事务中所有的数据库操作都执行成功,才算整个事务成功。如果事务中任何一个操作执行失败,则已经执行成功的操作也必须撤销,将数据库状态退回到执行事务之前。
一致性:事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
隔离性:事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即在事务提交前对其他事务不可见。
持久性:事务一旦提交,其结果是永久性的。即使发生宕机等故障,数据库也能将数据恢复。这种保证,是从事务角度来保证结果的永久性的。不是说,所有的硬盘都损坏也可以保证数据不丢失。

事务的隔离性是由 锁、MVCC实现的。而原子性、一致性和持久性,则是由 redo logundo log 协同实现的:

  • **原子性 (Atomicity)**:主要由 undo log 来保证。当事务需要回滚时,系统会利用undo log记录的”反向”操作,将数据恢复到事务开始前的状态,实现”全有或全无”的效果。
  • **持久性 (Durability)**:主要由 redo log 来保证。当事务提交时,redo log 必须被持久化到磁盘。即使数据库发生宕机,也可以通过 redo log 恢复已提交事务的修改,确保数据不丢失。
  • **一致性 (Consistency)**:一致性是一个更宏观的概念,它建立在原子性和持久性之上。数据库的约束(如主键、外键)保证了数据规则的正确,而 undo logredo log 则保证了事务本身要么完整地从一个一致性状态转换到另一个,要么在失败时完全退回,不会留下中间状态。

二、redo log:保证事务持久性的关键

undo 不是 redo 的逆过程。redo 和 undo 的作用都可以视为是一种恢复操作。redo 恢复提交事务修改的页操作,所以通常是 物理日志。而 undo 回滚行记录到某个特定版本,其存储的是逻辑日志,根据每行记录进行记录。

2.1 redo log 的诞生背景

我们知道InnoDB存储引擎是以页为单位来管理存储空间的,我们进行的增删改查操作其实本质上都是在访问页面(包括读页面、写页面、创建新页面等操作)。我们前面介绍Buffer Pool的时候说过,在真正访问页面之前,需要把在磁盘上的页缓存到内存中的Buffer Pool之后才可以访问。但是在介绍事务的时候又强调过一个称之为持久性的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。但是如果我们只在内存的Buffer Pool中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这是我们所不能忍受的(想想ATM机已经提示狗哥转账成功,但之后由于服务器出现故障,重启之后猫爷发现自己没收到钱,猫爷就被砍死了)。那么如何保证这个持久性呢?一个很简单的做法就是在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题:

刷新一个完整的数据页太浪费了
有时候我们仅仅修改了某个页面中的一个字节,但是我们知道在InnoDB中是以页为单位来进行磁盘IO的,也就是说我们在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘,我们又知道一个页面默认是16KB大小,只修改一个字节就要刷新16KB的数据到磁盘上显然是太浪费了。
随机IO刷起来比较慢
一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,倒霉催的是该事务修改的这些页面可能并不相邻,这就意味着在将某个事务修改的Buffer Pool中的页面刷新到磁盘时,需要进行很多的随机IO,随机IO比顺序IO要慢,尤其对于传统的机械硬盘来说。

咋办呢?再次回到我们的初心:我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改了哪些东西记录一下就好,比方说某个事务将系统表空间中的第100号页面中偏移量为1000处的那个字节的值1改成2我们只需要记录一下:

将第0号表空间的100号页面的偏移量为1000处的值更新为2。

这样我们在事务提交时,把上述内容刷新到磁盘中,即使之后系统崩溃了,重启之后只要按照上述内容所记录的步骤重新更新一下数据页,那么该事务对数据库中所做的修改又可以被恢复出来,也就意味着满足持久性的要求。因为在系统奔溃重启时需要按照上述内容所记录的步骤重新更新数据页,所以上述内容也被称之为重做日志,英文名为redo log,我们也可以土洋结合,称之为redo日志。与在事务提交时将所有修改过的内存中的页面刷新到磁盘中相比,只将该事务执行过程中产生的redo日志刷新到磁盘的好处如下:

redo日志占用的空间非常小
存储表空间ID、页号、偏移量以及需要更新的值所需的存储空间是很小的,关于redo日志的格式我们稍后会详细介绍,现在只要知道一条redo日志占用的空间不是很大就好了。
redo日志是顺序写入磁盘的
在执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO。

上面内容部分摘自 《MySQL是怎么运行的?》,上面我们只提到了简单的redo日志类型。但是实际情况下,一条SQL语句会涉及到很多页中内容的修改,所以又有很多其他复杂的 redo日志类型,帮助去解决这个问题。这里就不说太多。

2.2 redo log 的内部结构:log block 与 log buffer

简单redo日志格式为:「表空间号+数据页号+偏移量+修改几个字节的值+具体的值」,但是redo log 并不是一行一行写入 日志文件的。它采用block来进行管理,block内部包含了12字节的header块,496字节的body块,和4字节的trailer块尾。

真正的redo日志都是存储到占用496字节大小的log block body中,图中的log block header和log block trailer存储的是一些管理信息。

设计InnoDB的大佬为了解决磁盘速度过慢的问题而引入了Buffer Pool。同理,写入redo日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为redo log buffer的连续内存空间,翻译成中文就是redo日志缓冲区,我们也可以简称为log buffer。这片内存空间被划分成若干个连续的redo log block

redo log 日志先按照 redo log block 格式,写入到缓冲区中的 block中(redo log buffer),再由 redo log buffer 写入到磁盘。
redo log buffer 大小可以通过参数 innodb_log_buffer_size 来指定。MySQL5.7默认此参数为16MB。

2.3 redo log 的刷盘策略:write 与 fsync

通常来说,redo log 刷盘的时机是在事务提交的 commit 阶段采取的。在此之前 redo log 都存在于 redo log buffer中。

● **刷盘(write)**:指的是MySQL从buffer pool中将内容写到系统的page cache中,并没有持久化到系统磁盘上。这个速度其实是很快的。
● **落盘(fsync)**:指的是从系统的cache中将数据持久化到系统磁盘上。这个速度可以认为比较慢,而且也是IOPS升高的真正原因。

相关参数

innodb_flush_logs_at_trx_commit(该参数针对redo log)

取值0:每次提交事务都只把redo log留在redo log buffer中。
取值1:每次提交事务都将redo log 持久化到磁盘上,也就是write+fsync
取值2:每次都把redo log写到系统的page cache中,也就是只write,不fsync

sync_binlog(该参数针对binlog)

取值0:每次提交都将binlog 从binlog cache中 write到磁盘上,而不fsync到磁盘
取值1:每次提交事务都将binlog fsync到磁盘上
取值N:每次提交事务都将binlog write到磁盘上,累计N个事务之后,执行fsync

三、undo log:保障事务原子性与一致性的基石

undo log 主要有两个作用:事务回滚MVCC

  • 事务回滚:当事务执行失败或用户显式执行ROLLBACK时,InnoDB会利用undo log中记录的逻辑”反向”操作,将数据恢复到事务开始前的状态,从而保证原子性。
  • MVCC(多版本并发控制):在读已提交(Read Committed)和可重复读(Repeatable Read)隔离级别下,当一个事务需要读取被其他未提交事务修改的行时,InnoDB会通过undo log找到该行之前的版本,从而实现非锁定读,提高并发性能。

undo log记录的是逻辑日志,例如:

  • INSERT操作,undo log记录一条对应的DELETE操作。
  • DELETE操作,undo log记录一条对应的INSERT操作。
  • UPDATE操作,undo log记录一条修改为旧值的UPDATE操作。

四、redo log 与 binlog 的数据一致性(两阶段提交)

binlog 的一个主要功能就是数据库建的主从同步。
binlog 是默认不开启的。但是如果开启了 binlog 后,就需要注意 redolog 与 binlog 的数据一致性问题。如果没有两阶段提交机制,会发生什么问题呢?

  • 场景一:先写 redo log,再写 binlog
    • 如果 redo log 写入成功后、binlog 写入前,数据库发生宕机,那么主库在重启后会通过 redo log 恢复数据,但 binlog 中没有这条记录,导致从库无法同步该事务,造成主从不一致。
  • 场景二:先写 binlog,再写 redo log
    • 如果 binlog 写入成功后、redo log 写入前,数据库发生宕机,那么从库会同步该事务,但主库在重启后由于没有 redo log 记录,事务无效,同样造成主从不一致。

为了解决这个问题,InnoDB提出了 两段提交方式。两段即 prepare阶段、commit阶段。其实就是将 redolog 的写入拆分成了两个步骤

五、参考资料

参考的优秀文章: