目录
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事务号对应的事务)的提交结果。如果先来删除事务提交了,那么该元组对后来删除事务不可见,后来删除事务无元组需要删除;如果先来删除事务回滚了,那么该元组对后来删除事务依然可见,后来删除事务可以继续执行对该元组的删除操作。
文章图片
【【连载】如何掌握openGauss数据库核心技术(秘诀四(拿捏事务机制(4)))】图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数据库安全”,敬请期待......
推荐阅读
- 数据库|SQL行转列方式优化查询性能实践
- mysql|一文深入理解mysql
- 达梦数据库|DM8表空间备份恢复
- 数据技术|一文了解Gauss数据库(开发历程、OLTP&OLAP特点、行式&列式存储,及与Oracle和AWS对比)
- SqlServer|sql server的UPDLOCK、HOLDLOCK试验
- 谈灾难恢复指标(RTO与RPO是什么鬼())
- RPO与RTO
- 数据库|效率最高的Excel数据导入---(c#调用SSIS Package将数据库数据导入到Excel文件中【附源代码下载】)...