浅谈分布式事务控制在银行应用的实现
作者 | 刘文涛
责编 | 仲培艺
对于分布式数据库而言,分布式事务控制是重点和难点,一直以来没有成熟的方案可以突破CAP理论,几乎每个分布式数据库研发团队都在分布式事务控制方案上结合了各自应用特点,进行了针对性的取舍,可以说是八仙过海各显神通。以下是我对分布式事务控制的理解:
分布式事务控制的最终目标是实现一致性,方案大体分为实时一致性和最终一致性两种。两阶段提交是比较典型的实时一致性方案,提供补偿事务和基于消息队列的异步处理方案是最终一致性方案。两阶段提交由于同步阻塞、存在脏读可能性等问题,在某些银行的应用场景下无法使用,如果将隔离级别修改为串行化则可以解决脏读问题,但对性能影响较大。基于消息队列的异步处理方案将事务拆分成多个本地子事务,子事务之间通过消息队列衔接,实现串行执行,单个子事务占用资源的时间很短,并发度高。但这种最终一致性方案如果应用到银行的应用中势必影响用户体验,而且对应用侵略性较大,实施成本高。
在分析了以上事务处理方案的优缺点之后,根据银行业务对实时一致性的要求,考虑到用户体验和实施成本的影响,我们提出了基于全局事务ID(Global transaction ID,以下简称GTID)的分布式事务解决方案。
基本原理
在基于GTID的分布式事务方案(以下简称本方案)中,我们把协调参与者和记录全局事务状态这两个功能分开,用计算节点协调各事务参与者进行事务操作,全局事务管理器仅管理全局事务的状态。为了确保事务状态正常,全局事务管理采用了实时持久化和实时同步到备机等多重保障机制。本方案事务管理架构如图1所示。
图1 基于GTID的分布式事务管理方案
两(三)阶段提交的核心思想是通过前期的多次准备和协调工作,尽可能让最后的提交操作能够成功。而本方案认为大部分事务都可以一次提交成功,因此采用一阶段提交+补偿事务的方式,如果事务在提交阶段有部分节点提交失败,本方案将回滚已成功提交的事务,而不是让失败的节点不断重试。与两(三)阶段提交相比,本方案在大部分情况下减少了与数据节点的交互次数,降低了锁冲突概率,提升了事务处理效率。
建表时增加一个隐藏字段,用于记录GTID。
每个事务开始时为其申请一个GTID,该GTID是全局唯一且单调递增的,GTID申请成功后,我们称该GTID为活跃(Active)状态,对应该GTID的事务状态为未提交状态,若涉及到数据更新,则将GTID更新到本事务将要更新的数据中,事务成功提交后,将GTID释放,此时我们称GTID为非活跃(UnActive)状态,对应的事务状态为已提交状态。
当事务提交失败时,提交失败节点会自动回滚,对于已成功提交的节点,需要将其回滚,数据恢复到更新前的状态,全部节点回滚完成后,同样需要将GTID释放。
事务的原子性
保证事务的原子性是分布式事物的最大难点,在分布式环境下,保证事务原子性主要有两种方案,一种是在提交命令发出后不回滚,尽可能保证提交成功;另一种是在提交命令发出后,根据响应结果判断是提交成功还是该进行回滚。
我们采用的是第二种方式,由于我们的方案采用的是普通事务的提交方式,目前的主流数据库在本地事务提交后都不能回滚,我们必须自己实现已提交事务的回滚。已提交事务回滚架构如图2所示。
图2 已提交事务回滚示意图
在每个数据节点上部署一个回滚模块用于已提交事务回滚,当部分数据节点提交失败时,计算节点向已经提交成功的数据库节点的回滚模块发送已提交事务回滚命令,命令中包含事务对应的GTID,回滚模块根据GTID进行回滚,步骤如下:
- 定位:根据GTID相关信息定位要进行分析的数据库日志文件列表;
- 查询:遍历数据库日志文件,找到GTID对应的事务日志块;
- 分析:分析日志块,为事务中每条SQL语句生成反向SQL语句;
- 执行回滚:将所有反向SQL语句逆序执行,并保证在一个事务中。
由于该回滚操作不是数据库原生回滚机制,在实际使用中需要经过大量优化才能保证回滚的性能达到可用级别。
事务的一致性
在单机数据库事务中,事务的一致性是指事务的任何操作都不会使得数据违反数据库定义的约束、触发器等规则。在分布式数据库中,由于数据分布在不同节点,有些约束难以保证,比如主键和唯一性约束,中信银行当前实现的版本未从数据库本身保证该约束的完整性,只能从使用规范角度进行约束,由应用保证主键和唯一索引的全局唯一性。
事务的隔离性
事务隔离性的本质就是如何正确处理读写冲突和写写冲突,这在分布式事务中又是一个难点,因为在我们的分布式事务控制方案中,可能会出现提交不同步的现象,这个时候就有可能出现“部分已经提交”的事务。一旦并发应用访问“已经提交”节点中的数据,就需要根据GTID的状态来判断是“部分提交”还是“全部提交”,否则就出现了分布式数据库中特有的一种“脏读”。因此GTID方案可以确保分布式事务的隔离性。
事务的持久性
和单机一样,分布式事务也需要保证事务的持久性,通过单节点数据的持久化和全局事务状态的持久化来完成,数据的持久化由单节点数据库保证,全局事务状态的持久化由全局事务管理器负责,全局事务管理器采用定时全量和实时增量方式实现事务状态的持久化:将GTID申请和释放的动作实时写到磁盘,同时每隔一定时间将全局事务管理中的活跃GTID列表以异步方式写到磁盘,通过定时的全量活跃GTID列表和实时的增量记录,可以获得任意时刻的活跃GTID列表。
异常处理
分布式环境下,事务处理涉及的组件、服务器和网络比单机复杂太多,各个环节都可能出现故障,因此异常处理也成为分布式事务的重点。根据故障环节的不同可分为数据节点异常、计算节点异常和全局事务管理器异常。
数据节点异常
数据节点异常时,全局事务将无法提交,已经提交的本地事务将会被回滚。具体考虑如下几个场景(假设分布式事务涉及三个数据节点:DB1、DB2、DB3,其中DB2发生了异常):
1. 分布式事务还未发起提交:向DB1、DB3发起回滚操作,DB2的回滚由数据节点自身保证;
2. 分布式事务已经发起提交:DB2上也已提交,但结果未知。此时需要向所有数据节点发起已提交事务回滚。
计算节点异常
分布式事务正常运行时,计算节点(假设为计算节点A)发生异常,与数据节点集群及客户端的所有连接都已中断,数据节点上未提交的事务由数据节点自动回滚。客户端通过其他计算节点(假设为计算节点B)重新建立连接进行数据库集群访问,不会影响业务新发起的事务,但由于计算节点A异常时,处于部分已提交状态的事务将无法结束,计算节点B上的事务一旦访问到这些事务涉及的数据就会被阻塞,直到这些事务回滚。
具体考虑以下两种场景:
1. 每台计算节点上部署监控程序,当计算节点异常时,监控程序将重启计算节点,重启完成后由计算节点自己与全局事务管理器交互并完成异常事务的回滚;
2. 如果计算节点服务器已经宕机且无法启动或者监控程序无法重启计算节点服务,则由计算节点管理器协调对等的计算节点(该集群的其他计算节点),完成异常事务的回滚。
全局事务管理器异常
全局事务管理器采用主备部署,申请或释放GTID时通过实时同步到备机内存、实时增量持久化到本地磁盘、定时全量持久化三重保护机制确保全局事务信息不丢失。在单机异常时会进行主备切换,在双机都异常时,需通过持久化的全局事务信息进行恢复。
组合异常
1. 组合异常,考虑如下两种场景:
数据节点和计算节点同时异常。数据节点和计算节点走各自的异常处理流程即可解决问题,影响的是计算节点上的当前活跃事务以及涉及异常数据节点上的活跃事务的合集。
数据节点、计算节点和全局事务管理器全部异常。此时全局事务管理器上所有的GTID都需要回滚,可能需要先配置额外的计算节点,并通过计算节点管理器触发所有活跃事务的2. 回滚。具体流程分析如下:
2-1. 所有未发起提交操作的分布式事务,数据节点恢复后将自动回滚;
2-2. 恢复计算节点,若计算节点不能恢复则需要配置额外的计算节点;
2-3. 由恢复后的计算节点或者计算节点管理器协调新的计算节点处理活跃事务的回滚,其中未发起提交操作的事务不会发生实际回滚动作(由第一步中的数据节点回滚),已经发起提交操作的事务将由数据节点上的回滚模块完成已提交事务的回滚。