基础概念
本地事务四大特性(ACID)
- 原子性(atomicity):一个事务中的所有操作,不可分割,要么全部成功,要么全部失败;
- 一致性(consistency):一个事务执行前与执行后数据的完整性必须保持一致,与cap的一致性是两个概念,详情请看:http://arthurjq.com/2020/12/25/consistency/
- 隔离性(isolation):一个事务的执行,不能被其他事务干扰,多并发时事务之间要相互隔离,细节请看:http://arthurjq.com/2020/12/25/acid-isolation/
- 持久性(durability):一个事务一旦被提交,它对数据库中数据的改变是永久性的。
CAP理论
- 一致性(Consistency) :在分布式系统中所有的数据备份,在同一时刻都保持一致状态,如无法保证状态一致,直接返回错误;对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题;从服务器来看,则是更新如何复制分布到整个系统,以保证数据最终一致;
- 可用性(Availability):在集群中一部分节点故障,也能保证客户端访问系统并得到正确响应,允许一定时间内数据状态不一致;
- 分区容错性(Partition tolerance):分布式系统在遇到任何网络分区故障时,仍然能保证对外提供满足一致性和可用性的服务,除非整个网络环境都发生故障;
BASE理论
基本可用(Basically Available):分布式系统在出现故障时,保证核心可用,允许损失部分可用性。(响应时间上的损失、功能上的损失)
软状态(Soft State):系统中的数据允许存在中间状态,中间状态不影响系统的整体可用性。(如:支付中、处理中)数据同步允许一定的延迟
最终一致性(Eventually Consistent):系统中的数据不可一直处于软状态,必须在有时间期限,在期限过后应当保证数据的一致性。(支付中 => 支付成功)
- 相比于本地事务的ADIC强一致性模型,BASE理论提出通过牺牲一定的强一致性来获得可用性;
- 不同业务单元和业务组件对数据一致性的要求不一样,因此分布式系统中BASE理论和ACID特性会结合使用。
幂等性
- 幂等(Idempotent)是一个数学与计算机学中的概念:
f(n) = 1^n
// 无论n等于多少,f(n)永远值等于1; - 在程序中,使用相同参数执行同一个方法,每一次执行结果都是相同的,即具有
幂等性
; - 以订单状态处理为例的幂等性设计,不论执行多少次orderProcess()方法,都只会扣减一次库存,并且返回true。
接口幂等性实现
- 唯一id(建去重表):每次操作都根据操作和内容生成唯一id,在执行之前先判断id是否存在,不存在才执行,并且保存到数据库或redis等
- 服务端提供发送token的接口,业务调用接口前先获取token,然后调用业务接口请求时把token携带过去,服务器判断token是否存在redis,不存在说明第一次请求,执行完成后需要把redis中token删掉,token需要设置过期时间
- 版本控制:类似乐观锁,增加版本号,当版本号符合才更新数据
- 状态控制:例如订单有状态 已支付,未支付,支付中,支付失败,当处于未支付时才允许修改为支付中。
事务的演进过程
单体架构
对于一些小型项目,使用单体架构可以快速的开发,并且很容易控制我们的业务逻辑和事务处理。通过本地事务的ACID特性,保证我们数据的一致性。
分库
实际项目中,我们可能会遇到一种情况就是,并发量不大,但常年累积下来数据量很大,这时候我们考虑到了分库策略。由于垮库情况下本地事务已经无法保证多库之间的数据一致性,这时我们就需要考虑分布式事务了。
分布式架构(服务拆分)
也有可能所使用的数据库性能比较好,但我们的单应用的性能无法满足业务需求,这时候我们可以选择对服务进行拆分的策略。此时虽然还是使用同一数据库,但我们多个服务之间互相调用来完成原有单体架构下的业务逻辑,这种情况下原有的本地事务也无法保证数据的一致性,这时我们也需要考虑分布式事务。
微服务架构
服务的拆分可以提高应用性能,让应用更专注于处理自己所负责的事情。数据库的拆分,一定程度上提升IO性能、数据库连接数、单机硬件资源的瓶颈。这时,服务间互相调用,每个服务都存在一个自己特定的业务数据库,所以我们需要考虑分布式事务。
分布式事务
XA规范(协议)
X/Open组织(现在的Open Group)定义了一套DTP(Distributed Transaction Processing)分布式事务处理模型,主要包含以下四部分:
- AP(应用程序)
- TM(事务管理器):交易中间件,根据事务所有的参与者的状态决定一起提交还是回滚
- RM(资源管理器):数据库
- CRM(通信资源管理器):消息中间件
XA规范则是DTP模型定义TM和RM之间通讯的接口规范。XA接口函数由数据库厂商提供。TM用它来通知数据库事务的开始、结束、提交、回滚。基于XA规范衍生出下面的二阶段提交(2PC)、三阶段提交(3PC)。
XA规范包括两套函数,以
xa_
开头的及以ax_
开头的。
以下的函数使事务管理器可以对资源管理器进行的操作:
- xa_open,xa_close:建立和关闭与资源管理器的连接。
- xa_start,xa_end:开始和结束一个本地事务。
- xa_prepare,xa_commit,xa_rollback:预提交、提交、回滚一个本地事务。
- xa_recover:回滚一个已进行预提交的事务。
- ax_开头的函数使资源管理器可以动态地在事务管理器中进行注册,并可以对XID(TRANSACTION IDS)进行操作。
- ax_reg,ax_unreg;允许一个资源管理器在一个TMS(TRANSACTION MANAGER SERVER)中 动态注册 或 撤消注册 。
XA的一些问题:
性能(阻塞、响应时间增加(一阶段不提交,且锁粒度更大)、死锁(收到 XA commit 或 XA rollback 前必须阻塞等待));
XA 在整个事务处理过程结束前,涉及数据都被锁定,读写都按隔离级别的定义约束起来;
依赖于独立的J2EE中间件,
Weblogic
、Jboss
,后期轻量级的Atomikos
、Narayana
、Bitronix
;不是所有资源(RM,数据库)都支持XA协议;
Seata 事务框架中的XA事务模式:
如图所示,XA 模式其实就是 Seata 底层利用了 XA 接口,在一阶段二阶段时自动处理。如一阶段时,XA 的 RM 通过代理用户数据源,创建 XAConnection,进行开启 XA 事务(XA start)和 XA-prepare(此时 XA 的任何操作都会被持久化,即便宕机也能恢复),在二阶段时,TC 通知 RM 进行 XA 分支的 Commit/Rollback 操作。
JTA(Java Transaction API)
即Java的事务API,基于XA实现,也就是RM需要支持XA,所以也有JTA(XA)的说法,JTA仅定义了接口。主要包括javax.sql.XADataResource
、javax.sql.XAConnection
、javax.sql.XAException
、javax.transaction.xa.XAResource
、javax.transaction.Xid
。
JTA的实现有几种形式:
- J2EE容器提供的JTA实现(Weblogic、JBoss );
- JOTM(Java Open Transaction Manager)、Atomikos,可独立于J2EE容器的环境下实现JTA;
二阶段提交(2PC)
2PC就是分布式事务中将事务分为两步进行提交。基于数据库的XA协议完成事务本质上就是二阶段提交(XA、JTA/JTS)。
- 协调者(Coordinater):事务管理器(TM)
- 参与者(participants):资源管理器(RM)
准备阶段:
协调者向参与者发送prepare信息,以询问参与者是否能够提交事务;
参与者在收到prepare信息后,进行本地事务的预处理,但不提交。并根据处理结果返回,失败not commit
or 成功ready
;提交阶段:
如协调者收到参与者的失败消息,则向每个参与者发送rollback
消息进行回滚;
所有参与者都返回ready
,则向每个参与者发送提交commit
消息,通知参与者进行事务提交;
整理于:https://zhuanlan.zhihu.com/p/315164700
二阶段提交的一些问题:
- 单点故障,事务管理器一旦发生故障,参与者会被阻塞。尤其在提交阶段,所有参与者都处于锁定资源状态中,无法完成事务操作;(可以选择新的协调者,但无法解决参与者被阻塞的问题);
- 同步阻塞,参与者和协调者资源都被锁住,提交和回滚后才能释放;
- 数据不一致,提交阶段协调者向参与者发送commit信息,发生局部网络故障,会导致存在参与者未收到commit信息无法提交事务情况,导致出现数据不一致现象;
三阶段提交(3PC)
相比于2PC,3PC把2PC的准备阶段再次进行拆分,并且3PC引入了参与者超时机制。主要解决了2PC单点故障的问题,但性能问题和不一致问题仍然没有根本解决。
canCommit:协调者询问参与者,是否具备执行事务的条件,参与者进行自身事务必要条件的检查;
preCommit:协调者通知参与者进行事务的预提交;
doCommit:协调者根据
preCommit
阶段参与者的反馈结果通知参与者是否进行事务提交或是进行事务回滚;
虽然 3PC 用超时机制,解决了协调者故障后参与者的阻塞问题,但与此同时却多了一次网络通信,性能上反而变得更差,也不太推荐。
事务补偿方案TCC(业务层面)
TCC的核心思想就是校验、资源锁定、补偿,对每个操作(Try)都提供确认(Confirm)和取消(cancel)的操作,这样根据操作的结果,来确认是进行Confirm还是Cancel。
可以看出XA的两阶段提交是基于数据库层面的,而TCC也是一种两阶段提交,但它是基于业务层面的。
- Try(检查资源):主要负责对业务进行数据检查和资源预留,例如:对资金进行
冻结
;对状态更改为处理中
; - Confirm:确认执行业务的操作,例如:进行实际
资金扣除
;更改状态为最终结果
; - Cancel:取消执行业务的操作,反向操作,例如:
解冻资金
;更改状态为未处理
;
TCC对业务侵入性较强,改造难度大,每个操作都要try,confirm,cancel三个接口实现,confirm和cancel接口必须实现幂等性。
Seata 事务框架中的TCC事务:
TCC存在的一些问题:
- 业务操作的是不同服务的Try来进行资源预留,每个Try都是独立完成本地事务,因此不会对资源一直加锁。
- 业务服务需要提供try、confirm、cancel三种方法来支持,业务侵入性强,如不适用三方框架要做到对各阶段状态的感知,比较麻烦,而且这种模式并不能很好地复用,会导致并发量激增。
- 常用TCC框架:
tcc-transaction
、ByteTCC
、spring-cloud-rest-tcc
、Himly
- Confirm/Cancel要做幂等性设计。
常见的微服务系统大部分接口调用是同步的,这时候使用TCC来保证一致性是比较合适的。
Saga(补偿)
Saga的核心是补偿,与TCC不同的是Saga不需要Try,而是直接进行confirm
、cancel
操作。
- Confirm:依次按顺序依次执行资源操作,各个资源直接处理本地事务,如无问题,二阶段什么都不用做;
- Cancel:异常情况下需要调用的补偿事务(逆操作)来保证数据的一致性。
可以看出,Saga和TCC有些类似,都是补偿型事务
适用场景:
- 业务流程长、业务流程多
- 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
优势:
- 一阶段提交本地事务,无锁,高性能;
- 事件驱动模式,参与者可异步执行,高吞吐;
- 应用成本低,补偿服务易于实现;
劣势:无法保证隔离性(脏写)
脏写:先前的写入尚未提交,后面的写入又覆盖了这个尚未提交的值
可靠消息最终一致性(RocketMQ)
有一些情况,服务间调用时异步的,服务A将消息发送到MQ,服务B进行消息的消费。这时我们就需要用到可靠消息最终一致性
来解决分布式事务问题。首先字面理解
- 可靠消息:即这个消息一定是可靠的,并且最终一定需要被消费的。
- 最终一致性:过程中数据存在一定时间内的不一致,但超过限定时间后,需要最终会保持一致。
确保以上两点情况下,通过消息中间件(RocketMQ)来完成分布式事务处理,因为RocketMQ支持事务消息,可以方便的让我们进行分布式事务控制。
因此首先需要了解一下,RocketMQ的事务消息的原理。
half message:半消息,此时消息不能被consumer所发现和消费,需producer进行二次消息确认。
producer
发送half message
给MQ Server
;producer
根据MQ Server
应答结果判断half message
是否发送成功;producer
处理本地事务;producer
发送最终确认消息commit / rollback
;commit
:consumer
对消息可见并进行消费;rollback
:discard
抛弃消息,consumer
无法进行消息消费;- 如遇异常情况下
step4
最终确认消息未达到MQ Server
,MQ Server
会定期查询当前处于半消息状态下的消息,主动进行消息回查来询问producer
该消息的最终状态; producer
检查本地事务执行的最终结果;producer
根据检查到的结果,再次提交确认消息,MQ Server
仍然按照step4
进行后续操作。
事务消息发送对应步骤1、2、3、4,事务消息回查对应步骤5、6、7。
由以上步骤可以看出通过事务性消息的两步操作,避免了消息直接投递所产生一些问题。最终投递到MQ Server的消息,是真实可靠且必须被消费的。
Seata
阿里开源的Seata 是一款分布式事务解决方案,提供了 AT、TCC、SAGA 和 XA 事务模式。
Seata架构的亮点主要有几个:
- 应用层基于SQL解析实现了自动补偿,从而最大程度的降低业务侵入性;
- 将分布式事务中TC(事务协调者)独立部署,负责事务的注册、回滚(支持多种注册中心形式以及本地文件形式);
- 通过全局锁实现了写隔离与读隔离。
Seata的介绍、安装和使用在另一篇文章中。
在实际业务开发中,考虑设计一套好分布式事务框架,需要根据具体业务情况结合上述一些理论,进行权衡取舍。
需要考虑的特性:
- 实现复杂度:事务模式与当前业务结合,实施成本是否过高;
- 业务侵入性:基于注解、XML、补偿逻辑;
- TC/TM部署:独立部署、与应用部署;
- 性能:回滚概率、回滚所付出的代价、响应时间、吞吐量;
- 高可用:数据库、注册中心、配置中心
- 持久化:文件、数据库;
- 同步/异步:分布式事务执行过程中是否阻塞,还是非阻塞;
具体的介绍和实践在:http://arthurjq.com/2021/02/04/seata/
总结 AT、TCC、Saga、XA 模式分析
分布式事务模式 | 介绍 | 技术栈 |
---|---|---|
AT 模式 | 无侵入的分布式事务解决方案,适用于不希望对业务进行改造的场景,几乎0学习成本(sql都由框架托管统一执行,会存在脏写问题) | seata、shardingsphere |
TCC 模式 | 高性能分布式事务解决方案,适用于核心系统等对性能有很高要求的场景(第一阶段会产生行锁,事务执行太久会锁行很久) | seata、service-comb |
Saga 模式 | 长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统(第一阶段就操作DB,会存在脏读问题) | seata、shardingsphere、service-comb |
XA模式 | 分布式强一致性的解决方案,但性能低而使用较少。 | seata、shardingsphere |
Saga和TCC模式区别不大,TCC就是多了个锁行的步骤(避免了脏读,但事务执行太久会导致锁行很久,不适用于长事务)
注:
- shardingsphere的Saga 模式 引擎使用的是service-comb(较comb实现了解析sql自动回滚)
- shardingsphere的AT模式引擎是用的Seata引擎
- shardingsphere是从Sharding-JDBC进化而来,分库分表使用很火
(当当开源)shardingsphere开源地址:https://github.com/apache/shardingsphere
(阿里2019年开源)seata开源地址:https://github.com/seata/seata
(华为2017年开源)service-comb开源地址:https://github.com/apache/servicecomb-pack