分布式事务 - 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 无法同时都满足,因此分布式系统常被分为 CPAP 系统

  • CP:保证了数据的强一致性,比如实时性要求极高的转账系统、Zookeeper 等,保证数据的强一致性意味着可能会面临造成服务、网络的阻塞而牺牲高可用
  • AP:保证服务的高可用,此处牺牲的是数据的强一致性,而保证最终一致性,比如转账后数个工作日内到账,再比如之前提到的 Eureka 在大面积心跳丢失时启动自我保护不剔除对应实例 等

CP 系统的分布式事务

对于 CP 系统,要保证数据的强一致性,比如实时转账,账户 A 扣款的同时账户 B 需要增加余额,倘若由于账户 B 所在系统出现故障加钱失败,则账户 A 是不会扣款的

这种场景的解决方法:

  • 借助中间件,其过程比较容易理解,即由中间件充当 事务协调者,当所有 资源服务 的事务提交都没有问题时,统一提交事务,比如 SpringBoot + Atomikos
  • 提供对应业务的回滚操作,比如发现账户 B 加钱失败时,给对应的 A 账户再补回对应金额即可,对复杂的业务场景并不适用

AP 系统的分布式事务

对于 AP 系统,它的 分布式事务 其实有别于传统的理解。一般的,倘若服务 A 调用服务 B 我们可以直接通过 HTTP 方式进行调用(包括各种客户端工具比如 Feign

而实际上业务设计并不会直接去同步调用,因为这意味着不可预知的资源消耗和时间阻塞,我们通常是以异步的方式进行,最常用的便是借助 消息中间件 了,通过消息发送和订阅的方式,实现异步调用

此时,我们只需要保证服务 A 实现业务后成功发送消息,再由服务 B 来确保成功消费并完成对应的业务,便保证了服务 AB 事务的最终一致

事务最终一致

实际上,大多数的业务场景都是后者:保证事务的最终一致即可,比如转账后数个工作日内到账、订单完成后发放优惠券 等

这里有两个要素:

服务 A

对于服务 A 来说,需要保证本地事务的提交和消息的发送保持一致,这个其实有很多种实现方式。最直观的方式应该是这样的

  • 先完成本地业务,成功后再发送消息
  • 消息发送异常或失败都回滚本地事务
  • 保证消息发送成功后,本地事务不会再被回滚,这个实际上没法严格保证的

鉴于上述第三点无法严格保证,我们便可以借助 RocketMQ 的事务消息机制来实现,后文会详细给出这种解决思想及其 demo

服务 B

服务 B 需要保证:

  • 确保最终成功消费这条消息,服务 B 可能会因为宕机等原因无法第一时间消费对应消息(但这并不妨碍服务 A 的业务,这便是 AP 系统高可用的体现),但服务 B 要保证最终成功消费这条消息,比如 人工介入 等手段
  • 保证消息消费的 幂等性,基于各种 消息中间件 的实现,消息重发是必然会发生的,因此服务 B 作为消费方,是要保证 幂等性

RocketMQ 事务消息

最后,本文就保证 事务的最终一致性,给出一个解决 demo

正如前文所述,服务 A 要保证本地事务和消息发送的一致性,RocketMQ 的事务消息便提供了这一机制,交互简述如下:

  • 完成本地业务
  • 发送 半事务消息,该消息并不会被消费方感知
  • 消息存储,该过程与本地业务在同一事务内以保证同步
  • 提供了 回查机制,根据回查结果来决定是否提交事务(该功能实际已被移除,但下面 demo 中依旧也会展示)
  • 本地事务执行成功,则结束并提交,消息 “真正” 发送,被消费方感知(原理是 Topic 的改变)
  • 本地事务回滚时, “删除” 对应 半事务消息 即可

demo

demoSpringBoot 整合 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 技术内幕》

基于RocketMQ分布式事务 - 完整示例

Java微服务下的分布式事务介绍及其解决方案

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值