Lecture #20:Database Logging

崩溃恢复

恢复算法是一种确保数据库ACID的技术,数据库崩溃后, 所有已经提交但未刷新到磁盘的数据都有丢失的风险, 崩溃恢复算法就是为了防止数据库崩溃后数据丢失的情况。

缓冲池管理策略

DBMS需要以下确保以下保证:

  • 一旦DBMS告知某人该事务已经提交, 则该事务的更改都是持久的。

  • 如果事务终止, 则任何部分更改都不会持久, 即回滚。

为了实现这两个保证, 数据库要做两件事: undo和redo。

undo就是移除未完成的事务的影响或者更改; 重做就是对于已经提交的事务, 我们要确保系统重启之后, 能够保留所有更改。

窃取策略

决定了某个事务提交时, 他能否将另一个活跃事务未提交更改写入磁盘。比如对于事务T1和T2. 他们都修改了同一个元组里面的不同字段A和B。 T2已经提交, 但是T1回滚了。

如果采用了窃取策略, 那么就允许T2在T1尚未提交的情况下将提交写入磁盘, 即Andy所说的驱逐页面。 这样能够减少内存的使用, 让BufferPool中的frame更灵活。 如果不适用窃取, 那么就不允许提交, 也就是说所做的所有更改在未提交之前都要留在内存中,占用的内存就会更多。

强制策略

强制规定了DBMS是否允许事务在未提交时将已经做的更改提现到非易失性存储中。

强制策略使回复变得容易, 因为所做的所有更改都已经体现到了磁盘中, 但是这会导致性能变差。

NO-STEAL-FORCE

NO-STEAL-FORCE是一种使用强制策略时因为冲突而使用的策略。 就比如还是这个例子:对于事务T1和T2. 他们都修改了同一个元组里面的不同字段A和B。 T2已经提交, 但是T1回滚了。 T2修改了B, T1修改了A。 此时如果是强制策略, 那么应该写回,因为T2提交了。 但是又因为T1没提交, 而且没采用窃取,所以不应该提交。

这里采用的方法是将T2所做的更改复制成一个新的元组,这个元组中不包含T1所做的更改。然后将这个新数据写回磁盘。

最容易实现的就是NO-STEAL-FORCE, 因为DBMS无需undo已经中止的事务所做的更改,因为已经中止的事务没有将数据提现到磁盘; 同时DBMS不需要redo已经提交事务的更改, 因为已经提交的事务所做的更改已经提现到磁盘。

但是会有大量的额外开销,因为要复制新的元组。

影子分页

影子分页就是事务在对数据进行更改时, 对数据所在页进行复制, 即NO-STEAL-FORCE。

  • master是已经提交的数据所在的原数据库。
  • 分页是包含事务未提交的正在更改的临时数据库。

更新只在影子分页中进行, 当事务提交后, 影子分页变成master页, 原master页被垃圾回收。

执行

内存中和磁盘中都有一个页表。

如果此时来了一个读写事务, 那么首先就要复制一个影子分页表。

这个影子分页表在执行事务过程中, 如果有数据被更改, 那么他的内存到磁盘的映射就会改变位置。

影子页表新映射的位置储存的就是新的数据。 然后如果又来一组事务进行读取, 读取的仍然是主页表的映射。 但是如果第一个事务提交后, 那么影子页表就要替换成主页表。

恢复

  • undo:删除影子页面,如果事务没有提交, 只需要删除影子页面, 垃圾回收即可。
  • redo:不需要, 因为事务只要提交后, 如果有更改,主页表就已经被替换成为最新的。磁盘中的数据就是最新的数据。

缺点

  • 复制页表的成本高。 提交的成本高(TODO, 这里不理解为什么, 如果提交只是切换主表和分表, 那么交换指针即可。)

  • 会导致数据存储随机化, 如同上面的演示, 原本是顺序存储, 但是使用影子分表复制后, 再提交事务, 那么数据就变得不再顺序。这样导致磁盘IO的速度降低, 因为磁盘不支持随机读写。

  • 一次仅支持一个写入事务和批量事务。对于一次写入批量事务, 会导致对于一个执行一毫秒的事务, 和一个执行一秒的事务, 两个事务为一组的话, 那么一毫秒的事务必须等待一秒的事务结束后才能一起提交。 更不用说一次写入一个事务速度会更低。

日志文件

SQLite之前做过类似影子分页, 但不全是的工作。 影子分页是指向修改后的数据, 而SQLite用过的日志文件是指向修改前的数据。

当要修改一个数据, 他要先把这个数据的原始版本写入磁盘的一个日志文件(影子分页是将修改后版本复制到磁盘)。 当事务崩溃了, 那么他就要读取日志文件中的数据, 然后覆盖掉已经磁盘中的已经修改的一些数据, 防止事务提交了一半到磁盘, 一半留在内存丢失。 这是用来redo。 而不是undo。 这个的一个缺点也是要进行随机读写, 因为恢复的时候要从磁盘中找出那些被更新过的数据。

预写日志(WAL)

WAL中包含了一些事务的记录信息, 比如时间戳, 校验和等等以及最基本的那个事务进行了更改, 更改了什么, 更改前的值(用于undo), 更改后的值(用于redo)。

一但通知了外层系统, 当前事务已经提交, 那么就必须保证当前事务对数据库所做的所有更改已经都记录到了磁盘上的日志文件中。 即, 在将缓冲池中的任何脏页写入磁盘前, 必须先将使该页面变脏的日志记录刷新到磁盘。

这样当BufferPool驱逐页面时, 他就可以根据日志记录看一下这个记录, 在磁盘上是安全的, 这个记录在磁盘上是不安全的。

他应用的使STEAL + NO-FORCE。 这里的STEAL就是未提交事务的日志记录已经写入到了磁盘, 我们就允许这些未提交的事务所产生的脏页被刷新到磁盘。 NO-FORCE意味着我们不必在提交的时候就把脏页刷新到磁盘。只需要确保日志记录刷新到磁盘。

执行

一开始, 我们开启一个事务, 在WAL Buffer中记录这条事务已经开始。

然后, 对A进行修改, 这个时候要在日志中添加一条记录, 里面包含了修改谁, 被修改前的值, 被修改后的值。

然后, 接下来就可以将内存中的数据修改, 变成脏页:

然后W(B)同理, 最后COMMIT的时候要将WAL Buffer写入磁盘。

当写入成功后, 就可以告知外部系统此时事务已经提交。 此时Buffer Pool中的脏页驱逐与否已经不重要,现在程序崩溃:

但是我们需要的所有信息已经在磁盘上, 我们重新执行一遍WAL Buffer即可, 就相当于重新执行一遍事务。

上面的所有都是顺序IO, 因为我们只是写入了WAL Buffer。 而且如果我们更改了10亿个页面, 我们的WAL Buffer就相当于有10组事务记录。 相比十亿个复制页面, 十亿组事务记录小的多得多。

然后对于组提交, 组提交可以支持多个WAL Buffer。 同时设置一个系统时间,类似时间戳, 规定每超时一次时间戳, 那么就将WAL Buffer里面的数据写入一次磁盘。 同时, 如果WAL Buffer 被写满了, 也要写入磁盘。也就是不必等到COMMIT的时候才刷新到磁盘了。 同时多WAL Buffer, 比如双WAL Buffer。 类似双缓冲, 这样就可以轮流记录事务记录了。

首先,T1BEGIN

然后T1写A, 写B:

然后T2BEGIN, 写C, 此时WAL Buffer 满了:

满了后就要刷新到磁盘了。

然后开始向下一个WAL Buffer写入:

如果在某个时刻, T1和T2因为某些原因暂停了。 经过了5s,假如系统设置的时间戳也是5s,那么就超时了。 就需要写入磁盘:

此时第一个WAL Buffer已经写入, 可以让第一个WAL Buffer回来接收下面的记录, 该记录去写入磁盘。

缓冲池策略

对比性能, 对于影子分页, 采用了FORCE, NOSTEAL, 所以它的恢复非常快, 因为他不需要redo,只需要undo直接丢弃。 但是运行时很慢, 因为他要盲目的复制整个页面。 开销极大。

对于预写日志, 采用了NO-FORCE, STEAL。 这样它的恢复比较慢,因为他要去日志中查记录,然后相当于重新执行了一遍事务。 但是运行时非常快, 因为他只是记录了事务的记录。有几条事务, 就有几组日志记录。

大多数DBMS都采用NO-FORCE + STEAL策略, 因为它比FORCE + NO-STEAL策略更具有出色的运行时性能。 然而, 在恢复阶段, NO-FORCE策略需要数据库进行reco, 而STEAL策略需要数据库进行undo(undo什么? 这里不理解为什么STEAL需要undo。TODO), 这使得其恢复时间比FORCE + NO-STEAL策略更慢。

日志方案

三种方案:

  • Physical Logging

  • Logical Logging

  • Physiological Logging

物理日志基本上是进行字节级别的差异比较, 记录页面修改前后的内容。

逻辑日志不记录底层的数据修改, 而是记录我执行的查询语句。

生理日志介于两者之间, 类似于物理日志记录, 他会追踪事务对哪些页面进行了修改。但他不会告诉在这个页面上进行修改的具体偏移量。在恢复时,自行选择在哪里进行恢复。

Physical记录了Page也好, Offset具体偏移量; Logical记录了整个sql; Physiological记录了Page页号, Slot则记录了槽号。

检查点

对于预写日志, 如果不限制, 那么这个WAL Buffer会无限增长, 比如运行了10年的数据库, 挂掉了。 然后恢复很明显不能恢复10年的WAL。 所以就要添加一些检查点。 比如定期要将所有的脏页刷新到磁盘, 然后在WAL Buffer中添加检查点, 以后就从检查点开始向后恢复, 而不是从头开始恢复了。

相关推荐
小马爱打代码24 分钟前
Spring Boot:将应用部署到Kubernetes的完整指南
spring boot·后端·kubernetes
卜锦元38 分钟前
Go中使用wire进行统一依赖注入管理
开发语言·后端·golang
SoniaChen332 小时前
Rust基础-part3-函数
开发语言·后端·rust
全干engineer2 小时前
Flask 入门教程:用 Python 快速搭建你的第一个 Web 应用
后端·python·flask·web
William一直在路上3 小时前
SpringBoot 拦截器和过滤器的区别
hive·spring boot·后端
小马爱打代码3 小时前
Spring Boot 3.4 :@Fallback 注解 - 让微服务容错更简单
spring boot·后端·微服务
曾曜4 小时前
PostgreSQL逻辑复制的原理和实践
后端
豌豆花下猫4 小时前
Python 潮流周刊#110:JIT 编译器两年回顾,AI 智能体工具大爆发(摘要)
后端·python·ai
轻语呢喃4 小时前
JavaScript :事件循环机制的深度解析
javascript·后端
ezl1fe4 小时前
RAG 每日一技(四):让AI读懂你的话,初探RAG的“灵魂”——Embedding
后端