分布式事务
[TOC]
事务
Transactions provide an “all-or-nothing“ proposition, stating that each work-unit performed in a database must either complete in its entirety or have no effect whatsoever.
事务应该具有4个属性:原子性、一致性、隔离性、持久性
分布式事务场景
什么是分布式事务
A distributed transaction
is a database transaction in which two or more network hosts are involved. Usually, hosts provide transactional resources… Wiki Definition
分布式事务是指多个提供事务资源的网络节点参与的数据库事务
跨越多个服务,操作多个数据库。保证对于多个资源服务器的数据的操作,要么全部成功,要么全部失败。本质上来说,是为了保证不同资源服务器的数据一致性。
典型场景:
单服务,夸库事务
不同业务库
分库分表
服务化(多业务服务)
分布式系统理论
CAP
在设计一个分布式系统的时候,会遇到三个特性,而一个分布式系统最多只能满足其中的2项:
- 一致性:等同于所有节点访问同一份最新的数据副本(
all nodes see the same data at the same time
)。即更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致,不能存在中间状态。关于一致性:- 强一致性:时刻保证数据都是一致的;
- 最终一致性:允许存在中间状态,只要求经过一段时间后,数据最终一致;
- 弱一致性:如果允许存在部分数据不一致。
- 可用性:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
- 分区容错性:分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
分区容错性是分布式系统最基本的要求。因此需要把精力花在如何根据业务特点在C(一致性)和A(可用性)之间寻求平衡。
BASE
BASE
理论是对CAP
中一致性和可用性权衡的结果。核心思想是即使无法做到强一致性,但应用可以根据自身特点采用适当的方式达到最终一致性。
- 基本可用(Basically Available)
指分布式系统在出现不可预知故障的时候,允许损失部分可用性。 - 软状态( Soft State)
指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性。 - 最终一致( Eventual Consistency)
强调的是所有的数据更新操作,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
BASE
通过牺牲强一致性来获得可用性。允许数据在一段时间内是不一致,但最终达到一致状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID
特性和BASE
理论往往又会结合在一起。
分布式事务解决方案
- 强调一致性的分布式解决方案:
XA|JTA 两阶段提交协议
,由于可用性较低,实际应用的并不多。 - 基于BASE理论,强调可用性的分布式解决方案:
TCC
、TXC
、最大努力通知
、可靠消息
。
从架构、运作原理、缺点、实现层面逐个解释他们的运行原理。
强一致性方案
XA/JTA
XA
,JTA(Java Transaction API)
是XA
的JAVA版本规范。
架构
应用程序(Application Program ,简称AP):用于定义事务边界(即定义事务的开始和结束),并且在事务边界内对资源进行操作
资源管理器(Resource Manager,简称RM):如数据库、文件系统等,并提供访问资源的方式。
事务管理器(Transaction Manager ,简称TM):负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等。
两阶段提交协议(Two Phase Commit)
的升级版
运作流程
- 步骤1:
AP
向TM
创建全局事务,TM
向AP
返回全局事务号。 - 步骤2:
AP
使用全局事务号,访问RM
的资源(当RM为数据库时,资源访问就是SQL操作)。当RM第一次收到访问时,使用该全局事务号向TM注册,TM返回事务分支事务号。 - 步骤3:
AP
向TM
发出全局事务提交请求,TM
与参与事务的RM
通信,进行提交处理,全部完成后,向AP
返回结果。
- 步骤1:
改进:
只读断言: 在
阶段1
中,RM
可以断言“我这边不涉及数据增删改”来答复TM的prepare请求,从而让这个RM
脱离当前的全局事务,从而免去了Phase 2。
这种优化发生在其他RM都完成prepare之前的话,使用了只读断言的RM
早于AP
其他动作(比如说这个RM返回那些只读数据给AP)前,就释放了相关数据的上下文(比如读锁之类的),这时候其他全局事务或者本地事务就有机会去改变这些数据,结果就是无法保障整个系统的可序列化特性——通俗点说那就会有脏读的风险。
一阶段提交: 如果需要增删改的数据都在同一个RM上,TM可以使用一阶段提交——跳过两阶段提交中的阶段1
,直接执行阶段2
。缺点
- 同步阻塞:两阶段提交方案下全局事务的
ACID
特性,是依赖于RM的。 - 单点故障:由于协调者的重要性,一旦协调者TM发生故障。参与者RM会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
- 同步阻塞:两阶段提交方案下全局事务的
实现
柔性事务方案(最终一致性)
TCC(Try-Confirm-Cancel
)
TCC方案是目前最火的一种柔性事务方案。通过对(由业务系统提供的
)业务逻辑的调度来实现分布式事务。TCC事务机制需要业务系统提供三段业务逻辑:初步操作Try
、确认操作Confirm
、取消操作Cancel
。
TCC将[传统事务机制]的业务逻辑 = [TCC事务机制]的初步操作(Try)
+ [TCC事务机制]的确认逻辑(Confirm)
,TCC机制将传统事务机制中的业务逻辑一分为二,拆分后保留的部分即为初步操作(Try)
;而分离出的部分即为确认操作(Confirm)
,被延迟到事务提交阶段执行。
TCC事务机制以初步操作(Try)为中心,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其不良影响进行回撤。
初步操作(Try)
TCC事务机制中的业务逻辑(Try),从执行阶段来看,与传统事务机制中业务逻辑相同。但从业务角度来看,是不一样的。TCC机制中的Try仅是一个初步操作,它和后续的次确认一起才能真正构成一个完整的业务逻辑。确认操作(Confirm)
确认操作(Confirm)是对初步操作(Try)的一个补充。当TCC事务管理器认为全局事务可以正确提交时,就会逐个执行初步操作(Try)指定的确认操作(Confirm),将初步操作(Try)未完成的事项最终完成。取消操作(Cancel)
取消操作(Cancel)是对初步操作(Try)的一个回撤。当TCC事务管理器认为全局事务不能正确提交时,就会逐个执行初步操作(Try)指定的取消操作(Cancel),将初步操作(Try)已完成的事项全部撤回。
TCC事务模型的优缺点:
- 优点:XA两阶段提交资源层面的,而TCC实际上把资源层面二阶段提交上提到了业务层面来实现。有效了的避免了XA两阶段提交占用资源锁时间过长导致的性能低下问题。
- 缺点:主业务服务和从业务服务都需要进行改造,从业务方改造成本更高。牺牲了应用灵活性,需要开发人员实现事务检查与回滚的细节,面临着花费大量精力保证应用正确性的问题。
TXC/GTS
阿里提出了TXC|GTS,GTS方案认为XA性能低效的根本原因是采用了阻塞协议。在分布式事务提交的第一阶段等待最慢的一个事务分支完成,即使在不存在锁冲突的情况下,各事务分支的数据库连接依然会被挂起所占用的资源都不能够释放,以防止全局事务提交前释放资源所造成的数据不一致。对于业务流量极高的大规模互联网企业,难以接受XA
两阶段提交协议所带来的巨大性能开销。
可以说TXC是一个通用的事务引擎:有三种模式:
处理流程与XA一致,也包括全局事务注册
、数据访问
与全局事务提交
三个步骤。不同在于:
- 第二步数据访问中,各事务分支完成数据操作的同时,会将全局事务信息(锁与日志信息)存储在当前数据库的表中。
- 第三步全局事务提交中,采用一阶段本地事务提交+二阶段异步清理的方式。首先对各数据库做本地事务的提交,并释放数据库连接等系统资源,然后,向TM发出全局事务提交请求,TM收到请求后,立即返回成功,TM后续实际工作是对各个数据库使用全局事务标识符进行全局事务信息的清理。
GTS与XA在全局事务的故障恢复处理与并发控制采用了不同的实现机制:
- XA两阶段协议是基于数据库内核的日志与锁信息实现全局事务的回滚与并发控制。由于GTS一阶段本地事务提交中,会直接提交本地事务并释放连接,此时数据库内核的日志与锁表对全局事务不再有效。在第二步中,GTS会将日志和锁信息存储在表中,当事务本地提交后,日志和锁信息被持久化保存,用于实现全局事务的并发控制与故障恢复。
- GTS的故障恢复只有UNDO操作没有REDO操作,日志表中存储了UNDO需要的信息,包括行记录标识、全局事务号、镜像查询语句、操作的前像与操作的后像。当发生故障时,对于已经本地提交的数据库,从UNDO表中找到修改的记录,记录的操作前像和操作后像,使用镜像查询语句从数据库中读取该记录的当前值。如果当前值与记录操作后像相同,则直接使用操作前像进行恢复,否则报警,进行人工处理。
- GTS的全局锁表中存储了记录的加锁信息。封锁的粒度是行(记录),锁的类型包括共享锁和互斥锁,对于同一个记录,加锁的规则是共享锁与共享锁不冲突,共享锁与互斥锁冲突、互斥锁与互斥锁冲突。对插入(INSERT)、修改(UPDATE)、删除(DELETE)、更新模式的锁定查询(SELECT… FOR UPDATE) 操作加互斥锁。对于共享模式的锁定查询 (SELECT…LOCK IN SHARE MODE) 操作加共享锁。若没有锁冲突,在GTS锁表中,增加一行记录,表示加锁成功。
- GTS的默认隔离级别为读未提交(脏数据),使用
SELECT… FOR UPDATE
和SELECT…LOCK IN SHARE MODE
,可使查询隔离级别提升至读已提交。
架构
与XA
架构相同,GTS
架构由应用
、事务管理器
、资源管理器
三个部分组成。资源管理器由事务分支处理模块、镜像查询构造模块、并发控制模块、恢复控制模块,以及存储在数据库中的GTS
事务信息(GTS锁表与GTS日志表
)等组成。
- 事务分支处理模块:是资源管理器的外部接口,并完成内部各模块的调用。
- 镜像查询构造模块:从
Insert、Update、Delete
语句,生成该操作对应记录集的镜像查询语句。例如table_name表包含两个字段column1和column2,column1为主键,则镜像查询语句为select column1, column2 from table_name where column1=v1。 - 并发控制模块:基于GTS事务锁表,维护读写并发控制。锁表定义如下:
主要流程序列图
分别描述了insert/delete/update
操作、读已提交操作、提交操作和回滚操作等四个操作的序列图(一种可能的实现方式)。
insert/delete/update操作流程序列图:
读已提交操作流程序列图
提交操作流程序列图
回滚操作流程序列图
GTS的优势与约束
与基于消息队列与TCC补偿模式的分布式事务相比,在性能满足的情况下,GTS更好的应用灵活性与数据一致性:
- 灵活性:数据库应用基本实现零修改,同时,基于XA模型,可方便的支持消息队列数据库等多种RM。
- 数据一致性:GTS 的缺省事务隔离级别为读未提交,该模式下可以达到分布式事务的最大性能,但可能会读到脏数据。对于一致性要求高的应用,在性能允许的情况下,可以采用已提交读语句(for update、lock in share mode)将隔离级别提升至读已提交。
根据GTS
实现机制的特点,其应用场景上有以下约束:加锁操作记录数量不能太大,操作冲突不能太多,加锁时间不能太长。违法以上约束时,GTS
内部会占用过多资源、锁冲突和回滚增加,导致性能的下降。电商、物流、金融、零售行业中的核心交易场景有着高并发,高性能,单次操作数据集小,事务响应时间敏感的特点,GTS类方案在此类场景中有着广泛和良好的应用前景。
事务隔离级别
在隔离性上TXC默认事务隔离级别的是读未提交(read uncommited),避免了XA实现在分布式事务的情况下长时间占用锁的情况。在允许脏读的业务场景中,能充分发挥性能上的优势。
也同时提供读已提交隔离级别。实现原理是在select
语句上加hint
,TXC会牺牲性能,先做检测,如果相关记录已属于中间状态,则会过一段时间重试以读取最终状态,或者这段时间过去之后返回失败。
最大努力通知
最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,适合于通知类场景。对业务最终一致性时间敏感性低、跨企业的业务活动
特点:
不可靠消息
:允许消息丢失。业务活动主动方,在完成业务处理之后,向业务活动的被动方发送消息,直到通知N次后不再通知。定期校对
:业务主动方根据定时策略,向业务被动方查询发送状态,恢复丢失的业务消息。
运行过程
短信发送流程如下:
- 业务方将短信发送请求提交给短信平台
- 短信平台接收到要发送的短信,记录到数据库中,并标记其状态为”已接收”
- 短信平台调用外部短信发送供应商的接口,发送短信。外部供应商的接口也是异步将短信发送到用户手机上,因此这个接口调用后,立即返回,进入第4步。
- 更新短信发送状态为”已发送”
- 短信发送供应商异步通知短信平台短信发送结果。而通知可能失败,因此最多只会通知N次。
- 短信平台接收到短信发送结果后,更新短信发送状态,可能是成功,也可能失败(如手机欠费)。到底是成功还是失败并不重要,重要的是我们知道了这调短信发送的最终结果
- 如果最多只通知N次,如果都失败了的话,那么短信平台将不知道短信到底有没有成功发送。因此短信发送供应商需要提供一个查询接口,以方便短信平台驱动的去查询,进行定期校对。
条件:
- 业务方提供被动方事务执行状态通知回调接口
- 业务被动方提供状态查询接口
约束:
- 被动方的处理结果不影响主动方的处理结果
实现案例:
可靠消息
(基于可靠消息
的最终一致性,可以异步,但数据绝对不能丢,而且一定要记账成功)
业务处理服务在业务事务提交前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送。业务处理服务在业务事务提交后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才真正发送。
两种方案:一种是基于MQ的事务消息
(RocketMQ、ActiveMQ、RabbitMQ
)
第一阶段,MQ发送Prepared消息时,会拿到消息的地址;第二阶段执行本地事务;第三阶段通过第一阶段拿到的地址去访问消息,并修改消息的状态。
如果确认消息发送失败了怎么办?RocketMQ会定期扫描消息集群中的事务消息,如果发现了Prepared消息,它会向消息发送端(生产者)确认。RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
如果消费失败怎么办?阿里提供给我们的解决方法是:人工解决。
另外一种实现,并不是所有的MQ都支持事务消息,如kafka。此时可以使用独立消息服务或者本地事务表。
对比与场景事务选择
TCC两阶段提交 VS XA两阶段提交
- 在阶段1:
在XA中,各个RM准备提交各自的事务分支,事实上就是准备提交资源的更新操作(insert、delete、update等);而在TCC中,是主业务活动请求(try)各个从业务服务预留资源。 - 在阶段2:
XA根据第一阶段每个RM是否都prepare成功,判断是要提交还是回滚。如果都prepare成功,那么就commit每个事务分支,反之则rollback每个事务分支。TCC中,如果在第一阶段所有业务资源都预留成功,那么confirm各个从业务服务,否则取消(cancel)所有从业务服务的资源预留请求。
TCC两阶段提交与XA两阶段提交的区别是:
- XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁:
XA事务中的两阶段提交内部过程是对开发者屏蔽的,回顾我们之前讲解JTA规范时,通过UserTransaction的commit方法来提交全局事务,这只是一次方法调用,其内部会委派给TransactionManager进行真正的两阶段提交,因此开发者从代码层面是感知不到这个过程的。而事务管理器在两阶段提交过程中,从prepare到commit/rollback过程中,资源实际上一直都是被加锁的。如果有其他人需要更新这两条记录,那么就必须等待锁释放。
- TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁:
TCC中的两阶段提交并没有对开发者完全屏蔽,也就是说从代码层面,开发者是可以感受到两阶段提交的存在。如上述航班预定案例:在第一阶段,航空公司需要提供try接口(机票资源预留)。在第二阶段,航空公司提需要提供confirm/cancel
接口(确认购买机票/取消预留)。开发者明显的感知到了两阶段提交过程的存在。try、confirm/cancel
在执行过程中,一般都会开启各自的本地事务,来保证方法内部业务逻辑的ACID
特性。