rocketmq分布式事务最终一致性解决方案

背景

分布式系统中,我们时常会遇到分布式事务的问题,如更新订单然后发送短信提醒,但是这两个操作需要操作不同的数据库,那么此时数据库的事务就不能处理好了
传统方式存在的问题:
1、先发送消息,再执行数据库事务,可能出现消息发送成功,但是本地事务执行失败,导致数据不一致

begin transaction
sendMsg()
updateStatus() ->可能失败,但是消息已经发送成功
commit transaction

2、先执行数据库事务,再发送消息,可能出现发送消息时超时返回失败,导致回滚了本地事务,但是消息其实已经成功发送,导致数据不一致

begin transaction
updateStatus()
sendMsg() ->可能出现超时等导致回滚事务,但是消息已经成功发送
commit transaction

RocketMq分布式事务方案

rocketmq事务性消息可以保证本地数据库等事务和发送给mq的消息(要么同时成功,要么同时失败)。但是不确保消费者可以成功消费
基本流程:
1、发送一个预消息给mq,此mq消息对于消费者暂时不可见
2、发送预消息成功,处理本地和数据库相关的事务,处理结果记录到数据库
3、把预消息更改为了正常消息,消费者可以消费。此时正常流程已经结束
4、消息回查,对于预消息进行回调,查询对应的消息在数据库记录的状态

  • 1、如果第2步失败了,本地业务代码或者数据库操作等报错,则数据库记录的失败,
    那么返回LocalTransactionState.ROLLBACK_MESSAGE丢弃预消息即可

  • 2、如果第2步成功了,那么第3步就必须成功,数据库记录该消息必定是成功(此时如果mq未收到对应的success,则进入回调),
    返回LocalTransactionState.COMMIT_MESSAGE把消息正常投递让消费者消费即可

直接上代码
生产者

public class TransactionProducer {

    public static void main(String[] args) throws MQClientException, UnsupportedEncodingException {
        TransactionMQProducer transactionMQProducer = new TransactionMQProducer("tx_producer");
        transactionMQProducer.setNamesrvAddr("192.168.1.1:9876");
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        transactionMQProducer.setExecutorService(executorService);
        transactionMQProducer.setTransactionListener(new TransactionListenerLocal()); //本地事务的监听

        transactionMQProducer.start();

        for (int i = 0; i < 10; i++) {
            String orderId = UUID.randomUUID().toString();
            String body = "{'operation':'doOrder','orderId':'" + orderId + "'}";
            Message message = new Message("order_tx_topic",
                    "TagA", orderId, body.getBytes(RemotingHelper.DEFAULT_CHARSET));
            // TODO 1、发送事务消息,此时消费对于消费者不可见,会先执行事务监听
            //  TransactionListenerLocal的executeLocalTransaction,并且传递参数
            // 实际业务时调用sendMessageInTransaction后即可调用executeLocalTransaction,
            // 如果成功则返回客户成功。mq最终必定收到消息,失败则返回客户失败,mq会回查然后丢弃该消息
            transactionMQProducer.sendMessageInTransaction(message, orderId + "&" + i);
            System.out.println("投递消息成功");
        }

    }
}
public class TransactionListenerLocal implements TransactionListener {

    private Map<String, Boolean> results = new ConcurrentHashMap<>();

    //TODO 2、执行本地事务,处理数据库相关,成功后results模拟处理结果,
    // 实际场景可能是存储到数据库记录某条消息的本地事务是否处理成功
    @Override
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        String orderId = o.toString();
        //模拟数据库保存(成功/失败)
        boolean result = Math.abs(Objects.hash(orderId)) % 2 == 0;
        System.out.println("开始执行本地事务:" + o.toString() + ",result:" + result);
        results.put(orderId.substring(0, orderId.indexOf("&")), result);

        // 失败返回UNKNOW,则预消息会进入回调方法checkLocalTransaction回查
        return result ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.UNKNOW;
    }

    //TODO 3、对于对消费者不可见的消息,这里进行回查,数据库记录成功的消息就提交mq事务,数据库记录失败的消息就直接丢弃
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {String orderId = messageExt.getKeys();
        System.out.println("执行事务回调检查: orderId:" + orderId);
        Boolean rs = results.get(orderId);
        System.out.println("数据的处理结果:" + rs); //只有成功/失败
        return (rs != null && rs) ? LocalTransactionState.COMMIT_MESSAGE : LocalTransactionState.ROLLBACK_MESSAGE;
    }

}

消费者:

public class TransactionConsumer {

    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer defaultMQPushConsumer=
                new DefaultMQPushConsumer("tx_consumer");
        defaultMQPushConsumer.setNamesrvAddr("192.168.1.1:9876");
        defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
        defaultMQPushConsumer.subscribe("order_tx_topic","*");
        defaultMQPushConsumer.registerMessageListener((MessageListenerConcurrently) (list, consumeConcurrentlyContext) -> {
            list.stream().forEach(message->{
                //扣减库存
                System.out.println("开始业务处理逻辑:消息体:"+new String(message.getBody())+"->key:"+message.getKeys());
            });
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; //签收
        });
        defaultMQPushConsumer.start();
    }

}

总结:rocketmq的事务性消息可以确保数据的最终一致性,核心机制是提供了回查功能,即可以根据数据库记录的本地事务的成功或者失败,最终回调来处理是否应该成功发送mq消息

分布式事务中使用RocketMQ时,可能会遇到一些坑。其中一个主要的坑是如何保证分布式事务的正确执行。有几种常用的分布式事务解决方案,包括XA方案(两阶段提交方案)、TCC方案(try、confirm、cancel)、SAGA方案、可靠消息最终一致性方案和最大努力通知方案。 在RocketMQ中,主要采用了可靠消息最终一致性方案来实现分布式事务。这个方案的主要思路是,在发送消息时,将消息和事务绑定,然后将消息先存储在Broker节点上,等到事务提交成功后再真正发送消息。如果事务提交失败,就会回滚消息,保证消息的一致性。这个方案相对来说较为简单,但是需要保证消息的可靠性和幂等性。 当然,在使用RocketMQ时,也需要考虑具体的业务需求、时间、成本以及开发团队的实力。分布式还有很多其他的坑,具体要根据情况来决定是否使用分布式架构。 希望以上信息对您有所帮助。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [这三年被分布式坑惨了,曝光十大坑](https://blog.csdn.net/jackson0714/article/details/108775573)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [RocketMQ事务消息学习及刨坑过程](https://blog.csdn.net/huangying2124/article/details/102634761)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值