分布式事务项目实践

首先本人关于分布式事务的基础知识来自于吴水成老师的分布式事务教程。最近在项目上也做了一些实践,因此准备将在项目中的相关经验记录下来。

先讲一下项目的事务架构:Kafka消息队列作为中间的消息传递,redis存储热消息,HBase存储全量消息,分布式服务使用dubbo。在项目中主要使用的事务处理就是最终一致性、最大努力通知和TCC,其中最终一致性使用比较普遍,最大努力通知一般用于和第三方对接的情况比较多,TCC适合数据可靠、实时性要求高的比如涉及金钱或者高风险的数据。

项目中消息系统作为连接各个服务的中间层,原本在dubbo中,A服务调用B服务是通过直接通过B服务的接口调用的,实际上也是通过dubbo的协议进行通信的,而现在则是通过消息中间层,A服务发送消息到Kafka的某个Topic,每一种服务通过key标识,B服务则通过及时消费Kafka的数据进行处理逻辑。关于Kafka的快速处理可以参考我的另一篇文章(Kafka的项目实践)

这里我讲一下消息中间层的做法,下面再根据这个中间层实现最终一致性和最大努力通知。
消息中间层包括消息状态机、缓存和全量存储。消息在中间层的状态变更,使用状态模式实现,同时消息保存一份到redis,时效为1小时,用于幂等性校验,另存一份到HBase,作为消息的全量存储。这里提一下,凡是涉及缓存的,都会可能出现缓存双写一致性和缓存重建并发冲突问题,关于缓存模块的问题我会另外再写一篇进行说明,这里的主题还是事务问题。
这里的消息中间层最重要的是持久化消息,确保所有消息都能被及时消费。

最终一致性的实现

下面借用一张图展示相应的做法:
分布式事务项目实践_第1张图片

如果是过程中出现的失败的消息,会在系统中提供单独的页面展示,以便进行手工重发。被动方应用处理消息之前,先根据消息的key在redis里面查找,如果没有重复的才开始处理逻辑,这个就是幂等性校验的做法。

最大努力通知的实现

1、首先定义通知规则,比如第一次通知后,1分钟才通知第二次,第二次通知之后2分钟才通知第三次,设计阶梯式通知规则。2、将消息依次放入延时队列,对象只能在其到期时才能从队列中取走。

public static DelayQueue tasks = new DelayQueue();

3、每次通知之后都修改通知次数和最后通知时间。4、通知成功之后修改该消息状态。
分布式事务项目实践_第2张图片

TCC型事务

TCC分布式事务框架的简单介绍:TCC框架通过可补偿、事务上下文2个AOP,将try、confirm、cancel方法进行拦截,分别进行创建事务和保存try、confirm、cancel的传入参数和方法名,try阶段失败就重新恢复try,一旦try成功,只能进行confirm或cancel,如果出现异常,则主服务会分别反射调用从服务的cancel方法。
分布式事务项目实践_第3张图片

项目实践的总结

下面讲一下在项目中的踩过的一些坑,也算是一些经验总结吧。
1、消息中间层的消息格式重新封装
由于各个服务之间通过消费消息才执行业务,导致消息的种类越来越多,并且无法从消息中知道下一个的调用者。后面我在每个消息中加上了将要调用的类、方法名以便反射调用,并且将Kafka消费者用线程池方式实现,形成一个工具jar包,每个服务只需要引入即可,不会影响被调用服务的代码。这样既和被调用服务解耦,又没有代码侵入。

2、获取资源型数据
项目中有一个业务需要从其他服务拉取资源,比如从主机服务拉取主机信息,从用户服务拉取凭证等,只有这些资源全部拉取成功之后才能执行该业务逻辑。由于业务的执行是同步进行的,所以不能使用异步型的最终一致性解决方案,只有TCC可用,但另一方面TCC必须将每个拉取资源的方法拆分为3个,而且confirm和cancel在这里是不需要的,造成一定的资源浪费,所以最终我选择参考TCC实现简易型的只有try的解决方案。
设计思路如下:
(1)创建一个AOP(环绕通知),进入拦截方法之前,将事务相关的传入参数、类路径和方法名全部进行持久化,然后开始进入主服务逻辑。
(2)主服务调用从服务拉取资源信息时,同样先进入从服务的拦截器中,持久化参数、类和方法名并把从服务加入到主服务的事务中,作为主事务的一部分,即分支事务。所以主服务的机器上面保存了所有从服务的分支事务。
(3)然后才真正进入从服务的机器上面进行业务处理。从服务处理完成后,删除该分支事务的信息。
(4)独立一个线程作为事务恢复,即是这个过程中,主服务执行失败会立刻回滚,其他的不管哪个从服务执行失败都可以从持久化的数据中获取相应的参数、类和方法名,并在主服务中进行反射调用。

暂时先想到这些,后续发现了新的再继续补充。

你可能感兴趣的:(分布式系统)