6.5 事务实现原理之1:Redo Log
介绍事务怎么用后,下面探讨事务的实现原理。事务有ACID四个核心属性:
A:原子性。事务要么不执行,要么完全执行。如果执行到一半,宕机重启,已执行的一半要回滚回去。
C:一致性。各种约束条件,比如主键不能为空、参照完整性等。
I:隔离性。隔离性和并发性密切相关,因为如果事务全是串行的(第四个隔离级别),也不需要隔离。
D:持久性。这个很容易理解,一旦事务提交了,数据就不能丢。
在这四个属性中,D比较容易,C主要是由上层的各种规则来约束,也相对简单。而A和I牵涉并发问题、崩溃恢复的问题,将是讨论的重点。
说到事务的实现原理,会追溯到ARIES算法理论,ARIES(Algorithms for Recovery AndIsolation Expoliting Semantics)是20世纪90年代由IBM的几位研究员提出的一个算法集,主论文是ARIES: A TransactionRecovery Method Supporting Fine-Granularity Locking and Partial Rollbacks UsingWrite-Ahead Loggging。ARIES的思想影响深远,现代的关系型数据库(DB2、MySQL、InnoDB、SQL Server、Oracle)在事务实现的很多方面都吸收了该思想,在大学的教科书上如果讲到事务的实现,也都会介绍AREIS方法。
接下来,就以InnoDB为背景,分析事务的ACID其中的三个属性(A、I、D)是如何实现的。先从最简单的D开始(I/O问题),然后是A,最后讨论I。
6.5.1 Write-Ahead
一个事务要修改多张表的多条记录,多条记录分布在不同的Page里面,对应到磁盘的不同位置。如果每个事务都直接写磁盘,一次事务提交就要多次磁盘的随机I/O,性能达不到要求。怎么办呢?不写磁盘,在内存中进行事务提交。然后再通过后台线程,异步地把内存中的数据写入到磁盘中。但有个问题:机器宕机,内存中的数据还没来得及刷盘,数据就丢失了。
为此,就有了Write-aheadLog的思路:先在内存中提交事务,然后写日志(所谓的Write-ahead Log),然后后台任务把内存中的数据异步刷到磁盘。日志是顺序地在尾部Append,从而也就避免了一个事务发生多次磁盘随机I/O的问题。明明是先在内存中提交事务,后写的日志,为什么叫作Write-Ahead呢?这里的Ahead,其实是指相对于真正的数据刷到磁盘,因为是先写的日志,后把内存数据刷到磁盘,所以叫Write-Ahead Log。
内存操作数据 +Write-Ahead Log的这种思想非常普遍,后面讲LSM树的时候,还会再次提到这个思想。在多备份一致性中,复制状态机的模型也是基于此。
具体到InnoDB中,Write-Ahead Log是Redo Log。在InnoDB中,不光事务修改的数据库表数据是异步刷盘的,连Redo Log的写入本身也是异步的。如图6-7所示,在事务提交之后,Redo Log先写入到内存中的Redo Log Buffer中,然后异步地刷到磁盘上的Redo Log。
为此,InnoDB有个关键的参数innodb_flush_log_at_trx_commit控制Redo Log的刷盘策略,该参数有三个取值:
0:每秒刷一次磁盘,把Redo Log Buffer中的数据刷到Redo Log(默认为0)。
1:每提交一个事务,就刷一次磁盘(这个最安全)。
2:不刷盘。然后根据参数innodb_flush_log_at_timeout设置的值决定刷盘频率。
很显然,该参数设置为0或者2都可能丢失数据。设置为1最安全,但性能最差。InnoDB设置此参数,也是为了让应用在数据安全性和性能之间做一个权衡。
图6-7 Redo Log的异步刷盘示意图
6.5.2 Redo Log的逻辑与物理结构
知道了Redo Log的基本设计思想,下面来看Redo Log的详细结构。
从逻辑上来讲,日志就是一个无限延长的字节流,从数据库安装好并启动的时间点开始,日志便源源不断地追加,永无结束。
但从物理上来讲,日志不可能是一个永不结束的字节流,日志的物理结构和逻辑结构,有两个非常显著的差异点:
(1)磁盘的读取和写入都不是按一个个字节来处理的,磁盘是“块”设备,为了保证磁盘的I/O效率,都是整块地读取和写入。对于Redo Log来说,就是Redo Log Block,每个Redo Log Block是512字节。为什么是512字节呢?因为早期的磁盘,一个扇区(最细粒度的磁盘存储单位)就是存储512字节数据。
(2)日志文件不可能无限制膨胀,过了一定时期,之前的历史日志就不需要了,通俗地讲叫“归档”,专业术语是Checkpoint。所以Redo Log其实是一个固定大小的文件,循环使用,写到尾部之后,回到头部覆写(实际Redo Log是一组文件,但这里就当成一个大文件,不影响对原理的理解)。之所以能覆写,因为一旦Page数据刷到磁盘上,日志数据就没有存在的必要了。
图6-8展示了Redo Log逻辑与物理结构的差异,LSN(Log Sequence Number)是逻辑上日志按照时间顺序从小到大的编号。在InnoDB中,LSN是一个64位的整数,取的是从数据库安装启动开始,到当前所写入的总的日志字节数。实际上LSN没有从0开始,而是从8192开始,这个是InnoDB源代码里面的一个常量LOG_START_LSN。因为事务有大有小,每个事务产生的日志数据量是不一样的,所以日志是变长记录,因此LSN是单调递增的,但肯定不是呈单调连续递增。
图6-8 Redo Log逻辑结构与物理结构的差异
物理上面,一个固定的文件大小,每512个字节一个Block,循环使用。显然,很容易通过LSN换算出所属的Block。反过来,给定Redo Log,也很容易算出第一条日志在什么位置。假设在Redo Log中,从头到尾所记录的LSN依次如下所示:
(200,289,378,478,30,46,58,69,129)
很显然,第1条日志是30,最后1条日志是478,30以前的已经被覆盖。
6.5.3 Physiological Logging
知道了Redo Log的整体结构,下面进一步来看每个Log Block里面Log的存储格式。这个问题很关键,是数据库事务实现的一个核心点。
(1)记法1。类似Binlog的statement格式,记原始的SQL语句,insert/delete/update。
(2) 记法2。类似Binlog的RAW格式,记录每张表的每条记录的修改前的值、修改后的值,类似(表,行,修改前的值,修改后的值)。
(3) 记法3。记录修改的每个Page的字节数据。由于每个Page有16KB,记录这16KB里哪些部分被修改了。一个Page如果被修改了多个地方,就会有多条物理日志,如下所示:
(Page ID,offset1,len1,改之前的值,改之后的值)
(Page ID,offset2,len2,改之前的值,改之后的值)
前两种记法都是逻辑记法;第三种是物理记法。Redo Log采用了哪种记法呢?它采用了逻辑和物理的综合体,就是先以Page为单位记录日志,每个Page里面再采取逻辑记法(记录Page里面的哪一行被修改了)。这种记法有个专业术语,叫PhysiologicalLogging。
要搞清楚为什么要采用PhysiologicalLogging,就得知道逻辑日志和物理日志的对应关系:
(1)一条逻辑日志可能产生多个Page的物理日志。比如往某个表中插入一条记录,逻辑上是一条日志,但物理上可能会操作两个以上的Page?为什么呢,因为一个表可能有多个索引,每个索引都是一颗B+树,插入一条记录,同时更新多个索引,自然可能修改多个Page。
如果Redo Log采用逻辑日志的记法,一条记录牵涉的多个Page写到一半系统宕机了,要恢复的时候很难知道到底哪个Page写成功了,哪个失败了。
(2)即使1条逻辑日志只对应一个Page,也可能要修改这个Page的多个地方。因为一个Page里面的记录是用链表串联的,所以如果在中间插入一条记录,不仅要插入数据,还要修改记录前后的链表指针。对应到Page就是多个位置要修改,会产生多条物理日志。
所以纯粹的逻辑日志宕机后不好恢复;物理日志又太大,一条逻辑日志就可能对应多条物理日志。Physiological Logging综合了两种记法的优点,先以Page为单位记录日志,在每个Page里面再采用逻辑记法。
6.5.4 I/O写入的原子性(Double Write)
要实现事务的原子性,先得考虑磁盘I/O的原子性。一个LogBlock是512个字节。假设调用操作系统的一次Write,往磁盘上写入一个Log Block(512个字节),如果写到一半机器宕机后再重启,请问写入成功的字节数是0,还是[0,512]之间的任意一个数值?
这个问题的答案并不唯一,可能与操作系统底层和磁盘的机制有关,如果底层实现了512个字节写入的原子性,上层就不需要做什么事情;否则,在上层就需要考虑这个问题。假设底层没有保证512个字节的原子性,可以通过在日志中加入checksum解决。通过checksum能判断出宕机之后重启,一个Log Block是否完整。如果不完整,就可以丢弃这个LogBlock,对日志来说,就是做截断操作。
除了日志写入有原子性问题,数据写入的原子性问题更大。一个Page有16KB,往磁盘上刷盘,如果刷到一半系统宕机再重启,请问这个Page是什么状态?在这种情况下,Page既不是一个脏的Page,也不是一个干净的Page,而是一个损坏的Page。既然已经有Redo Log了,不能用Redo Log恢复这个Page吗?
因为Redo Log也恢复不了。因为Redo Log是Physiological Logging,里面只是一个对Page的修改的逻辑记录,Redo Log记录了哪个地方修改了,但不知道哪个地方损坏了。另外,即使为这个Page加了checksum,也只能判断出Page损坏了,只能丢弃,但无法恢复数据。有两个解决办法:
(1)让硬件支持16KB写入的原子性。要么写入0个字节,要么16KB全部成功。
(2)Doublewrite。把16KB写入到一个临时的磁盘位置,写入成功后再拷贝到目标磁盘位置。
这样,即使目标磁盘位置的16KB因为宕机被损坏了,还可以用备份去恢复。
Redo Log的原理比较复杂,在接下来的1篇中,将接着这个话题继续探讨。
后记: 本文节选自作者书籍《软件架构设计:大型网站技术架构与业务架构融合之道》。
作者微信公众号:架构之道与术。公众号底部菜单有书友群可以加入,与作者和其他读者进行深入讨论。也可以在京东、天猫上购买纸质书籍。