14|数据库事务:事务提交了,你的数据就一定不会丢吗?

思考并回答以下问题:

你好,我是邓明。今天我们来学习数据库中非常重要的一部分——数据库事务。这节课的内容和前面MVCC的内容联系很紧密,你要结合在一起学习。

数据库事务在面试中占据了比较重的分量。如果你面的是非常初级的岗位,那么可能就是问问事务的ACID特性。不然的话,基本上都要深入redo log和undo log这种事务实现机制上去。所以这一节课我会带你深入分析redo log和undo log。同时我也会告诉你,哪些知识是比较基础的,你一定要掌握;哪些知识是比较高级的,你尽量记住,面试时作为亮点来展示。

最后我会给出两个比较高级的方案,一个是侧重理论的写入语义大讨论,一个是侧重实践的调整MySQL参数。我们先从上一节课已经初步接触过的undo log开始说起。

前置知识

undo log

上一节课,我说过版本链是存放在的undo log里面的。那么undo log到底是什么呢?

undo log是指回滚日志,用一个比喻来说,就是后悔药,它记录着事务执行过程中被修改的数据。当事务回滚的时候,InnoDB会根据undo log里的数据撤销事务的更改,把数据库恢复到原来的状态。

既然undo log是用来回滚的,那么不同的语句对应的undo log形态会不一样。

  • 对于INSERT来说,对应的undo log应该是DELETE。
  • 对于DELETE来说,对应的undo log应该是INSERT。
  • 对于UPDATE来说,对应的undo log也应该是UPDATE。比如说有一个数据的值原本是3,要把它更新成5。那么对应的undo log就是把数据更新回3。

但是实际上undo log的实现机制要更复杂一点,我这里简化一下模型帮助你记忆。对于INSERT来说,对应的undo log记录了该行的主键。那么后续只需要根据undo log里面的主键去原本的聚簇索引里面删掉记录,就可以实现回滚效果。

对于DELETE来说,对应的undo log记录了该行的主键。因为在事务执行DELETE的时候,实际上并没有真的把记录删除,只是把原记录的删除标记位设置成了true。所以这里undo log记录了主键之后,在回滚的时候就可以根据undo log找到原本的记录,然后再把删除标记位设置成false。

对于UPDATE来说,要更加复杂一些。分为两种情况,如果没有更新主键,那么undo log里面就记录原记录的主键和被修改的列的原值。

如果更新了主键,那么可以看作是删除了原本的行,然后插入了一个新行。因此undo log可以看作是一个DELETE原数据的undo log再加上插入一个新行的undo log。

从上面你大概也能看出来,undo log和MVCC中版本链的关系。

这部分知识有些难,如果你不是数据库岗位或者专家岗,了解这部分就可以了。接下来我们看一个和事务有关的另外一个日志redo log。

redo log

InnoDB引擎在数据库发生更改的时候,把更改操作记录在redo log里,以便在数据库发生崩溃或出现其他问题的时候,能够通过redo log来重做。

你可能会觉得奇怪,InnoDB引擎不是直接修改了数据吗?为什么需要redo log?答案是InnoDB引擎读写都不是直接操作磁盘的,而是读写内存里的buffer pool,后面再把buffer pool里面修改过的数据刷新到磁盘里面。这是两个步骤,所以就可能会出现buffer pool中的数据修改了,但是还没来得及刷新到磁盘数据库就崩溃了的情况。

为了解决这个问题,InnoDB引擎就引入了redo log。相当于InnoDB先把buffer pool里面的数据更新了,再写一份redo log。等到事务结束之后,就把buffer pool的数据刷新到磁盘里面。万一事务提交了,但是buffer pool的数据没写回去,就可以用redo log来恢复。

你可能会困惑,redo log不需要写磁盘吗?如果redo log也要写磁盘,干嘛不直接修改数据呢?redo log是需要写磁盘的,但是redo log是顺序写的,所以也是WAL(write-ahead-log)的一种。也就是说,不管你要修改什么数据,一会修改这条数据,一会修改另外一条数据,redo log在磁盘上都是紧挨着的。

这是中间件设计里常用的技巧——顺序写取代随机写。顺序写的性能比随机写要好很多,即便是在SSD上,顺序写也能比随机写速度快上一个数量级。但是还要考虑一点:redo log是直接一步到位写到磁盘的吗?

并不是的,redo log本身也是先写进redo log buffer,后面再刷新到操作系统的page cache,或者一步到位刷新到磁盘。

InnoDB引擎本身提供了参数innodb_flush_log_at_trx_commit来控制写到磁盘的时机,里面有三个不同值。

  • 0:每秒刷新到磁盘,是从redo log buffer到磁盘。
  • 1:每次提交的时候刷新到磁盘上,也就是最安全的选项,InnoDB的默认值
  • 2:每次提交的时候刷新到page cache里,依赖于操作系统后续刷新到磁盘。

这时候你就应该意识到这样一个问题,除非把innodb_flush_log_at_trx_commit设置成1,否则其他两个都有丢失的风险。

  • 0:你提交之后,InnoDB还没把redo log buffer中的数据刷新到磁盘,就宕机了。
  • 2:你提交之后,InnoDB把redo log刷新到了page cache里面,紧接着宕机了。

在这两个场景下,你的业务都认为事务提交成功了,但是数据库实际上丢失了这个事务。但是并不是说InnoDB引擎会严格遵循参数说明的那样来刷新磁盘,还有两种例外情况。

1,如果redo log buffer快要满了,也会触发把redo log刷新到磁盘里这个动作。一般来说,默认放一半了就会刷新。

2,如果某个事务提交的时候触发了刷新到磁盘的动作,那么当下所有事务的redo log也会一并刷新。毕竟大家的redo log都是放在redo log buffer里,有人需要刷新了,就顺手一起刷新了。类似于你去买奶茶,顺手帮你同事带一杯。

现在你已经了解了undo log和redo log的用法和机制,那么我们来总结一下InnoDB是如何综合利用它们来实现事务的。

事务执行过程

这里我用一个UPDATE语句执行的例子来向你介绍事务执行过程。假如说原本a=3,现在要执行UPDATE tab SET a = 5 WHERE id = 1

1,事务开始,在执行UPDATE语句之前会先查找到目标行,加上锁,然后写入到buffer pool里面。

2,写undo log。

3,InnoDB引擎在内存上更新值,实际上就是把buffer pool的值更新为目标值5。

4,写redo log。

5,提交事务,根据innodb_flush_log_at_trx_commit决定是否刷新redo log。

6,刷新buffer pool到磁盘。

上面这个是最正常的流程。那么它还有两个比较重要的子流程。

1,如果在redo log已经刷新到磁盘,然后数据库宕机了,buffer pool丢失了修改,那么在MySQL重启之后就会回放这个redo log,从而纠正数据库里的数据。

2,如果都没有提交,中途回滚,就可以利用undo log去修复buffer pool和磁盘上的数据。因为有时,buffer pool脏页会在事务提交前刷新磁盘,所以undo log也可以用来修复磁盘数据。

实际上,事务执行过程有很多细节,也要比这里描述得复杂很多。如果你感兴趣,可以再继续深挖下去。不过如果是为了在面试中展示,上面的这些内容也够用了。

binlog

binlog是一个看起来和redo log、undo log很像的东西,但是它们之间的差异还是挺大的。binlog是用于存储MySQL中二进制日志(Binary Log)的操作日志文件,它是MySQL Server级别的日志,也就是说所有引擎都有。它记录了MySQL中数据库的增删改操作,因此binlog主要有两个用途,一是在数据库出现故障时恢复数据。二是用于主从同步,即便是Canal这一类的中间件本质上也是把自己伪装成一个从节点。

在事务执行过程中,写入binlog的时机有点巧妙。它和redo log的提交过程结合在一起称为MySQL的两阶段提交。

  • 第一阶段:redo log Prepare(准备)
  • 第二阶段:redo log Commit(提交)

如果redo log Prepare执行完毕后,binlog已经写成功了,那么即便redo log提交失败,MySQL也会认为事务已经提交了。如果binlog没写成功,那么MySQL就认为提交失败了。比较简单的记忆方式就是看binlog写成功了没有。binlog本身有一些完整性校验的规则,所以在MySQL看来,写binlog要么成功,要么失败,不存在中间状态。

我用正统的两阶段提交图来说明一下,在这个图里我把写入binlog拆成了Prepare和Commit两个阶段。

从图里面,你就可以看出来,之所以以binlog为判断标准,是因为在两阶段提交里面,两个参与方binlog和redo log中,binlog已经提交成功了,那么redo log自然可以认为也提交成功了。但是我要强调,这只是为了方便你理解,实际上binlog没有显式的Prepare和Commit两个阶段。

binlog也有刷新磁盘的问题,不过你可以通过sync_binlog参数来控制它。

  • 0:由操作系统决定,写入page cache就认为成功了。0也是默认值,这个时候数据库的性能最好。
  • N:每N次提交就刷新到磁盘,N越小性能越差。如果N=1,那么就是每次事务提交都把binlog刷新到磁盘。

你可以对比一下sync_binlog和redo log的innodb_flush_log_at_trx_commit参数,加深印象。最后我再补充一点非常基础的事务的ACID特性。

ACID特性

事务的ACID特性是指原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)还有持久性(Durability)。

  • 原子性:事务是一个不可分割的整体,它在执行过程中不能被中断或推迟,它的所有操作都必须一次性执行,要么都成功,要么都失败。
  • 一致性:事务执行的结果必须满足数据约束条件,不会出现矛盾的结果。注意这里的一致性和我们讨论的分布式环境下的一致性语义有所差别,后者强调的是不同数据源之间数据一致。
  • 隔离性:事务在执行的时候可以隔离其他事务的干扰,也就是不同事务之间不会相互影响。
  • 持久性:事务执行的结果必须保证在数据库里永久保存,即使系统出现故障或者数据库被删除,事务的结果也不会丢失。

这个知识点就比较基础,是你一定要记住的。

面试准备

你需要弄清楚自己公司内部的一些配置。

  • 有没有修改过sync_binlog、innodb_flush_log_at_trx_commit的值?如果修改过,为什么修改?
  • 你使用过的中间件有没有类似redo log、binlog的刷盘机制?

因为事务的机制比较复杂,涉及redo log和undo log的各种配合,所以你要提前思考一下事务执行过程的各种异常情况,就是当中途某个操作执行成功了,万一数据库宕机,数据库恢复过来之后会怎么处理这个事务。

我这里给你一个极简版口令,非常好记。

  • 在redo log刷新到磁盘之前,都是回滚。
  • 如果redo log刷新到了磁盘,那么就是重放redo log。

进一步结合binlog,就是如果binlog都已经提交成功了,那么就重放,确保事务一定成功了,否则回滚。而回滚就是一句话:用undo log来恢复数据。

上面这些极简描述,是不管怎样你都要记住的。但是如果你有时间的话最好记住前置知识里面提到的所有细节。

此外,你要对undo log、redo log的必要性有深刻理解。因为面试官可能会问出一些反直觉的问题。

  • 没有undo log会怎样?没有undo log就没有后悔药,没有办法回滚。
  • 没有redo log会怎样?没有redo log的话,写到buffer pool,宕机了就会丢失数据。
  • 为什么非得引入redo log,干嘛不直接修改数据?直接修改数据就是随机写,性能极差。

在面试过程中,如果面试官聊起了操作系统中的写入操作,谈到了page cache等内容,那么你就可以谈redo log、undo log和binlog的写入语义,还可以直接把话题引到后面的写入语义亮点上。如果聊到了其他中间件的刷盘问题,那么你就可以用redo log和binlog作为例子来说明。如果他提到了两阶段提交协议,那么你可以把话题引导到redo log和binlog的两阶段提交协议上。

基本思路

事务的面试可以说是方向多、细节多。比较常见的是问你ACID中四个基本特性。这里你其实可以抓住机会来展示一个亮点,关键词是隔离级别

ACID中的隔离性是比较有意思的,它和数据库的隔离级别概念密切相关。我个人认为隔离级别中未提交读和已提交读看起来不太符合这里隔离性的定义。按理来说,可重复读也不满足。但是MySQL InnoDB引擎的可重复读解决了幻读问题,所以我认为MySQL的可重复读和串行化才算是满足了这里隔离性的要求。

这里大概率会把话题引到隔离级别上,这一部分内容我们上一节课已经学过了,这里不再重复。

有些时候面试官就会直接问你undo log、redo log,或者直接问你InnoDB的事务实现机制。

那么你就可以先介绍undo log,然后可以进一步补充,聊一聊undo log在INSERT、DELETE和UPDATE下是如何运作的,这算是一个小亮点。其次介绍redo log,这里你可以进一步介绍redo log的刷盘问题,也算是一个小亮点。大体上来说你能够解释清楚undo log和redo log就可以了。如果面试官此时还没有打断你的意思,你就继续介绍事务执行过程,可以用我给出的那个UPDATE例子来说明。

就事务执行而言,到这一步你基本上已经把核心知识都交代清楚了。剩下的就是面试官会根据你的回答中的一些进行深挖。一般的面试不太可能超出前置知识的范畴,你做好心理准备就可以。至于binlog,你完全可以等面试官进一步问。如果你能记住binlog和redo log的两阶段提交协议,那么也算是回答中的一个小亮点。

亮点方案

其实如果你能在面试的时候把前置知识都用上,那么你至少能够得到一个MySQL基础扎实的评价。不过你还是可以进一步展示更加高级的亮点。这里我给出了两个,第一个是写入语义,第二个调整刷盘时机。

写入语义

在前面的分析中你已经知道了,当我们说写入redo log成功的时候,实际上有不同的含义。比如说可能只是写到了redo log buffer,也可能是写到了page cache,还可能是一步到位写到了磁盘上。

综合来说,中间件的写入语义可能是以下3种。

  • 中间件写到自身的日志中或者缓冲区,就认为写入成功了。
  • 中间件发起了系统调用,写到了操作系统的page cache里面,就认为写入成功了。
  • 中间件强制发起刷盘,数据被持久化到了磁盘上,才认为写入成功了。

除了一步到位直接写到磁盘,其他两种都要考虑一个问题,就是最终什么时候把数据刷到磁盘呢?思路也有两个,一个是定时,比如说每秒刷到磁盘。另一个是定量,这个就很灵活了,比如说在事务这里基本都是按照提交次数。而在消息队列里面,就可以按照消息条数。

但是这里并没有考虑到另外一个问题,即在分布式环境下,写入一份数据,往往是指要写入主节点,也要写入从节点。

所以在分布式环境下的写入语义更加含糊。

  • 主节点写入成功,就认为写入成功了。
  • 主节点写入成功,至少有一个从节点写入成功了,就认为写入成功了。
  • 主节点写入成功,大多数从节点写入成功了,就认为写入成功了。
  • 主节点写入成功,特定数量的从节点写入成功了,就认为写入成功了。一般来说,这个“特定数量”是可配置的。
  • 主节点写入成功,全部从节点也写入成功了,才认为写入成功了。

那么不管是主节点还是从节点的“写入成功”,都要考虑前面说到的刷盘问题。所以你就可以在谈到刷盘的时候进一步引申到这里。

redo log和binlog的刷盘问题,在中间件里面经常遇到。一般来说,中间件的写入语义可能是中间件写入到自身的日志中或者缓冲区、中间件发起了系统调用,写入操作系统的page cache、中间件强制发起刷盘,数据被持久化到了磁盘上这3种。如果不是直接写到磁盘,那么中间件就会考虑定时或者定量刷新数据到磁盘。
在分布式环境下,写入语义会更加复杂,因为要考虑是否写从节点,以及写多少个从节点的问题。比如说在Kafka里面acks机制,就是控制写入的时候要不要同步写入到从分区里面,或者Redis里面控制AOF刷盘时机。

我在最后举了Kafka和Redis的例子,这里你可以考虑换成你自己了解的其他例子。

调整刷盘时机

这类似于你在上一节课刷亮点的技巧,也就是要结合公司的实际情况,考虑调整redo log和binlog的刷盘时机。那么基本方向有两个。第一个是对数据不丢失、一致性要求高的业务,你可以考虑调整sync_binlog的值为1

在我的系统里面,有一个业务对数据不丢失,一致性要求非常高。因此我们尝试调整sync_binlog为1。但是代价就是数据库的性能比较差,因为每次提交都需要刷新binlog到磁盘上。当然这时候innodb_flush_log_at_trx_commit也要使用默认值1。

另外一个是性能优先,能够容忍一定数据丢失的。那么你可以考虑将innodb_flush_log_at_trx_commit调整为0或者2,同时还可以把sync_binlog调整为比较大的值,比如说调到100。

我们有一个对性能非常敏感,但是对数据丢失容忍度比较高的业务,那么我就尝试将innodb_flush_log_at_trx_commit设置为2,让操作系统来决定什么时候刷新redo log。同时还把sync_binlog的值调整为100,进一步提高数据库的性能。

或者你综合两者。

之前我们有一个数据库,给两类业务使用。一类业务对数据不丢失,一致性要求极高,另外一类对性能敏感,但是可以容忍一定程度的数据丢失。后来我将这两类业务的表分开,放在两个数据库上。

然后再分别阐述对应的调整策略就好了。当然,这个方案和数据库的性能有关,也可以做成你MySQL性能调优方案中的一环。

面试思路总结

这一节课的思路要难一些,下面列举的这些知识点是你一定要记住的。

  • undo log、redo log的原理、作用。
  • 事务执行过程。
  • binlog,尽量结合redo log一起理解两阶段提交协议。
  • ACID特性,绝对不能忘的基本功。

此外我还给出了两个你在面试中可以展示的亮点。一个是写入语义,我们要综合考虑单机写到哪里、分布式写不写从节点、写多少从节点的问题。另一个是调整刷盘时机,看重数据的就调小sync_binlog,看重性能的就调大sync_binlog以及调整innodb_flush_log_at_trx_commit。

我在写入语义这个亮点给你演示了在面试中刷亮点的另外一种思路:充分地横向对比。简单来说就是我们计算机行业里面其实很多设计理念、解决方法都是充满了套路的。你要做的就是找出这个套路,然后在面试中说出来,并且举不同中间件作为例子佐证。

思考题

最后你来思考两个问题。

  • 你用过的中间件是怎么处理刷盘的?如果有主从结构,如何控制写入到从节点?
  • 在聊到隔离性和隔离级别的时候我说到一个个人观点,即未提交读和已提交读,不能看作完全实现了隔离性,你怎么看待这两者?
0%