Undo Log 和 Redo Log 这次一定要搞清楚

2022-05-16 14:21:17 浏览数 (1)

事务和ACID

我们学数据库的时候经常看到事务和ACID的说法。

什么是事务呢?

在数据库系统中,一个事务是指:由一系列数据库操作组成的一个完整的逻辑过程。

例如银行转帐:

1.从原账户扣除金额;

2.向目标账户添加金额。

这两个数据库操作的总和,构成一个完整的逻辑过程,不可拆分。这个过程被称为一个事务,具有ACID特性。

那什么又是ACID呢?

维基百科上ACID的定义如下:

★ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability)。”

  • 原子性(Atomic):在同一项业务处理过程中,事务保证了多个对数据的修改,要么同时成功,要么一起被撤销。比如转账,要么转账成功,要么转账失败,不存在转了一半的情况。
  • 隔离性(Isolation):在不同的业务处理过程中,事务保证了各自业务正在读、写的数据互相独立,不会彼此影响。数据库一般有四种隔离级别:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、可串行化(Serializable)。
  • 持久性(Durability):事务应当保证所有被成功提交的数据修改都能够正确地被持久化,也就是保存到磁盘上了,不会丢失数据。
  • 一致性(Consistency):保证系统中的数据是正确的,不同数据间不会产生矛盾,结果是一致的。

其实原子性、隔离性、持久性的最终目的就是为了数据的一致性。

如何实现原子性和持久性

原子性保证了一个事务中的多个操作要么都成功,要么都失败,不存在成功一半的情况。持久性保证了事务一旦生效,就不会因为任何原因导致数据被修改或者丢失。

那么如果才能实现原子性和持久性呢?

我们很容易就能想到,数据库把数据写入磁盘不就行了吗?

是的,这是正确的方法,但问题是“写入磁盘”这个操作不是原子的,写操作可以有开始写入、写入中、写入成功,甚至还有写入失败等状态。

而且一个事务中经常包含多个操作,比如我们去网上下单买东西,一般涉及到这几个操作:从我们的账户中扣款、在商家的账户增加货款、把商品的库存减掉等等。这些操作是在一个事务中的,也就是说要么全部成功,要么全部失败。

崩溃恢复

如果我们的账户中扣了100块钱,这个操作成功写入了磁盘,而在给商家增加100块钱的时候系统崩溃了(这么倒霉?),或者停电了(不会吧?),导致写入失败(经常会出现吧?)。

为了避免这种情况发生,数据库就得想办法知道系统崩溃前完整的操作是怎么样的,这样等服务器恢复后,数据库要把还没来得及写入磁盘的那一部分数据重新写入,给商家的账号上加100块钱,完成未竟的事业。

那么问题来了,系统恢复后数据库如何知道之前事务的所有信息呢?

好记性不如烂笔头,我们先写下来不就行了吗?

Redo Log

这就要求数据库在写磁盘之前要把事务所有的操作都先记录下来,比如修改什么数据、数据物理上位于哪个内存页和磁盘块中、从什么值改成什么值等等,以日志的形式先写到磁盘中。

只有在日志记录全部都安全落盘,然后在最后写上“Commit Record”后,表示所有的操作记录我都写完啦。

这时候数据库才会根据日志上的信息,对真正的数据进行修改,修改完成后,在日志中加入一条“End Record”,表示我已经按照日志里的步骤都做完啦,事务持久化的工作也就做完了。

这种事务实现方法被称为“Commit Logging”。

这种方式实现数据持久性、原子性的原理如下:

首先, 一旦日志成功写入了Commit Record,那就表示事务相关的所有信息都已经写到日志中了,如果修改数据的过程中系统崩溃了,重启后只要再根据日志的内容重新操作一遍就行了,这就保证了持久性。

其次,如果日志还没写完系统就崩溃了,系统重启后,数据库一看日志里没有Commit Record,这话就说明日志是不全的,还没有写完,那么就将这部分日志标记为回滚状态,整个事务就回滚了,这就保证了原子性。

换句话说就是,我先把我要改的东西记录在日志里,我再根据日志统一写到磁盘中,万一我在写入磁盘的过程中晕倒了,等我醒来的时候,我先查看日志的完整性。

如果日志是完整的,里面有Commit Record,我就照着日志重新做一遍,最后也能成功。如果日志是不完整的,里面没有Commit Record,我就回滚整个事务,什么都不做。

这个日志就叫做 Redo Log,也就是“重做日志”,中途崩溃的数据库,根据这个日志把事务重做一遍。

Undo Log

不过Redo Log有个问题,就是效率太慢。

因为数据库对数据的所有真实修改,都必须发生在事务提交之后,并且在日志写入了 Commit Record 之后才能进行,没有写完Redo Log,数据库是不敢先写的。

即使事务提交前磁盘 I/O 有足够空闲、即使某个事务修改的数据量非常庞大,占用大量的内存缓冲,无论何种理由,都决不允许在事务提交之前就开始修改磁盘上的数据,万一系统崩溃了,数据出差谁负责呀?

但是当一个事务中数据量特别大的时候,等全部变更写入Redo Log然后再统一写入磁盘,这样性能就不是很好,就会很慢,老板就会不开心。

那能不能在事务提交之前,偷偷地先写一点数据到磁盘呢(偷跑)?

答案是可以的,这就是STEAL策略,但是问题来了,你偷摸地写了数据,万一事务要回滚,或者系统崩溃了,这些提前写入的数据就变成了脏数据,必须想办法把它恢复才行。

这就需要引入Undo Log(回滚日志),在偷摸写入数据之前,必须先在Undo Log中记录都写入了什么数据,改了什么地方,到时候事务回滚了,就按照Undo Log日志,一条条恢复到原来的样子,就像没有改过一样。

Undo Log还有一个作用,就是实现多个行版本控制(MVCC),当读取的某一行被其他事务锁定时,它可以从 Undo Log 中获取该行记录以前的数据是什么,从而提供该行版本信息,让用户读取。

总结

Undo Log(重做日志) 和 Redo Log(回滚日志)之间的区别,没那么高深,我们只要按字面意思理解就行了。

Redo Log(重做日志)是为了系统崩溃之后恢复数据用的,让数据库照着日志,把没做好的事情重做一遍。 有了Redo Log,就可以保证即使数据库发崩溃重启后,之前提交的记录都不会丢失,这个能力称为 crash-safe。

Undo Log(回滚日志)是为了回滚用的。 在事务提交之前就开始写数据,万一事务到最后又打算不提交了,要回滚,或者系统崩溃了,这些提前写入的数据就变成了脏数据,这时候就必须用Undo Log恢复了。

这种在写磁盘之前先写日志的方式就叫做:Write-Ahead Logging(WAL),WAL让性能更高了,不过同时也更复杂了,虽然复杂点,但是效果很好啊,mysql、sqlite、postgresql、sql server等数据库都实现了WAL机制呢。

0 人点赞