【连载】如何掌握openGauss数据库核心技术(秘诀四(拿捏事务机制(1)))

目录
openGauss数据库SQL引擎
openGauss数据库执行器技术
openGauss存储技术
openGauss事务机制
Ⅰ.openGauss数据库事务概览
1.显示事务和隐式事务
2.单机事务和分布式事务
Ⅱ.openGauss事务ACID特性介绍
Ⅲ.openGauss并发控制
Ⅳ.openGauss分布式事务
openGauss数据库安全
openGauss事务机制
事务是数据库为用户提供的最核心、最具吸引力的功能之一。简单地说,事务是用户定义的一系列数据库操作(如查询、插入、修改或删除等)的集合,数据库从内部保证了该操作集合(作为一个整体)的原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),统称事务的ACID特性。其中:
§ A:原子性是指一个事务中的所有操作要么全部执行成功,要么全部执行失败。一个事务执行以后,数据库只可能处于上述两种状态之一。即使数据库在这些操作执行过程中发生故障,数据库也不会出现只有部分操作执行成功的状态。
§ C:一致性是指一个事务的执行会导致数据从一个一致的状态转移到另一个一致的状态,事务的执行不会违反一致性约束、触发器等定义的规则。
§ I:隔离性是指在一个事务的执行过程中,所看到的数据库状态受并发事务的影响程度。根据该影响程度的轻重,一般将事务的隔离级别分为读未提交、读已提交、可重复读和可串行化四个级别(受并发事务影响由重到轻)。
§ D:持久性是指一旦一个事务的提交以后,那么即使数据库发生故障重启,该事务的执行结果不会丢失,仍然对后续事务可见。
本章主要结合openGauss数据库的事务机制和实现原理,来阐述在openGauss是如何保证事务的ACID特性的。
一.openGauss数据库事务概览
经过前序文章的介绍,大家已经知道openGauss是一个分布式的数据库。同样的,openGauss数据库的事务机制也是一个从单机到分布式的双层构架。
【连载】如何掌握openGauss数据库核心技术(秘诀四(拿捏事务机制(1)))
文章图片

图1 openGauss集群事务组件构成示意图
如图1所示,在openGauss集群中,事务的执行和管理主要涉及GTM、CN和DN三种组件,其中:
§ GTM,全称Global Transaction Manager,即全局事务管理器,负责全局事务号的分发,事务提交时间戳的分发以及全局事务运行状态的登记。对于采用多版本并发控制(Multi-Version Concurrency Control,简称MVCC)的事务模型(以openGauss和Oracle为例),GTM本质上可以简化为一个递增序列号(或时间戳)生成器,其为集群的所有事务进行了全局的统一排序,以确定快照(Snapshot)内容和由此决定的事务可见性。在本章第三节openGauss数据库并发控制中,将进一步详述GTM的作用。
§ CN,全称Coordinator Node,即协调者实例,负责管理和推进一个具体事务的执行流程,维护和推进事务执行的事务块状态机。
§ DN,全称Data Node,即数据实例,负责一个具体事务在某一个数据分片内的所有读写操作。
本节主要介绍:
§ 显式事务和隐式事务执行流程中,CN和DN上事务块状态机的推演。
§ 单机事务和分布式事务的异同。
显式事务和隐式事务01
§ 显式事务是指,用户在所执行的一条或多条SQL语句的前后,显式添加了开启事务START TRANSACTION语句和提交事务COMMIT语句。
§ 隐式事务是指,用户在所执行的一条或多条SQL语句的前后,没有显式添加开启事务和提交事务的语句。在这种情况下,每一条SQL语句在开始执行时,openGauss内部都会为其开启一个事务,并且在该语句执行完成之后,自动提交该事务。
以一条SELECT语句和一条INSERT语句为例,简要描述显式事务和隐式事务在openGauss集群中的主要执行流程。
显式事务的SQL语句如下(假设表t只包含一个整数类型字段a,且为分布列):
START TRANSACTION;
SELECT * FROM t;
INSERT INTO t(a) VALUES (100);
COMMIT;
1)START TRANSACTION
该SQL语句只在CN上执行,CN显式开启一个事务,并将CN本地事务块状态机从空闲状态置为进行中状态,然后返回客户端,等待下一条SQL命令。
2)SELECT * FROM t
该SQL语句首先在CN上执行,由于openGauss分片采用一致性哈希算法,因此对于不带分布列上谓词条件的查询语句,CN需要将该SQL语句发送到所有DN实例上执行。对于每一个分片对应的DN实例,由于采用了显式事务,CN会先发送一条START TRANSACTION命令给该DN,让该DN显式开启事务(DN上的事务块状态机从空闲状态变为进行中状态),然后CN将SELECT语句发送给该DN。此后,CN在收到所有DN的查询结果之后,返回客户端,等待下一条SQL命令。
3)INSERT INTO t(a) VALUES (100)
该SQL语句首先在CN上执行,由于a为表t的分布列,因此CN可以根据被插入记录中a的具体取值,来决定应该由哪个数据分片对应的DN实例来执行实际的插入操作(这里假设该分片为DN1)。由于采用了显式事务,CN先发送一条START TRANSACTION命令给DN1,由于经过第(2)步DN1的事务块状态机已经处于进行中状态,因此对于该语句DN1并不会执行什么实际的操作,然后,CN将具体的INSERT语句发送给DN1,并等待DN1执行插入成功之后,返回客户端,等待下一条SQL命令。
4)COMMIT
该SQL语句首先在CN上执行,CN进入提交事务阶段后,将COMMIT语句发送给所有参与第(2)步和第(3)步的DN,让这些DN结束该事务,并将DN本地的事务块状态机从进行中状态置为空闲状态。CN在收到所有DN的事务提交结果之后,再将CN本地的事务块状态机从进行中状态置为空闲状态。然后,CN返回客户端,该事务执行完成。
上述操作的隐式事务语句如下(假设表t只包含一个整数类型字段a,且为分布列):
SELECT * FROM t;
INSERT INTO t(a) VALUES (1);
1)SELECT * FROM t
该SQL语句首先在CN上执行,CN隐式开启一个事务,将CN本地的事务块状态机从空闲状态置为开启状态(注意不同于显式事务中的进行中状态)。然后,CN需要将该语句发送到所有DN实例上执行。对于每一个分片对应的DN实例,由于采用了隐式事务且该语句为只读查询,CN直接将SELECT语句发送给该DN。
DN收到该SELECT语句之后,亦采用隐式事务:第一步,隐式开启事务,将DN本地的事务块状态机从空闲状态置为开启状态;第二步,执行该查询语句,将查询结果返回给CN;第三步,隐式提交事务,将DN本地的事务块状态机从开启状态置为空闲状态。
CN在收到所有DN的查询结果之后,返回客户端,并隐式提交事务,将CN本地的事务块状态机从开启状态置为空闲状态。
2)INSERT INTO t(a) VALUES (1)
该SQL语句首先在CN上执行,CN隐式开启一个事务,将CN本地的事务块状态机从空闲状态置为开启状态。然后,CN需要将该INSERT语句发送到目的分片的DN实例上执行(这里假设该分片为DN1)。
虽然该语句采用了隐式事务,但是由于该语句为写操作,因此在DN1上会采取显式事务:CN会先发送一条START TRANSACTION命令给DN1,让DN1显式开启事务(DN1上的事务块状态机从空闲状态变为进行中状态),然后CN将INSERT语句发送给DN1,DN1执行完成后,返回执行结果给CN。
CN收到执行结果之后,进入提交事务阶段。先发送COMMIT语句到DN1。DN1收到COMMIT语句后,进行显式提交,将DN1本地的事务块状态机从进行中状态置为空闲状态。CN在收到DN1的事务提交结果之后,本地再进行隐式提交事务,将CN本地的事务块状态机从开启状态置为空闲状态,返回客户端,该事务执行完成。
综上,对于CN来说,使用显式事务还是隐式事务,完全取决于用户输入的SQL语句;对于DN来说,只有当SQL为隐式只读事务时,才会使用隐式事务,当SQL为显式事务或者隐式写事务时,都会使用显式事务。
单机事务和分布式事务02
在openGauss这样的分布式集群中,单机事务(亦称单分片事务)是指一个事务中所有的操作都发生在同一个分片(即DN实例)上,分布式事务是指一个事务中有两个或以上的分片参与了该事务的执行。
对于单机事务,其写操作的原子性和读操作的一致性由该DN自身的事务机制就能保证;对于分布式事务,不同分片之间写操作的原子性和不同分片之间读操作的一致性,需要额外的机制来保障。下面结合SQL语句简要介绍下分布式事务的原子性和一致性要求,具体的原理机制将在后续文章的第四节中说明。
首先,考虑涉及多分片的写操作事务,以如下事务T1为例(假设表t只包含一个整数类型字段a,且为分布列):
START TRANSACTION;
INSERT INTO t(a) VALUES (v1);
INSERT INTO t(a) VALUES (v2);
COMMIT;
上面事务T1的两条INSERT语句均为只涉及一个分片的写(插入)事务,如果v1和v2分布在同一个分片内,那么该事务为单机事务,如果v1和v2分布在两个不同的分片内,那么该事务为分布式事务。
对于只涉及一个DN分片的单机事务,其对于数据库的修改和影响全部发生在同一个分片内,因此该分片的事务提交结果即是该事务在整个集群的提交结果,该分片事务提交的原子性就能够保证整个事务的原子性。在事务T1示例中,如果v1和v2全分布在DN1上,那么在DN1上,如果事务提交,那么这两条记录就全部插入成功;如果DN1上事务回滚,那么这两条记录的插入就全部失败。
对于分布式事务,为了保证事务在整个集群范围内的原子性,必须保证所有参与写操作的分片要么全部提交,那么全部回滚,不能出现部分分片提交,部分分片回滚的“中间态”。如图2所示,如果v1插入到DN1上,且DN1提交成功,同时,v2插入到DN2上,且DN2最终回滚,那么最终该事务只有一部分操作成功,破坏了事务的原子性要求。为了避免这种情况的发生,openGauss采用两阶段提交(Two Phase Commit,简称2PC)协议,来保证分布式事务的原子性,在后续文章的第四节中会对两阶段提交相关内容进行更详细的介绍。
【连载】如何掌握openGauss数据库核心技术(秘诀四(拿捏事务机制(1)))
文章图片

图2 分布式事务原子性问题示意图
其次,考虑涉及多分片的读操作事务T2,以如下SQL语句为例(假设表t只包含一个整数类型字段a,且为分布列):
START TRANSACTION;
SELECT * FROM t where a = v1 or a = v2;
COMMIT;
上面查询事务T2中,如果v1和v2分布在同一个分片内,那么该事务为单机事务,如果v1和v2分布在两个不同的分片内,那么该事务为分布式事务。
【【连载】如何掌握openGauss数据库核心技术(秘诀四(拿捏事务机制(1)))】对于单机事务,其查询的数据完全来自于同一个分片内,因此该分片事务的可见性和一致性就能够保证整个事务的一致性。
在事务T1和T2示例中,考虑T1和T2并发执行的场景(假设T1提交成功),如果v1和v2全分布在DN1上,那么,在DN1上,如果T1对T2可见,那么T2就能查询到所有的两条记录,如果T1对T2不可见,那么T2不会查询到两条记录中的任何一条。
对于分布式事务,其查询的数据来自不同的分片,单个分片的可见性和一致性无法完全保证整个事务的一致性,不同分片之间事务提交的先后顺序和可见性判断会导致查询结果存在某种“不确定性”。
仍考虑T1和T2并发执行的场景(假设T1提交成功)。如图3所示,如果v1和v2分别分布在DN1和DN2上,若在DN1上,T1事务提交先于T2的查询执行,且对于T2可见,而在DN2上,T2的查询执行先于T1事务提交(或T1事务提交先于T2查询执行,但对T2不可见),那么T2最终只会查询到v1这一条记录。对于以银行为代表的传统数据库用户来说,这种现象破坏了事务作为一个整体的一致性要求。在分布式事务中,亦称为强一致性要求。
【连载】如何掌握openGauss数据库核心技术(秘诀四(拿捏事务机制(1)))
文章图片

图3 分布式事务一致性问题示意图
另一方面,如果T1先完成提交,并等待足够长的时间以后(保证所有分片均完成T1的提交,并保证提交结果对T2可见),再执行T2,那么T2将可以看到T1插入的所有两条记录。在分布式事务中,这种一致性表现被称为最终一致性。与传统数据库用户不同,在互联网等新兴业务中,最终一致性是被广泛接受的。
openGauss通过全局一致性的时间戳(快照)技术和本地两阶段事务补偿技术,提供分布式强一致事务的能力,同时,对于追求性能的新兴数据库业务,也支持可选的最终一致性事务的能力。
未完待续......

    推荐阅读