,,详解Java TCC分布式事务实现原理

,,详解Java TCC分布式事务实现原理

本文主要详细介绍Java TCC分布式事务的实现原理。对分布式事务感兴趣的同学一定要看看。

目录

业务场景概述,TCC分布式事务TCC实现的进一步思考TCC实现阶段1: TryTCC实现阶段2: ConfirmTCC实现阶段3:取消总结,思考实际生产中如何保证99.99%高可用可靠消息的最终一致性。最终一致性方案的核心流程上游服务传递消息下游服务接收消息如何保证上游服务100%可靠地传递消息?如何保证下游服务对消息的100%可靠接收?如何基于RocketMQ实现可靠消息的最终一致性方案?可靠消息最终一致性方案高可用性保障的生产实践背景介绍了一种基于KV存储的队列支持的高可用性降级方案。

概述

之前在网上看到很多关于分布式事务的文章,但大多只是简单介绍了分布式事务的各种技术方案。很多朋友还不知道分布式事务是怎么回事,以及如何在项目中使用。

因此,本文通过白话文手绘,结合一个电子商务系统的案例实践,将告诉你什么是TCC分布式事务。

业务场景介绍

让我们先来看看业务场景。假设您现在有一个带有订单支付场景的电子商务系统。

为订单付款后,我们需要执行以下步骤:

已更改订单的状态为已支付。

扣除商品库存

给会员加分。

创建销售出库单通知仓库发货。

这是一系列真实的步骤,不管你有没有做过电商系统,都应该可以理解。

进一步思考

好了,现在业务场景有了,我们要更进一步,实现TCC分布式事务的效果。

你什么意思?也就是说,[1]订单服务-修改订单状态,[2]库存服务-扣除库存,[3]积分服务-增加积分,[4]仓储服务-创建销售出库单。

这些步骤,如果他们一起成功或失败,必须是一个整体。

例如,现在订单的状态更改为已支付,库存服务无法扣除库存。那个商品的库存以前是100件,现在卖了两件,应该是98件。

结果如何?由于库存服务操作数据库异常,库存数量仍为100。这不是在骗人吗?当然,这是不能允许发生的!

但是如果不使用TCC分布式事务方案,可以用一个Spring Cloud开发这样一个微服务系统,很可能会做这样的事情。

我们来看下图,直观的表达了上述过程:

因此,有必要使用TCC分布式事务机制来确保所有服务形成一个整体事务。

以上步骤要么全部成功,如果任何一个服务失败,全部一起回滚,取消已完成的操作。

比如库存服务扣款失败,那么订单服务就要取消修改订单状态的操作,然后停止添加积分和通知发货这两个操作。

说了这么多,老规矩,还是给大家一张图吧。让我们沿着图片直观感受一下:

落地实现 TCC 分布式事务

那么现在如何实现一个TCC分布式事务,让所有服务一起成功呢?还是一起失败?

大家放轻松。我们一步步来分析吧。下面以一个Spring云开发系统为背景来说明一下。

TCC 实现阶段一:Try

首先,订单服务,它的代码应该大致是这样的:

公共类订购服务{

//库存服务

@自动连线

private inventory service inventory service;

//积分服务

@自动连线

私人信贷服务;

//仓储服务

@自动连线

私有WmsService wmsService

//完成此订单的付款

公共void pay(){

//将本地订单数据库的订单状态修改为“已支付”

order Dao . updatestatus(order status。PAYED);

//调用库存服务扣除库存

inventory service . reduce stock();

//调用积分服务来增加积分

credit service . add credit();

//调用仓储服务通知发货。

WMS service . sale delivery();

}

}

如果之前看过Spring Cloud架构原理的文章,对Spring Cloud有一定的了解,应该可以理解上面的代码。

实际上,订单服务完成本地数据库操作后,通过Spring Cloud的Feign调用其他服务。

但是光靠这段代码还不足以实现TCC分布式事务。兄弟们别急,我们来修改一下这个订单服务的代码。

首先,上面的订单服务将其状态更改为:OrderStatus.UPDATING。

这是什么意思?也就是说,在pay()方法中,不要直接将订单状态更改为已支付!首先,您将订单状态更改为正在更新,这意味着正在更改。

这个状态就是这样一个没有任何意义的状态,意味着有人在修改这个状态。

然后,在库存服务直接提供的reduceStock()接口中,不要直接扣除库存。你可以冻结库存。

比如本来你的库存是100,那就不要直接从100-2=98中扣除这个库存!

可以设置可售库存:100-2=98,这个没问题,然后在单独的冻结库存字段设置2。也就是说,有两只股票被冻结了。

服务的addCredit()接口也是如此。不要直接给用户加会员积分。您可以首先在点表中预先添加的点字段中添加点。

比如用户的积分本来是1190,现在要加10分,而不是直接1190 10=1200分!

可以把积分保持在1190。在预先添加的字段中,例如prepare_add_credit字段,设置10,表示有10个点要添加。

仓储服务的saleDelivery()接口也是如此。可以先创建销售出库单,但该销售出库单的状态为“未知”。

也就是说,这个销售出库单刚刚创建,此时状态还不确定!

上面修改接口的过程,其实就是所谓TCC分布式事务中第一个T字母所代表的阶段,也就是Try阶段。

总结一下上面的流程,如果你要实现一个TCC分布式事务,首先你的业务的主要流程和每个接口提供的业务含义不是指直接完成那个业务操作,而是完成一个Try操作。

这个操作,一般就是锁定一个资源,设置一个预备类的状态,冻结一些数据等等,大概就是这种操作。

我们来看看下面这张图,结合上面的文字,然后把整个过程捋一捋:

TCC 实现阶段二:Confirm

然后分为两种情况。第一种情况比较理想,就是每个服务执行自己的Try操作,并且全部执行成功,Bingo!

这时候就需要依靠TCC分布式事务框架来推动后续的执行。总之,想玩TCC分布式事务,必须引入一个TCC分布式事务框架,比如国内开源的ByteTCC,Himly,TCC-transaction。

否则,感知每一阶段的实施并推动下一阶段的实施太复杂,不太可能手工实现。

如果在每个服务中引入一个TCC分布式事务框架,那么嵌入在订单服务中的TCC分布式事务框架可以感知到每个服务的Try操作是成功的。

此时,TCC分布式事务框架将控制进入TCC的下一个阶段,即第一个C阶段,这是确认阶段。

为了实现这个阶段,您需要为每个服务添加更多的代码。比如在订单服务中,可以添加一个确认逻辑,即正式将订单的状态设置为“已支付”,大概是这样的:

公共类OrderServiceConfirm {

公共void pay(){

order Dao . updatestatus(order status。PAYED);

}

}

库存也差不多。您可以拥有一个InventoryServiceConfirm类,它提供了reduceStock()接口的确认逻辑。这里,先前冻结的存货字段中的两个存货被扣除为0。

这样之前可售库存早就变成98了,现在两个冻结库存都没了,库存的抵扣正式完成。

积分服务也类似。可以在积分服务中提供一个CreditServiceConfirm类,它有一个addCredit()接口的确认逻辑,就是在预加字段中扣除10个积分然后加到实际成员积分字段中,从1190变为1120。

仓储服务也类似。仓储服务中可以提供WmsServiceConfirm类,可以提供saleDelivery()接口的确认逻辑。销售出库单的状态可以正式变为已创建,仓储管理人员可以查看和使用,而不是停留在以前的未知的中间状态。

好了,上述服务的确认逻辑已经实现。一旦订单服务中的TCC分布式事务框架检测到每个服务的Try阶段成功,它将执行每个服务的确认逻辑。

服务内的TCC事务框架负责与其他服务内的TCC事务框架通信,依次调用每个服务的确认逻辑。然后,正式完成每个服务的所有业务逻辑的执行。

同样,给你一张图,跟着看全过程:

TCC 实现阶段三:Cancel

好吧,这是正常情况。如果是异常情况呢?

比如在Try阶段,比如点服务栏,它在执行中出现错误。这个时候会发生什么?

订单服务中的TCC事务框架是可以感知的,然后它会决定回滚整个TCC分布式事务。

也就是说,将执行每个服务的第二个C阶段,即取消阶段。类似地,为了实现这个取消阶段,每个服务都必须添加一些代码。

首先,订单服务要提供一个OrderServiceCancel类,其中有一个pay()接口的取消逻辑,即订单的状态可以设置为“已取消”,也就是这个订单的状态为已取消。

库存也是一样,可以提供reduceStock()的取消逻辑,即冻结库存减去2,再加回可售库存,98 ^ 2=100。

该服务还需要提供addCredit()接口的取消逻辑,以在预加积分字段中扣除10个积分。

仓储服务还需要提供saleDelivery()接口的取消逻辑,将销售出库单的状态修改为“已取消”并设置为已取消。

然后,一旦订单服务的TCC分布式事务框架感知到任何一个服务的Try逻辑失败,就会和每个服务中的TCC分布式事务框架进行通信,然后调用每个服务的Cancel逻辑。

看看下图,直观感受一下:

总结与思考

好了,伙计们,就这样。基本上大家都应该知道TCC分布式事务是怎么回事了吧!

综上所述,如果你想玩TCC分布式事务:首先你需要选择一个TCC分布式事务框架,这个TCC分布式事务框架会运行在各个服务中。

那么你原来的界面要转化成三个逻辑,尝试-确认-取消:

首先,服务调用链接依次执行Try逻辑。

如果一切正常,TCC分布式事务框架促进确认逻辑的执行以完成整个事务。

如果某个服务的Try逻辑出现问题,TCC分布式事务框架会在感知到后,推动每个服务的Cancel逻辑的执行,并取消之前的所有操作。

这就是所谓的TCC分布式事务。TCC分布式事务的核心思想,说白了就是当遇到以下情况时:

服务的数据库已关闭。

服务会自行挂起。

该服务的基础设施,如Redis、Elasticsearch、MQ,已关闭。

有些资源不足,比如库存不足。

先试试吧。不要把商业逻辑说完。先试一下,看看每个服务基本能不能正常运行,先冻结我需要的资源。

如果Try还可以,也就是说底层数据库,Redis,Elasticsearch,MQ都可以写数据,你预留了一些需要使用的资源(比如冻结一部分库存)。

然后执行每个服务的确认逻辑,基本上确认可以保证一个分布式事务大概率完成。

例如,如果服务在Try阶段失败,则底层数据库关闭,或者Redis关闭,等等。

此时会自动执行每个服务的取消逻辑,回滚之前的尝试逻辑。所有服务都不会执行任何设计好的业务逻辑。确保每个人要么成功,要么一起失败。

等一下,你心里有问题吗?如果出现意外情况,比如订单服务突然挂起,然后重启,TCC分布式事务框架如何保证之前未完成的分布式事务继续执行?

因此,TCC事务框架记录了一些分布式事务的活动日志,这些日志可以记录在磁盘上的日志文件中,也可以记录在数据库中。保存分布式事务操作的所有阶段和状态。

问题还没有结束。如果服务的取消或确认逻辑执行总是失败,该怎么办?

那也很简单。TCC事务框架将通过活动日志记录每个服务的状态。例如,如果您发现服务的取消或确认没有成功,您将尝试一次又一次地调用它的取消或确认逻辑。确保成功!

当然,如果你的代码没有写任何bug,有充分的测试,基本上在尝试阶段就尝试了,那么一般确认和取消都可以成功!

最后再给大家一张图,看看我们的业务加入分布式事务后的整个执行过程:

很多大公司其实都是自己开发TCC分布式交易框架,专门在公司内部使用。比如我们是这样的。

但是如果你的公司不开发TCC分布式事务框架,一般会选择开源框架。

这里推荐几个好的框架,国内都是开源的:ByteTCC,TCC-transaction,Himly。

有兴趣的可以去他们的GitHub地址,学习如何使用,如何与Spring Cloud、Dubbo等服务框架集成。

只要把那些框架集成到你的系统中,就很容易达到上面那种梦幻般的TCC分布式事务效果。

现在来说说可靠消息最终一致性方案实现的分布式事务,说说我们在实际生产中遇到的高可用性保障架构。

最终一致性分布式事务如何保障实际生产中 99.99% 高可用

我们来谈谈TCC分布式事务。对于常见的微服务系统,大多数接口调用是同步的,即一个服务直接调用另一个服务的接口。

此时,使用TCC分布式事务方案来确保所有接口都被成功调用或一起回滚是比较合适的。

然而,在实际系统的开发过程中,服务之间的调用可能是异步的。即一个服务向MQ发送一条消息,即消息中间件,如RocketMQ、RabbitMQ、Kafka、ActiveMQ等。

然后,另一个服务使用来自MQ的消息并处理它。这变成了基于MQ的异步调用。

那么,对于这种基于MQ的异步调用,如何保证服务之间的分布式事务呢?也就是说,我要的是基于MQ实现异步调用多个服务的业务逻辑,要么一起成功,要么一起失败。

这时就需要使用可靠的消息最终一致性方案来实现分布式事务。

从上图可以看出,如果不考虑高并发、高可用等各种技术挑战,仅从“消息可靠”和“最终一致性”的角度来看,这种分布式事务方案还是比较简单的。

可靠消息最终一致性方案的核心流程

上游服务投递消息

如果想实现可靠消息的最终一致性方案,一般可以自己编写一个可靠的消息服务,实现一些业务逻辑。

首先,上游服务需要向可靠的消息服务发送消息。说白了,你可以把这个消息看作是对下游服务的一个接口的调用,这个调用包含了一些相应的请求参数。

然后,可靠的消息服务必须将该消息以“待确认”的状态存储在它自己的数据库中。

然后,上游服务可以执行自己的本地数据库操作,并根据自己的执行结果再次调用可靠消息服务的接口。

如果本地数据库操作成功执行,那么就要找到一个可靠的消息服务来确认该消息。如果本地数据库操作失败,那么找到一个可靠的消息服务来删除该消息。

如果是确认消息,可靠消息服务会将数据库中的消息状态更新为“sent ”,并将消息发送到MQ。

这里的一个关键点是更新数据库中的消息状态,并将消息发送到MQ。这两个操作,你要把它们放在一个方法里,你要启动本地事务。

你什么意思?如果在数据库中更新消息状态失败,那么抛出异常并退出,不要发布到MQ;如果MQ交付失败并报告了一个错误,将抛出一个异常来回滚本地数据库事务。这两项行动必须同时成功或同时失败。

如果上游服务是通知删除消息,则可靠消息服务必须删除该消息。

下游服务接收消息

下游服务一直在等待使用来自MQ的消息,如果它们这样做了,它们将操作它们的本地数据库。

如果操作成功,则可靠消息服务将被告知其处理成功,然后可靠消息服务将消息的状态设置为“已完成”。

如何保证上游服务对消息的 100% 可靠投递?

上面的核心流程大家都看了:一个很大的问题是,如果上面的消息传递流程各个环节都出现了问题,怎么办?

如何确保消息100%可靠的传递,消息一定会从上游服务传递到下游服务?别急,我们一个一个来分析。

如果上游服务错误地将待确认的消息发送给可靠的消息服务,也没关系。上游服务可以感知到调用异常,所以不用执行后面的流程,没问题。

如果上游服务在操作本地数据库后通知可靠消息服务确认消息或删除消息,则存在问题。

例如,通知不成功,或者执行不成功,或者可靠消息服务无法将消息传递给MQ。这一系列步骤出了问题怎么办?

其实没关系,因为在这些情况下,那条消息在可靠消息服务的数据库中的状态永远是“待确认”。

此时,我们在可靠消息服务中开发一个定时在后台运行的线程,不断检查每条消息的状态。

如果一直处于“待确认”状态,这个消息就有问题。此时你可以回调上游服务提供的一个接口,问兄弟,你成功执行了这条消息对应的数据库操作了吗?

如果上游服务回复我的执行成功,可靠消息服务会将消息状态更改为“已发送”,并将消息交付给MQ。

如果上游服务回复执行失败,可靠消息服务可以删除数据库中的消息。

通过这种机制,可以保证可靠的消息服务会尝试将消息传递给MQ。

如何保证下游服务对消息的 100% 可靠接收?

下游服务消费消息有问题,没有消费怎么办?或者下游服务无法处理消息。我该怎么办?

其实无所谓。在可靠消息服务中开发一个后台线程,以不断检查消息状态。

如果消息状态为“已发送”且从未更改为“已完成”,则下游服务从未被成功处理。

此时,可靠的消息服务可以尝试再次将消息重新传递给MQ,并让下游服务再次处理它。

只要下游服务的接口逻辑是幂等的,就保证一个消息会被多次处理,而不会插入重复的数据。

如何基于 RocketMQ 来实现可靠消息最终一致性方案?

在上述总体方案设计中,完全依靠可靠消息服务的各种自检机制来确保:

如果上游服务的数据库操作不成功,下游服务将不会收到任何通知。

如果上游服务的数据库操作成功,可靠的消息服务将确保一个调用消息被传递到下游服务,下游服务肯定会成功地处理这个消息。

通过这种机制,保证了基于MQ的异步调用/通知服务之间的分布式事务保证。其实阿里开源的RocketMQ已经实现了可靠消息服务的所有功能,其核心思想和上面类似。

但是为了保证高并发、高可用、高性能,RocketMQ做了更复杂的架构实现,非常优秀。感兴趣的同学可以查看RocketMQ对分布式事务的支持。

可靠消息最终一致性方案的高可用保障生产实践

背景引入

上面的方案和思路很多同学应该都知道是怎么回事,我们主要是为这套理论思路做铺垫。

在实际生产中,如果没有高并发的场景,可以参考上面的思路,基于一个MQ中间件开发一个可靠的消息服务。

如果有高并发的场景,可以使用RocketMQ的分布式事务支持。以上过程都可以实现。

今天跟大家分享的一个核心主题就是如何保证这个解决方案99.99%的高可用性。

您应该已经发现,在这个方案中确保高可用性的最大依赖之一是MQ的高可用性。

任何MQ中间件都有一套完整的高可用性保障机制,无论是RabbitMQ、RocketMQ还是Kafka。

因此,在大公司使用可靠消息的最终一致性方案时,我们通常依赖于公司基础设施团队对MQ的高可用性保证。

也就是说,大家要相信兄弟团队,99.99%可以保证MQ的高可用性,绝对不会因为MQ集群整体宕机而导致公司业务系统的分布式事务无法运行。

但现实是残酷的。很多中小型公司,甚至一些大中型公司,都或多或少遇到过MQ集群整体失效的场景。

一旦MQ完全不可用,将导致业务系统的服务之间无法通过MQ传递消息,导致业务流程中断。

比如最近一个朋友的公司,也是做电子商务的,就遇到了MQ中间件部署在自己公司机器上的整个集群出现故障,不可用的情况,导致所有依赖MQ的分布式事务失败,大量业务流程中断。

在这种情况下,有必要为这种分布式事务方案实现一套高可用性保证机制。

基于 KV 存储的队列支持的高可用降级方案

我们来看看下图,是我朋友的一家公司为可靠信息的最终一致性方案设计的一套高可用性保障降级机制。

这个机制并不太复杂,保证那个朋友公司的高可用性保障场景,可以非常简单有效。一旦MQ中间件出现故障,就会自动降级为备份解决方案。

自行封装MQ客户端组件和故障感知。

首先,要想自动感知MQ的故障,然后自动降级,就必须将MQ客户端打包发布到公司Nexus私有服务器上。

然后公司需要支持MQ降级的业务服务都使用这个自封装的组件向MQ发送消息,消费来自MQ的消息。

在自己封装的MQ客户端组件中,可以根据编写MQ的情况来判断MQ是否有故障。

比如你连续10次尝试向MQ传递消息,发现异常错误,网络连接不上,说明MQ出故障了。这时候就可以自动感应,自动触发降级开关。

基于队列的KV存储降级方案

如果您想在MQ挂起后继续传递消息,您必须找到MQ的替代者。

比如我朋友的公司没有高并发场景,消息量小,但是可用性要求高。这时可以用Redis这样的KV存储中的队列来代替。

因为Redis本身支持队列的功能和各种类似队列的数据结构,所以可以将消息写入KV存储格式的队列数据结构中。

PS:请查阅Redis的数据存储格式、支持的数据结构等基础知识。网上有很多。

不过,这里有几个大坑,一定要注意:

第一,建议不要在任何KV存储的聚合数据结构中写入过多的数据,否则会导致大值的发生,从而导致严重的后果。

所以千万不要在Redis里做一个Key,想尽办法不停的往这个数据结构里写消息,这肯定是不行的。

其次,绝对不能连续的将数据写入到几个键对应的数据结构中,这样会导致热键的产生,也就是有些键特别热。

众所周知,一般情况下,KV集群是根据Key通过Hash分配到各个机器上的。如果一直写几个键,会导致KV集群中一台机器访问过高,过载。

基于以上考虑,以下是作者当时设计的方案:

根据他们每天的消息量,在KV存储中被分成几百个队列,对应的键有几百个。这样就保证了每个键对应的数据结构中不会写入太多的消息,少数键不会被频繁写入。一旦MQ故障发生,可靠消息服务可以通过Hash算法将每条消息统一写入几百个键对应的固定KV存储队列中。

同时,需要通过ZK触发降级开关,使整个系统在MQ中的所有读写立即降级。

下游服务消费MQ降级感知

下游服务消费MQ也是通过自封装组件完成的。此时,如果该组件感知到降级开关从ZK打开,它将首先判断是否可以继续消耗来自MQ的数据?

如果没有,启动多线程,从KV存储中的数百个预设队列中获取数据。

每获得一条数据,就交给下游服务的业务逻辑去执行。通过该机制,实现了MQ故障的自动感知和自动降级。如果系统的负载和并发不是很高,使用这种方案一般没问题。

因为在生产落地的过程中,包括大量的容灾演练和实际生产故障的表现,可以有效保证MQ故障时业务流程继续自动运行。

故障自动恢复。

如果自封装组件在降级开关打开后需要启动一个线程,可以尝试每隔一段时间向MQ发布一条消息,看看是否恢复。

如果MQ可以正常传递消息,那么降级开关可以被ZK关闭,然后可靠的消息服务可以继续向MQ传递消息。在确认KV中存储的每个队列中没有数据之后,下游服务可以再次切换到使用来自MQ的消息。

更多业务详情

以上方案是一般的降级方案,但具体的落地是结合各个公司不同的业务细节来确定的,很多细节在文章中是体现不出来的。

比如要不要保证新闻的秩序?是否涉及到需要根据业务动态生成大量密钥?等一下。

另外,实现这个解决方案还是有一定成本的,所以建议你尽量做推送公司的基础架构团队,保证MQ 99.99%的可用性,不停机。

其次,是根据贵公司对高可用性的实际需求决定的。如果你觉得MQ偶尔降级是可以忍受的,那么你就不必实施这个降级方案。

但是,如果公司领导认为MQ中间件宕机后业务系统流程必须继续运行,那么就要考虑一些高可用的降级方案,比如本文提到的方案。

最后,如果一些公司涉及到每秒上万个高并发请求,那么针对MQ的降级方案会更加复杂,远非简单。

以上是Java TCC分布式事务实现原理的详细说明。更多关于Java TCC分布式事务的信息,请关注我们的其他相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: