《PostgreSQL数据库内核分析》之事务处理与并发控制(七)

举报
毛竹 发表于 2024/02/02 16:44:02 2024/02/02
【摘要】 7.1事务系统简介1.事务管理器 事务系统的中枢,实现是一个有限状态自动机(Fine State Machine),通过接受外部系统的命令或信号,并根据当前事务所处的状态,决定事务的下一步执行过程2.锁管理器 实现系统并发控制所需要的各种锁 PG中,事务执行的读阶段采用多版本并发控制(MVCC)即对元组的度和写互不阻塞;在事务中写阶段则需要由各种锁来保证事务的隔离级别3.日志管理器 记录事务...

7.1事务系统简介

1.事务管理器 事务系统的中枢,实现是一个有限状态自动机(Fine State Machine),通过接受外部系统的命令或信号,并根据当前事务所处的状态,决定事务的下一步执行过程

2.锁管理器 实现系统并发控制所需要的各种锁 PG中,事务执行的读阶段采用多版本并发控制(MVCC)即对元组的度和写互不阻塞;在事务中写阶段则需要由各种锁来保证事务的隔离级别

3.日志管理器 记录事务执行的状态及数据的变化过程,包括事务提交日志(CLOG,记录事务执行的结果状态)和事务日志(XLOG,日志,记录了数据变化过程并保持一定的冗余数据)

7.2事务系统的上层

事务保证ACID特性,提高了用户操作数据的灵活性。

PG中,通过事务块是实现数据库内部底层事务和终端命令的交互,事务块对应着通常数据库理论中提到的“事务”概念。

pg中事务则体现了数据库理论中锁提到的命令概念,状态仅仅反映的是实际一个SQL语句的执行状态。

事务块的状态数量要比底层事务的状态数量多得多。

执行一条SQL语句前会调用StartTransactionCommand函数,执行结束时调用CommitTransactionCommand函数。如果调用失败则会调用AbortCurrentTransaction函数。

7.2.1事务块状态

7.2.2事务块操作

StartTransactionCommand: 每条语句执行前调用

CommitTransactionCommand: 每条语句执行后调用

AbortCurrentTransaction: 系统遇到错误时调用

7.3事务系统的底层

7.3.1事务状态

TransState

7.3.2事务操作函数

改变事务块状态的函数:BeginTransactionBlock、EndTransactionBlock、UserAbortTransactionBlock

实际的事务操作由:StartTransaction、CommitTransaction、AbortTransaction、CleanupTransaction

7.3.3简单查询事务执行过程实例

BEGIN

SEELCT

END

7.4事务保存点和子事务

SAVEPOINT

ROLLBACK TRANSACTION TO

7.4.1保存点实现原理

7.4.2子事务

PG拥有一套与事务操作函数类似的子事务操作函数,包括StartSubTransaction、CommitSubTransation、CleanupSubTransaction和AbortSubTransaction等。在PostgreSQL中,子事务的主要作用是实现保存点,从而增强事务操作的灵活性。

7.5两阶段提交

Two Phase Commit,2PC支持分布式数据库的事务处理

7.5.1预提交阶段

在预提交阶段,各个分布式数据库系统将检查当前各自事务的情况,视图保证这个本地事务如果执行,就不会终止。即使因系统出错而需要进行系统恢复,与这个事务相关的恢复操作也是应该Redo而不是Undo。

7.5.2全局提交阶段

两阶段提交协议是分布式数据库系统中保证分布式事务ACID特性的经典解决方案。PostgreSQL为其提供了很好地操作接口,应用程序开发者利用该操作接口可以很好地实现该协议。

7.6PostgreSQL的并发控制

多个会话试图同时访问同一数据的情况,并发控制的目标是保证所有会话高效地访问,同时维护数据完整性。
PostgreSQL为开发者提供了丰富的对数据并发控制进行管理的工具。在内部,PostgreSQL利用多版本并发控制(MVCC)来维护数据的一致性。意味着当检索数据时,每个事务看到的都只是一段时间之前(不同隔离级别下所看到都只是一段时间之前)的数据快照(一个数据库版本)。如果对每个数据库会话进行事务隔离,就可以避免一个事务看到其他并发事务的更新而导致不一致的数据。
在PostgreSQL里也有表和行级别的锁定机制,因为MVCC并不能解决所有的并发控制情况,所以还需要使用传统数据库中的锁机制来保证事务的并发。另外,PostgreSQL还提供了会话锁机制,利用它可以扩大锁的使用范围,即一次对某个对象加锁可以保证多个事务都有效。
SQL标准考虑了三个必须在并行的事务之间避免的现象:
1.脏读(Dirty ):一个事务读取了另一个未提交的并行事务写的数据。
2.不可重复读(Non-repeatable Reads):一个事务重新读取前面读取过的数据,发现该数据已被另一个已提交的事务修改过。
3.幻读(Phantom Read):一个事务重新执行一个查询,返回一套符合查询条件的数据,发现这些数据因为其他最近提交的事务而发生了改变。
为避免出现这三种现象,SQL标准定义了4个事务隔离级别
隔离级别
脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 可能
可串行化 不可能 不可能 不可能
PostgreSQL中实际上只有两种独立的隔离级别
1.读已提交(Read Committed): 缺省隔离级别。select语句只能看到查询开始之前提交的数据而无法看到未提交的数据或在查询执行时其他并行的事务提交所做的改变。
2.可串行化(Serializable):最严格的事务隔离。可串行事务等待第一个正在更新的事务提交或回滚。如果第一个事务回滚,那么它的影响会被忽略,可以完成更新操作。如果更新,可串行化事务将回滚,从头开始重新进行整个事务。

7.7PostgreSQL的三种锁

1.SpinLock 最底层的锁,使用互斥信号量实现,与操作系统和硬件环境练习紧密。
2.LWLock 轻量锁,主要提供对共享存储的数据结构的互斥访问。排他模式和共享模式。
3.RegularLock 一般数据库事务管理中所指的锁,也简称Lock。RegularLock由LWLock实现,其特点是:有等待对列,有死锁监测,有自动释放锁。(最上层)

7.8锁管理机制

锁的管理包括对不同粒度锁的操作,这些粒度包括表、内存页(目前只在索引中使用对内存页的加锁)、元组、事务XID、事务VXID以及当前数据库中的一般对象。

7.8.1表粒度的锁操作

加锁对象为一个表
操作
实现函数 功能 说明
初始化锁 RelationInitLockInfo 初始化表描述符中的锁信息 如果要对一个表加锁,就是对一个<DBID,RELID>加锁,所以这里就是将这个二元组放到表结构的字段(rd_lockinfo)中。在创建任何表时都要调用该函数
加锁 LockRelation 对一个表加指定锁模式的锁 调用RegularLock模块中的LockAcquire函数完成。另外对已经打开的表获取一个额外的锁可以调用ConditionalLockRelation函数
解锁 UnlockRelation 释放对一个表所加的指定锁模式的锁 实际调用RegularLock模块中的LcokRelease函数完成。与LockRelation成对使用。
加会话锁 LockRelationIdForSession 在目标表上获取一个会话锁 会话锁式跨事务边界而存在的,所以应用范围更广。
解会话锁 UnLockRelationIdForSession 释放对一个表所加的会话锁 与LockRelationForSession成对使用。
加扩展锁 LockRelationForExtension 对一个表加指定模式式的扩展锁 这个锁标签式用来实现表中新增页的互锁,需要这个锁式因为BUFMGR/SMGR中定义的P_NEW可能存在争用现象(RaceCondition)
解扩展锁 UnLockRelationForExtension 释放对一个表加的扩展锁 与LockRelationForExtension成对使用

​7.8.2页粒度的锁操作

操作
实现函数 功能 说明
加锁 LockPage 获取一个页面级的锁 这个函数目前被一些索引访问方法用于对索引页面加载。另外,还有一个带条件的加锁函数ConditionLockPage,只有在不阻塞当前进程的前提下才对一个页面加指定模式的锁,当且仅当获得锁时返回TRUE
解锁 UnlockPage 释放对一个页面所加的锁 与LockPage成对使用

7.8.3元组粒度的锁操作

加锁对象为一个元组
操作
实现函数 功能 说明
加锁 LockTuple 对一个元组加指定锁模式的锁 使用这种锁要谨慎,因为不可能为每一个元组在共享的存储区提供一个单独的锁。另外,还提供一个带条件的加锁函数ConditionLockTuple,条件是在不阻塞进程的前提下,对一个元组加指定模式的锁,当且仅当获得锁时返回TRUE
解锁 UnlockTuple 用于释放对一个元组的所加的锁 与函数LockTuple成对使用

7.8.4事务粒度的锁操作

操作
实现函数 功能 说明
加锁 XactLockTableInsert 获取一个锁以声明事务正在运行中 在事务或其子事务获取一个XID时会自动调用,且获取的锁一直使用到事务的结束
解锁 XactLockTableDelete 释放指定事务上的锁 只能释放子事务所持有的锁,主事务锁在事务结束的时候被释放

7.8.5一般对象的锁操作

操作
实现函数 功能 说明
加锁 LockDatabaseObject 实现对数据库一般对象的加锁 用于获取当前数据库的一般对象的一个锁,这种锁不能用在共享的对象上
解锁 UnlockDatabaseObject 实现对数据库一般对象的解锁 与函数LockDatabaseObject成对使用

7.9死锁处理机制

PostgreSQL对于死锁的预防分为两步:
1.当进程请求加锁时,如果失败,会进入等待对列。如果在对列中已存在一些进程要求本进程中已持有的锁,那么为了尽量避免死锁,可简单地把本地进程插入到它们前面。
2.当一个锁被释放时,将会试图唤醒等待对列中的进程。如果其中的某个进程的要求与排在它前面但由于某些原因不能被唤醒的进程冲突,这个进程将不被唤醒。这么做可以保证互相冲突的加锁请求按照到达的先后被处理。
PostgreSQL还提供一套死锁监测机制。

7.9.1死锁处理相关数据结构

7.9.2死锁处理相关操作

1.死锁处理相关数据结构的初始化
2.死锁监测入口函数
3.死锁监测调整函数
4.等待图的环检测函数

7.10多版本并发控制

保证数据的一致性
检索数据时,每个事务看到的只是一段时间之前的数据快照,这样,如果对每个数据库会话进行事务隔离,就可以避免一个事务看到其他并发事务的更新而导致不一致的数据。
使用多版本并发控制的主要优点是:对检索(读取)数据的锁请求与写数据的锁请求并不冲突,所以读不会阻塞写,而写也从不阻塞读。极大地提高了并发处理能力。
多版本并发控制示例

事务T1
事务T2 表A的变化 事务T2
BEGIN select * from A; A A
UPDATE   A -> AA  
  select * from A;   A
COMMIT      
  select * from A:   AA

7.10.1MVCC相关数据结构

在PostgreSQL中,更新数据并不是用新值覆盖旧值,而是在表中另开辟一片空间来存放新的元组,让新值与旧值同时存放在数据库中,通过设置一些参数,让系统识别它们。

7.10.2MVCC相关操作

核心功能用来判断元组的状态,包括元组的有效性、可见性、可更新性。

7.10.3MVCC与快照

快照(SnapShot)记录了数据库当前某个时刻的活跃事务列表。通过快照,可以确定某个元组的版本对于当前快照是否可见。

7.11日志管理

一种安全的方式记录数据库变更的历史,当系统出现故障后,数据库系统通过使用日志来重建对数据库所做的更新过程,以恢复到一致状态,从而保证数据库的一致性和完整性。
XLOG---事务日志,所认知的日志记录,记录了事务对数据更新的过程和事务的最终状态。
CLOG---事务提交日志,XLOG的一种辅助形式,记录了事务的最终状态。更高效,但占用空间也非常有限。为支持嵌套事务,PostgreSQL还引入了SUBTRANS日志记录----即子事务日志,记录每个事务的父事务的事务ID,这样通过一个事务可以递归查找到父事务,但并不能通过一个事务查找到其子事务。同时,为了支持多版本并发控制,PostgreSQL引入了组合事务ID(MultiXactID),记录事务的组合关系,并维护从众事务ID到MultiXactID的映射关系。
日志通过日志文件存放,如果每个日志记录在创建时都被立刻写到磁盘,那么将增加大量I/O开销。通常向磁盘写入日志以块为单位进行,大多数情况下,一个日志记录比一个块要小得多,为了降低写入日志的I/O开销,数据库系统在实现时往往设置了日志缓冲区,即先将日志记录写到主存中的日志缓冲区中,当日志缓冲区满了以后以块为单位向磁盘写出。
日志缓冲区通过日志管理器管理。一般通过日志缓冲区来使用日志文件,而缓冲区和磁盘间的交互、同步则由日志管理器来完成。
PostgreSQL通过同一种缓冲区来实现对CLOG日志,SUBSTRANS日志及MULTIXACT日志的管理,即SLRU缓冲池---采用简单LRU算法作为页面置换算法的缓冲池。
获取一个事务的状态,通过事务日志接口例程进行操作。在事务日志接口例程中,一方面定义了可使用事务ID号的范围、可使用对象ID号的范围,另一方面提供了可设置和获取事务状态信息的接口例程。
为减少I/O次数,也使用了缓冲区机制,即建立了一个单独的缓冲区,用于缓冲最近获取的事务ID及其状态。而且为了方便事务管理,建立了一个共享变量缓冲数据结构,存储下一可分配事务ID、对象ID及已分配对象数。
资源管理器主要用于在日志系统中把各种需要记录日志的数据分类,通过在日志中标识资源管理号,使系统在恢复或读取日志记录时,能够很方便地知道该日志记录的源数据属于哪一类,从而通过资源管理器的方法可准确地选择对应的方法。

7.11.1SLRU缓冲池

PostgreSQL对CLOG和SUBSTRANS日志的物理存储组织方法。CLOG和SUBSTRANS日志在磁盘中由一个个小的物理文件组成。每一个物理日志文件定义为一个段,一个段由32个磁盘页面组成,每个磁盘页面的大小为8KB。每一个段文件以段号来命令,通过一个段号可以找到对应的日志文件。只要有日志的页面号,就可以找到该页面所在的段文件及该页在段文件中的偏移,从而将该日志页面调入到内存缓冲区中,供日志管理器使用。通过二元组<Segmentno,Pageno>就可以定位日志页在哪个段文件中以及在该文件中的偏移位置。
SLRU由8个缓冲区组成。每个缓冲区为有一个页面,对应一个磁盘块,大小为8KB。页面调度算法采用SLRU算法,即简单的最近最少使用算法。因为缓冲区只有8个页面,所以实现搜索时,只需要使用简单的线性搜索算法。
写日志:缓冲区 -> 磁盘的日志段文件
读日志:日志记录页面 -> 缓冲池 -> 读取具体的日志记录
1.缓冲池的并发控制
2.缓冲池的相关数据结构
3.缓冲池的主要操作
缓冲池的初始化、缓冲池页面的选择、页面的初始化、缓冲池页面的换入换出、缓冲池页面的删除等

7.11.2CLOG日志管理器

CLOG日志记录的是事务的最终状态。CLOG日志管理器管理着CLOG日志缓冲池,该日志缓冲池是基于SLRU缓冲池实现的。

7.11.3SUBSTRANS日志管理器

嵌套事务中存在一个事务树
从根开始,每个事务都可以建立更低层次的事务(子事务),子事务被嵌套在父节点的控制区域之内。为此PostgreSQL引入了SUBTRANS日志,记录每个事务的父事务ID。
SUBSTRANS日志管理器是一个类似提交日志管理器的管理器,存储的是每一个事务的父事务ID。它是嵌套事务实现的一个基础部分。一个主事务的父事务时非法事务ID,每一个子事务都有 一个直接的父事务。遍历事务树可以很容易地由一个子事务到父事务,但是反过来并不能实现。
健壮性要求与CLOG完全不同,只记录当前打开事务的子事务信息,由于系统崩溃或重启时不需要保存数据,因此不需要和XLOG进行交互,也没有相应的REDO函数。在数据库启动时,只要使当前活跃的子事务页面为全0即可。

7.11.4MULTIXACT日志管理器

MULTIXACT日志是PostgreSQL用来记录组合事务ID的一种日志。由于PG采用了多版本并发控制,因此同一个元组相关联的事务ID可能有多个,为了在加锁(行共享锁)时统一操作,PG将于该元组相关联的多个事务ID组合起来用一个MultiXactID代替来管理。同CLOG、SUBSTRANS日志一样,MULTIXACT日志也是利用SLRU缓冲池来实现。

7.11.5XLOG日志管理器

XLOG传统数据库理论中的事务日志,详细记录了服务进程对数据库的操作过程。XLOG日志文件在内存中按页进行存放,每个页面大小为8KB,每个页都有一个头部,头部信息之后才是XLOG日志记录。
每个XLOG文件都有一个ID,但事实上它被分为一个个大小为16MB的XLOG段文件来存放。XLOG文件号和段文件号可用来唯一地确定这个段文件,确定日志文件内一个日志记录的地址时,只需要用一个XLOG文件号和日志记录在该文件内的偏移量即可。
对于XLOG文件中每个段文件的第一个页面,其头部时一个长头部(Long Header)。一个XLOG文件头部是不是长头部,可以由其头部格式的标志判断出来。
1.XLOG日志管理器相关数据结构
1)日志页面头部信息
2)日志记录控制信息
3)日志记录数据信息
4)XLOG控制结构
5)XLOG日志的检查点策略
2.XLOG日志管理器主要操作
1)XLOG日志的启动
2)XLOG日志文件的创建
3)XLOG日志的插入
4)日志文件的归档
5)日志文件的刷新
6)日志文件的打开操作
7)日志文件的拷贝
8)日志文件的恢复
9)日志文件的读取操作
10)创建检查点
11)创建RestartPoint
3.XLOG日志恢复策略
系统崩溃重启时会调用StartupXlog入口函数,该函数首先会扫描全局信息控制文件(global/pg_control)读取系统的控制信息,然后扫描XLOG日志目录的结构检测其是否完整,进而读取到最新的日志检查点记录,接下来根据日志记录序列的偏序关系检测到系统是否处于非正常状态下,若系统处理非正常状态,则触发恢复机制进行恢复。恢复完成后重新建立检查点并初始化XlogCtl控制信息,然后启动事务提交日志及相关辅助日志模块。
集中典型日志的恢复操作:
1.Database类型的日志操作 没有备份块,可能的操作有Create/Drop
Create 强制刷新所有缓冲区,再将日志中记录的源DB目录拷贝到新DB目录即可。
Drop 直接删除该数据库对应的缓冲区即可。
2.Heap类型的Redo操作 根据日志序列号(LSN)以及日志记录找出是否有备份块,如果有则恢复备份块到Page中,并置页面为“脏”,然后根据日志标志选择对应的操作,典型的有INSERT/DELETE/UPDATE操作。对于这几种操作,首先判断该日志记录是否有备份块,如果有则说明已恢复,直接返回即可;如果没有则需要读取日志重建HeapTuple
3.B-Tree类型的Redo操作
Btree时一种较复杂的索引结构,涉及的操作有叶子节点的插入操作,及针对不同的位置节点的分割操作等,所以恢复时根据标志信息确定做哪种类型的Redo操作。
4.Xlog类型和Redo操作
记录下一个可分配的OID,设置检查点等操作。恢复的时候比较简单,将日志记录原信息拷贝出来即可。

7.11.6日志管理总结

7.12小结

事务管理器,核心功能是根据当前状态和接收到的外部状态(用户命令)决定当前需要执行的操作,所以它承担了整个系统的决策功能。
事务系统串联了整个数据库中各个不同的模块,事务系统的调度决策驱动了整个数据库系统的执行进程。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。