跳至主要內容

数据库是如何保证原子性和一致性的

ruleeeer原创数据库数据库事务原子性一致性大约 6 分钟约 1828 字

原子性和一致性

众所周知,数据库一般需要保持四大特性,分别为原子性、一致性、隔离性和持久性,这里我们只谈论原子性和一致性。

原子性:原子性保证了事务的多个操作状态统一,要么都生效要么都不生效,不会存在中间状态。

一致性:持久性保证了一旦事务生效,就不会再因为任何原因而导致其修改的内容被撤销或丢失。

如何保证原子性和一致性?

首先,如果想要保一致性,这就要求数据必须写入硬盘成功,但是写入硬盘的状态是有风险的,例如一个经典的例子,A 在银行给 B 转账 10 元。

理论上来说下面两个操作要具有原子性和一致性

// A 余额减少 10 元
a.balance -= 10;
// B 余额增加 10 元
b.balance += 10;

但是这将有以下两种很常见的场景

第一种场景:

// A 余额减少 10 元
a.balance -= 10;
success!

// 此时数据库断电

这种情况下当数据库重启之后必须能保证当前有一个未完成的转账事务,需要事务回滚

第二种场景

// A 余额减少 10 元
a.balance -= 10;
b.balance += 10;
// 事务提交
commit;
// 提交后立即断电

这种情况下数据库重启之后必须保证崩溃前发生过依次完成的转账事务,但是数据还未落盘

提交日志(Commit Logging

实现

由于无法保证数据库永不崩溃(即使能防止人祸但是无法防止天灾),为了保证数据库的原子性和一致性,只能在崩溃之后采取措施来完成 / 撤销崩溃前的操作,这种操作成为崩溃恢复(Crash Recovery,有的资料也称为 Failure RecoveryTransaction Recovery)。

Logging 为了能顺利的完成崩溃恢复,需要将修改数据的详细信息(修改什么数据,从什么改成什么等等)都需要详细的以顺序追加的方式记录到日志文件中(注意:此时还没有开始修改数据),这种日志文件更多被称为 Redo Log(重放日志)确定了需要修改信息的详细记录已经落盘,而且捕捉到代表事务提交成功的提交记录(Commit Record)后,此时认为此次事务是成功的,才会根据日志中的详细记录对数据进行修改,修改完成之后会再次追加写入结束记录(End Record),这种实现方式称为 "Commit Logging"(提交日志),大概流程如下

​ 1、客户端发送修改请求

​ 2、日志顺序追加的方式记录需要修改的详细数据(修改哪些、从什么改成什么等等)

​ 3、事务成功提交

​ 4、开始根据日志对数据进行真实修改

​ 5、修改完成后追加 End Record

按照这样的过程,无论在哪一步出现崩溃,对数据库来说都是可以恢复的,如果在步骤 2 中发生崩溃,此时整个事务是失败的,重启后直接将事务标记为失败即可(无需回滚数据,因为还没有开始真正的修改),如果在步骤 3-5 中发生崩溃,此时事务是成功的,只是修改没有落盘,重启之后按照日志对数据就行修改即可。

缺陷

该种方式实现原理清晰,但是有个非常大的缺陷,就是在即使磁盘 I/O 有足够空闲、即使某个事务修改的数据量非常庞大,占用了大量的内存缓冲区,无论有何种理由,都决不允许在事务提交之前就修改磁盘上的数据。

由于在修改数据之前必须完整的记录需要修改数据的详细日志,这在庞大事务的情况下非常影响性能,甚至会出现记录日志的时间远超真实修改所耗费时间的情况,为了解决该问题,又出现了一种新的解决方案,“Write-Ahead Logging”(提前写入)

提前写入(Write-Ahead Logging

实现

首先按照 Write-Ahead Logging(WAL)理论将何时写入数据,按照事务的提交时间点为界,分为两种情况

FORCE:当事务提交后,要求变动数据必须同时完成写入则称为 FORCE,如果不强制变 动数据必须同时完成写入则称为 NO-FORCE。现实中绝大多数数据库采用的都是 NO-FORCE 策略,因为只要有了日志,变动数据随时可以持久化,从优化磁盘 I/O 性能考虑,没有必要强制数据写入立即进行。

STEAL:在事务提交前,允许变动数据提前写入则称为 STEAL,不允许则称为 NO-STEAL。从优化磁盘 I/O 性能考虑,允许数据提前写入,有利于利用空闲 I/O 资源,也有利于 节省数据库缓存区的内存。

从理论的角度来看,Commit Logging 的方式是不是 FORCE 并不是那么重要,因为只要有了日志数据变动可以随时持久化,但是绝对是 NO-STEAL 的,因为一旦提前写入,发生崩溃的情况下这部分数据就成了错误数据。

Write-Ahead Logging 允许 NO-FORCE,也允许 STEAL,它的实现方式是新增加了一种新的日志类型 —— Undo Log(回滚日志),在变动数据写入磁盘之前,必须优先写入 Undo Log,然后数据落盘(此时事务还未提交),如果发生崩溃,必须按照 Undo Log 将已经落盘的数据进行回滚。

如果使用 WAL 方式,则在数据库崩溃之后将会发生以下流程

1、分析阶段,从 Checkpoint(可以简单理解在这个点之前所有变动数据已经落盘)开始扫描日志,收集所有没有 End Record 记录的事务

2、根据 Redo Log,找出包含 Commit Record 的事务,将这些变动落盘,落盘完成之后追加 End Record 记录。

3、此时剩余的事务为没有 Commit Record 的事务(也就是事务尚未提交)但是由于 WAL 会提前将修改落盘,所以必须根据 Undo Log 将提前写入的数据进行回滚。

Redo LogUndo Log 的区别?

Redo Log 是用来崩溃回复的,主要用途是将已经完成事务提交但尚未落盘的数据待数据库恢复落盘,所以 Redo Log 处理的是完成事务提交但尚未落盘的数据。

Undo Log 是用来回滚数据的,由于 WAL 方式会在事务尚未提交时就将数据修改落盘,所以数据库恢复之后必须将已经落盘的数据回滚,所以 Undo Log 处理的是未完成事务提交但已落盘的数据。

上次编辑于:
贡献者: ruleeeer