目录
openGauss 数据库 SQL 引擎
openGauss 数据库执行器技术
openGauss 存储技术
openGauss 事务机制
Ⅰ.openGauss 数据库事务概览
Ⅱ.openGauss 事务 ACID 个性介绍
Ⅲ.openGauss 并发管制
1. 读 - 读并发管制
2. 读 - 写并发管制
3. 写 - 写并发管制
4. 并发管制和隔离级别
5. 对象属性的并发管制
6. 表级锁、轻量锁和死锁检测
Ⅳ.openGauss 分布式事务
1. 分布式事务原子性和两阶段提交协定
2. 分布式事务一致性和全局事务管理
openGauss 数据库安全
openGauss 并发管制
在第二节的介绍中,咱们曾经理解了,当数据库中存在并发执行事务的状况下,要保障 ACID 个性,须要一些非凡的机制来反对。并发管制就是指这样的一种管制机制,可能保障并发事务同时拜访同一个对象或数据下的 ACID 个性。
openGauss 并发管制是非常高效的,其外围是 MVCC 和快照机制。如第二节的第 4 大节中所述,通过应用 MVCC 和快照,能够无效解决读写抵触,使得并发的读事务和写事务工作在同一条元组的不同版本上,彼此不会互相阻塞。对于并发的两个写事务,openGauss 通过事务级别的锁机制(事务执行过程中持锁,事务提交时开释),来保障写事务的一致性和隔离性。
另一方面,对于底层数据的拜访和批改,如物理页面和元组,为了保障读写操作的原子性,须要在每次的读、写操作期间加上共享锁或排他锁。当每次读、写操作实现之后,即可开释上述锁资源,无需期待事务提交,持锁窗口绝对较短。
读 - 读并发管制 01
在绝大多数状况下,并发的读 - 读事务,是不会、也没有必要互相阻塞的。因为没有批改数据库,因而每个读事务应用本人的快照,就能保障查问后果的一致性和隔离性;同时,对于底层的页面和元组,只波及读操作,只须要对它们加共享锁即可,不会产生锁期待的状况。
一个比拟非凡的状况是执行 SELECT FOR UPDATE 查问。该查问会对所查到的每条记录在元组层面加排他锁,以避免在查问实现之后,查问后果集被后续其它写事务批改。该语句获取到的元组排他锁,在事务提交时才会开释。对于并发的 SELECT FOR UPDATE 事务,如果它们的查问后果集有交加,那么在交集中的元组上会产生锁抵触和锁期待。
读 - 写并发管制 02
如第二节的第 4 大节中图 10 的例子所示,openGauss 中对于读、写事务的并发管制基于 MVCC 和快照机制,彼此之间不会存在事务级的长时间阻塞。相比之下,采纳两阶段锁协定(Two-Phase Locking Protocol,简称 2PL 协定)的并发管制(如 IBM DB2 数据库),因为读、写均在记录的同一个版本上操作,因而排在锁期待队列前面的事务至多要阻塞到持锁者事务提交之后能力继续执行。
另一方面,为了保障底层物理页面和元组的读、写原子性,在实际操作页面和元组时,须要临时加上相应对象的共享锁或排他锁,在实现对象的读、写操作之后,就能够放锁。
对于所有可能的三种读 - 写并发场景,即查问 - 插入并发、查问 - 删除并发和查问 - 更新并发,在图 11、图 12 和图 13 中别离给出了它们的并发管制示意图。
图 10-11 查问 - 插入并发管制示意图
图 12 查问 - 删除并发管制示意图
图 13 查问 - 更新并发管制示意图
写 - 写并发管制 03
尽管通过 MVCC,能够让并发的读 - 写事务工作在同一条记录的不同版本上(读老版本,写新版本),从而互不阻塞,然而对于并发的写 - 写事务,它们都必须工作在最新版本的元组上,因而如果并发的写 - 写事务波及同一条记录的写操作,那么必然导致事务级的阻塞。
写 - 写并发的场景有以下 6 种:插入 - 插入并发、插入 - 删除并发、插入 - 更新并发、删除 - 删除并发、删除 - 更新并发、更新 - 更新并发。上面就插入 - 插入并发、删除 - 删除并发和更新 - 更新并发的管制流程做简要形容,另外三种并发场景下的管制流程供读者自行思考。
图 14 为插入 - 插入事务的并发管制流程图。对于每个插入事务,它们都会在表的物理页面中插入一条新元组,因而并不会在同一条元组上产生并发写抵触。然而,当表具备惟一索引时,为了防止违反唯一性束缚,若并发插入 - 插入事务在惟一键上有抵触(即键值反复),起初的插入事务必须期待先来的插入事务提交当前,再依据先来插入事务的提交后果,能力进一步判断是否可能继续执行插入操作。如果先来插入事务提交了,那么起初插入事务必须回滚,以避免惟一键反复;如果先来插入事务回滚了,那么起初插入事务能够持续插入该键值的记录。
图 14 插入 - 插入并发管制示意图
图 15 为删除 - 删除事务的并发管制流程图。对于并发的删除 - 删除事务,它们都会尝试去批改同一条元组的 xmax 值。咱们通过页面排他锁来管制该抵触。对于后加上锁的删除事务,它在再次标记元组 xmax 值之前,首先须要判断先来删除事务(即元组以后 xmax 事务号对应的事务)的提交后果。如果先来删除事务提交了,那么该元组对起初删除事务不可见,起初删除事务无元组须要删除;如果先来删除事务回滚了,那么该元组对起初删除事务仍然可见,起初删除事务能够继续执行对该元组的删除操作。
图 15 删除 - 删除并发管制示意图
图 16 为更新 - 更新事务的并发管制流程图。对于并发的更新 - 更新事务,与并发删除 - 删除事务相似,它们首先都会尝试去批改同一条元组的 xmax 值。咱们通过页面排他锁来管制该抵触。对于后加上锁的更新事务,它在再次标记元组 xmax 值之前,首先须要判断先来更新事务(即元组以后 xmax 事务号对应的事务)的提交后果。如果先来更新事务提交了,那么该元组对起初更新事务不可见,此时,起初更新事务会去判断该元组更新后的值(先来更新事务插入)是否还合乎起初更新事务的谓词条件(即删除范畴),如果合乎,那么起初的更新事务会在这条新的元组上进行更新操作,如果不合乎,那么起初的更新事务无元组须要更新;如果先来更新事务回滚了,那么该元组对起初更新事务仍然可见,起初更新事务能够持续在该元组上进行更新操作。
图 16 更新 - 更新并发管制示意图
并发管制和隔离级别 04
在第三节的第 3 大节介绍写 - 写并发管制的机制时,其实默认了应用读已提交的隔离级别。回顾图 14、图 15 和图 16,咱们能够发现,当在某条元组上产生并发写 - 写抵触时,本来先来事务是在起初事务的快照中的,起初事务是不应该看到先来事务的提交后果的,然而为了解决上述抵触,起初事务会期待先来事务提交之后,再去校验先来事务对元组的操作后果。这种形式是合乎读已提交隔离级别要求的,然而显然起初事务在期待之后,又刷新了本人的快照内容(将先来事务从快照中移除)。
基于上述起因,在 MVCC 和快照隔离的并发控制策略下,若应用可反复读的隔离级别,当产生上述写 - 写抵触时,起初事务不会再期待先来事务的提交后果,而是将间接报错回滚。这也是 openGauss 在可反复读隔离级别下,对于写 - 写抵触的解决模式。
进一步,如果要反对可串行化的隔离级别,对于应用 MVCC 和快照隔离的并发控制策略,须要解决写偏序(Write Skew)的异常现象,有趣味的读者能够参考 2008 年 SIGMOD 最佳论文《Serializable Isolation for Snapshot Databases》。
对象属性的并发管制 05
在下面并发管制的介绍中,咱们笼罩了 DML 和查问事务的并发管制机制。对于 DDL 语句,其尽管不波及表数据元组的批改,然而其会批改表的构造(Schema),因而很多场景下不能和 DML、查问并发执行。
图 17 DDL-DML 并发管制示意图
以减少字段的 DDL 事务和插入事务并发执行为例,它们的并发执行流程如图 17 所示。首先,DDL 事务会获取表级的排他锁,而 DML 事务在执行之前,须要获取表级的共享锁。DDL 事务持锁之后,会执行新增字段操作。而后,DDL 事务会给其它所有并发事务发送表构造生效音讯,通知其它并发事务,这个表的构造被批改了。最初,DDL 事务开释表级排他锁,提交返回。
DDL 事务放锁之后,DML 事务能够获取到该表的共享锁。加锁之后,DML 事务首先须要解决所有在等锁过程中可能收到的表构造生效音讯,并加载新的表构造信息。而后,DML 才能够执行增删改操作,并提交返回。
表级锁、轻量锁和死锁检测 06
在前几节,曾经向读者初步介绍了在事务并发管制中,须要有锁机制的参加。事实上,在 openGauss 中,次要有两种类型的锁:表级锁和轻量锁。
表级锁次要用于提供各种类型语句对于表的下层访问控制。依据访问控制的排他性级别,表级锁分为 1 级到 8 级锁。对于两个表级锁(同一张表)的持有者,如果他们持有的表级锁的级别之和大于等于 8 级,那么这两个持有者的表级锁会互相阻塞。
在典型的数据库操作中,查问语句须要获取 1 级锁,DML 语句须要获取 3 级锁,因而这两个操作在表级层面不会互相阻塞(这得益于第三节的第 2 大节中介绍 MVCC 和快照机制)。相比之下,DDL 语句通常须要获取 8 级锁,因而对同一张表的 DDL 操作会和查问语句、DML 语句互相阻塞。正如第三节的第 5 大节中图 17 的例子所示,以批改表构造类型的 DDL 语句为代表,如果容许在该 DDL 执行过程中同时插入多条数据,那么前后插入的数据的字段个数可能不统一,甚至雷同字段的类型亦可能呈现不统一。
另一方面,在创立一个表的索引过程中,个别不容许有并发的 DML 操作,否则可能会导致索引不正确,或者须要引入简单的并发索引修改机制。在 openGauss 中,创立索引语句须要对指标表获取 5 级锁,该锁级别和 DML 的 3 级锁会互相阻塞。
在 openGauss 中,为表级锁的所有期待者保护了期待队列信息。基于该期待队列,openGauss 对于表级锁提供了死锁检测。死锁检测的基本原理是尝试在所有表级锁的期待队列中寻找是否存在可能形成环形期待队列的状况,如果存在环形期待队列,那么就示意可能产生了死锁,须要让其中某个期待者回滚事务退出队列,从而突破该环形期待队列。
在 openGauss 中,第二种宽泛应用的锁是轻量锁。轻量锁只有共享和排他两种级别,并且没有期待队列和死锁检测。个别轻量锁并不对数据库用户提供,仅供数据库开发人员应用,须要开发人员本人来保障并发状况下不会产生死锁的场景。在本章中已经介绍过的页面锁即是一种轻量锁,表级锁也是基于轻量锁来实现的。
openGauss 分布式事务
在第一节的第 2 大节中,咱们简要介绍了单机事务和分布式事务的区别,也指出了在分布式状况下,可能存在特有的原子性和一致性问题。本节次要介绍在 openGauss 中,如何保障分布式事务的原子性和强一致性。
分布式事务原子性和两阶段提交协定 01
为了保障分布式事务的原子性,防止出现图 2 中所示的局部 DN 提交、局部 DN 回滚的“两头态”事务,openGauss 采纳两阶段提交(2PC)协定。
图 18 两阶段提交流程示意图
如图 18 所示,顾名思义,两阶段提交协定将事务的提交操作分为两个阶段:
§ 阶段一,筹备阶段(prepare phase),在这个阶段,将所有提交操作所须要应用到的信息和资源全副写入磁盘,实现长久化;
§ 阶段二,提交阶段(commit prepared phase),依据之前筹备好的提交信息和资源,执行提交或回滚操作。
两阶段提交协定之所以可能保障分布式事务原子性的关键在于:一旦筹备阶段执行胜利,那么提交须要的所有信息都实现长久化下盘,即便后续提交阶段某个 DN 产生执行谬误,该 DN 能够再次从长久化的提交信息中尝试提交,直至提交胜利。最终该分布式事务在所有 DN 上的状态肯定是雷同的,要么所有 DN 都提交,要么所有 DN 都回滚。因而,对外来说,该事务的状态变动是原子的。
表 3 总结了在 openGauss 分布式事务中的不同阶段,如果产生故障或执行失败,分布式事务的最终提交 / 回滚状态,读者可自行推演,本文不再赘述。
表 3 产生故障或执行失败时事务的最终状态
分布式事务一致性和全局事务管理 02
为了避免图 3 中的刹时不统一景象,反对分布式事务的强一致性,咱们须要全局范畴内的事务号和快照,以保障全局 MVCC 和快照的一致性。在 openGauss 中,GTM 负责提供和散发全局的事务号和快照。对于任何一个读事务,其都须要到 GTM 上获取全局快照;对于任何一个写事务,其都须要到 GTM 上获取全局事务号。
在图 3 中退出 GTM,并思考两阶段提交流程之后,分布式读 - 写并发事务的流程如图 19 所示。对于读事务来说,因为写事务在其从 GTM 获取的快照中,因而即便写事务在不同 DN 上的提交程序和读事务的执行程序不同,也不会造成不统一的可见性判断和不统一的读取后果。
图 19 读 - 写并发下全局事务号和快照的散发流程示意图
仔细的读者会发现,在图 19 的两阶段提交流程中,写事务 T1 在各个 DN 上实现筹备阶段之后,首先第一步是到 GTM 上完结 T1 事务(将 T1 从全局快照中移除),而后第二步再到各个 DN 上进行提交阶段。在这种状况下,如果查问事务 T2 是在第一步和第二步之间在 GTM 上获取快照,并到各个 DN 上执行查问的话,那么 T2 事务读到的 T1 事务插入的记录 v1 和 v2,它们 xmin 对应的 XID1 曾经不在 T2 事务获取到的全局快照中,因而 v1 和 v2 的可见性判断会齐全基于 T1 事务的提交状态。然而,此时 XID1 对应的 T1 事务在各个 DN 上可能还没有全副或局部实现提交阶段,那么就会呈现各个 DN 上可见性不统一的状况。
为了避免下面这种问题呈现,在 openGauss 中采纳本地二阶段事务弥补机制。如图 20 所示,对于在 DN 上读取到的记录,如果其 xmin 或者 xmax 曾经不在快照中,然而它们对应的写事务还在筹备阶段,那么查问事务将会等到这些写事务在 DN 本地实现提交阶段之后,再进行可见性判断。思考到通过两阶段提交协定,能够保障各个 DN 上事务最终的提交或回滚状态肯定是统一的,因而在这种状况下各个 DN 上记录的可见性判断也肯定是统一的。
图 20 读 - 写并发下本地两阶段事务弥补流程示意图
至此,本章节全副完结,下一章节将开始讲述“openGauss 数据库安全”,敬请期待 ……