分布式事务 - RocketMQ 实现事务的最终一致
前言
基于本人的理解对 分布式事务 进行一个总体的概述,借助一段 demo
说明如何借助 RocketMQ 实现事务的最终一致
分布式事务
伴随 微服务 概念盛行,多个服务实例间的调用就不免涉及到 分布式事务,比如经典场景:银行转账,账户 A
扣款和账户 B
加钱;订单创建后优惠券发放 等等
C A P
先提一嘴分布式系统的 C A P 理论,更详细的叙述可以参考该文章:[分布式]:分布式系统的CAP理论
- C
Consistency
:所有服务在同一时间保证数据的一致性,通俗的举例,服务A
更新完的数据可以立即在服务B
查询到 - A
Availability
:保证每一次请求都是成功的,比如 Eureka 通过自我保护、多节点部署等方式保证注册中心的高可用 - P
Partition Tolerance
:分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务
C A P 无法同时都满足,因此分布式系统常被分为 CP
和 AP
系统
- CP:保证了数据的强一致性,比如实时性要求极高的转账系统、Zookeeper 等,保证数据的强一致性意味着可能会面临造成服务、网络的阻塞而牺牲高可用
- AP:保证服务的高可用,此处牺牲的是数据的强一致性,而保证最终一致性,比如转账后数个工作日内到账,再比如之前提到的 Eureka 在大面积心跳丢失时启动自我保护不剔除对应实例 等
CP 系统的分布式事务
对于 CP 系统,要保证数据的强一致性,比如实时转账,账户 A
扣款的同时账户 B
需要增加余额,倘若由于账户 B
所在系统出现故障加钱失败,则账户 A
是不会扣款的
这种场景的解决方法:
- 借助中间件,其过程比较容易理解,即由中间件充当 事务协调者,当所有 资源服务 的事务提交都没有问题时,统一提交事务,比如 SpringBoot + Atomikos
- 提供对应业务的回滚操作,比如发现账户
B
加钱失败时,给对应的A
账户再补回对应金额即可,对复杂的业务场景并不适用
AP 系统的分布式事务
对于 AP 系统,它的 分布式事务 其实有别于传统的理解。一般的,倘若服务 A
调用服务 B
我们可以直接通过 HTTP 方式进行调用(包括各种客户端工具比如 Feign)
而实际上业务设计并不会直接去同步调用,因为这意味着不可预知的资源消耗和时间阻塞,我们通常是以异步的方式进行,最常用的便是借助 消息中间件 了,通过消息发送和订阅的方式,实现异步调用
此时,我们只需要保证服务 A
实现业务后成功发送消息,再由服务 B
来确保成功消费并完成对应的业务,便保证了服务 A
和 B
事务的最终一致
事务最终一致
实际上,大多数的业务场景都是后者:保证事务的最终一致即可,比如转账后数个工作日内到账、订单完成后发放优惠券 等
这里有两个要素:
服务 A
对于服务 A
来说,需要保证本地事务的提交和消息的发送保持一致,这个其实有很多种实现方式。最直观的方式应该是这样的
- 先完成本地业务,成功后再发送消息
- 消息发送异常或失败都回滚本地事务
- 保证消息发送成功后,本地事务不会再被回滚,这个实际上没法严格保证的
鉴于上述第三点无法严格保证,我们便可以借助 RocketMQ 的事务消息机制来实现,后文会详细给出这种解决思想及其 demo
服务 B
服务 B
需要保证:
- 确保最终成功消费这条消息,服务
B
可能会因为宕机等原因无法第一时间消费对应消息(但这并不妨碍服务A
的业务,这便是 AP 系统高可用的体现),但服务B
要保证最终成功消费这条消息,比如 人工介入 等手段 - 保证消息消费的 幂等性,基于各种 消息中间件 的实现,消息重发是必然会发生的,因此服务
B
作为消费方,是要保证 幂等性 的
RocketMQ 事务消息
最后,本文就保证 事务的最终一致性,给出一个解决 demo
正如前文所述,服务 A
要保证本地事务和消息发送的一致性,RocketMQ 的事务消息便提供了这一机制,交互简述如下:
- 完成本地业务
- 发送
半事务消息
,该消息并不会被消费方感知 - 消息存储,该过程与本地业务在同一事务内以保证同步
- 提供了 回查机制,根据回查结果来决定是否提交事务(该功能实际已被移除,但下面
demo
中依旧也会展示) - 本地事务执行成功,则结束并提交,消息 “真正” 发送,被消费方感知(原理是 Topic 的改变)
- 本地事务回滚时, “删除” 对应
半事务消息
即可
demo
该 demo
为 SpringBoot 整合 RocketMQ,模拟服务 A
处理业务插入一条数据后发送事务消息,由其他服务消费该消息执行相关业务
启动类及相关配置
@SpringBootApplication
@EnableRocketMQ
@MapperScan("com.example.rocketmq.transaction.mapper")
public class RocketmqApplication {
@Autowired
DefaultMQPushConsumer defaultMQPushConsumer;
public static void main(String[] args) {
SpringApplication.run(RocketmqApplication.class, args);
}
/**
* 此处模拟 消费方 业务
*/
@Component
class Test implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
defaultMQPushConsumer.registerMessageListener(
new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
System.out.println("======= message received successfully ========");
/**
* 此处需要保证消息的幂等性
* 对于一直无法处理的消息,需要采取对应的措施,比如 通知人工介入 等
*/
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}
);
defaultMQPushConsumer.subscribe("A", "*");
defaultMQPushConsumer.start();
}
}
/**
* RocketMQ 相关配置类
*/
@Configuration
static class MQConfig {
@Autowired
private RocketMQProperties rocketMQProperties;
@Bean
public TransactionMQProducer transactionMQProducer() {
TransactionMQProducer transactionMQProducer = new TransactionMQProducer(rocketMQProperties.getProducer().getGroup());
transactionMQProducer.setNamesrvAddr(rocketMQProperties.getNameServer());
return transactionMQProducer;
}
@Bean
public DefaultMQPushConsumer defaultMQPushConsumer() {
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer(rocketMQProperties.getProducer().getGroup());
defaultMQPushConsumer.setNamesrvAddr(rocketMQProperties.getNameServer());
return defaultMQPushConsumer;
}
}
}
- 不关心消费方的处理(比如 幂等、确定消费 等),只做一个简单的模拟来证明消息被消费
- 事务消息必须由 TransactionMQProducer 发送
本地业务实现
@Service
public class AServiceImpl extends ServiceImpl<AMapper, A> implements IAService {
@Autowired
TransactionMQProducer transactionMQProducer;
@Autowired
TransactionCheckListener transactionCheckListener;
@Autowired
LocalTransactionExecuter localTransactionExecuter;
@Override
public void a(A a) {
/**
* 发送 半事务消息
*/
// 事务回查监听器
transactionMQProducer.setTransactionCheckListener(transactionCheckListener);
try {
transactionMQProducer.start();
} catch (MQClientException e) {
e.printStackTrace();
throw new RuntimeException("mq start fail");
}
// 事务回查线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2
, 4
, 10
, TimeUnit.SECONDS
, new ArrayBlockingQueue<>(4));
transactionMQProducer.setCallbackExecutor(threadPoolExecutor);
// 发送 半事务消息
Message message = new Message("A", JSONObject.toJSON(a).toString().getBytes());
message.putUserProperty("bizId", UUID.randomUUID().toString().replace("-", ""));
try {
transactionMQProducer.sendMessageInTransaction(message, localTransactionExecuter, null);
} catch (MQClientException e) {
throw new RuntimeException("transaction message send fail");
}
}
}
- 指定事务回查监听器 TransactionCheckListener 实例,见下文(再次申明,事务回查机制 在新版本已被取消)
- 指定事务回查线程池
- 指定业务逻辑 LocalTransactionExecuter 实例,见下文
- 半事务消息发送,指定 Topic,消息指定业务主键
bizId
,方便存储、追踪,发送方法为TransactionMQProducer#sendMessageInTransaction
TransactionCheckListener
/**
* 事务回查监听器,新版本已取消该功能
*/
@Component
public class ATransactionListener implements TransactionCheckListener {
@Autowired
LogMapper logMapper;
@Override
public LocalTransactionState checkLocalTransactionState(MessageExt messageExt) {
String transactionId = messageExt.getUserProperty("bizId");
Log log = logMapper.selectById(transactionId);
if (log == null) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.COMMIT_MESSAGE;
}
}
虽然该机制已被取消,但此处仍然做了实现以更好的理解回查机制,可以从本地业务库查到事务实例即认为本地事务提交成功,返回 LocalTransactionState.COMMIT_MESSAGE
确认事务消息可被消费者订阅,否则返回 LocalTransactionState.ROLLBACK_MESSAGE
LocalTransactionExecuter
@Component
public class ALocalTransactionExecuter implements LocalTransactionExecuter {
@Autowired
AMapper aMapper;
@Autowired
LogMapper logMapper;
@Override
@Transactional
public LocalTransactionState executeLocalTransactionBranch(Message message, Object o) {
// 本地业务落库
A a = JSONObject.parseObject(new String(message.getBody()), A.class);
int test = aMapper.insert(a);
// 将事务消息落库
int insert =
logMapper.insert(
new Log(message.getUserProperty("bizId")
, message.getTopic(), ""));
return LocalTransactionState.COMMIT_MESSAGE;
}
}
本地业务和事务信息落库,可以看到它们在同一个事务内,全都成功返回 LocalTransactionState.COMMIT_MESSAGE
,确认发送事务消息,可被消费者订阅
总结
至此,RocketMQ 保证事务最终一致的大体逻辑叙述完毕,同时还可以自行实现已被取消的 事务回查机制 来完善上述体系,并不在本文叙述范畴内
可以看到,事实上 分布式事务 并没有成模板的 一站式
的解决方法,更多是依赖于具体业务场景,甚至存在是否需要实现分布式事务的抉择,但大体的思想是明确的,方法也是多种多样的,结合具体场景具体选择
参考
《RocketMQ 技术内幕》