eygle.com   eygle.com
eygle.com eygle
eygle.com  
 

« openGauss 数据库事务概览 | Blog首页 | openGauss 分布式事务 »

openGauss 数据库并发控制
modb.pro

文章来源于墨天轮:https://www.modb.pro/db/174555

当数据库中存在并发执行事务的情况下,要保证ACID特性,需要一些特殊的机制来支持。并发控制就是指这样的一种控制机制,能够保证并发事务同时访问同一个对象或数据下的ACID特性。

openGauss并发控制是十分高效的,其核心是MVCC和快照机制。如前文中所述,通过使用MVCC和快照,可以有效解决读写冲突,使得并发的读事务和写事务工作在同一条元组的不同版本上,彼此不会相互阻塞。对于并发的两个写事务,openGauss通过事务级别的锁机制(事务执行过程中持锁,事务提交时释放),来保证写事务的一致性和隔离性。

另一方面,对于底层数据的访问和修改,如物理页面和元组,为了保证读写操作的原子性,需要在每次的读、写操作期间加上共享锁或排他锁。当每次读、写操作完成之后,即可释放上述锁资源,无需等待事务提交,持锁窗口相对较短。

 

1 读-读并发控制

在绝大多数情况下,并发的读-读事务,是不会、也没有必要相互阻塞的。由于没有修改数据库,因此每个读事务使用自己的快照,就能保证查询结果的一致性和隔离性;同时,对于底层的页面和元组,只涉及读操作,只需要对它们加共享锁即可,不会发生锁等待的情况。

一个比较特殊的情况是执行SELECT FOR UPDATE查询。该查询会对所查到的每条记录在元组层面加排他锁,以防止在查询完成之后,查询结果集被后续其它写事务修改。该语句获取到的元组排他锁,在事务提交时才会释放。对于并发的SELECT FOR UPDATE事务,如果它们的查询结果集有交集,那么在交集中的元组上会发生锁冲突和锁等待。

 

2 读-写并发控制

如下图的例子所示,openGauss中对于读、写事务的并发控制基于MVCC和快照机制,彼此之间不会存在事务级的长时间阻塞。相比之下,采用两阶段锁协议(Two-Phase Locking Protocol,简称2PL协议)的并发控制(如IBM DB2数据库),由于读、写均在记录的同一个版本上操作,因此排在锁等待队列后面的事务至少要阻塞到持锁者事务提交之后才能继续执行。

读已提交和可重复读隔离级别在并发事务下的表现区别.png

读已提交和可重复读隔离级别在并发事务下的表现区别

另一方面,为了保证底层物理页面和元组的读、写原子性,在实际操作页面和元组时,需要暂时加上相应对象的共享锁或排他锁,在完成对象的读、写操作之后,就可以放锁。

对于所有可能的三种读-写并发场景,即查询-插入并发、查询-删除并发和查询-更新并发,在下面图1、图2和图3中分别给出了它们的并发控制示意图。

图1 查询-插入并发控制示意图.png

图1 查询-插入并发控制示意图

图2 查询-删除并发控制示意图.png

图2 查询-删除并发控制示意图

图3 查询-更新并发控制示意图.png

图3 查询-更新并发控制示意图

 

3 写-写并发控制

虽然通过MVCC,可以让并发的读-写事务工作在同一条记录的不同版本上(读老版本,写新版本),从而互不阻塞,但是对于并发的写-写事务,它们都必须工作在最新版本的元组上,因此如果并发的写-写事务涉及同一条记录的写操作,那么必然导致事务级的阻塞。

写-写并发的场景有以下6种:插入-插入并发、插入-删除并发、插入-更新并发、删除-删除并发、删除-更新并发、更新-更新并发。下面就插入-插入并发、删除-删除并发和更新-更新并发的控制流程做简要描述,另外三种并发场景下的控制流程供读者自行思考。

图4为插入-插入事务的并发控制流程图。对于每个插入事务,它们都会在表的物理页面中插入一条新元组,因此并不会在同一条元组上发生并发写冲突。然而,当表具有唯一索引时,为了避免违反唯一性约束,若并发插入-插入事务在唯一键上有冲突(即键值重复),后来的插入事务必须等待先来的插入事务提交以后,再根据先来插入事务的提交结果,才能进一步判断是否能够继续执行插入操作。如果先来插入事务提交了,那么后来插入事务必须回滚,以防止唯一键重复;如果先来插入事务回滚了,那么后来插入事务可以继续插入该键值的记录。

图4 插入-插入并发控制示意图.png

图4 插入-插入并发控制示意

图5为删除-删除事务的并发控制流程图。对于并发的删除-删除事务,它们都会尝试去修改同一条元组的xmax值。我们通过页面排他锁来控制该冲突。对于后加上锁的删除事务,它在再次标记元组xmax值之前,首先需要判断先来删除事务(即元组当前xmax事务号对应的事务)的提交结果。如果先来删除事务提交了,那么该元组对后来删除事务不可见,后来删除事务无元组需要删除;如果先来删除事务回滚了,那么该元组对后来删除事务依然可见,后来删除事务可以继续执行对该元组的删除操作。

图5 删除-删除并发控制示意图.png

图5 删除-删除并发控制示意图

图6为更新-更新事务的并发控制流程图。对于并发的更新-更新事务,与并发删除-删除事务类似,它们首先都会尝试去修改同一条元组的xmax值。我们通过页面排他锁来控制该冲突。对于后加上锁的更新事务,它在再次标记元组xmax值之前,首先需要判断先来更新事务(即元组当前xmax事务号对应的事务)的提交结果。如果先来更新事务提交了,那么该元组对后来更新事务不可见,此时,后来更新事务会去判断该元组更新后的值(先来更新事务插入)是否还符合后来更新事务的谓词条件(即删除范围),如果符合,那么后来的更新事务会在这条新的元组上进行更新操作,如果不符合,那么后来的更新事务无元组需要更新;如果先来更新事务回滚了,那么该元组对后来更新事务依然可见,后来更新事务可以继续在该元组上进行更新操作。

图6 更新-更新并发控制示意图.png

图6 更新-更新并发控制示意图

 

4 并发控制和隔离级别

在上文中介绍写-写并发控制的机制时,其实默认了使用读已提交的隔离级别。回顾图4、图5和图6,我们可以发现,当在某条元组上发生并发写-写冲突时,原本先来事务是在后来事务的快照中的,后来事务是不应该看到先来事务的提交结果的,但是为了解决上述冲突,后来事务会等待先来事务提交之后,再去校验先来事务对元组的操作结果。这种方式是符合读已提交隔离级别要求的,但是显然后来事务在等待之后,又刷新了自己的快照内容(将先来事务从快照中移除)。

基于上述原因,在MVCC和快照隔离的并发控制策略下,若使用可重复读的隔离级别,当发生上述写-写冲突时,后来事务不会再等待先来事务的提交结果,而是将直接报错回滚。这也是openGauss在可重复读隔离级别下,对于写-写冲突的处理模式。

进一步,如果要支持可串行化的隔离级别,对于使用MVCC和快照隔离的并发控制策略,需要解决写偏序(Write Skew)的异常现象,有兴趣的读者可以参考2008年SIGMOD最佳论文《Serializable Isolation for Snapshot Databases》。

 

5 对象属性的并发控制

在上面并发控制的介绍中,我们覆盖了DML和查询事务的并发控制机制。对于DDL语句,其虽然不涉及表数据元组的修改,但是其会修改表的结构(Schema),因此很多场景下不能和DML、查询并发执行。

图7 DDL-DML并发控制示意图.png

图7 DDL-DML并发控制示意图

以增加字段的DDL事务和插入事务并发执行为例,它们的并发执行流程如图7所示。首先,DDL事务会获取表级的排他锁,而DML事务在执行之前,需要获取表级的共享锁。DDL事务持锁之后,会执行新增字段操作。然后,DDL事务会给其它所有并发事务发送表结构失效消息,告诉其它并发事务,这个表的结构被修改了。最后,DDL事务释放表级排他锁,提交返回。

DDL事务放锁之后,DML事务可以获取到该表的共享锁。加锁之后,DML事务首先需要处理所有在等锁过程中可能收到的表结构失效消息,并加载新的表结构信息。然后,DML才可以执行增删改操作,并提交返回。

 

6 表级锁、轻量锁和死锁检测

在前面,已经向读者初步介绍了在事务并发控制中,需要有锁机制的参与。事实上,在openGauss中,主要有两种类型的锁:表级锁和轻量锁。

表级锁主要用于提供各种类型语句对于表的上层访问控制。根据访问控制的排他性级别,表级锁分为1级到8级锁。对于两个表级锁(同一张表)的持有者,如果他们持有的表级锁的级别之和大于等于8级,那么这两个持有者的表级锁会相互阻塞。

在典型的数据库操作中,查询语句需要获取1级锁,DML语句需要获取3级锁,因此这两个操作在表级层面不会相互阻塞(这得益于MVCC和快照机制)。相比之下,DDL语句通常需要获取8级锁,因此对同一张表的DDL操作会和查询语句、DML语句相互阻塞。以修改表结构类型的DDL语句为代表,如果允许在该DDL执行过程中同时插入多条数据,那么前后插入的数据的字段个数可能不一致,甚至相同字段的类型亦可能出现不一致。

另一方面,在创建一个表的索引过程中,一般不允许有并发的DML操作,否则可能会导致索引不正确,或者需要引入复杂的并发索引修正机制。在openGauss中,创建索引语句需要对目标表获取5级锁,该锁级别和DML的3级锁会相互阻塞。

在openGauss中,为表级锁的所有等待者维护了等待队列信息。基于该等待队列,openGauss对于表级锁提供了死锁检测。死锁检测的基本原理是尝试在所有表级锁的等待队列中寻找是否存在能够构成环形等待队列的情况,如果存在环形等待队列,那么就表示可能发生了死锁,需要让其中某个等待者回滚事务退出队列,从而打破该环形等待队列。

在openGauss中,第二种广泛使用的锁是轻量锁。轻量锁只有共享和排他两种级别,并且没有等待队列和死锁检测。一般轻量锁并不对数据库用户提供,仅供数据库开发人员使用,需要开发人员自己来保证并发情况下不会发生死锁的场景。在本章中曾经介绍过的页面锁即是一种轻量锁,表级锁也是基于轻量锁来实现的。

墨天轮,围绕数据人的学习成长提供一站式的全面服务,打造集新闻资讯、在线问答、活动直播、在线课程、文档阅览、资源下载、知识分享及在线运维为一体的统一平台,持续促进数据领域的知识传播和技术创新。


历史上的今天...
    >> 2014-11-24文章:
    >> 2011-11-24文章:
    >> 2009-11-24文章:
    >> 2006-11-24文章:
    >> 2005-11-24文章:

By enmotech on 2021-11-24 16:17 | Comments (0) | | 3440 |


CopyRight © 2004~2020 云和恩墨,成就未来!, All rights reserved.
数据恢复·紧急救援·性能优化 云和恩墨 24x7 热线电话:400-600-8755 业务咨询:010-59007017-7040 or 7037 业务合作: marketing@enmotech.com