分布式事务可以说是在分布式架构系统中,比较棘手的问题了.如果根据业务场景制定相应的事务,如利用消息的事务最终一致性解决方案,XA等2PC强一致方案等等.今天就列举一下相关解决方案;
1.根据同步结果保证事务
注:该种方式适用于单一的调用其他服务,利用业务执行顺序,根据同步结果保证事务
| |
这种方式也存在一定弊端,如虽然调用其他服务成功,但是最终本地事务提交不成功,造成不一致;调用服务超时,本地异常回滚,但是最终调用的服务成功提交了,造成不一致;还需要衡量使用;
2.使用XA等强一致事务
注:该种方式适用于对数据实时一致性要求比较高,对业务的并发量要求不高;
| |
3.使用Spring的链式事务
注:该种方式适用于对业务的并发量要求不高,数据实时性高,但是允许出现数据不一致的少数情况产生;其内部实现其实是将多个事务进行迭代事务提交,最大努力单阶段提交模式;
| |
4.使用相关开源框架
例如使用阿里的SEATA框架,它提供了多种事务公式;
1.AT模式:
| |
1)它对业务代码的侵入性很小,只需简单配置:如更改事务注解,修改数据库连接池为相关代理,数据库层面新建管理器需要的表和各服务需要的日志表;
2)它使用的是二阶段提交方式,一阶段提交业务和日志的本地事务,二阶段根据全局事务管理器进行全局回滚或提交;
3)它的原理大致是这样:
首先根据事务管理器生成XID事务唯一编码,并保存在当前线程上下文中等待传递;
在本地服务中,利用代理数据库连接池解析执行的语句,生成前后SQL镜像,及相应的行锁BranchID,本地事务提交保存,用来后面的回滚;
在调用外部服务时,如使用Feign,则使用请求拦截器传递XID到其他服务,其他服务在收到请求时如Http协议则根据Header获取到XID也保存到当前线程上下文保存;
当一阶段结束,则全局事务管理器执行二阶段提交,回滚则根据各服务日志进行回滚,提交则批量删除日志的前后镜像及行锁等信息,其中客户端资源管理器和独立的事务协调器使用Netty通信;
4)看以上能看出,它在一次事务中,会多次访问数据库;
2.TCC模式:
1)它对业务代码的侵入性就比较大了,类似于下面代码;它与AT模式的不同之处在于提交与回滚不依托于数据库,而是业务自定义;
| |
3.Saga模式:
1)Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
5.使用消息队列框架实现最终一致性
注:该种方式设计得当有很高并发度,以及很高灵活度,但是也因为灵活度太高,也会造成业务侵入高,以及设计编写难度较高;
1.使用RocketMQ事务消息
1)设置本地事务状态,以及回查监听:
当本地事务中调用发送消息后,首先会发送一个半消息到Broker,此时对消费端是隐藏的;当半消息发送成功后后,会回调本地事务方法,然后根据本地执行事务的成功与否,进行提交消息状态,当提交COMMIT_MESSAGE则消费端可以接收到消息;
当本地事务结束,没有正常提交半消息状态,则Broker会根据半消息回查监听接口,确定半消息状态;默认第一次回查在本地结束后15s,往后一直每分钟回查一次;
| |
2)调用服务:
| |
3)通过事务消息给我们提供的半消息以及回查机制,我们就可以灵活的控制分布式事务的提交;
2.使用RabbitMQ消息确认机制
1)开启Confirm模式后,当消息未能成功发送到Broker,则回调confirm监听接口告知生产者;
2)开启Return消息机制后,当消息发送到指定路由或队列失败,则回调告知生产者;
3)开启手动确认消息,保障消息能够成功发送到消费者;
4)当使用Spring提供的事务管理器时,RabbitMQ能够保证在本地事务提交成功后才发送消息,通过设置也可实现提交失败也发送消息;底层使用的Channel开启了事务信道的支持;经测试ActiveMQ等都支持此特性;
3.在使用消息队列保证事务一致性时,可以使用定时方法等协助消息的发送
例如:当使用RabbitMQ发送消息时在本地事务中增加唯一性消息日志,在confirm方法中更新发送状态;然后使用定时器扫描未发送消息进行重发;
4.设计保证幂等性的接口(不限于消费者,不同场景接口幂等处理方式)
1) 查询状态,适用于并发很低的情况并且对重复处理有一定容忍度,比如(伪代码):
| |
2)可以使用消息日志方式,保存消息记录,进而保证幂等性,例如如下方式:
其实在insert的时候,由于在事务中,所以会触发数据库的排他锁,并发的线程会等待第一个线程事务提交结束;
| |
3)悲观锁,比如Java中的synchronized,简单粗暴;
4)乐观锁,再表中加入version等字段,每次修改数据库时进行判断提交时版本;该种方式可与第一种查询方法一起使用;
5)利用redis等存储一次性token,比如在提交页面时获取后端唯一token并存储,消费时删除token进行唯一页面提交;
6)状态机幂等,例如处理一个订单,会涉及不同状态,并且处理订单不同业务都受订单状态影响;
| |
7)还是状态机幂等,加入方式2的日志表方式:
| |
在同一事务下,订单处理插入状态表,通过唯一键保障该业务幂等;
8)尽可能将业务设计为不会出现幂等性的方式:
| |
9)尽量的避免程序中引入分布式事务问题
有时候不要跟风将系统拆分成类似互联网大公司的微服务,拆的非常细,在链路调用中会有成倍的问题问题出现,耗费非常大的精力解决;
可以结合业务场景,性能要求等,以业务流的方式拆分,例如:
以前下单这块涉及的模块想拆成:库存中心,订单中心,余额中心,分别至少三个库不同服务的调用;
现在根据业务流将以上三个合为一,充分使用数据库的本地事务来避免事务问题,一劳永逸;将其他非强一致事务或对一致性要求不高的业务拆分服务及数据库;然后使用上述分布式事务方式进行实践;
其实,在业务中,分布式事务终归是一个非常难以有统一解决方案的问题,只有最适合自己业务的方案;