【Java】分布式事务解决方案
分布式事务是指在分布式系统中,为了保证多个节点上的操作要么全部成功提交,要么全部失败回滚,所采取的一系列技术手段和协议。
CAP理论
在一个分布式系统中以下三个基本属性无法被同时满足:
- C(一致性):一致性是指写操作后的读操作可以读到最新的数据状态,当数据分布在多个节点上,从任意节点读取到的数据都是最新的状态。
- A(可用性):可用性是指任何事务操作都可以得到响应结果并且不会出现响应超时或者响应错误的情况发生。
- P(分区容错性):分区容错性是指通常分布式系统各个节点部署在不同的子网上,不可避免会出现由于网络问题而导致节点之间通信失败,但此时系统仍可对外提供服务。
CA without P:说明在流量飙升后,系统直接挂了,通常只适用于单机部署。
CP without A:系统优先考虑一致性和分区容忍性。这意味着在网络出现分区的情况下,为了保持数据的一致性,系统可能会暂时拒绝服务。
AP without C:系统更注重可用性和分区容忍性,即在网络分区期间仍然提供服务,但是可能牺牲了数据的一致性。
分布式事务中的相关概念
AP:应用程序,一般是指事务的发起者,定义事务内的操作。
RM:资源管理器。管理共享资源,提供资源访问接口,使得外部程序可以访问共享资源。
TM:分布式事务的协调者,接收AP的事务请求,与各个RM进行通信,协调并完成事务的处理。
全局事务:由TM协调和控制的跨多个RM和AP的事务。
分支事务:每个参与者的本地事务。
XA规范主要定义了事务管理器TM和资源管理器RM之间通信的接口规范。
解决方案
两阶段提交(2PC)
执行流程
2PC:两阶段提交协议,2是指两个阶段,P是指准备阶段,C是指提交阶段。将整个事务流程分为两个阶段,准备阶段,提交阶段。(这一部分在MySQL中也有。)
当AP准备提交时,TM会向RM发送prepare指令,RM就会去锁定资源(seata应当是数据库显式加行锁)。锁定好之后返回ok。当所有RM都返回ok后,TM再向所有的RM发送commit指令,各个RM再进行提交。提交成功返回ok,提交失败返回error。
一旦接收到error信息,TM就会向已经提交成功的RM发送rollback指令。
存在的问题
- 资源锁定问题:atomikos对应的AP和TM是集成在一块的,假设AP挂了,那TM也就挂了。TM挂了之后就会导致RM锁定的资源永远无法被提交或还原。
- 性能问题:如果有一个RM迟迟不返回ok/rollback,其他所有RM都要等待。会导致该请求迟迟没有结果
- 数据不一致问题:TM在给RM1和RM2发送指令的时候肯定不是同时发送的,如果给RM1发送完指令后TM挂掉,RM1和RM2的数据就会不一致。
三阶段提交(3PC)
执行流程
3pc三阶段提交协议。将整个事务流程分为三个阶段:cancommit precommit docommit
cancommit:向事务参与者询问是否能提交
precommit:通知参与者锁定资源
docommit:实际执行提交操作
3pc相对于2pc引入了超时机制。
- 在cancommit阶段,如果协调者在超时时间内未收到所有参与者通知的cancommit,那会向参与者发送abort指令表示放弃进入下一阶段。如果参与者超时,则直接终止本地事务
- 在precommit阶段,如果协调者在超时时间内未收到所有参与者的precommit结果,则会向参与者发送abort指令表示放弃进入下一阶段。如果参与者超时,则会提交事务。
优劣势
3pc相对于2pc的主要改动点:
- 引入新阶段-将2pc第一个阶段拆分成两个阶段
- 引入超时机制-在协调者和参与者中都引入超时机制。
3pc存在的问题:
- 性能瓶颈,因为3pc引入了一个新阶段,必然会产生更多的网络开销,性能表现不如2pc
- 数据不一致:由于precommit阶段假设参与者超时,本地事务会自动提交,会导致本应回滚的事务提交了,产生数据不一致。
TCC
全称是try-confirm-cancel
它将整个事务阶段分为三阶段,核心思想即针对每个业务操作,都要注册一个与其对应的确认和补偿的操作。
try:对各个服务的资源做检测以及对资源进行锁定或者预留。比如说对于一个订单需要扣减商品库存,此时不应该直接扣减库存,而是将这部分资源作为预留。
confirm:执行真正的业务操作,将try阶段预留的业务资源执行确认生效。
cancel:如果任何一个服务的业务方法执行出错,释放try阶段预留的业务资源。
执行流程
tcc场景:
系统有订单服务和商品服务,用户在下订单时,同时需要生成订单和扣减库存。
try阶段:
订单服务生成一个未生效的订单
商品服务设置预留库存=原预留库存+扣减库存数 实际库存数=原实际库存数-扣减库存数
confirm阶段:
订单服务将订单改为生效状态
商品服务设置预留库存=原预留库存-扣减库存数 实际库存数不变
cancel阶段:
订单服务将订单改为失效状态
商品服务设置预留库存=原预留库存-扣减库存数 实际库存数=原实际库存数+扣减库存数
实现上要注意:
- 业务操作要拆分为两阶段完成
- 允许空回滚:由于网络原因可能try阶段锁定资源没有锁定成功,此时会进入cancel阶段。这时不应该走原来的把订单改为失效状态的逻辑了,因为没有订单
- 防悬挂控制:由于网络原因先执行了cancel,再执行try,此时try中创建的订单就会一直留在库里没有操作。
- 幂等控制
优劣势
tcc优点:实现上更加灵活,性能上相对于2pc更加优越
tcc缺点:
- 业务侵入性高,业务操作需要实现确认和回滚逻辑
- 实现复杂,不是所有操作都可以分成两个阶段
- 非强一致性,补偿式事务。
本地消息表
执行流程
图上这个示例有点不是很清晰,还是用订单扣减库存这个例子。此时订单服务和商品服务之间使用消息队列进行通信。
本地消息表需要在数据库中创建一个消息处理用的消息表。
在订单服务创建订单后,向订单表写入一条扣减库存状态为发送中的消息,两个步骤在同一事务内。然后向mq推消息,商品服务订阅这个topic之后进行消费。
消费完又向消息队列中推送一条扣减库存成功topic的消息,此时订单服务作为消费者进行订阅监听,接收到消息后将消息表的状态改为已发送。
上述是整体流程。
消息发送会出现的问题:
- 订单表插入成功,发送消息到mq失败
- mq发送成功,但消息在mq内部被丢失
- 消费者消息出现异常或者消息丢失
实现上要注意:
- 商品服务需要确保处理消息时的幂等性
- 订单服务在推送消息的时候需要有重试机制,且需要设置最大重试次数
优劣势
实现缺点:
- 业务侵入度高
- 需要定时任务监听消息表的状态做补偿
- 基于数据库做本地消息表,性能较低。
MQ事务消息
执行流程
这里的半消息队列实际上没有消费者监听,所以可以暂时存储。
暂不打算深入研究rocketmq,所以放两篇博文在这,如果后续用到的时候可以参考。
RocketMQ发送消息原理(含事务消息)
深度剖析 RocketMQ 事务消息!
最大努力通知
执行流程
一个短信发送平台,背景是公司内部有多个业务都有发送短信的需求,如果每个业务独立实现短信发送功能,存在功能实现上的重复。
有一个短信平台项目,所有的业务方都接入这个短信平台,来实现发送短信的功能。
1、业务方将短信发送请求提交给短信平台
2、短信平台接收到要发送的短信,记录到数据库中,并标记其状态为已接收
3、短信平台调用外部短信发送供应商的接口,发送短信
4、更新短信发送状态为已发送
5、短信发送供应商异步通知短信平台短信发送结果,而通知可能失败,因此最多只会通知N次
6、短信平台接收到短信发送结果后,更新短信发送状态,可能是成功,也可能失败(如手机欠费)
7、如果最多只通知N次,如果都失败了的话,那么短信平台将不知道短信到底有没有成功发送
8、短信发送供应商需要提供一个查询接口,以方便短信平台驱动的去查询,进行定期校对
实现方案上一般有如下特点:
- 不可靠消息:
- 业务活动主动方,在完成业务处理之后,向业务活动的被动方发送消息,直到通知N次后不再通知,允许消息丢失(不可靠消息)。
- 定期校对:
- 业务活动的被动方,根据定时策略,向业务活动主动方查询(主动方提供查询接口),恢复丢失的业务消息。
分布式系列第二弹:分布式事务!
轻量级分布式事务实现:掌握最大努力通知方案
Saga
saga是一种纯业务补偿模式,其设计理念为,业务在调用的时候正常提交。当一个服务失败的时候,其所有依赖的上游服务都进行业务补偿操作。
saga把分布式事务看作一组本地事务构成的事务链:
-
事务链中的每一个正向事务操作,都对应一个可逆的事务操作
-
saga事务协调器负责按顺序执行事务链中的分支事务,分支事务执行完毕即释放资源。如果某个分支事务失败了,则按照反方向执行事务补偿操作。
执行流程
假如一个Saga的分布式事务链有n个分支事务构成,[T1,T2,…,Tn],那么该分布式事务的执行情况有三种:
- T1,T2,…Tn都执行成功
- T1,T2,…Ti,Ci,…,C2,C1:执行到第i(i<=n)个事务的时候失败了,则按照i->1的顺序依次调用补偿操作。如果补偿失败了就一直重试。
- T1,T2,…,TI(失败),Ti(重试),Ti(重试),Ti(重试),…,Tn:适用于事务必须成功的场景,如果发生失败了就一直重试,不会执行补偿操作。
实现上要注意的点:
- 允许空补偿
- 防悬挂控制
- 幂等控制
优劣势
- saga是不完美补偿,补偿操作会留下之前的原始提交的操作痕迹,需要考虑对业务的影响。
- 不能做到事务隔离,因为saga的正向流程已经提交了,所有的事务回滚操作都是基于当前最新版本的数据进行。很有可能出现补偿错误。