前言碎语

楼主之前推荐过2pc的分布式事务框架LCN。今天来详细聊聊TCC事务协议。

2pc实现:https://github.com/codingapi/tx-lcn

tcc实现:https://github.com/yu199195/hmily

首先我们了解下什么是tcc,如下图

tcc分布式事务协议控制整体业务事务分为三个阶段。

try:执行业务逻辑

confirm:确定业务逻辑执行无误后,确定业务逻辑执行完成

cancel:假如try阶段有问题,执行cancel阶段逻辑,取消try阶段的数据

这就需要我们在设计业务时,在try阶段多想想业务处理的折中状态,比如,处理中,支付中,进行中等,在confirm阶段变更为处理完成,或者在cancel阶段变更为处理失败。

下面以电商下单为例子:.

假设我们有一个电商下单的业务,有三个服务组成,订单服务处理下单逻辑,库存服务处理减库存逻辑,支付服务处理减账户余额逻辑。在下单服务里先后调用减库存和减余额的方法。如果使用tcc分布式事务来协调事务,我们服务就要做如下设计:

订单服务:

  • try:支付状态设置为支付中
  • confirm:设置为支付完成
  • cancel:设置为支付失败

库存服务:

多加一个锁定库存的字段记录,用于记录业务处理中状态

  • try:总库存-1,锁定库存+1
  • confirm:锁定库存-1
  • cancel:总库存+1,锁定库存-1

支付服务:

多加一个冻结金额的字段记录,用于记录业务处理中状态

  • try:余额-1,冻结金额+1
  • confirm:冻结金额-1
  • cancel:余额+1,冻结金额-1

tcc分布式事务在这里起到了一个事务协调者的角色。真实业务只需要调用try阶段的方法。confirm和cancel阶段的额方法由tcc框架来帮我们调用完成最终业务逻辑。下面我们假设如下三个场景的业务情况,看tcc如何协调业务最终一致的。

  1. 服务一切正常:所有服务的try方法执行后都没有问题,库存足够,余额足够。tcc事务协调器会触发订单服务的confirm方法,将订单更新为支付完成,触发库存服务的confirm方法锁定库存-1,触发支付服务的confirm方法冻结金额-1
  1. 库存服务故障,无法调通:这个时候订单已经生成,状态为待支付。当调用库存超时抛异常后,tcc事务协调器会触发订单服务的cancel方法将订单状态更新为支付失败。
  1. 支付服务故障,无法调通:这个时候订单已经生成,状态为待支付,总库存-1,锁定库存+1了。当调用支付服务超时抛异常时,tcc事务协调器会触发订单服务的cancel方法将订单状态更新为支付失败,触发库存服务的cancel方法将库存+1,锁定库存-1。

hmily事务框架怎么做的?

通过上面对tcc事务协议说明大家应该都了解了tcc的处理协调机制,下面我们来看看hmily是怎么做到的,我们以接入支持dubbo服务为例。

概要:首先最基础两个应用点是aop和dubbo的filter机制,其次针对一组事务,定义了启动事务处理器,参与事务处理器去协调处理不同的事务单元。外加一个disruptor+ScheduledService处理事务日志,补偿处理失败的事务。

hmily框架以@Hmily注解为切入点,定义了一个环绕织入的切面,注解必填两个参数confirmMethod和cancelMethod,也就是tcc协调的两个阶段方法。在需要tcc事务的方法上面加上这个注解,也就托管了tcc三个阶段的处理流程。下面是aspect切面的抽象类,不同的RPC框架支持会有不同的实现 。其中真正处理业务逻辑需要实现HmilyTransactionInterceptor接口

@Aspect
public abstract class AbstractHmilyTransactionAspect { private HmilyTransactionInterceptor hmilyTransactionInterceptor; protected void setHmilyTransactionInterceptor(final HmilyTransactionInterceptor hmilyTransactionInterceptor) {
this.hmilyTransactionInterceptor = hmilyTransactionInterceptor;
} /**
* this is point cut with {@linkplain Hmily }.
*/
@Pointcut("@annotation(org.dromara.hmily.annotation.Hmily)")
public void hmilyInterceptor() {
} /**
* this is around in {@linkplain Hmily }.
* @param proceedingJoinPoint proceedingJoinPoint
* @return Object
* @throws Throwable Throwable
*/
@Around("hmilyInterceptor()")
public Object interceptTccMethod(final ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
return hmilyTransactionInterceptor.interceptor(proceedingJoinPoint);
} /**
* spring Order.
*
* @return int
*/
public abstract int getOrder();
}

dubbo的aspect抽象实现

@Aspect
@Component
public class DubboHmilyTransactionAspect extends AbstractHmilyTransactionAspect implements Ordered { @Autowired
public DubboHmilyTransactionAspect(final DubboHmilyTransactionInterceptor dubboHmilyTransactionInterceptor) {
super.setHmilyTransactionInterceptor(dubboHmilyTransactionInterceptor);
} @Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}

dubbo的HmilyTransactionInterceptor实现

@Component
public class DubboHmilyTransactionInterceptor implements HmilyTransactionInterceptor { private final HmilyTransactionAspectService hmilyTransactionAspectService; @Autowired
public DubboHmilyTransactionInterceptor(final HmilyTransactionAspectService hmilyTransactionAspectService) {
this.hmilyTransactionAspectService = hmilyTransactionAspectService;
} @Override
public Object interceptor(final ProceedingJoinPoint pjp) throws Throwable {
final String context = RpcContext.getContext().getAttachment(CommonConstant.HMILY_TRANSACTION_CONTEXT);
HmilyTransactionContext hmilyTransactionContext;
//判断dubbo上下文中是否携带了tcc事务,如果有就取出反序列化为事务上下文对象
if (StringUtils.isNoneBlank(context)) {
hmilyTransactionContext = GsonUtils.getInstance().fromJson(context, HmilyTransactionContext.class);
RpcContext.getContext().getAttachments().remove(CommonConstant.HMILY_TRANSACTION_CONTEXT);
} else {
//如果dubbo上下文中没有,就从当前上下文中获取。如果是事务发起者,这里其实也获取不到事务
hmilyTransactionContext = HmilyTransactionContextLocal.getInstance().get();
}
return hmilyTransactionAspectService.invoke(hmilyTransactionContext, pjp);
} }

这里主要判断了dubbo上下文中是否携带了tcc事务。如果没有就从当前线程上下文中获取,如果是事务的发起者,这里其实获取不到事务上下文对象的。在invoke里有个获取事务处理器的逻辑,如果事务上下文入参 为null,那么获取到的就是启动事务处理器。启动事务处理器处理逻辑如下

public Object handler(final ProceedingJoinPoint point, final HmilyTransactionContext context)
throws Throwable {
System.err.println("StarterHmilyTransactionHandler");
Object returnValue;
try {
HmilyTransaction hmilyTransaction = hmilyTransactionExecutor.begin(point);
try {
//execute try
returnValue = point.proceed();
hmilyTransaction.setStatus(HmilyActionEnum.TRYING.getCode());
hmilyTransactionExecutor.updateStatus(hmilyTransaction);
} catch (Throwable throwable) {
//if exception ,execute cancel
final HmilyTransaction currentTransaction = hmilyTransactionExecutor.getCurrentTransaction();
executor.execute(() -> hmilyTransactionExecutor
.cancel(currentTransaction));
throw throwable;
}
//execute confirm
final HmilyTransaction currentTransaction = hmilyTransactionExecutor.getCurrentTransaction();
executor.execute(() -> hmilyTransactionExecutor.confirm(currentTransaction));
} finally {
HmilyTransactionContextLocal.getInstance().remove();
hmilyTransactionExecutor.remove();
}
return returnValue;
}

真正业务处理方法,point.proceed();被try,catch包起来了,如果try里面的方法出现异常,就会走hmilyTransactionExecutor.cancel(currentTransaction)的逻辑,如果成功,就走hmilyTransactionExecutor.confirm(currentTransaction)逻辑。其中cancel和confirm里都有协调参与者事务的处理逻辑,以confirm逻辑为例。

public void confirm(final HmilyTransaction currentTransaction) throws HmilyRuntimeException {
LogUtil.debug(LOGGER, () -> "tcc confirm .......!start");
if (Objects.isNull(currentTransaction) || CollectionUtils.isEmpty(currentTransaction.getHmilyParticipants())) {
return;
}
currentTransaction.setStatus(HmilyActionEnum.CONFIRMING.getCode());
updateStatus(currentTransaction);
final ListhmilyParticipants = currentTransaction.getHmilyParticipants();
ListfailList = Lists.newArrayListWithCapacity(hmilyParticipants.size());
boolean success = true;
if (CollectionUtils.isNotEmpty(hmilyParticipants)) {
for (HmilyParticipant hmilyParticipant : hmilyParticipants) {
try {
HmilyTransactionContext context = new HmilyTransactionContext();
context.setAction(HmilyActionEnum.CONFIRMING.getCode());
context.setRole(HmilyRoleEnum.START.getCode());
context.setTransId(hmilyParticipant.getTransId());
HmilyTransactionContextLocal.getInstance().set(context);
executeParticipantMethod(hmilyParticipant.getConfirmHmilyInvocation());
} catch (Exception e) {
LogUtil.error(LOGGER, "execute confirm :{}", () -> e);
success = false;
failList.add(hmilyParticipant);
} finally {
HmilyTransactionContextLocal.getInstance().remove();
}
}
executeHandler(success, currentTransaction, failList);
}
}

可以看到executeParticipantMethod(hmilyParticipant.getConfirmHmilyInvocation()),这里执行了事务参与者的confirm方法。同理cancel里面也有类似代码,执行事务参与者的cancel方法。那么事务参与者的信息是怎么获取到的呢?我们需要回到一开始提到的dubbo的filter机制。

@Activate(group = {Constants.SERVER_KEY, Constants.CONSUMER})
public class DubboHmilyTransactionFilter implements Filter { private HmilyTransactionExecutor hmilyTransactionExecutor; /**
* this is init by dubbo spi
* set hmilyTransactionExecutor.
*
* @param hmilyTransactionExecutor {@linkplain HmilyTransactionExecutor }
*/
public void setHmilyTransactionExecutor(final HmilyTransactionExecutor hmilyTransactionExecutor) {
this.hmilyTransactionExecutor = hmilyTransactionExecutor;
} @Override
@SuppressWarnings("unchecked")
public Result invoke(final Invoker invoker, final Invocation invocation) throws RpcException {
String methodName = invocation.getMethodName();
Class clazz = invoker.getInterface();
Class[] args = invocation.getParameterTypes();
final Object[] arguments = invocation.getArguments();
converterParamsClass(args, arguments);
Method method = null;
Hmily hmily = null;
try {
method = clazz.getMethod(methodName, args);
hmily = method.getAnnotation(Hmily.class);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
if (Objects.nonNull(hmily)) {
try {
final HmilyTransactionContext hmilyTransactionContext = HmilyTransactionContextLocal.getInstance().get();
if (Objects.nonNull(hmilyTransactionContext)) {
if (hmilyTransactionContext.getRole() == HmilyRoleEnum.LOCAL.getCode()) {
hmilyTransactionContext.setRole(HmilyRoleEnum.INLINE.getCode());
}
RpcContext.getContext().setAttachment(CommonConstant.HMILY_TRANSACTION_CONTEXT, GsonUtils.getInstance().toJson(hmilyTransactionContext));
}
final Result result = invoker.invoke(invocation);
//if result has not exception
if (!result.hasException()) {
final HmilyParticipant hmilyParticipant = buildParticipant(hmilyTransactionContext, hmily, method, clazz, arguments, args);
if (hmilyTransactionContext.getRole() == HmilyRoleEnum.INLINE.getCode()) {
hmilyTransactionExecutor.registerByNested(hmilyTransactionContext.getTransId(),
hmilyParticipant);
} else {
hmilyTransactionExecutor.enlistParticipant(hmilyParticipant);
}
} else {
throw new HmilyRuntimeException("rpc invoke exception{}", result.getException());
}
return result;
} catch (RpcException e) {
e.printStackTrace();
throw e;
}
} else {
return invoker.invoke(invocation);
}
} @SuppressWarnings("unchecked")
private HmilyParticipant buildParticipant(final HmilyTransactionContext hmilyTransactionContext,
final Hmily hmily,
final Method method, final Class clazz,
final Object[] arguments, final Class... args) throws HmilyRuntimeException { if (Objects.isNull(hmilyTransactionContext)
|| (HmilyActionEnum.TRYING.getCode() != hmilyTransactionContext.getAction())) {returnnull;}//获取协调方法String confirmMethodName = hmily.confirmMethod();if(StringUtils.isBlank(confirmMethodName)){
confirmMethodName = method.getName();}String cancelMethodName = hmily.cancelMethod();if(StringUtils.isBlank(cancelMethodName)){
cancelMethodName = method.getName();}HmilyInvocation confirmInvocation =newHmilyInvocation(clazz, confirmMethodName, args, arguments);HmilyInvocation cancelInvocation =newHmilyInvocation(clazz, cancelMethodName, args, arguments);//封装调用点returnnewHmilyParticipant(hmilyTransactionContext.getTransId(), confirmInvocation, cancelInvocation);}privatevoid converterParamsClass(finalClass[] args,finalObject[] arguments){if(arguments ==null|| arguments.length <1){return;}for(int i =0; i < arguments.length; i++){
args[i]= arguments[i].getClass();}}}

需要注意三个地方。

  1. 一个是filter的group定义@Activate(group = {Constants.SERVER_KEY, Constants.CONSUMER}),这里这样定义后,就只有服务的消费者会生效,也就是事务的发起者,服务的调用方会进filter的invoke逻辑。
  1. 只有加@Hmily注解的方法或进事务处理逻辑,其他的方法直接跳过处理
  1. 最关键的是buildParticipant(hmilyTransactionContext, hmily, method, clazz, arguments, args)方法。dubbo的filter唯一的作用就是收集事务参与者信息并更新当前事务上线文信息。那么在事务协调时就能够从当前事务上线文里面获取到需要协调的事务参与者信息了。

参数者事务处理器

public Object handler(final ProceedingJoinPoint point, final HmilyTransactionContext context) throws Throwable {
HmilyTransaction hmilyTransaction = null;
HmilyTransaction currentTransaction;
switch (HmilyActionEnum.acquireByCode(context.getAction())) {
case TRYING:
try {
hmilyTransaction = hmilyTransactionExecutor.beginParticipant(context, point);
final Object proceed = point.proceed();
hmilyTransaction.setStatus(HmilyActionEnum.TRYING.getCode());
//update log status to try
hmilyTransactionExecutor.updateStatus(hmilyTransaction);
return proceed;
} catch (Throwable throwable) {
//if exception ,delete log.
hmilyTransactionExecutor.deleteTransaction(hmilyTransaction);
throw throwable;
} finally {
HmilyTransactionContextLocal.getInstance().remove();
}
case CONFIRMING:
currentTransaction = HmilyTransactionCacheManager.getInstance().getTccTransaction(context.getTransId());
hmilyTransactionExecutor.confirm(currentTransaction);
break;
case CANCELING:
currentTransaction = HmilyTransactionCacheManager.getInstance().getTccTransaction(context.getTransId());
hmilyTransactionExecutor.cancel(currentTransaction);
break;
default:
break;
}
Method method = ((MethodSignature) (point.getSignature())).getMethod();
logger.error(HmilyActionEnum.acquireByCode(context.getAction()).getDesc()); return DefaultValueUtils.getDefaultValue(method.getReturnType());
}

参与者事务处理器的逻辑比启动事务处理器要简单很多,try阶段记录事务日志用于事务补偿的时候使用。其他的confirm和cancel都是由启动事务管理器来触发调用执行的。这个地方之前纠结了楼主几个小时,怎么一个环绕织入的切面会被触发执行两次,其实是启动事务处理器里的confirm或cancel触发的。

disruptor+ScheduledService处理事务日志,补偿处理失败的事务

这个不细聊了,简述下。disruptor是一个高性能的队列。对事务日志落地的所有操作都是通过disruptor来异步完成的。ScheduledService默认128秒执行一次,来检查是否有处理失败的事务日志,用于补偿事务协调失败的事务

文末结语

相比较2pc的LCN而言,tcc分布式事务对业务侵入性更高。也因2pc的长时间占用事务资源,tcc的性能肯定比2pc要好。两者之间本身不存在谁优谁劣的问题。所以在做分布式事务选型时,选一个对的适合自身业务的分布式事务框架就比较重要了。

tcc分布式事务框架解析的更多相关文章

  1. 终于有人把“TCC分布式事务”实现原理讲明白了!

    之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下.很多朋友看了还是不知道分布式事务到底怎么回事,在项目里到底如何使用. 所以这篇文章,就用大白话+手工绘图,并结合一 ...

  2. TCC分布式事务的实现原理(转载 石杉的架构笔记)

    拜托,面试请不要再问我TCC分布式事务的实现原理![石杉的架构笔记] 原创: 中华石杉 目录 一.写在前面 二.业务场景介绍 三.进一步思考 四.落地实现TCC分布式事务 (1)TCC实现阶段一:Tr ...

  3. 拜托,面试请不要再问我TCC分布式事务的实现原理!(转)

    一.写在前面 之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下.很多朋友看了不少文章,还是不知道分布式事务到底怎么回事,在项目里到底如何使用. 所以咱们这篇文章,就 ...

  4. 【转载】终于有人把“TCC分布式事务”的实现原理讲明白了

    之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下.很多朋友看了还是不知道分布式事务到底怎么回事,在项目里到底如何使用. 所以这篇文章,就用大白话+手工绘图,并结合一 ...

  5. 终于有人把“TCC分布式事务”实现原理讲明白了

    所以这篇文章,就用大白话+手工绘图,并结合一个电商系统的案例实践,来给大家讲清楚到底什么是 TCC 分布式事务. 首先说一下,这里可能会牵扯到一些 Spring Cloud 的原理,如果有不太清楚的同 ...

  6. 关于如何实现一个Saga分布式事务框架的思考

    关于Saga模式的介绍,已经有一篇文章介绍的很清楚了,链接在这里:分布式事务:Saga模式. 关于TCC模式的介绍,也已经有一篇文章介绍的很清楚了,链接在这里:关于如何实现一个TCC分布式事务框架的一 ...

  7. TCC分布式事务的实现原理

    目录 一.写在前面 二.业务场景介绍 三.进一步思考 四.落地实现TCC分布式事务 (1)TCC实现阶段一:Try (2)TCC实现阶段二:Confirm (3)TCC实现阶段三:Cancel 五.总 ...

  8. TCC分布式事框架务详解

    之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下.很多朋友看了还是不知道分布式事务到底怎么回事,在项目里到底如何使用. 所以这篇文章,就用大白话+手工绘图,并结合一 ...

  9. 分布式事务之——tcc-transaction分布式TCC型事务框架搭建与实战案例(基于Dubbo/Dubbox)

    转载请注明出处:http://blog.csdn.net/l1028386804/article/details/73731363 一.背景 有一定分布式开发经验的朋友都知道,产品/项目/系统最初为了 ...

随机推荐

  1. 不吹不擂,你想要的Python面试都在这里了【315+道题】

    写在前面 近日恰逢老男孩全栈8期毕业季,课程后期大家“期待+苦逼”的时刻莫过于每天早上内容回顾和面试题问答部分[临近毕业每天课前用40-60分钟对之前内容回顾.提问和补充,专挑班里不爱说话就的同学回答 ...

  2. 【BZOJ2339】【HNOI2011】卡农

    题解: 首先用二进制表示每个音阶是否使用,那么共有$2^{n}-1$(空集不可行)种片段,用$a_{i}$来表示每个片段,问题就是求满足$a_{1}\left (xor\right)a_{2}\lef ...

  3. (3)STM32使用HAL库操作外部中断——实战操作

    有了上一篇的基础入门知识,使用Cube创建一个简单的外部中断就容易多了. 一.Cube配置 需求:使用PD10作为外部中断(下降沿触发)控制LED(PD12-PD14) 1.选型 STM32-F4-D ...

  4. linux 挂载共享文件夹

    1.背景 通常会有这样的场景,开发人员在Windows编写代码,然后放在linux环境编译,我们通过mount命令就可以实现将代码直接挂到linux环境上去,使Windows上的共享文件夹就像linu ...

  5. WebGL学习之纹理贴图

    为了使图形能获得接近于真实物体的材质效果,一般会使用贴图,贴图类型主要包括两种:漫反射贴图和镜面高光贴图.其中漫反射贴图可以同时实现漫反射光和环境光的效果. 实际效果请看demo:纹理贴图 2D纹理 ...

  6. 【转】百度站长平台MIP引入工具使用心得

    MIP引入主动推送流程 对于 MIP 站点改造好了,我们如何提交数据,并且 MIP 提交后,我们能得到哪些数据的反馈,在这里简单的写一篇文章,说一下. 改造 MIP,我们一般是添加了一个二级域名站点进 ...

  7. JAVA使用POI获取Excel的列数与行数

    Apache POI 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java程式对Microsoft Office格式档案读和写的功能. 下面这篇文章给大家介 ...

  8. go语言调度器源代码情景分析之三:内存

    本文是<go调度器源代码情景分析>系列 第一章 预备知识的第2小节. 内存是计算机系统的存储设备,其主要作用是协助CPU在执行程序时存储数据和指令. 内存由大量内存单元组成,内存单元大小为 ...

  9. ASP.NET Core开发者成长路线图

    目录 ASP.NET Core开发者路线图RoadMap 免责声明 请给一个星星! ⭐ 路线图 资源 总结 贡献 许可协议 ASP.NET Core开发者路线图RoadMap 来源: MoienTaj ...

  10. JPushDemo【极光推送集成,基于v3.1.8版本】

    版权声明:本文为HaiyuKing原创文章,转载请注明出处! 前言 这个Demo只是记录极光推送的集成,不能运行. 使用步骤 一.项目组织结构图 注意事项: 1.  导入类文件后需要change包名以 ...