【数据库系统】课程笔记 - part 3

· · 算法·理论

写在前面:

成绩 93/100,绩点 4.8

由于 db 的笔记有点太多了(12 个 md 文档加在一起有 122K),因此将其按顺序均分为 3 个部分,每个部分包含 4 个 Lecture。

注:Lecture 1 为导论课,Lecture 7 为期中考试复习课,因此没有记录笔记。

另注:我已经尽可能写的详细了,但仍不保证知识点覆盖完全。

【数据库系统】课程笔记 - part 1:https://www.luogu.com.cn/article/47sozboa

【数据库系统】课程笔记 - part 2:https://www.luogu.com.cn/article/xcqqbg8c

【数据库系统】课程笔记 - part 3:https://www.luogu.com.cn/article/fk3j2hgg

Lecture 11 - Query Optimization

Introduction

同一个查询语句可能有多重不同的写法,而执行它们所需的代价可能差别很大,因此这一章就聚焦于如何优化查询。

如上图,两个语法树是等价的,但显然右侧的执行速度会更快,因为它先对 instructor 表进行了一次筛选,之后的连接操作就会少很多项。

Transformation of Relational Expressions

定义:两个关系代数的表达式是等价的,当且仅当对任意的例子两个表达式得到的结果都是一样的(结果中各个元组的顺序和各个字段的顺序是无所谓的)

Equivalence Rules

上图就用了先选后连,将 \sigma_{\rm dept\_name="music"} 提前到连接操作之前先对 instructor 做。

上图同理,将两个选择操作下放到先对两张表进行选择再做连接。

Enumeration of Equivalent Expressions

普通的方法就是暴力搜索所有可能的优化方法,但显然搜索的时间是无法接受的,因此实际中需要基于一些经验规则进行启发式的优化。

Statistics for Cost Estimation

在具体分析之前,先给出一些参数的定义:

Selection Size Estimation

复杂的选择语句

涉及到多个选择条件 \theta_1,\theta_2,\cdots 的合取或者析取,设 s_i 表示满足条件 \theta_i 的元组个数,P_i=s_i/n_r 表示满足条件 \theta_i 的概率,那么:

注意,上述估计需要保证各个条件是独立的,否则分析会更复杂一些。

Estimation of the Size of Joins

来分析一下连接操作得到结果的大小。

Size Estimation for Other Operations

Estimation of Number of Distinct Values

估计 \sigma_{\theta}(r) 中不同值的个数 V(A,\sigma_{\theta}(r))

估计 r\bowtie s 中不同值的个数 V(A,r\bowtie s)

其他几种操作(投影,聚合等)不作介绍。

Choice of Evaluation Plans

选择一个综合最好的执行方案可能比较困难,通常按照经验规则,同时这里主要考虑连接操作的优化。

--- ### Dynamic Programming for Choosing Evaluation Plans 一个朴素的动态规划算法: - 如果当前需要连接的表个数为 $1$,则提前做选择等操作先缩减表格大小 - 否则,将当前需要连接的表划分为两个非空子集,分别计算两个部分进行连接的最优方案并取最好的划分方法 很显然,动态规划的过程中有个枚举子集,因此时间复杂度为 $O(3^n)$(不考虑 $n=1$ 的额外操作) 另一个朴素的动态规划算法: - 每次只考虑连接一个表格,这样相当于 dp 所有表格的最优连接顺序 - 使用状压 dp 容易做到 $O(2^n\times n)$ 的时间复杂度 ##### Interesting Sort Orders (我也不知道为什么会有这个东西) 有些使用使用不同的连接算法会得到不同顺序的结果(例如 hash-join 的结果就是无序的,merge-join 的结果是有序的),而某些顺序可以在之后的连接中复用,得到更高的效率(例如始终保持有序)。 这样的顺序就可以称为 Interesting Sort Orders。 ##### Cost Based Optimization with Equivalence Rules 不知道在说什么,放一张图: ![](https://cdn.luogu.com.cn/upload/image_hosting/ugj398tp.png) ##### Heuristic Optimization 启发式优化方式 即使使用动态规划,寻找最优方案的时间开销依然非常巨大,此时使用一些经验策略会更好: - 更早地去做选择,投影操作 - 先做限制更高/结果更小的连接操作 - 等等 --- ### Additional Optimization Techniques To be done. --- ## Lecture 12 - Transaction ### Transaction Concept 对一个 DBMS 来说,有两个问题是无法避免,必须解决的: - 由多个用户,或者多个程序产生的并发操作 - 诸多错误,例如硬件错误或者系统崩溃等 例如数据库中有一个数据 $A=10$,两个用户同时写 $A:=A-2$,那么实际上 $A$ 应该减少 $4$,但由于并发操作,可能只记录到了一个操作,就产生了问题。 **事务 (transaction)** 就是用于维持数据库的正确性 (correctness),一致性 (consistency) 和完整性 (integrity) 的。 事务是数据进行访问以及(可能会有)修改的操作的单元(直接看原文更好理解一点:A transaction is a unit of program execution that accesses and possibly updates various data items.) 通常来说一个事务是一些 SQL 语句,并以 `commit` 或 `rollback` 结尾,在事务执行过程中数据库的一致性可能无法保证,但事务执行完毕之后一定是满足一致性的。 #### ACID Properties ACID 是四条特性,用于维持数据库的正确性,一致性和完整性: - 原子性 (**A**tomicity):一个事务中的所有操作要么都成功完成要么全部不完成,不存在执行一半的情况 - 一致性 (**C**onsistency):事务执行完毕之后需要保持数据库的状态合法(例如转账前后两人账户总金额应保持不变) - 隔离性 (**I**solation):多个事务并发执行的时候,彼此之间相互隔离,一个事务执行过程中的中间结果对其他事务不可见,且最终效果等价于事务按串行顺序依次执行 - 持久性 (**D**urability):事务成功提交完成后,它对数据库做出的修改会永久保存下来,哪怕之后出现服务器断电、系统崩溃这类故障,修改后的数据也不会丢失 这里举一个隔离性的例子: | T1 | T2 | | ---------- | ---------------------------- | | `read(A)` | | | `A:=A-50` | | | `write(A)` | | | | `read(A),read(B),write(A+B)` | | `read(B)` | | | `B:=B+50` | | | `write(B)` | | 这里两个事务 $T_1,T_2$ 违反了隔离性,$T_2$ 使用了 $T_1$ 的中间结果,这样会导致 $A+B$ 的结果不对。 --- ### Transaction State 一个事务可能处于不同的状态下: - 活跃 (Active):初始状态,事务在执行过程中会始终处于这个状态 - 部分提交 (Partially committed):事务执行完毕,但此时要输出的结果可能还在内存 buffer 中 - 失败 (Failed):在发现操作无法进行之后进入失败状态 - 中止 (Aborted):在重做事务,或者中止事务时进入此状态(例如 `rollback`) - 提交 (Committed):事务成功完成 ![](https://cdn.luogu.com.cn/upload/image_hosting/zkqyk2zn.png) --- ### Implementation of Atomicity and Durability recovery-management component 用于维持事务的原子性和持久性,这里介绍一种方法:**影子数据库 (shadow-database)**。 影子数据库的核心是:在副本上修改,修改完整体替换为副本。 - 维护一个指针,指向当前满足一致性状态的数据库 - 在处理事务时,先创建一个当前满足一致性状态的数据库副本 - 那么: - 事务回滚:直接删除新创建的副本即可 - 事务提交:先将新创建并修改的副本写入磁盘,然后将指针指向这个副本,并删除原来的数据库 很显然,这种做法逻辑简单,便于实现,无需复杂的日志和回滚机制,天然保证原子性和持久性(删除副本和修改指针都是原子操作),但需要大量的数据库复制,无法支持高并发的应用场景,在大型数据库中效率极低。 --- ### Concurrent Executions 数据库通常需要并行执行多个不同的操作,这样可以提升处理器和磁盘的利用率,并降低平均相应时间;但问题就是并行执行可能破坏一致性(如并发售票问题)。 #### Schedule **调度 (schedule)** 指的是对于多个并行执行的事务,各个操作执行的时间顺序。 - 一个调度中必须包含所有事务中的所有操作 - 一个调度中属于同一个事务的各个操作的前后顺序不变 ##### Serial schedule 最简单的调度方式就是串行调度,即将并行执行的事务再排个顺序依次执行,这种方法在满足交换律的情况下一定能保证一致性,但效率较低。 ##### Concurrent schedule 并行调度在串行调度上调整了操作执行顺序,效率提高,但可能会破坏一致性。 ![](https://cdn.luogu.com.cn/upload/image_hosting/qoo1msgd.png) --- ### Serializability 最基本的设定:各个事务都是合法的,不破坏数据库的一致性。 一些简化: - 忽略除了 `read` 和 `write` 之外的所有操作 - 认为在 `read` 和 `write` 之间会在 buffer 中做任意的操作 #### Conflicting Instructions 一些指令是无法交换执行顺序的,这种情况被称为**冲突 (conflict)**,这里和体系结构中的 WAR,RAW,WAW 类似: - `read(Q),read(Q)` 无冲突,可以交换 - `read(Q),write(Q)`,`write(Q),read(Q)`,`write(Q),write(Q)` 有冲突,不能交换 如果可以通过交换不冲突的指令将一个调度转化为一个串行调度,则称这个调度是**冲突可串行化的 (conflict serializable)**,例如下图就是一个串行化的结果。 ![](https://cdn.luogu.com.cn/upload/image_hosting/dr8yiu2i.png) 而下面是一个不可串行化的例子: | T1 | T2 | | ---------- | ---------- | | `read(Q)` | | | | `write(Q)` | | `write(Q)` | | #### View Serializability 称两个调度 $S,S'$ 是视图等价的,当且仅当: - 首读:若在 $S$ 中 $T_i$ 读了 $Q$ 的初始值,则在 $S'$ 中也是 $T_i$ 读了 $Q$ 的初始值(读原始数据的事务相同) - 读写来源:若在 $S$ 中 $T_i$ 读 $Q$ 是由 $T_j$ 写(`write(Q)`)出来的,则在 $S'$ 中也是如此(每个读操作的数据源相同) - 末写:若在 $S$ 中 $T_i$ 是最后一个写 $Q$ 的事务,则在 $S'$ 中也是如此(最终项的结果相同) 换言之,视图等价只关心事务的读/写操作,不关心事务内部的其他计算逻辑。 一个调度是视图可串行化的,当且仅当它和一个串行调度是视图等价的。 冲突可串行化的都是视图可串行化的,反之不一定。 --- ### Recoverability **可恢复调度 (Recoverable schedule)**:如果 $T_j$ 读了之前被 $T_i$ 写的数据,则 $T_j$ 的 `commit` 必须在 $T_i$ 的 `commit` 之后,不然如果 $T_i$ 发生了 fail,则 $T_j$ 会 `commit` 一个错误的数据,且无法恢复。 | T1 | T2 | | ---------- | --------- | | `read(A)` | | | `write(A)` | | | | `read(A)` | | | `commit` | | `read(B)` | | 如上表中,$T_1$ 如果 fail,则 $T_2$ 会读取到一个错误的结果并 `commit`,这是不允许发生的。 #### Cascading Rollbacks 由上述情况,不难延伸得到:如果有多层依赖,则会发生级联回滚,例如下图所示: ![](https://cdn.luogu.com.cn/upload/image_hosting/b6dgrnm5.png) 这可能会导致非常大的工作量。 --- ### Implementation of Isolation (这一节好像没有新内容,直接贴一下 ppt) ![](https://cdn.luogu.com.cn/upload/image_hosting/8b0l2gan.png) --- ### Transaction Definition in SQL (好像也没有什么新东西) ![](https://cdn.luogu.com.cn/upload/image_hosting/vxn46ssc.png) --- ### Testing for Serializability 来考虑一下如何判定一个事务能否串行化。 建立一张有向图,每个顶点都是一个事务,有向边代表一个冲突关系,从 $T_i$ 指向 $T_j$ 表示 $T_i$ 先于 $T_j$ 执行。 那么显然可串行化当且仅当这张图无环,用一些简单的算法(如 Tarjan)可以做到 $O(V+E)$ 的时间判定是否有环。 如果无环,则任意一个拓扑序都是一个合法的串行化方式。 另外,视图可串行化的判定是 NP-complete 的。 --- ## Lecture 13 - Concurrency Control ### Lock-Based Protocols 这部分讨论关于锁的并发控制方法: - 排他锁 (Exclusive,X 锁):持有 X 锁的事务可以对数据进行读和写,其他事务禁止读和写 - 共享锁 (Share,S 锁):持有 S 锁的事务仅可以读数据,不能写数据 由上定义不难发现:只有 S 锁可以在一个数据上同时存在多个,所有其他状态都是违法的。 只有在申请得到锁之后,事务才可以继续执行下去。而如果锁请求不兼容,则事务会被**阻塞 (wait)**,进入等待队列,直到锁可以兼容了,管理器批准发放一个锁。 例如下面就是一个给锁并释放锁的过程: ``` lock-S(A) read(A) unlock(A) lock-S(B) read(B) unlock(B) display(A+B) ``` 但这个过程其实有问题,因为如果在 `unlock(A)` 和 `lock-S(B)` 之间对 $A,B$ 做了修改,那么显示的 $A+B$ 值就会出错,为了防止这种情况发生,locking protocal 出现了,它是用于控制什么时候申请锁,什么时候释放锁的一个规则。 #### dead lock(死锁) 这种情况发生在锁的申请释放形成环的时候: | T3 | T4 | | ----------- | ----------- | | `lock-X(B)` | | | `read(B)` | | | `B:=B-50` | | | `write(B)` | | | | `lock-S(A)` | | | `read(A)` | | | `lock-S(B)` | | `lock-X(A)` | | 此时执行过程中就会出问题,执行到 $T_4$ 的 `lock-S(B)` 时需要 $T_3$ 释放此时在 $B$ 上的锁,因此继续执行 $T_3$,而下一句 $T_3$ 的 `lock-X(A)` 又需要 $T_4$ 释放此时在 $A$ 上的锁,就彼此卡死了,谁都无法继续执行。 另外还有可能的**饥饿 (starvation)** 问题,即由于 $S$ 锁可以彼此叠加,因此只要有源源不断的 S 锁申请,就可能有某个 X 锁永远都无法申请到,这种问题可以通过设计并发控制管理器来规避。 #### The Two-Phase Locking Protocol 在这种协议下,每个事务都有两个阶段: - Growing Phase:只能获取锁,不能释放锁 - Shrinking Phase:只能释放锁,不能获取锁 其中,两个阶段之间的时刻称为这个事务的 lock point(ppt 中的定义为获取最后一个锁的时刻,不同材料有不同的定义)。可以证明,这种协议保证了冲突可串行性,且执行顺序就是 lock point 的顺序,但它不能保证不出现死锁。 二阶段锁不能覆盖到所有冲突可串行化的调度,有些情况无法用二阶段封锁实现,如: | T1 | T2 | | ---------- | ---------- | | `read(A)` | | | | `write(A)` | | `write(B)` | | | | `read(B)` | 此时 $T_2$ 的 `write(A)` 的加锁必须在 `read(A)` 的锁释放之后,而显然这不符合二阶段封锁的要求。 证明在作业题中: > 证明:两个如果 $T_i$ 和 $T_j$ 存在冲突(即 $T_i$ 的某个操作 $O_i$ 必须先于 $T_j$ 的某个操作 $O_j$ 执行),则说明 $O_i,O_j$ 访问了同一个数据,且至少有一个是写操作。因此,在 \(T_j\) 执行 \(O_j\) 前,必须等 \(T_i\) 释放该数据上持有的冲突锁,而 \(T_i\) 释放这把锁,一定发生在 \(T_i\) 的 lock point 之后。同理,$T_j$ 获得锁一定在 $T_j$ 的 lock point 之前。 > > 综上,如果 $T_i$ 和 $T_j$ 存在冲突,则 $T_i$ 的 lock point 在 $T_j$ 的 lock point 之前。 > > 而建立冲突关系形成的图时,如果 $T_i\rightarrow T_j$ 边存在,则 $T_i$ 的 lock point 在 $T_j$ 的 lock point 之前,这表明所有事务的 lock point 形成了这张图的一个拓扑序,从而所有事务是冲突可串行化的,且执行顺序就是 lock point 的顺序。 ##### 严格二阶段锁 (Strict two-phase locking) 在事务提交或中止前,不释放任何 X 锁,这样其他事务永远拿不到未提交事务的修改数据,自然不会因为别人回滚而被迫回滚(即避免了级联回滚) ##### 强二阶段锁 (Rigorous two-phase locking) 比严格二阶段锁更强一点,在事务提交或中止前,不释放任何锁,这样事务的可串行化顺序,和它们提交的顺序完全一致。 ##### Lock Conversions 即认为 X 锁比 S 锁更高级,并允许在两阶段中进行 X 锁与 S 锁的转化。 - Growing Phase:只能获取锁,或者将 S 锁变成 X 锁,不能释放锁 - Shrinking Phase:只能释放锁,或者将 X 锁变成 S 锁,不能获取锁 因此不难得到 `read` 和 `write` 的写法: ``` read(D): if T_i has a lock on D then read(D) else begin wait until no other transaction has lock-X on D grant T_i a lock-S on D read(D) end write(D): if T_i has a lock-X on D then write(D) else begin wait until no other transaction has lock on D if T_i has lock-S on D then upgrade lock on D to lock-X else grant T_i a lock-X on D write(D) end ``` ##### Lock Table 介绍锁管理器和锁表。 锁管理器是一个独立的服务/进程,所有事务的加锁、解锁请求都要通过它来处理,是锁机制的控制中心。其维护了一个数据结构,即锁表,用来记录所有数据项的锁状态。 ![](https://cdn.luogu.com.cn/upload/image_hosting/jigpiysn.png) #### Graph-Based Protocols 图协议的定义如下,假设已知了一个隐藏的偏序关系,并建出了一张有向无环图: - 点为各个数据 - 如果 $d_i\rightarrow d_j$ 边存在,则任意访问了 $d_i,d_j$ 的事务,$d_i$ 一定先于 $d_j$ 访问 这个图被称为 **数据库图 (database graph)**,方便期间,将这张图的结构限制为外向有根树,就得到了树协议。 ![](https://cdn.luogu.com.cn/upload/image_hosting/iybwoigf.png) 在树协议中,只有 X 锁,且每个事务对每个数据只能添加锁和释放锁一次,并满足以下要求: - $T_i$ 的第一个锁可以加在任意数据上 - 之后如果 $T_i$ 要在 $Q$ 上加锁,则此时 $Q$ 的父节点必须也被 $T_i$ 锁住 - 可以在任意时刻解锁数据 - 由 $T_i$ 加锁或解锁的数据不能再次被 $T_i$ 加锁 那么所有满足树协议的调度都是冲突可串行的。 ![](https://cdn.luogu.com.cn/upload/image_hosting/v86ktchl.png) ##### 优缺点 - 优点:保证了冲突可串行性,且保证不会出现死锁,同时树协议下解锁的时间比二阶段锁协议的解锁时间更早,可以提高并发度 - 缺点:无法保证可恢复性和无级联性,可能会锁住事务不需要的数据 --- ### Timestamp-Based Protocols 每个事务在进入系统的时候被关联了一个时间戳 $TS(T_i)$,并对每个数据 $Q$ 定义: - W-timestamp(Q) 表示所有成功写 $Q$ 的事务的最大时间戳 - R-timestamp(Q) 表示所有成功读 $Q$ 的事务的最大时间戳 接下来: - $T_i$ 需要读 $Q$: - 若 $TS(T_i)$ 小于 W-timestamp(Q),则说明 $T_i$ 要读一个已经被写掉的值,因此读操作被拒绝,需要回滚 $T_i

如果事务需要被回滚,则需要赋予新的时间戳并重新启动这个事务。

这种协议可以保证冲突可串行,且无死锁,但无法保证可恢复性(需要回滚一个已经 commit 的事务)以及无级联性。

Thomas' Write Rule

在上面写操作的第二条中,若 TS(T_i) 小于 W-timestamp(Q),直接忽略此时的 write 操作即可。

Validation-Based Protocols

也被称为乐观并发控制 (Optimistic Concurrency Control),即先假设冲突不会发生,让事务先执行,最后再检查冲突;如果冲突了,就回滚重来。在冲突概率很低的时候,这种控制方法效率很高,能提供更高的并发度。

事务执行的三个阶段:

这里验证阶段和写阶段都被认为是原子性的,串行执行的,即同一时间只有一个事务能执行验证/写。最终串行执行顺序就按照验证阶段的时间戳来。

至于验证方法,在对事务 T_j 进行验证时,需要对所有 {\rm Validation}(T_i)<{\rm Validation}(T_j) 的事务 T_i 进行检查:

只要每个 T_i 都满足二者其一即可,否则需要回滚 T_j

Multiple Granularity

这部分讲的是多粒度锁,即可以为不同大小的数据单元加锁。

这里不同大小的数据单元可以用一个树形结构组织起来,所有叶子节点都代表一个数据项,其他内部节点为一个表示数据集合的逻辑单元。当给一个节点加上锁时,就等价地为其子树中所有叶子节点加上了同种类型的锁。

对比一下更大的数据单元和更小的数据单元:

同时,加锁时为了避免遍历整棵子树,引入了意向锁 (Intention Lock) 的概念:

这几种锁之间的兼容关系如下图所示:

那么加锁和解锁的方式如下:

同时事务的加锁和解锁需要遵循二阶段锁协议。这样可以增强并发性,并降低加锁开销。

Multiversion Schemes

这一部分讲的是在修改时记录数据的历史版本,这样在 read 可以直接取指定的历史版本而不用回滚事务。

Multiversion Timestamp Ordering

每个成功的写操作都会创建一个新的版本,因此一个数据 Q 有多个历史版本 Q_1,Q_2,\cdots,Q_m,每个 Q_i 包括三个部分:

在创建一个新的版本 Q_k 时,其 W-timestamp 和 R-timestamp 都被初始化为这个事务的时间戳,在每次读操作之后,也需要更新对应 Q_i 的 R-timestamp。

接下来处理读操作和写操作,假设 T_i 要对 Q 进行读或写,设 Q_k 为 W-timestamp 不大于 TS(T_i) 的最大的那个版本:

不难发现这个做法中读操作永远可以成功完成,并且可以串行化。

Multiversion Two-Phase Locking

将所有事务分成两类:更新事务和只读事务,并维护一个时间戳 ts-counter

Deadlock Handling

一个系统进入了死锁状态,当且仅当每个事务都在等待另一个事务(即每个点都有一条出边,则一定会有环,即死锁)。

Deadlock prevention

死锁预防协议是用来防止这种情况发生的,下面是几种处理死锁的办法:

Deadlock Detection

将事务之间的等待关系建出一张有向图(等待图, wait-for gragh),则只需检查这张图中是否存在环,即可确定是否存在死锁。

Deadlock Recovery

如果已经发生了死锁,就需要回滚一些事务(称为 “受害者”)来强行打开死锁:

同时,也可能一直选择一个事务回滚(发生 “饥饿” 状态),因此需要记录被选为受害者的次数,并在评估回滚代价时候将这个数据考虑在内。

Insert and Delete Operations

这里除了 readwrite 操作,再加入 insertdelete 操作:

phantom phenomenon

当一个事务在执行过程中,多次执行相同的查询(谓词读取 (predicate read)),但由于其他并发事务插入或删除了符合查询条件的新记录,导致后一次查询的结果集与前一次查询的结果集不同,就好像出现了“幽灵”一样,这些新出现的或消失的记录就是“幽灵”。

解决办法有以下两种:

Concurrency in Index Structures

To be done.

Lecture 14 - Recovery System

Failure Classification

失败的情况有很多种,一般可以分成一下三类:

Recovery Algorithms

假设一个事务 T_i 需要从账户 A 转账 50 元到账户 B,那么需要做两个更新:A 减去 50,B 加上 50。

但中途可能遇到故障:

因此恢复算法需要做的有两个部分:

Storage Structure

之前介绍到两类存储介质:

因此这里提出第三种

Data Access

事务访问磁盘时有三个部分:

其中磁盘与 buffer 之间的传输称为 input/output,buffer 和工作区之间的传输称为 read/write,三者之间的关系如图所示:

这里会有两种可能的情况:

这两个情况期间如果发生断电,则会违反一致性,硬盘中的数据有问题。

Recovery and Atomicity

(好像没什么内容)

Log-Based Recovery

日志 (log) 假定存储于稳定介质 (stable storage) 上,不然如果日志都可能丢失的话所有分析都无法进行。

记录日志的方式:

这里会有两种不同的修改方法:

Deferred Database Modification

正常记录日志,但在 commit 之后才进行所有的写操作。

上面的三个例子中,假设写完了这些日志内容之后发生了崩溃,则:

Immediate Database Modification

与延迟修改的模式不同,立即修改的模式会在事务执行过程中不断地向 buffer 中写入数据,而 buffer 的总量是有限的,因此需要或主动或被动地先将 buffer 中的信息刷入磁盘,而这就可能导致内存和磁盘中的数据不一致,从而加大恢复数据的难度。

这两条大概就是事务的原子性,要么都做完要么都没做,不能做一半。

即:提交过的不能丢,没提交的不能留

Checkpoint

不难发现在之前的模型中只要一个事务有 start 则一定会被 redo/undo,这个操作的效率是很低的,因此引入了检查点 (checkpoint) 的技术。

因此在建立一个检查点时,需要进行的操作有:

那么不难看到,在进行恢复的时候,找到最近的一个 <checkpoint L>,那么之后 L 中的事务以及在这个 checkpoint 之后开始的事务需要进行 redo 或 undo。

如上图,在 T_c 处的活跃事务列表为 T_2,因此需要检查的为 T_2 以及 T_c 之后开始的事务(T_3,T_4):

Shadow Paging

类似于之前影子数据库的想法,将目前状态复制一份形成一个新的副本,然后在新的副本中修改,在确认修改完成并刷入磁盘之后就将指针指向这个新的副本,并删除原来的版本。那么在恢复的时候直接将指针切换回原来的版本即可。

这种技术比较适合串行执行的场景,在高并发的情况下效率不高。

Recovery With Concurrent Transactions

假设我们使用的协议是严格两阶段锁(Strict 2PL),并且使用日志技术进行恢复。

问恢复结果的简单做法:

Buffer Management

缓冲区管理的最重要的两条原则: