ITPub博客

首页 > 数据库 > 数据库开发技术 > 并发访问控制(5)--MVCC算法6--MVCC实现实例之二

并发访问控制(5)--MVCC算法6--MVCC实现实例之二

原创 数据库开发技术 作者:那海蓝蓝 时间:2024-06-10 09:09:00 0 删除 编辑

注释:如下所提及的文献,请按照索引号对应参考《分布式数据库原理、架构与实践》一书的引用参考。

接续:《并发访问控制(5)--MVCC算法6--MVCC实现实例之一》

锁管理器

SSI技术主要是能够发现读写冲突(即前述的读写依赖)。所以需要对读操作进行检测和判断。

但是,在任何隔离级别下,PostgreSQL都没有实现过读锁,这样就不能获取新的SIREAD模式的读锁(参见4.4.3节),所以也不可能标记出读写依赖。另外,PostgreSQL的元组级写锁存储在元组头中,而不是内存表中,这会导致难以有简单的方法用来检测读写冲突(每次判读是否存在读写冲突都遍历所有元组是不现实的事情)。

PostgreSQL的SSI锁管理器仅存储SIREAD锁,不支持任何其他锁定模式,所以不会产生阻塞(读锁不阻塞任何操作)。而SIREAD锁的施加,是发生在读操作进行的时候:

  • 如果是表扫描,则需要在读取元组的时候,为每个元组加SIREAD锁。

  • 如果是索引扫描,则需要在索引页面上施加SIREAD锁。这样做的目的是节约内存空间。

上述两种方式,和MySQL的InnoDB有很大不同,因为InnoDB没有在页面级别上加锁,而是在索引的记录级别上加锁(索引记录上加间隙锁),这样锁的粒度小但是锁表会很大。

PostgreSQL的SSI锁管理器支主要作用有两个:一是在关系(relation,表或视图对象)、页面(page)或元组(tuple)上获取SIREAD锁;二是在写入元组时检查SIREAD锁以判断是否存在冲突的读写依赖,检查是否存在SIREAD锁的过程,这是一个锁的粒度从粗到细的过程,先检查relation,之后是page,最后才是tuple,这样使PostgreSQL居优乐锁升级的功能[1]。

另外,事务提交后,其产生的SIREAD锁需要得到保留,因此SIREAD锁表可能会很大(因此PostgreSQL采取了很多节约内存的措施)。

SIREAD锁不存在冲突,所以不需要进行死锁检测。

冲突检测与解决

PostgreSQL的SSI实现,依赖于两部分内容:一是使用了其已有的MVCC的多版本数据,二是实现了一个新的锁管理器来检测读写冲突。使用哪一个部分进行检测判断,取决于写和读操作按时间发生的顺序:

  • 如果先写:写操作发生,则可以从MVCC的多版本数据推断写冲突(写冲突导致有新版本产生),而不使用任何锁。

  • 如果先读:读操作发生,则每当事务读取元组时,需要进行可见性判断(参见文献[21]的9.2.1节),通过检查元组的xmin和xmax,能够确定元组是否在事务的快照中可见。

  • 如果元组不可见,则会存在读写冲突。这是因为创建该元组的事务在读取操作发生前已经生成了快照,在此快照下元组不可见表明此元组还没有提交,这就是读写冲突(生成快照的读在前,生成不可见版本的元组的写在后)。

  • 如果元组已被删除,则会存在读写冲突。元组已被删除即元组有一个“xmax”值,但是此元组仍然可以被读到,这就是读写冲突(生成快照的读在前,删除可见的版本的写在后)。

  • 如果先发生读,则还需要处理在写入之前发生读取的情况。这个时候,不能只使用MVCC数据,需要使用SIREAD锁跟踪读取的依赖关系。

在本节“理论基础”部分,我们得知,一个读写冲突是不足以为SI技术带来写偏序异常的。只有满足图4-20所示的情况,才可能会发生写偏序,才需要SSI技术来解决写偏序异常。而PostgreSQL为每个事务保留所有读写依赖(rw-antidependencies)列表,但不保留写写和写读依赖关系,这个事务列表是按事务提交的顺序进行排序的。所以PostgreSQL的SSI技术,就是使用事务列表检测出两个读写依赖(如连续相邻),然后回滚其中一个事务以消除发生写偏序的隐患。

但是,回滚操作存在着一些值得讨论的话题。第一个就是Safe Retry(安全重试),其含义为:如果事务被中止且立即重试同一事务,则不会导致它再次失败,也不会导致相同的序列化失败。Safe Retry原则表明,满足写偏序异常的两种事务操作(见图4-13和图4-14)。其中某个事务是可以被回滚且不用担心会给系统带来负面影响。Safe Retry原则要求某个事务被回滚后再执行,不会再次造成同样的写偏序异常。但是,这不表明发生写偏序异常的三个事务任何一个都可以被“随机”回滚(如图4-20),其中已经提交的事务是不可以回滚的,只能从没有提交的事务中选择一个进行回滚,通常的倾向是选择三个事务中的第二个事务回滚(如图4-20中所示的事务T2),但不尽然。总的而言,回滚的规则如下(见图4-20):

  • 第一种情况:不回滚T1或T2事务,直到T3事务提交。

  • 第二种情况:回滚T2事务(因为第一种情况,T3事务已经提交)。这么做是因为T3已经提交,重新执行T2事务不会和T3事务再并发执行,所以重新执行T2事务则不会再次发生同样的读写冲突。如果回滚T1事务,则T1事务再次执行时,如果T2事务依然在执行,则可能还会发生如图4-20所示的情况。

  • 第三种情况:如果T2、T3事务都已经提交,没得可选,只有T1事务可被回滚。这是符合Safe Retry原则的,因为T1事务再次执行后不可能与已经提交的T2、T3事务处于并发状态。

另外,对于第二种情况,特例情况如XA支持的2PC阶段,如果事务T2已经处于PREPARED状态,则T2事务是必须提交的,这时只能选择回滚T1事务,尽管这可能不符合Safe Retry原则。

内存相关问题

PostgreSQL V9.1为SSI实现了新的事务管理器,此管理器需要记录读数据项的情况。但是一个事务提交后,这样的信息还需要用于其他事务做读写依赖的判断,即事务读记录的这个信息不能随着事务的结束而丢弃,如写偏序中的第二种情况,T1事务是只读的,但其提交后还需要保留其“历史上曾经读过数据项X”的信息,用以为T2事务判断是否存在读写依赖提供依据。此种情况下,假如T2事务是一个长事务,则之前大量的类似T1事务所持有过的SIREAD锁的信息就被迫保存在事务管理器中,耗费了大量的内存资源。一旦内存被耗费干净,则整个数据库引擎将难以正常工作。

为解决这个问题,PostgreSQL提供了四种特性:

  • 安全快照和可延迟事务:可以减少长时间运行的只读事务的影响。

  • 粒度提升:多个细粒度锁可以组合成一个粗粒度锁,这样可以减少占用的空间。

  • 积极清理提交的事务:提交后不再需要的事务应立即删除。

  • 事务汇总:如有必要,可以将多个提交事务的状态合并为更紧凑的表示形式,但是这样会提高误回滚率。

其中前两个特性前边已经讲述过,所以下面我们重点介绍后面的两种。

若一个事务持有SIREAD锁的信息,当与其并发的事务都提交了,则持有SIREAD锁的信息可以被释放(读写倚赖只能发生在并发事务之间)。因此,当最老的活动事务提交时,可以清理SIREAD锁的信息。这就是“积极清理提交的事务”。所以事务提交时,存在SIREAD锁清理的操作,这样的操作由ClearOldPredicateLocks()函数完成。但是,事务提交的标志,不仅是事务提交的函数,而且还有诸如SxactGlobalXminCount这样的变量,当其值为零,则可以清理SIREAD锁。比如,GetSafeSnapshot()函数会等待并发事务完成,这样一个只读事务将是安全的事务所以可以清理SIREAD锁。

SSI技术下的锁表容量有限(节约内存),若已经提交的事务有很多却不能被释放时,锁表有可能会被用光。为了允许接入更多的新事务,PostgreSQL提供了一种事务汇总技术,即把已经提交但不能被释放的事务的信息汇总在一个dummy事务上,然后释放那些事务,这样就能省出锁表空间而接纳新的事务。这样的技术被称为事务汇总。

那么,什么样情况下事务可以被汇总呢?

第一种情况:对于一个活动的事务,当其要修改一个元组时,需要检查这个元组上是否曾经有已经提交了的事务发生过读操作。如果是,则可能按图4-25所示带来存在连续两个读写依赖的危险,为了检测是否存在这样的危险,已经提交的事务上需要保留SIREAD锁信息,但是这个已经提交了但保留SIREAD锁信息的事务对于当前活动的事务而言,不需要知道已经提交的这个事务是谁,只需要知道有一个持有SIREAD锁的事务曾经在本事务要修改的元组上。因此,可以把已经提交的事务上的SIREAD锁等信息汇总到一个dummy事务上,然后释放这个已经提交的事务。

第二种情况:对于一个活动的事务,当其要读一个元组时,需要检查这个元组上是否存在并发事务的写操作。如果是,则可能存在图4-26和图4-27所示的两种子情况,这两种子情况可能带来存在连续两个读写依赖的危险。因为这时要判断本读事务和其他写事务的关系,所以可以从元组头中找出写操作对应的事务信息,即进行元组可见性判断。另外,还需要检查写事务是否是序列化隔离级别的。更进一步,对于图4-27所示的情况,需要确保第三个事务是已经提交的事务,这时因为存在事务汇总的情况,作为已经提交了的事务,T3被汇总到一个dummy事务上并从事务链上被去除,所以不能再根据事务链进行事务相邻且存在读写依赖的判断了,而是根据一个从“汇总事务ID”到标识读写依赖关系出边(a conflict out)的最老事务的提交顺序号的一个映射表进行判断(代码中注意SlruCtlData结构体、OldSerXidAdd()、OldSerXidGetMinConflictCommitSeqNo()等函数的使用,本书不再多述)。

事务的汇总,通过SummarizeOldestCommittedSxact()函数调用ReleaseOneSerializableXact()函数完成。


本节以文献
[68]为依据,分析了PostgreSQL序列化隔离级别的实现技术。而4.4.3节以文献[114]和文献[226]为依据,讲述了SSI实现的基本原理和算法。请注意,在理解PostgreSQL的SSI技术时,既要注意两者相同之处,又要注意不要把这文献[114]和文献[68]混在一起理解。

两者基于的原理是一样的,但实现方式不同。

[1] PostgreSQL提供锁升级的另外一个原因是在DDL操作发生的情况下,如CLUSTER和ALTER TABLE执行时,可能会造成物理元组的位置发生变化,此时这些元组上的SIREAD锁将不再合法,于是PostgreSQL把锁升级到relation级别可保证逻辑的正确性。索引上也存在类似的问题。但是,MySQL却不会发生这样的事情,因为MySQL采用的是S2PL机制,读锁会抑制DDL操作发生。


扫码加关注,知数行云,专业有深度


声明:本文禁止用于开发“任何软件程序,包括但不限于训练机器学习或人工智能(AI)系统”。


原文作者:那海蓝蓝知数行云;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2023-09-02

  • 博文量
    52
  • 访问量
    31003