如何优雅的解决,事务中发送MQ接收处理导致的一致性问题

背景描述:工作中经常发现,有同事(初中高级都有出现)在事务中或者事务最后发送MQ,然后消费进行处理

有的人是自己定义了一个方法,没有加事务,当时被别人写的方法调用了,所以事务传播了(Spring的事务传播特性,默认下是会传播当前事务的)

首先暂且不考虑发送MQ失败和MQ成功,当前事务回滚导致的不一致性问题 (这个大家都能理解,也会去考虑)

这次主要讲解成功提交事务所带来的不一致的场景

首先,我们参考图例场景

可以看出来 MethodC 最终查出来会概率性的为空,原因就不用介绍了,除非你连数据库的隔离级别都不清楚

原因是,如果sendMQ之后full gc, 或者cpu线程切换,导致MQ收到消息之后查询在生成者事务提前完成,查到的数据就为空了

如果避免这个问题了

  1. 靠人力水平来避免,Review

    首先我们能想到的是,不应该在事务里面发MQ,因为他会带来异常回滚的不一致性,但是不是每一个业务都那么重要,人员的水平也是参差不齐,如果想这样去做,需要提高编码人员的技术水平,及CodeReview的级别,这确实是一个解决问题的办法,但是不够好,成本太高

  2. 靠系统架构设计来避免

    一开始就把架构设计好,详细设计各种由架构师来定好,包括数据的各种扭转,各种的异常,重要的项目可以这样干,但是一个公司,90%的项目都是不那么重要的,多大的厂都是如此,根本不可能有这么高的成本投入。

  3. 利用技术手段来避免

怎么利用技术手段来避免这个问题

引入TransactionSynchronizationManager,增加事务后置处理。

例如:

TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
    @Override
    public void afterCommit() {
        // message queue send todo
    }
});

当然使用TransactionSynchronizationManager.registerSynchronization之前别忘记了增加isActive的判断,很重要,你怎么避免内部方法调用没有加@Transaction的问题呢

所以:

boolean isActive = TransactionSynchronizationManager.isSynchronizationActive();
 if (isActive) {
     TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
    @Override
    public void afterCommit() {
        // message queue send todo
    }
});
 } else {
        // message queue send todo
 }

这样实现很明显,如果你对原有代码进行重构,你的工作量是非常大的,而且这些代码看起来重复量很大。你每发个MQ都得这样写,所以我们为什么不使用AOP无侵入的增加这样一段代码在每次发送MQ之前呢???

优雅的实现

使用Spring AOP 对MQ发送进行切面处理,在发送的前后增加事务的后置处理,那么已经加了后置处理的会出现问题吗,也就是二层Post会有问题吗?并不会

# 如果这里在事务中 isActive=true
boolean isActive = TransactionSynchronizationManager.isSynchronizationActive();
if (isActive) {
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                # 当前已经没有在事务了,所以isActive=false
                boolean isActive = TransactionSynchronizationManager.isSynchronizationActive();
                if (isActive) {
                    TransactionSynchronizationManager.registerSynchronization(
                        new TransactionSynchronizationAdapter() {
                            @Override
                            public void afterCommit() {
                                // message queue send todo
                            }
                        });
                } else {
                    // message queue send todo
                }
            }
        });
} else {
    // message queue send todo
}

以下是AOP怎么实现切面拦截的具体代码,RabbitMQ为例

/**
 * 支持的方法,必须是无返回值的
 */
private static final List<String> SUPPORT_METHODS = Lists.newArrayList("convertAndSend", "send");

@Around("execution(* org.springframework.amqp.rabbit.core.RabbitTemplate.*(..))")
public Object sendAround(ProceedingJoinPoint joinPoint) throws Throwable {
  String name = joinPoint.getSignature().getName();
  if (SUPPORT_METHODS.contains(name)) {
    boolean isActive = TransactionSynchronizationManager.isSynchronizationActive();
    if (isActive) {
      final ProceedingJoinPoint joinPoint1 = joinPoint;
      TransactionSynchronizationManager.registerSynchronization(
          new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
              try {
                joinPoint1.proceed();
              } catch (Throwable e) {
                log.error("发送MQ出现异常: {}", e.getMessage(), e);
              }
            }
          });
      return null;
    }
    return joinPoint.proceed();
  }
  return joinPoint.proceed();
}

谢谢阅读!

posted @ 2021-10-28 14:17  随风森林  阅读(1150)  评论(0)    收藏  举报