如何优雅的解决,事务中发送MQ接收处理导致的一致性问题
背景描述:工作中经常发现,有同事(初中高级都有出现)在事务中或者事务最后发送MQ,然后消费进行处理
有的人是自己定义了一个方法,没有加事务,当时被别人写的方法调用了,所以事务传播了(Spring的事务传播特性,默认下是会传播当前事务的)
首先暂且不考虑发送MQ失败和MQ成功,当前事务回滚导致的不一致性问题 (这个大家都能理解,也会去考虑)
这次主要讲解成功提交事务所带来的不一致的场景
首先,我们参考图例场景

可以看出来 MethodC 最终查出来会概率性的为空,原因就不用介绍了,除非你连数据库的隔离级别都不清楚
原因是,如果sendMQ之后full gc, 或者cpu线程切换,导致MQ收到消息之后查询在生成者事务提前完成,查到的数据就为空了
如果避免这个问题了
-
靠人力水平来避免,Review
首先我们能想到的是,不应该在事务里面发MQ,因为他会带来异常回滚的不一致性,但是不是每一个业务都那么重要,人员的水平也是参差不齐,如果想这样去做,需要提高编码人员的技术水平,及CodeReview的级别,这确实是一个解决问题的办法,但是不够好,成本太高。
-
靠系统架构设计来避免
一开始就把架构设计好,详细设计各种由架构师来定好,包括数据的各种扭转,各种的异常,重要的项目可以这样干,但是一个公司,90%的项目都是不那么重要的,多大的厂都是如此,根本不可能有这么高的成本投入。
-
利用技术手段来避免
怎么利用技术手段来避免这个问题
引入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();
}
谢谢阅读!
浙公网安备 33010602011771号