给Seata TCC模式提了一个Issue,顺便说说我的解决思路

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 给Seata TCC模式提了一个Issue,顺便说说我的解决思路

前言

在昨天的文章中,我提到了之前发现的Seata TCC模式的一个BUG,并给社区提了一个Issue。根据昨天文章的源码分析,我们发现了问题就出现在Seata反序列化的时候不知道目标参数的数据类型,导致序列化前的参数类型和反序列化后的数据类型不一致。针对上述问题,说说我自己的解决思路。

解决思路

问题出现的原因就是反序列化的时候不知道目标数据类型,如果我们能够拿到数据类型的话,那么是不是这个问题就能很好地解决呢?

根据我的测试,fastjson可以在知道目标数据类型的情况下,将json字符串解析成目标数据类型,在Seata源码中,是这样处理的:

Map actionContextMap = null;
if (StringUtils.isNotBlank(applicationData)) {
    Map tccContext = JSON.parseObject(applicationData, Map.class);
    actionContextMap = (Map)tccContext.get(Constants.TCC_ACTION_CONTEXT);
}
复制代码

那么根据我们的思路,应该改成根据目标类型来转换:

// 已知目标类型表
Map<String, Class<?>> paramTypeMap = new HashMap<>();
// 解析json字符串为JSONObject
JSONObject jsonObject = JSON.parseObject(applicationData);
// 取出指定的actionContext
Map<String, Object> actionContextMap = jsonObject.getObject("actionContext", Map.class);
Iterator<Map.Entry<String, Object>> iterator = actionContext.entrySet().iterator();
// 遍历里面的每一个元素
while (iterator.hasNext()) {
    Map.Entry<String, Object> e = iterator.next();
    // 对应的key
    String id = e.getKey();
    // 目标值
    Object obj = e.getValue();
    // 对应的目标类型
    Class<?> paramType = paramTypeMap.get(id);
    // 如果没有指定类型,就跳过
    if (Objects.isNull(paramType)) {
        continue;
    }
    // 转换类型并更新actionContext
    actionContext.put(id, TypeUtils.cast(obj, new TypeReference(paramType){}.getType(), ParserConfig.getGlobalInstance()));
}
复制代码

1.咱们这个思路的前提是拿到了目标参数的数据类型,所以我们在上述伪代码中假设有一个key:value形式的类型列表;

2.我们先反序列化成Map对象,然后逐个遍历检查是否需要转换数据类型;如果有需要就将数据类型进行转换,否则向后继续遍历;

上面我们已经解决了数据类型转换的问题了,剩下的问题就是我们的类型列表从哪里获取?

我们可以观察一下下面这一段代码:

@Override
    public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId,
                                     String applicationData) throws TransactionException {
        // 从缓存中获取TCCResource
        TCCResource tccResource = (TCCResource)tccResourceCache.get(resourceId);
        if (tccResource == null) {
            throw new ShouldNeverHappenException(String.format("TCC resource is not exist, resourceId: %s", resourceId));
        }
        // 从TCCResource中获取目标对象
        Object targetTCCBean = tccResource.getTargetBean();
        // 从TCCResource中获取提交逻辑指定的方法
        Method commitMethod = tccResource.getCommitMethod();
复制代码

我们可以从TCCResource中获取commitMethod,那么意味着TCCResource中其实是包含prepareMethod资源预留方法相关的参数的,那么我们可以根据prepareMethod获取被BusinessActionContextParameter注解的参数及其数据类型,代码如下:

private static Map<String, Class<?>> parameterTypes(Method method) {
    Map<String, Class<?>> parameterTypeMap = new HashMap<>();
    // 获取目标方法的参数列表
    Parameter[] parameters = method.getParameters();
    if (ArrayUtils.isEmpty(parameters)) {
      return parameterTypeMap;
    }
    // 遍历所有参数
    for (Parameter parameter : parameters) {
      // 检查是否被BusinessActionContextParameter注解
      if (parameter.isAnnotationPresent(BusinessActionContextParameter.class)) {
        // 获取目标注解
        BusinessActionContextParameter parameterAnnotation = parameter.getDeclaredAnnotation(BusinessActionContextParameter.class);
        // 解析出目标参数数据类型
        loadActionContextParamClass(parameterAnnotation, parameter.getParameterizedType(), parameterTypeMap);
      }
    }
    // 返回数据类型列表
    return parameterTypeMap;
  }
  private static void loadActionContextParamClass(BusinessActionContextParameter annotation, Type type, Map<String, Class<?>> parameterTypeMap) {
    // paramName作为key
    String paramName = ActionContextUtil.getParamNameFromAnnotation(annotation);
    // 说明是列表,那么找出对应的泛型类型
    if (annotation.index() >= 0) {
      ParameterizedType parameterizedType = (ParameterizedType) type;
      parameterTypeMap.put(paramName, (Class<?>) parameterizedType.getActualTypeArguments()[0]);
      return;
    }
    // 从类字段中查找
    if (annotation.isParamInProperty()) {
      // 找到所有字段
      Field[] fields = ReflectionUtil.getAllFields((Class<?>) type);
      if (ArrayUtils.isEmpty(fields)) {
        return;
      }
      // 遍历字段类型
      for (Field field : fields) {
        // 是否被BusinessActionContextParameter注解
        if (field.isAnnotationPresent(BusinessActionContextParameter.class)) {
          BusinessActionContextParameter fieldDeclaredAnnotation = field.getDeclaredAnnotation(BusinessActionContextParameter.class);
          // 递归调用loadActionContextParamClass方法
          loadActionContextParamClass(fieldDeclaredAnnotation, field.getGenericType(), parameterTypeMap);
        }
      }
    } else {
      // 直接把参数类型放进去
      parameterTypeMap.put(paramName, (Class<?>) type);
    }
  }
复制代码

1.通过目标prepareMethod找到所有的参数列表,并且遍历所有被BusinessActionContextParameter注解的参数;

2.根据BusinessActionContextParameter参数值判断目标参数类型,并解析出目标参数的类型;

3.最后把解析出来的参数类型以paramName:class的方式存进Map中;

根据以上分析,我们的参数类型列表就出来了,结合前面我们给出的类型转换代码,就很好地解决了Seata TCC模式中,BusinessActionContext数据对象反序列化导致的数据类型不一致的问题。

结语

根据以上问题两篇文章,我们完成了一次【发现问题】--->【定位问题】--->【解决问题】的日常BUG排查工作,其中难度比较高的是【定位问题】,其次是【解决问题】。在一般情况下,如果没有深入了解源码是很难定位到相关问题出现的原因。后续我将把相关解决方案的代码提交给Seata社区,以便尽快解决该BUG;


相关文章
|
7月前
|
自然语言处理 监控 Dubbo
Seata常见问题之使用tcc模式配置yml如何解决
Seata 是一个开源的分布式事务解决方案,旨在提供高效且简单的事务协调机制,以解决微服务架构下跨服务调用(分布式场景)的一致性问题。以下是Seata常见问题的一个合集
217 4
|
23天前
|
数据库 微服务
SEATA模式
Seata 是一款开源的分布式事务解决方案,支持多种事务模式以适应不同的应用场景。其主要模式包括:AT(TCC)模式,事务分三阶段执行;TCC 模式,提供更灵活的事务控制;SAGA 模式,基于状态机实现跨服务的事务一致性;XA 模式,采用传统两阶段提交协议确保数据一致性。
41 5
|
29天前
Seata框架在AT模式下是如何保证数据一致性的?
通过以上这些机制的协同作用,Seata 在 AT 模式下能够有效地保证数据的一致性,确保分布式事务的可靠执行。你还可以进一步深入研究 Seata 的具体实现细节,以更好地理解其数据一致性保障的原理。
40 3
|
6月前
|
Apache 开发者
Apache Seata 如何解决 TCC 模式的幂等、悬挂和空回滚问题
【6月更文挑战第8天】Apache Seata 是一款分布式事务框架,解决TCC模式下的幂等、悬挂和空回滚问题。通过记录事务状态处理幂等,设置超时机制避免悬挂,明确标记Try操作成功来处理空回滚。Seata 提供丰富配置和管理功能,确保分布式事务的可靠性和效率,支持复杂事务处理场景,为企业业务发展提供支持。
242 7
|
7月前
|
存储 Java Nacos
Seata常见问题之xa模式出现错误xid is not valid如何解决
Seata 是一个开源的分布式事务解决方案,旨在提供高效且简单的事务协调机制,以解决微服务架构下跨服务调用(分布式场景)的一致性问题。以下是Seata常见问题的一个合集
241 4
|
7月前
|
NoSQL Java 数据库
Seata常见问题之xa模式下插入一条数据再更新这条数据会报错如何解决
Seata 是一个开源的分布式事务解决方案,旨在提供高效且简单的事务协调机制,以解决微服务架构下跨服务调用(分布式场景)的一致性问题。以下是Seata常见问题的一个合集
208 2
|
7月前
|
Java 关系型数据库 微服务
Seata常见问题之项目一直启动不成功如何解决
Seata 是一个开源的分布式事务解决方案,旨在提供高效且简单的事务协调机制,以解决微服务架构下跨服务调用(分布式场景)的一致性问题。以下是Seata常见问题的一个合集
600 0
|
1月前
|
Java 数据库
在Java中使用Seata框架实现分布式事务的详细步骤
通过以上步骤,利用 Seata 框架可以实现较为简单的分布式事务处理。在实际应用中,还需要根据具体业务需求进行更详细的配置和处理。同时,要注意处理各种异常情况,以确保分布式事务的正确执行。
|
19天前
|
消息中间件 SQL 中间件
大厂都在用的分布式事务方案,Seata+RocketMQ带你打破10万QPS瓶颈
分布式事务涉及跨多个数据库或服务的操作,确保数据一致性。本地事务通过数据库直接支持ACID特性,而分布式事务则需解决跨服务协调难、高并发压力及性能与一致性权衡等问题。常见的解决方案包括两阶段提交(2PC)、Seata提供的AT和TCC模式、以及基于消息队列的最终一致性方案。这些方法各有优劣,适用于不同业务场景,选择合适的方案需综合考虑业务需求、系统规模和技术团队能力。
133 7
|
1月前
|
存储 Java 关系型数据库
在Spring Boot中整合Seata框架实现分布式事务
可以在 Spring Boot 中成功整合 Seata 框架,实现分布式事务的管理和处理。在实际应用中,还需要根据具体的业务需求和技术架构进行进一步的优化和调整。同时,要注意处理各种可能出现的问题,以保障分布式事务的顺利执行。
54 6