数据库系统笔记9:并发控制与故障恢复

Transaction 事务

  • 由一系列的读和写组成
  • 这些工作要么全部执行,要么全部不执行
  • 保证数据库中存储的数据语义时刻是正确的
START TRANSACTION
[SQL statements]
COMMIT or ROLLBACK
  • Atomicity 原子性:事务是数据库最小执行单位
  • Consistency 一致性
  • Isolation 隔离性:一个事务执行不会被其他事务干扰
  • Durability 持久性:一个事务一旦提交,就是永久的

数据库的特性

  • Data Integrity 数据完整性:数据的语义正确,靠用户定义
  • Accuracy 数据精确性
  • Reliability 可靠性
  • Correctness 数据语义正确性

Concurrency Control 并发控制

Schedule 调度

调度:多个事务的多个动作实际执行的顺序

  • 忽略数据库的初始状态:在任何数据库上都要是好的调度
  • 忽略事务的实际语义
  • 事务调用的数值在内存中,而不是在磁盘上

定义

  • serial schedule(串行调度):不并发,能保证数据库的一致性
  • equivalent schedule(等价调度):得到相同结果的两个调度等价
  • serializable schedule(可串行化调度):该调度等价为串行调度
  • action conflict(动作冲突):如果交换两个动作,会改变调度的语义,则这两个动作冲突
    • 同一个事务的任何动作都不能交换,都是冲突的
    • 两个事务的两个动作,分别是对于同一个元素的写和读操作,这两个动作冲突

冲突可串行化调度

  • 定义Th→Tk:Th与Tk冲突,且Th必须先于Tk执行
  • 冲突等价调度:调度S1交换其非冲突的动作后,与S2相同,则S1和S2是冲突等价调度
  • 冲突可串行化调度:调度S1与串行调度S2冲突等价,则称S1是冲突可串行化调度

优先图P(S)

  • 定点:调度S中的事务T1, T2, ...
  • 边:Ti→Tj
  • 若S1、S2是冲突等价的,那么P(S1)=P(S2)
  • 优先图中,如果不形成环,则说明该调度是冲突可串行化调度

Two phase locking 两段锁协议

  • lj(A):事务Tj对A加锁
  • uj(A):事务Tj对A解锁

加锁解锁规则

  • well-formed transaction:一个事务在对A进行读/写操作之前,一定要对A元素加锁,完成操作后再解锁
  • legal scheduler:在一个调度中,事务Ti对A加锁后,在对A解锁前,不能有另外一个事务Tj再对A加锁
  • two phase locking:一个事务先加锁,一旦开始解锁,就不再加锁;即第一阶段先(对不同的元素)加锁,第二阶段(对不同的元素)解锁,解锁后不再有加锁操作

定理

  • 事务Ti第一个解锁操作记为SH(Ti),若Ti→Tj,那么SH(Ti)在SH(Tj)之前
  • 如果事务和调度满足两段锁协议,那么该调度是冲突可串行化调度

死锁

  • 一次封锁法:每个事务必须一次将所有要使用的数据全部加锁,否则就不能继续执行
  • 顺序封锁法:预先对数据对象规定一个加锁顺序,所有事务都按这个顺序实行封锁

锁的扩展

Shared Locks 共享锁

共享锁允许多个事务同时读同一个元素,记作l-Si(A),相应地,排他锁记作l-Xi(A)

下面,我们更新加锁解锁规则:

  • well-formed transaction:一个事务在对A进行读操作前,对A加共享锁;对A进行写操作时,对A加排他锁;完成操作后再解锁
  • legal scheduler:在一个调度中,事务Ti对A加共享锁后,在对A解锁前,不能有另外一个事务Tj再对A加排他锁,但允许加共享锁;事务Ti对A加排他锁后,在对A解锁前,不能有另外一个事务Tj再对A加任何锁
  • two phase locking:一个事务先加锁,一旦开始解锁,就不再加锁;即第一阶段先(对不同的元素)加锁,包括升级锁,第二阶段(对不同的元素)解锁,解锁后不再有加锁操作

Update Locks 更新锁

如果T1对元素A先读后写,有两种加锁方法:

  • 一种为直接加排他锁,但会降低并发度
  • 另一种为先加共享锁,写之前再升级为排他锁,这样提高了并发度,但有一些缺陷
    • 增加执行时间:T1加共享锁之后,其他事务也可以对A加共享锁,当T1需要加排他锁时,则需要等待
    • 出现死锁:如果T1、T2都采用此方式,先后对A加共享锁后,双方都无法升级为排他锁

因此,我们引入更新锁的概念,记作L-Ui(A)

  • 如果T1事务很可能对A元素先读后写,那么T1先对A加更新锁,写之前再升级为排他锁
  • 若一个事务对A元素加了更新锁,其他事务无法再对A加任何锁
  • 若一个事务对A元素加了共享锁,其他事务可以对A加更新锁

用矩阵表示各个锁之间的兼容性如下:

已经加锁\即将加锁SXU
STFT
XFFF
UFF

F

锁表

在调度的时候,我们通过锁表来查看元素上锁的情况

我们来看一个锁表的例子:

  •  对于元素A,缩表上记录其当前最高的锁(group mode),是否有事务在等待,以及每个事务对它申请加锁的情况的链表
  • 链表中记录着事务,申请的锁,是否等待,指向下一个申请同一元素的事务的指针,指向该事务申请的其他元素的锁的指针

Intention Locks 意向锁

意向共享锁记作IS,意向排他锁记作IX

  • 从要修改的元素上开始上锁
  • 对其所有父节点上相应的意向锁

用矩阵表示各个锁之间的兼容性如下:

已经加锁\即将加锁ISIXSX
ISTTTF
IXTTFF
STFTF
XFFFF

Timestamp 时间戳

根据调度随意访问数据库元素,记录优先图,检查优先图是否有环,如果有,则需要回滚事务

  • 每个事务启动的时候都记录一个时间戳
  • 记录最后读和写每个数据库元素的事务时间戳
  • 比较上述值,以保证按照事务的时间戳排序的串行调度

算法

  • 调度器赋给每个事务T一个唯一的数值TS(T)
  • 每个数据库元素X上有两个时间戳和一个附加位
    • RT(X):读X的事务中最高的时间戳(最晚读X的事务的时间戳)
    • WT(X):写X的事务中最高的时间戳(最晚写X的事务的时间戳)
    • C(X):X的提交位,当且仅当最近写X的事务已经提交时为真
  • 读元素X的时候
    • 如果TS(T)>=WT(X),则该请求物理上可实现
      • 如果C(X)为真,同意请求,如有必要更新RT(X)
      • 如果C(X)为假,推迟T直到C(X)为真或写X的事务终止
    • 如果TS(T)<WT(X),该请求物理上不可实现,回滚T
  • 写元素X的时候
    • 如果TS(T)>=RT(X)且TS(T)>=WT(X),则该请求物理上可实现,更新WT(X)并置C(X)为假
    • 如果TS(T)>=RT(X)但TS(T)<WT(X),则该请求物理上可实现
    • 如果TS(T)<RT(X),该请求物理上不可实现,回滚T
  • 事务T执行完后,提交并置C(X)为真

实例

  •  按照每个事务的时间戳,执行的顺序应该为T2→T3→T1
  • 执行r1(B)时,在B元素上留下T1的时间戳,即RT=200
  • 执行w1(A)时,200>=150,因此可以执行
  • 执行w2(C)时,150<175,因此不能执行,回滚T2,T2终止

Validation 有效性确认

  • 当事务将要提交时,检查事务的时间戳和数据库元素
  • 按照每个事务申请有效性确认的时间对事务进行排序

思想

  • 在每次读之前记录RS(Ti),在每次写之前记录WS(Ti)
  • 在读写之前,验证按照这步读写是否能保证可串行化
  • 如果验证通过,写磁盘;如果验证不通过,事务回滚
  • 为了完成有效性确认,需要维护两个集合
    • FIN:写盘了的事务
    • VAL:通过验证但还没有写盘的事务

算法

(1) When Tj starts reading DB:
    Ignore(Tj) ← FIN;
(2) At Validation of Tj:
    if Validates(Tj) then
        VAL ← VAL ∪ {Tj};
(3) do write to disk:
    FIN ← FIN ∪ {Tj};
    VAL ← VAL - {Tj};
Validates(Tj):
    for each U ∈ VAL do
        if (WS(U) ∩ RS(Tj) ≠ ∅ or WS(U) ∩ WS(Tj) ≠ ∅) then
            return False;
    end for;
    for each U ∈ FIN - Ignore(Tj) do
        if (WS(U) ∩ RS(Tj) ≠ ∅) then
            return False;
    end for;
    return True;

Recovery 故障恢复

Undo Logging

Log(日志)

事务的记录

  • <Ti, start>:事务开始
  • <Ti, commit>:事务成功执行并提交
  • <Ti, abort>:事务没有成功执行
  • <Ti, X, v>:Ti事务更新了元素X,其原来的值为v

记录规则

  • 在内存中将所有需要修改的数据计算完成,在内存中写入日志
  • 先在磁盘的日志中记录事务更新元素的旧值,再向磁盘中更新数据
  • 只有在磁盘中的相应数据全部更新,才在磁盘的日志中写入<Ti, commit>

恢复规则

  • 查找磁盘日志中没有<Ti, commit>也没有<Ti, abort>标记的事务
  • 从后向前扫描日志,进行回滚,把X的值恢复为v 👉 注意,日志中记录了旧值,但我们无法确定X的值是否更改了,有可能还是旧值v(不知道系统什么时候崩溃)
  • 回滚结束后,在日志中写入<Ti, abort>

Redo Logging

  • redo log 记录元素更新后的值
  • 先把日志(包括commit)写入磁盘,再向磁盘中更新数据
  • 查找磁盘日志中有<Ti, commit>标记的事务,从前向后扫描日志,把这些事务重新执行一遍

Simple Checkpointing 静止检查点机制

在日志文件中插入<CKPT>检查点,在检查点之前的记录都不用redo

  • 停止开始执行新的事务
  • 等所有的事务都完成(commit/ abort)
  • 把所有的日志都写到磁盘日志上
  • 把所有的记录都写到磁盘数据库上
  • 在磁盘日志中写入<CKPT>
  • 重新开始事务执行

Undo/Redo Logging

  • Undo:事务的所有写盘操作后才写入commit,可能增加磁盘I/O的代价
  • Redo:直到commit后才能写盘,可能导致内存缓冲迟缓
  • Undo/Redo:记录旧值和新值,为命令操作提供更大的灵活性

记录规则

  • 先在磁盘日志中记录<Ti, X, old, new>,再向磁盘中更新数据
  • 在内存中,事务完成后,将commit写到日志中
  • 磁盘更新数据可以在commit写到磁盘日志之前或之后

恢复规则

  • 对没有commit记录的事务,undo 撤回
    • 从后往前扫描,找到需要undo的事务,撤回
    • 把X元素更新为old值,写到磁盘上
  • 对有commit记录的事务,redo 重新执行一遍
    • 从前往后扫描,找到需要redo的事务,重新执行
    • 把X元素更新为new值,写到磁盘上

Nonquiescent Checkpointing 非静止检查点机制

  • 在内存日志中写入<START CKPT T1, ... , Tk>,代表开始检查点,T1, ... , Tk为还没有完成的事务(活跃事务)
  • 将内存里所有dirty buffers(内存与磁盘不一致的记录)都写出到磁盘,并执行更新
  • 写入<END CKPT>,代表检查点结束
  • 在开始检查点之前所有的操作全部完成,在开始检查点之后的操作需要undo/redo
    • Undo:从后往前扫描,undo操作,如果扫描到开始检查点,发现该事务是活跃事务,仍要继续往前扫描到该事务开始
    • Redo:从前向后扫描,redo操作
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值