GaussDB事务和并发控制机制
2)T2事务在t2,同时发起向DN1和DN2的读操作(对应read6和read8),DN1和DN2的读事务操作都需要阻塞到写事务结束后,再进行可见性判断,如果写事务最终被回滚时返回的是data_a_v1和data_b_v1,最终被提交时返回的是data_a_v2和data_b_v2,那么,此时T2读取的数据是一致的。在每个事务开始时,复制一份该数组的内容,当事务执行过程中扫描到某一个元组时,需要通
目录
事务是数据库的核心功能之一,其主要目的是保障数据库系统在并发处理、系统故障等场景下的数据一致性和完整性。数据库系统中,不同事务之间会存在多种并发执行操作,例如读读并发、读写并发、写写并发,都涉及到事务、语句的执行顺序以及数据对象的共享和保护问题,如果处理不正确就可能会引发数据的一致性问题。
数据一致性的常见问题包括:写读冲突引发的脏读(Dirty Read)、读写冲突引发的不可重复读(Non-repeatable Read)和幻读(Phantom Read)、写写冲突引发的丢失更新(Lost Update)。
为了有效应对这些挑战,数据库系统采用了三种关键技术来保障事务的正确执行和数据的一致性:并发控制机制、多版本并发控制(MVCC)机制以及快照机制。下面我们将对这三种机制进行详细介绍,并深入探讨华为云GaussDB在这三方面的实现路径。
一、并发控制机制
数据库的并发控制机制有多种实现方式,例如:悲观并发控制方式、乐观并发控制方式、多版本并发控制方式。
-
悲观并发控制:事务访问数据前先加锁,事务结束后释放锁,属于事前控制,避免冲突发生。悲观并发控制比较适合事务冲突较多的场景。
-
乐观并发控制:将数据拷贝到私有工作区,全部处理结束后,再集中检查是否满足调度隔离级别,如果不满足则回滚,属于事后控制,冲突发生后再处理。乐观并发控制比较适合事务冲突较少、执行时间较短的场景。
-
多版本并发控制(MVCC):一个数据项保存了多个物理版本,供不同的事务使用,事务的读操作无需等待其他事务的写操作,事务的写操作无需等待其他事务的读操作,通过空间复用的多版本信息缓解读写冲突。
二、MVCC实现方式
MVCC有两种主要实现方式:
方式一:数据记录的多个版本均存储在数据文件里,事务和可见性相关信息也包含在每个版本的数据记录中。这种MVCC实现更新和删除操作执行代价较小,后期对已被删除无效记录回收代价较高。
方式二:数据文件中只保留最新版本,事务信息都被集中管理,历史版本被存储于UNDO中,记录自身可以不包含事务和可见性信息,通过统一的事务管理区域来查询记录对应的事务状态。这种MVCC实现方式更新和删除操作执行代价稍高,后期对已被删除无效记录回收代价极低。
三、快照实现方式
快照主要有两种实现技术方式:
方式1:基于运行事务号的活跃事务列表实现方式
图1 活跃事务列表示意图
如图1所示,数据库进程中维护一个全局数组,其中的成员为正在执行的事务信息,包括事务的事务号,该数组即为活跃事务列表。在每个事务开始时,复制一份该数组的内容,当事务执行过程中扫描到某一个元组时,需要通过判断该元组中记录的事务号所对应的事务对于查询事务的可见性,来决定该元组是否对查询可见。
使用事务活跃列表方式的快照,性能和事务规模产生耦合,只适用小并发场景:
1)当并发连接增大时,活跃事务列表快照也会相应变大,导致系统性能严重劣化。
假设当前有一万个活跃事务,为构造一个快照,系统需要拷贝这一万个事务,如果每个事务ID是8个字节,那么需要拷贝80KB的数据。另外,在分布式系统下,计算节点通过网络访问活跃事务列表快照,产生巨大网络开销,进而影响性能。
2)当事务活跃列表比较大时,无法使用原子操作,系统在事务启动时获取事务快照和事务结束时清理事务状态快照时,都需要对活跃事务列表进行加锁操作。也就是说,无论对其读取(读-写操作)、还是修改(写-写并发操作),都需要加锁互斥,会产生大量的锁等待,高并发下活跃事务列表会成为加锁的热点和性能瓶颈。
方式2:基于提交时间戳的实现方式
每个数据记录都对应了一个事务提交时刻的时间戳。当一个新的查询开始的时候,系统会生成一个快照时间戳,取当前系统的最大时间戳加1作为该快照时间戳,并通过比较快照时间戳和数据记录中对应的提交时间戳,来做可见性判断。
四、GaussDB的并发控制机制
GaussDB采用多版本并发控制和两阶段锁(2PL)相结合的机制,这种方式可以显著提升事务并发处理效率,即:多版本并发控制提升读的并发性,两阶段锁解决写-写冲突。其中,2PL将加锁、解锁分为两个完全不相交的阶段。加锁阶段时,只加锁,不释放锁;解锁阶段时,只释放锁,不加锁。
另外,这种设计的优点在于,事务的读操作无需等待其他事务的写操作,同样,事务的写操作也无需等待其他事务的读操作。
GaussDB并发控制机制遵循以下基本原则:
1)当事务对数据项进行写操作时,系统会生成该数据项的一个新版本。当事务对数据项进行读操作时,读取的是事务开始时该数据项的最新版本。
2)读操作不加锁,避免了读操作间的阻塞,写操作采用严格两阶段锁机制,满足读已提交、可重复读的隔离级别,也可以避免幻读。这种机制称为快照隔离(snapshot isolation),就是为每个事务的读准备一个快照(一个时间戳的版本),这个快照一旦建立就不会再被修改,从而达到了事务间隔离的作用。
五、GaussDB基于事务提交时间戳的MVCC和快照机制
GaussDB的UStore存储引擎MVCC使用的是“方式2”,数据文件中只保留最新版本,历史版本被存储于UNDO中,比较适合更新频繁的业务场景。
GaussDB中的快照隔离机制是基于提交时间戳来实现的。GaussDB使用一个全局自增的长整数作为逻辑时间戳,用来模拟数据库内部的时序,该逻辑时间戳被称为提交序列号(commit sequence number,简称CSN)。每当一个事务提交的时候,在提交序列号日志中(commit sequence number log,CSN日志)会记录该事务号xid(事务的全局唯一标识)对应的逻辑时间戳CSN值。
关于MVCC可见性判断
图2 GaussDB快照可见性判断示意图
图2中,棕色竖线表示取snapshot时刻,如果使用活跃事务列表快照方式,那么棕色竖线对应snapshot的集合应该是{2,4,6}。如果采用GaussDB的基于提交时间戳CSN的快照方案,会获取当前的CSN值,也就是3,事务TX2、TX4、TX6、TX7、TX8的CSN分别为4、6、5、7、8,对于该snapshot而言,这几个事务的修改都不可见。
GaussDB MVCC快照可见性的具体判断流程如下:
图3 GaussDB快照根据CSN可见性判断流程图
1)如果当前事务ID 大于数据tuple的 xmax,那么说明此行数据的更新/删除发生于本事务开始之前,此行数据对本事务一定不可见。
2)如果当前事务ID小于数据tuple的xmin,那么说明此行数据的更新/删除发生于本事务开始之后,就需要检索xmin对应的事务状态(Clog,即Commit Log)来读取此事务状态,以此来判断此行数据是否对当前事务可见。如果tuple的xmin对应的事务是已提交的,则tuple对当前事务是可见的;如果tuple的xmin对应的事务已被回滚了,则tuple对当前事务是不可见的。
3)如果xid落在了xmin、xmax中间,就需要依据CSN 来判断本事务的快照下对应数据的可见性(通过检索CSN Log来进行对比判断)。
如果读取的xid对应的CSN已提交,并且CSN < snapshot.CSN,那么数据对当前事务是可见的。
如果CSN > snapshot.CSN,或者事务尚未提交,那么数据对当前事务是不可见的。
相对于事务活跃列表方式的快照,GaussDB基于提交时间戳CSN方式的快照具有如下优点:
-
通过CSN提交序列号进行可见性判断,无需遍历活跃事务列表。
-
无锁化原子操作提供CSN序列号,锁等待少。
-
节点间事务交互仅需要一个CSN,网络开销跟事务规模无关。
六、GaussDB分布式事务
在GaussDB分布式集群中,单机事务是指一个事务中所有操作都发生在同一个分片(即DN)上,而分布式事务是指一个事务中有两个或以上的分片参与了该事务的执行。
对于单机事务,写操作的原子性和读操作的一致性由该DN自身的事务机制进行保证,对于分布式事务,不同分片之间写操作的原子性和不同分片之间读操作的一致性需要额外的机制来保障。
6.1 GaussDB分布式事务的写一致性
GaussDB分布式数据库的写一致性是通过两阶段提交(2PC)协议实现的,两阶段提交协议将分布式事务的提交操作分为两个阶段,如图4所示:
图4 分布式事务两阶段协议提交示意图
-
阶段一,准备阶段(prepare phase),在这个阶段,将所有提交操作所需要使用到的信息和资源全部写入磁盘,完成持久化。
-
阶段二,提交阶段(commit prepared phase),根据之前准备好的提交信息和资源,执行提交或回滚操作。
2PC 协议有两类节点:协调者和参与者。一个事务会涉及一个协调者和多个参与者。当协调者或者参与者出现故障或者节点间出现网络问题时,2PC 事务就面临着失败或者残留的问题。在 GaussDB中,发起事务的 CN 节点就是协调者,其他参与此事务的 CN 或者 DN 节点就是参与者。
表1 GaussDB发生故障或执行失败时事务的最终状态
如表1所示,两阶段提交协议之所以能够保证分布式事务原子性的关键在于:一旦准备阶段执行成功,那么提交需要的所有信息都完成持久化下盘(写入磁盘),即使后续提交阶段某个DN发生执行错误,该DN可以再次从持久化的提交信息中尝试提交,直到提交成功。最终该分布式事务在所有DN上的状态一定是相同的,要么所有DN都提交,要么所有DN都回滚。对外来说,该事务的状态变化是原子性的。
图5 GaussDB分布式事务的写一致性流程图
如图5所示,CN节点作为协调者,DN节点作为参与者,GTM提供全局唯一的CSN值。
参与者节点(DN)在完成prepare阶段后,会将事务的状态设置为commit-in-progress。当所有DN prepare完毕后,协调者节点(CN)通知GTM进行事务提交,并获取到一个全局唯一的事务提交序列号CSN值。
在提交阶段(commit prepared phase),CN向所有DN发送包含事务唯一标识符和其他必要信息的提交(Commit)请求。DN收到提交请求后,执行实际的事务提交,修改事务状态为commit_determined已提交状态。
当CN收集到所有DN的提交确认后,它将向客户端返回分布式事务的最终提交结果。
6.2 GaussDB分布式事务的读一致性
为了防止瞬时不一致性,确保分布式事务的强一致性,一般需要全局范围内的事务快照,来保证全局MVCC和快照的一致性。
在GaussDB中,GTM负责提供和分发全局的快照,也就是CSN。任何一个读事务都需要到GTM上获取全局快照,任何一个写事务都需要到GTM上获取全局事务提交号,GaussDB通过全局一致性的时间戳快照来保证分布式事务的读一致性。
图6 GaussDB分布式事务的读一致性示意图
如图6所示,T1事务在DN1和DN2上修改了数据,T2在各个时机读取T1修改的数据,分析各种情况下的数据一致性:
1)T2事务在t1,同时发起向DN1和DN2的读操作(对应read3和read4),DN1和DN2分别返回data_a_v1和data_b_v1,两个都是V1版本,所以,此时T1读取的数据是一致的。
2)T2事务在t2,同时发起向DN1和DN2的读操作(对应read6和read8),DN1和DN2的读事务操作都需要阻塞到写事务结束后,再进行可见性判断,如果写事务最终被回滚时返回的是data_a_v1和data_b_v1,最终被提交时返回的是data_a_v2和data_b_v2,那么,此时T2读取的数据是一致的。
3)T2事务在t3,同时发起向DN1和DN2的读操作(对应read11和read12),DN1返回data_a_v2,DN2阻塞到commit13消息后返回data_b_v2,两个都是V2版本,所以,此时T3读取的数据是一致的。
4)T2事务在t4,同时发起向DN1和DN2的读操作(对应read14和read15),DN1返回data_a_v2,DN2阻塞到commit13消息后返回data_b_v2,两个都是V2版本,所以,此时T4读取的数据是一致的。
可见,在上述几个时机下,数据都是一致的。
对于各DN上的记录,如果其对应的写事务是处于活动状态时,可以根据该记录其相对于commit_in_progress和commit_determined的不同流程段进行可见性判定,具体判断规则如下:
-
若记录对应的写事务在DN上是处于活动状态时,且处于prepare阶段之前,本数据不可见;
-
若记录对应的写事务在DN上是处于活动状态时,且处于prepare和commit阶段之间,则需要阻塞等待到对应的写事务结束后,再进行可见性判定;
七、总结与展望
本文着重介绍了GaussDB的事务管理和并发控制机制,GaussDB采用多版本并发控制和两阶段锁相结合的机制,通过基于事务提交逻辑时间戳的MVCC和快照方式,有效地避免了性能和事务规模产生的耦合问题。这一设计不仅大量降低对资源消耗,还解决了高并发下锁维护的热点问题和性能瓶颈,在满足事务ACID功能的同时,可以显著的提升事务并发处理效率。
更多推荐
所有评论(0)