数据库笔记3——数据库管理系统体系结构,访问管理,数据分布,查询优化,恢复机制,并发控制

数据库管理系统体系结构

DBMS内核

内核自上而下
1.编译器,语法分析器:分析结果产生语法树
2.授权检查模块:看其是否有权限进行此类操作
3.语义分析和查询处理模块:调用具体函数,以及下层访问管理(即物理层提供的访问原语)对系统进行文件操作
4.并发控制,访问管理,恢复机制
再往下就是操作系统,再往上就是各种接口。

DBMS运行状态下的进程结构

单进程结构
应用程序和DBMS核心捆绑连接成一个exe,作为一个单独的进程运行
多进程结构
当我们connect某个数据库时,为当前应用创建一个DBMS核心进程,一个应用程序进程对应于一个DBMS核心进程,构建pipe、socket
多线程结构(Multi threads)
只有一个进程,每个应用进程都对应在同一进程里为其创建一个DBMS核心线程。

进程/线程之间的通信协议
线程的本质实际上是一种轻量级的进程,属于同一个进程的多个线程,可以共享这个进程的资源。
应用程序通过DBMS提供的API或嵌入式SQL访问数据库(管道0发送SQL语句,内部命令),DBMS核心通过管道1返回结果,实现同步控制。

DDBMS核心组件(分布式数据库管理系统的核心组件)

在这里插入图片描述
DC:通信控制,DD:目录管理,DDBK:核心,负责解析,分布式事务管理,并发控制,恢复和全局查询优化。

DDBMS全局查询优化的例子
全局查询优化可能会得到一个基于成本估算的执行计划,例如:
R1 &R2在分布式系统中S1 S2两个位置,要Select * From R1,R2 Where R1.a = R2.b;

  1. 把R2发给S1,得到R’,
  2. S1执行Select * From R1, R’ Where R1.a = R’.b;

访问管理

在关系型数据库中把对数据库的操作转化成对操作系统中文件的操作。它所提供的文件结构和访问路径将直接影响数据访问的速度。一种文件结构不可能对所有类型的数据访问都有效

访问类型

  1. 涉及到的元组数超过15%,就认定为需要操作most的数据。(因为硬盘是以块为单位进行操作的,并非以字节为单位,所以我们在数据库中读写其实也是以块为单位。假设数据是均匀分布的,其只要超过15%基本上就可覆盖所有块)
  2. 查找某个特定元组
  3. 查找小部分特定元组(<15%)
  4. 范围查找
  5. 更新操作

文件组织,关系型数据库底层数据结构:

堆文件:按插入顺序存储并只能按顺序检索的记录。配合索引扫描。适合查找most
**哈希文件:**根据某个属性的值通过hash函数映射记录地址。
堆文件+ B+树索引:最常用,可以提高数据访问效率。B+树是一种平衡树,且叶节点之间有双向链表。
大规模查找用堆文件顺序扫描,查找特定元组用B+树索引,范围查找可以用B+树的叶节点双向链表进行范围扫描。
簇型结构cluster,常用,raw disk:文件管理更底层的机制,允许用户自己控制数据在磁盘上怎么放。因为我们所谓的数据结构是逻辑上的存储,其经过操作系统在物理磁盘上真正的存储位置并不一定按照我们的想法(可能分散在不同磁片上)。如果我们可以在磁盘上按照物理顺序存储,在寻道查找的时候就很方便,raw disk就一次性申请需要大小的磁盘,可以自己实现一套文件管理。
动态哈希:动态调整哈希的范围以最大效率利用空间
栅格结构:多维数组(suitable for multi attributes queries)

索引技术

主要是B+树索引和簇索引。
B+树种插入数:
在这里插入图片描述
位图索引——索引本身就是数据:
在这里插入图片描述

数据分布

数据分布策略

灵活性,复杂性,优点和问题都随之升高:

  1. 集中式(Centralized):分布式系统,但数据仍然集中存储。这是最简单的,但是DDB没有任何优势。
  2. 分区(Partitioned):数据不重复分布。(没有副本)
  3. 复制(Replicated):每个站点上DB的完整副本。适用于检索密集型系统。
  4. 混合(以上的混合):在不同的站点DB的任意部分。最灵活和复杂的分配方法。

数据分布单位

  1. 根据关系(或文件),意味着无分区
  2. 根据片段:水平碎片:元组分区,垂直碎片:属性分区,混合

分割标准:

  • 完整性Completeness:每个元组或属性必须在某些片段中有自己的反射。
  • 重构Reconstruction:应该能够重构原始的全局关系。
  • 不相交性Disjointness:用于水平分割。

不同透明等级

  1. 碎片化透明用户只需要知道全局关系,他不需要知道它们是否碎片化以及它们是如何分布的。在这种情况下,用户感觉不到数据的分布,就好像他在使用集中式数据库一样。
  2. 位置透明性用户需要知道关系是如何碎片化的,但他不必担心每个片段的存储位置。
  3. 本地映射透明性用户需要知道关系是如何碎片化的,以及它们是如何分布的,但他不必担心每个本地数据库由什么样的DBMS管理,使用什么样的DML等等。
  4. 没有透明度

数据分布引发的问题

  1. 多副本的一致性
  2. 分布的一致性:主要是由于更新操作导致的元组存储位置的变化。解决方法:1重新分配Redistribution:更新后:选择→移动→插入→删除。2捎带检查Piggybacking:在更新时立即检查元组,如果有任何不一致,它将与ACK信息一起发送回来,然后发送到正确的位置。
  3. 将全局查询转换为片段查询和选择物理副本。
  4. 数据库片段的设计与片段的分配。

以上1)~3)应该在DDBMS中解决。而4)是一个分布式数据库设计问题。

并行数据库

采用Share Noting (SN) structure
垂直平行和水平平行:

  • Vertical parallel:一个复杂的查询可以分解成几个操作步骤,这些步骤的并行过程称为垂直并行。
  • horizontal parallel:对于扫描操作,如果要扫描的关系被预先分割成几个片段,并存储在一个SN结构的并行计算机的不同磁盘上,那么扫描可以在这些磁盘上并行处理。这种平行叫水平平行。

目录的分发

目录——关于数据(元数据)的数据。其主要功能是将用户的操作需求传递给系统中的物理目标。
目录的内容:(1)~(4)不经常更改,而(5)每次更新操作都会更改。

  1. 每个数据对象的类型(如基表、视图等)及其模式
  2. 分布信息(例如片段位置,…)
  3. 访问路由(如索引,…)
  4. 授权信息
  5. 查询优化中使用的一些统计信息

目录的特点:

  1. 主要对其进行读取操作
  2. 对于系统的效率和数据分布的透明性非常重要
  3. 这对区域自治非常重要
  4. 目录的分布是由DDBMS的体系结构决定的,而不是由应用程序需求决定的

目录分销策略:

  1. 集中的:存储在一个站点上的完整目录。扩展集中目录:先集中;使用后保存;在有更新时通知
  2. 完全复制:在每个站点复制目录。简单检索。复杂的更新。可怜的自主权。
  3. 本地目录:本地数据的目录存储在每个站点。这意味着目录是和数据一起存储的。如果想要其他站点数据的目录信息(通过广播查找):主目录:在某些站点上存储完整的目录。使每个目录信息都有两个副本。缓存:通过广播获取和使用其他站点数据的目录信息后,将其保存以备将来使用(缓存)。通过版本号的比较更新缓存的目录。
  4. 混合:对分发信息使用完全复制策略,对其他部分使用本地目录策略。或使用本地目录策略统计信息,使用完全复制策略到其他部分。

R*目录管理

特点:
无全局目录,独立命名和数据定义,目录平静地成长
System Wide Name(SWN)
::=User@UserSite.ObjectName@BirthSite

  • UserSite: 用户所在站点的ID。这样,不同站点上的不同用户就可以使用相同的用户名。
  • BirthSite:数据对象的诞生地点。在R*系统中没有全局目录。在BirthSite中,即使数据被迁移到其他站点,有关数据的信息也始终保持不变。
  • Print Name (PN): 用户访问数据对象时通常使用的名称。::=[User[@UserSite].]ObjectName[@BirthSite]

名称解析:将PN映射到SWN
为每个用户建立同义词表,eg:站点02的用户u1:Define Synonym S AS u1@02.student@02;

将PN按照以下规则进行不同形式的映射:

  1. PrintName = SWN,不需要转换
  2. 只有ObjectName:在当前站点上当前用户的同义词表中搜索“ObjectName”。
  3. User.ObjectName: 在当前站点上搜索用户“user”的同义词表。
  4. User@UserSite.ObjectName
  5. ObjectName@BirthSite

如果在(2)、(3)中没有找到ObjectName的匹配项,或者打印名称以(4)或(5)的形式出现,则使用名称补全。

名称填写规则:
缺少的用户将被当前用户替换
丢失的UserSite或BirthSite将被当前站点ID替换。

查询优化

关系型数据库有一个很明显的缺点是查询效率低,因为关系的连接靠另一张表连接,所以这张表可能会特别特别大。
所以我们用查询优化来解决这个问题。主要解决的问题就是,把用户的需求重写成一个效率更高的形式。然后再怎么查找。
所以这里分两步走:

  • 第一步,重写,就是代数优化。即调整用户需求的操作顺序,使得效率提升。(这里解释一下什么意思,举个例子,如果我们要求x^2 +2xy+y^2的值,我们直接可能是代数进去一步一步算,但实际上我们的需求可以变成一个完全平方式再算,就很快。)
  • 操作优化。根据数据结构。索引等实现优化。

DDBMS查询优化

分为全局查询和分片查询。
过程:
在这里插入图片描述

代数优化

假设下需求:将其简化,从而变成以下语法树:原理可以看出来,我们要避免先进行连接,而是先进行选择、增加一些投影操作再连接,(选择是为了删除不需要的行,投影是为了删除不需要的列)到这里你大概知道了什么叫做代数优化。

查询树
叶子节点为查询对象
中间节点为一元或二元操作,从树叶到树根的顺序即为操作执行顺序。

等价变换规则

  • 连接或者笛卡尔乘积的左右子树可以交换,且满足结合律
  • 投影操作:连续投影等价于直接投影到最小子集上
  • 选择操作:连续的选择操作可以合并
  • 选择与投影操作的交换律
  • 选择操作如果只包含某个对象,可以压至其二元操作之下,即先进行选择再进行二元操作。同理并关系与差关系也可。

将全局查询转换成分片查询
对于水平碎片:R = R1 ⋃ R2 ⋃ … ⋃ Rn
对于垂直碎片:S = S1 ⋈ S2 ⋈ … ⋈ Sn
用以上内容替换查询表达式中的全局关系。我们得到的表达式叫做正则表达式
基本原则

  • 利用等价变换规则,尽可能将一元操作往下压
  • 寻找并且合并公共表达式,在查询树中组合相同的叶子,用相同的操作数组合对应于相同操作的中间节点。
  • 查找并消除空的子表达式

查询分解:

考虑到存储片段的站点,需要将查询分解成几个子查询,这些子查询可以在不同的站点本地执行:
按后序遍历查询树,直到j变成2,然后得到第一个子树。剩下的可以类比推导,这样就可以得到所有的子树。

操作优化

关键问题:
具体化:选择查询中涉及的片段的合适副本
怎样找到一个好的策略来进行连接操作
选择每个操作的执行(主要是直接连接)
对效率影响最大的是连接运算,所以对其的操作优化最重要。

嵌套循环Nested loop
一层外循环O,一层内循环I,对外循环的每一个元组比较整个内循环。
在内存中申请两个物理块大小的缓冲区,一块放外循环数据,一块放内循环数据,每次对两个物理块进行比较。从而大大减少了IO读盘次数,同样只要内存够大,就能成倍减少,我们最好能将n-1块外循环数据放到内存,而内循环每次只放一块

归并扫描
参与的两个关系事先按照连接属性的值排好序,利用双指针算法,只需各自扫描一遍循环即可。

B+树索引/hash
使用索引或哈希查找映射元组,把没有索引的作为外循环,放入内存,每次按照内存中连接属性不同的值查找有索引的内循环,从而避免对内循环关系作顺序扫描。
(如果索引属性重复值超出20%,用索引就不合算)

hash连接
两个表中公共的属性值相等时,即有相同域,对他们进行相同的hash函数散列到相同的hash文件种,那么他们散列到的hash值是必定相等的

直接连接(R*中连接操作的实现方法)

连接操作的两种基本实现方法:嵌套循环和归并扫描。
这两种方法中关系的传递:

  • “shipped whole整批装运”:整个关系没有选择。对于内循环:在目的地建立临时关系以供进一步使用,外循环:关系不需要存储。
  • “Fetch as need按需取用” :整个关系没有封装。远程站点所需的元组是应其请求发送的。请求消息通常包含连接属性值。通常在请求的站点上有一个关于连接属性的索引。

R中join的六种实现策略:
在这里插入图片描述
显然外循环应该被整个封装,而内循环如果这样的话,索引就不能跟着被封装,此外,还需要临时关系。加工成本和储存成本都很高。
这六种策略不包括:
多重连接-转换为多重二元连接。
副本选择——因为R
不支持多副本。

恢复机制

作用是:发生一些故障后,将数据库恢复到一致状态。
恢复机制分为两大块:
1.prevention 即防止系统故障
2.solving 即从故障中恢复
这就要求我们的系统:

  • 数据的备份时必要的
  • 机制需要能够检测出所有可能的故障(不一定能解决)

事务与日志

数据库进行操作的基本单位是“事务”(transaction),事务就是对数据库进行一组操作的集合:具有以下性质:
ACID

  • 原子性(atomic action):即要么全部成功,要么一条也没做。
  • 保持一致性(consistency preservation):数据库从原来的一致状态,其数据全部是正确的,经过事务的运行,到现在的一致状态。
  • 隔离性(isolation)保证不同事务同时发生的时候不能互相干扰,就好像他们彼此独立占有整个数据库。
  • 持久性(durability):一个成功完成的事务对数据库产生的影响应该永久反应在数据库中,哪怕将来发生了故障,其也应该是可恢复的。

如果我们不显式的定义一个事务,那么数据库中的每一条sql语句都是一个事务

重要的数据结构
首先这些数据结构必须存储在非挥发性存储中(nonvolatile storage,即掉电之后数据还在,硬盘就是ns,但内存ram就不是)

  • 提交事务列表(commit list):每用begin transation 创建一个新的事务,其都会有一个事务表示号TID,所有已经成功完成已经提交的事务编号(TID)组成的列表
  • 正在运行列表(active list):正在运行的事务列表
  • 日志log:每个事务在log中都会有一条记录,其记录TID和两个链表/文件,一个链表记录B.I,一个记录A.I。日志的可靠性要求比一般数据高

常用恢复机制:

  1. 周期性备份存储(periodical dumping):发生故障恢复至最近一次备份即可,但这样上次备份至本次故障期间的就没了。所以我们提出I.D(incremental dumping)每次只备份一些系统的增量。我们可以每隔一长段时间进行备份,每隔一小段时间就进行一次ID。
  2. 备份+日志(backip+log):日志:从上一次备份以来,对数据库进行的所有操作都记录下来。要记录改变前后的两个值(before image B.I)&(after image A.I)。发生故障后,我们重演从上次备份以后的所有日志记录。

当我们进行恢复时:
如果某些事务发生了一半:我们用BI恢复,相当于此事务没有进行过。
如果事务已经完成了但是还没有被写入数据库,我们用AI值恢复,相当于其已经完成了。

3.多副本:每个数据对象都有多个副本。发生故障时,使用其他副本进行恢复。系统不能因为某个副本失败而崩溃。优点:提高可靠性,恢复非常容易。缺点:在集中式数据库系统中很难获得独立的故障模式。导致空间浪费。所以这种方法不适合集中式DBMS。

重要规则:

  1. Commit rule(提交规则):提交之前AI必须已经被写到NS里面
  2. log ahead rule先记后写规则:在我们对数据库进行更新之前,BI必须先写到日志里
  3. 恢复策略
    3.1redo &undo操作:用BI进行undo还原操作n次和一次等效,用AI进行redo重做n次和一次等效。

更新策略和故障恢复

第一种:AI在commite前写入DB:相当于直接改数据库。

  1. 从begin transation 开始,第一步为事务分配TID,然后将TID写入active list
  2. 每步操作BI先进log,然后AI写入DB
  3. 一旦提交,不需要做什么事情,我们将TID写入commit list,再将TID从active list中删除即可。
    在这里插入图片描述
    重启动恢复模块,检查每个事务的commit list和active list,以判断发生事务的瞬间,其所处的状态是怎样,有以下可能:
    在这里插入图片描述
    ——只在进行,没有提交:说明这个事务做到一半,我们要用BI还原。然后从active list中删除TID
    ——都有,说明完成且提交了,只差最后一步,只需要从active list中删除TID即可。
    ——只有已提交,说明事务已经完完全全的结束了,不需要进行任何操作。
    检查完了打一个检查点,所以我们每次从上一个检查点开始检查即可。

第二种:AI在提交之后才写入DB
适用于一些事务不是很容易发生,且经常容易失败的情况,我们就需要先保证这个操作没有问题,再去更改数据库。其并发性较好。
当我们需要进行更新操作的时候,我们始终先将AI写入log,然后一直等到commit的时候,再把log里所有更新写入db。
这样的流程如下:

  1. 从begin transation 开始,第一步为事务分配TID,然后将TID写入active list
  2. 每步更新操作先把AI写入log
  3. 提交时将TID写入commit list
  4. 提交之后要将log中所有的AI覆盖到DB中的BI,再将TID从active list中删除即可。
    在这里插入图片描述
    重启动恢复模块,检查每个事务的commit list和active list,以判断发生事务的瞬间,其所处的状态是怎样,有以下可能:
    ——如果只在active list中,没有commit说明其还没提交,更别提对数据库更改了,只需将TID从active list中删除即可。
    ——如果两个表中都有,说明已经提交且正在覆盖BI,需要redo AI覆盖BI的操作。然后将TID从active list中删除即可。
    ——如果只在commit list中,说明已经将其从活动列表中删除了,说明这个事务彻底完成了,不需要做任何事。
    在这里插入图片描述
    第三种:AI并发写入DB
    和2一样先将AI写入log,但其要充分利用计算机后台资源,相当于等硬盘有空时其帮忙搬运在log中的AI到数据库中去。
  • 从begin transation 开始,第一步为事务分配TID,然后将TID写入active list
  • 将AI和BI都记入log
  • 利用后台空闲周期将log中的AI写入DB
  • 提交阶段,将TID写入commit list
  • 将剩下log中的AI完全写入DB
    在这里插入图片描述
    重启动恢复模块,检查每个事务的commit list和active list,以判断发生事务的瞬间,其所处的状态是怎样,有以下可能:
    ——如果只在active list 中,说明其还没进入提交阶段,对数据库已经产生了部分影响,需要undo还原。然后将TID从active list中删除即可。
    ——如果两个表都在,已经提交,正在搬剩下的,需要redo保证所有的都被搬入db。然后删除两个表中的tid
    ——如果只在commit list,说明active list中的已经被删除了,完全完成了,不需要做任何事。
    在这里插入图片描述

恢复过程

  • 交易失败:由于超出预期的原因,交易不得不中止。因为它必须在提交之前发生:必要时撤销,从active list删除TID。
  • 系统故障:操作系统崩溃,但磁盘上的DB没有损坏。如突然停电,需要恢复系统,如有必要,撤消或重做
  • 介质故障:磁盘故障,磁盘上的数据库损坏。加载最新转储,根据日志重做

系统启动

  • 紧急重启:系统或介质故障后启动。开始前需要恢复。
  • 热启动:系统关闭后启动。不需要恢复。
  • 冷启动:从零开始启动系统。灾难性故障后启动或启动新的数据库

两阶段提交

分布式数据库管理系统中的事务是分布式事务,分布式事务管理的关键是如何保证所有子事务一起提交或一起中止。
子事务之间的和谐依赖于沟通,而沟通是不可靠的。
而又由于不存在固定长度协议。我们需要给信息编号。

并发控制

在多用户数据库管理系统中,允许多个事务并发访问一个数据库。
需要并发的原因:
提高系统利用率和响应时间。
不同的事务可以访问数据库的不同部分。

如果我们对事务不进行并发控制,可能出现如下情况:
在这里插入图片描述

  • 丢失更新(写写冲突):多个事务对数据库中同一个数据读取之后,进行写操作,导致某些写操作事实上没有发生。
  • 脏读(写读冲突):一个A事务连续读一组数,在读的期间另一个事务B修改了这组数据(即B对数据进行了写操作),导致后面A读到的数据是由B修改过的值(,这时我们称A读到的数据是脏数据。因为A读到的前一部分数据是B修改前的,但其读到的另一部分数据却是B修改后的。这个数据不满足时间上的一致性。脏读还会导致恢复时的“多米诺效应”:如果连续几个事务都在读上一个事务写的值,当第一个事务发生故障rollback回滚的时候,后续所有事务读的这个值都需要回滚。
  • 不可重复读(读写冲突):如果事务A在重复读一个数据x,但是某两次中间事务B对这个x进行了写操作,这导致我们A本应该读到相同x却得到了不同的值。影响到了隔离性。

可串行化serializable:并发一致性的标准

Serialization:假设{T1,T2,…Tn}是一组并发执行的事务。n个事务同时交给系统进行随机调度并发运行所产生的结果,只要其和按某一种顺序产生的结果一样(虽然得到的结果不同,但其只要是n!里面的一种),其运行结果就是可串行化的,即正确的。
可串行化可以分为视图可串行化和冲突可串行化:

视图等效和冲突等效

  • 视图等效:假设S和S是两个具有相同事务集的计划。如果S和S‘基于相同的初始执行条件对数据库产生相同的效果,则它们是视图等价的。
  • 冲突操作:冲突操作的顺序会影响执行的效果。
  • 非冲突操作:1)读读操作2)即使有写操作,操作的数据项也不一样。如Ri(x)和Wj(y)。
  • 冲突等价:如果一个调度S可以通过一系列非冲突操作的互换转化为调度S’,我们说S和S’是冲突等价的。

特征:冲突等价必须是视图等价的。相反,这是不对的。
冲突可串行化:
对于事务集{T1,T2,T3}的调度s = R2(x)W3(x)R1(y)W2(y)→R1(y)R2(x)W2(y)W3(x)= s’是冲突序列化,因为s是一个串行执行。
视图可串行化:
s = R1(x)W2(x)W1(x)W3(x)没有冲突等价调度s‘,但是我们可以找到一个调度s’= R1(x)W1(x)W2(x)W3(x)它与s是视图等价的,s’是串行执行,所以s是视图序列化。
视图等价的测试算法是一个NP问题,而冲突可串行化涵盖了可序列化调度的大部分实例,所以我们后面说的没有特别说明的话会指向冲突可串行化
我们建立有向图,顶点为包括参与计划的所有事务。
有向边由分析冲突决定方向,如果满足以下任一条件,添加一条边Ti→Tj:(对同一个数据x来说)

  • A读在B写之前
  • A写在B读之前
  • A写在B写之前
    最后,检查上图中是否有环路。如果其中有循环,则调度是不可序列化的,或者是可序列化的。

查找等效的串行执行

  1. 因为没有循环,所以一定有一些顶点的入度为0。从前面的图中移除这些顶点和相对边,并将这些顶点存储到队列中。
  2. 以与上面相同的方式处理左图,但是移除的顶点应该存储在队列中现有顶点的后面。
  3. 重复步骤1和2,直到所有顶点都移动到队列中。
    例如:对于{T1,T2,T3,T4}上的调度s,假设:**s = W3(y)R1(x)R2(y)W3(x)W2(x)W3(z)R4(z)W4(x)**是否可序列化?如果是,找出等价的串行执行。
    在这里插入图片描述
    并发控制的任务是强制执行以可串行化的时间表执行的并发事务。

封锁法及其锁协议

事务进行读写之前必须申请锁,如果要锁同一个数据,后者必须等前者释放这个锁才能对其进行读写操作。

协议

两段加锁协议(2pl,two phase locking)
一个事务中所有的加锁请求,都在锁释放之前,我们称这个事务是一个两阶段事务。对事务额这种限制称之为两端加锁协议。(意思就是释放锁之后就不再申请新的锁了)在申请锁的阶段我们称之为增长阶段,释放锁的阶段叫做衰减阶段。
well-formed协议
事务遵守规则,在访问数据前先申请锁再对其进行操作,其就是well-formed

定理
1、如果所有的事务都是2pl+well-formed的,其一定可串行化
2、如果所有事务都是2pl+well-formed+更新操作的锁推迟到EOT(事务结束之前)才释放,其一定可串行化,且可恢复。(不会出现多米诺效应,因为如果释放锁释放的太早了仍然可能被下一个事务读走,而我们推迟到EOT的时候再释放,就保证了后一个事务想读前一个事务的数据的时候,前一个事务必须已经结束了,前一个事务就不可能滚回了)
3、如果所有事务都是2pl+well-formed+推迟所有锁在EOT前释放,称其为严格的2pl协议。

1.x锁 排他锁

只提供一种x锁,不管其读还是写都申请的是这种锁。
其相容情况如下表:横行为事务上已有的锁,列上为即将进行的事务。
其中标记为:
NL ——no lock
X ——x lock
Y——compatible 相容
N——incompatible 不相容

2.S-Xlocks,读写锁

读操作申请s锁,shared -locking共享锁,不冲突
只有写操作时才申请x锁,排他锁

这样就保证了如果一个数据已经被其他事务申请了写操作x锁,你就不可能脏读,即不能再申请s锁。
同理,别人正在读,申请了s锁,你也不能申请x锁对其进行写操作。

3.s-u-x锁,更新锁

我们提出了u锁,即update lock,我们在更新数据的时候第一步得读这个数据,但是我们可能需要一段时间来处理我们想要得到的值再给他写进去,所以我们可以先申请u锁,在我们需要写的时候再将u锁升级成为x锁
所以我们可以尽量推迟申请排他锁的时间,以保证我在只读不写的时候别的事务可以读到这个事务(这里我们可以发现u锁的设计就是为了u-s并发),提高系统运行的并发效率
在这里插入图片描述
注意u锁和u锁不相容,x与其他所有锁都不相容
所以u锁的设计和我们上一节里讲到的AI在commit之后再写入db可以一起使用,即我们要对数据进行更新就先申请u锁,更新完写入日志(通过这个模式我们能理解为什么U-U锁不容,因为申请u锁的时候你可能已经在更新日志了,这个时候如果其他的事务也申请了u锁也更新日志的化就会导致丢失更新),等其进入提交阶段,将u锁升级为x锁,再将AI从日志中搬入DB。

死锁与活锁

死锁:多个并发运行的事务在竞争锁的时候出现了循环等待,导致没有事务可以拿到所需的资源。
如A事务先申请资源1,拿到1的x锁,B事务先申请资源2。现在A需要2,而B需要1,他俩就一直等待。

活锁:尽管其他事务在有限的时间都释放了资源,但是由于系统的调度问题,导致某个事务等待相当长的时间都拿不到锁。
如一个资源一直挨个有事务申请s锁进行读操作,这样的话如果事务B想对其进行写操作就一直拿不到x锁,导致事务B“饿死”。活锁方便解决,只需要我们按申请次序依次处理就可。调整调度策略,比如FIFO

死锁的解决办法:

一 防止法

  1. 要求事务在运行之前一次性申请所有锁(一次性封锁法)
  2. 给资源排序,让事务在申请各种资源时从权值最大的资源开始申请(顺序封锁法)
  3. 一旦发生冲突就都释放
  4. 以上三个在数据库系统中并没有操作系统中效果好,事务重试(transation retry)
    其给每个事务安排一个唯一时间戳(time stamp),功能一作为TID来标识事务,功能二用来比较两个事务的年龄,当AB冲突时,有两种办法解决:
    时间戳法:都是老事务先运行
    1)等待老的死亡法(wait die):比较年龄,如果A年龄比B大,就等待运行,如果A年轻,就把自己释放掉,过一会儿以之前相同的时间戳来运行。这样总是老事物等待运行,单方向等待不会有死锁。且因为他每次重试都以之前的时间戳运行,媳妇熬成婆,等老的都结束之后,他总会变成最老的,这个时候不会有年轻的事务跟他叫板,所以这个方法也不会有活锁。
    2)击伤等待法(wound wait):同样比较年龄,相反的是,如果年轻就等待,如果年纪大的就让年纪小的释放回滚,占有其资源,让他过会儿以先前的时间戳运行。这样就总是老事务先运行,单向等待不会死锁。活锁同理,他总会变成最老的,那个时候就可以杀掉所有别的事务,就可以运行,不会活锁。
    上面两种方法如果乍一看感觉一样,其实逻辑是一样的,但是仔细想。第一种是打不过就自杀法,第二种是他杀法。只要保证单向等待即可。
    如果不理解再换句话说,等待死亡法是老的才有资格等,击伤等待法是老的干脆不等,直接把新事务杀死

二 检测并解决法

  1. 设置TIMEOUT,规定事务等待时间,如果超时,就认为发生死锁(不一定真的发生了死锁),将其释放,过一会儿再重新运行这个事务即可。
  2. 构造有向等待图:只需定期在等待图中查找是否有环路就意味着出现死锁。(也可每个事务加入都检查一遍,但大多数系统都是每隔一段时间定期检查)一旦发现环路,就选择一个牺牲者(最年轻的事务,或者目前锁最少的事务,选择滚回代价最小的事务即可),释放掉其资源和锁,整个系统就可以接着运行,此时我们重启被牺牲的事务即可。

粒度封锁

为了减少锁定的开销,锁定单元应该越大越好;为了提高事务的并发度,锁单元应该越小越好。在大规模数据库管理系统中,锁单元分为几个级别:DB-File-Record-Field,在这种情况下,如果事务在某个级别的数据对象上获得锁,那么它在该数据对象的每个后代上隐式地获得相同的锁。
所以,多粒度锁方法中有两种锁:显示锁Explicit lock &隐式锁 Implicit lock

如何检查隐式锁上的冲突?

Intention lock意向锁
提供三种强度锁,即IS(Intention share lock意向共享锁)、IX(Intention exclusive lock内涵排他锁)和SIX(s+ix)。
例如,如果一个事务在某个较低级别的数据对象上添加了一个S锁,那么所有包含它的较高级别的数据对象都应该添加一个IS锁作为警告信息。如果另一个事务稍后想要对更高级别的数据对象应用X锁,它可以通过IS锁找到隐式冲突。
多粒度封锁的兼容性矩阵:
在这里插入图片描述

锁定规则:

  • 从根到叶请求锁,从叶到根释放锁。
    在这里插入图片描述

Phantom(幽灵现象导致幻读) and Its Prevention

当允许多粒度封锁时,数据库是对象的固定集合的假设是不成立的。那么即使是严格的2PL也不能保证可串行化:
例:
T1锁定所有等级为1的水手,并找到了一个年龄为71的水手。
此时,T2添加一个新的水手,其等级为1且年龄为96.并且T2删除等级为2的最老的水手,假设为80岁。提交。
T1再锁住所有等级为2的水手,并找到了一个年龄为63的。
这时我们看,T1的结果是:等级1最老的71,等级2最老的63.但是在T2执行前后,数据库的一致性状态都不等于这个情况。这是因为T2对这个表进行了“插入操作”,导致T1后来发现居然还有自己没有找到的数据,就好像发生了幻觉。.幻读强调的是:原来不存在的内容现在存在了,而不可重复读强调原来的事务本不应该变化却变化了

幻读解决方法:
范围锁:
一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读。
索引锁定:
如果等级这个域里由密集的索引,T1应该锁定包含等级= 1的数据条目的索引节点,并将其保留到EOT。
当T2想要插入一个新水手(等级= 1,年龄= 96)时,他不能在包含等级= 1的数据条目的索引节点上获得X锁,因此他不能插入新的索引项来实现新水手的插入。
如果没有合适的索引,T1必须锁定整个表,当然在T1提交之前不能添加新记录。

谓词锁
授予满足某些逻辑谓词的所有记录的锁,索引锁定是谓词锁定的一种特殊情况,对于这种情况,索引支持谓词锁定的有效实现。一般来说,谓词锁定有很多锁定开销。几乎不可能实现。

事务的隔离级别

在这里插入图片描述
其中锁的需求:

  • 读未提交:读不需要锁,写需要x锁,一直保持到结束
  • 读已提交:读需要s锁,写需要x锁,一直保持到结束
  • 可重复读:严格2PL
  • 可串行化:严格2PL+在EOT之前保持索引叶上的S锁

时间戳Time Stamp方法

1.时间戳 -计算机内部时钟按时间顺序产生的数字。
2. 事务时间戳:事务初始化时的时间戳
3. 数据对象时间戳:读取时间(tr) -任何事务拥有的读取对象的最高时间。写时间(tw) -任何事务写入对象的最高时间。
4. T.S方法的核心思想是系统将根据T.S顺序强制并发事务在与可串行执行等价的调度中执行。

T.S方法下的读/写操作:
设R为一个带有 tr和tw的数据对象。T为一个有时间戳t的事务。

事务T读对象R:

read R
if (t >= tw)
then /* OK */
tr = Max (tr, t) //如果t比tr大,t缩小到tr
else //小于t1事务已经在t1之前写了R,冲突
restart T with a new T.S

事务T写对象R:

if (t >= tr) //判断t比tr和tw都大
then if (t >= tw)
then 
write R //执行写操作并更新写时间tw为事务T的时间戳t
tw = t
else /* tr <= t < tw 即t在读时间和写时间之间*/ 
do nothing
else  //一个比T小的事务已经在T之前读了R,冲突
restart T with a new T.S

时间戳法优点:

  1. 相比锁的方法,最明显的优点是没有死锁,因为没有等待。
  2. 缺点:每个事务,每个数据对象都有T.S,每个操作都需要更新tr或tw,系统开销大。
  3. 解决方法:加大添加T.S .的数据对象粒度(低并发度)。T.数据对象的存储空间实际上并不存储在非易失性存储器中,而是存储在主存储器中,并保存一段指定的时间,并且假设存储空间不在主存储器中的数据对象的存储空间为零。
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值