RocketMQ事务消息学习过程

1、RocketMQ事务消息学习过程

个人博客:http://www.geek-make.com

1.1、背景

MQ是分布式服务系统不可缺少的一个套件,一方面在设计层面可以降低系统之间的耦合度,另一方面在高并发场景下可以做到削峰填谷。

一般来说,现在的数据库比如Mysql数据库事务是无法与MQ消息作为一个原子性事务要么一起执行成功要么一起执行失败。Apache RocketMQ 4.3之后的版本开始正式支持事务消息,本质上是为了解决Producer 端的消息发送与本地事务执行的原子性问题。

我们以积分兑换现金业务场景为例,假设积分与货币的兑换比例是1:1,那么100积分可以兑换100元现金。会员积分服务账户服务是两个独立的分布式服务,且各自拥有自己的数据库实例。那么可能出现以下三种场景:

  • 如果先扣除积分,再发送消息,积分服务挂了导致消息没有发送出去,结果100现金没有充值到用户账户
  • 如果先发送消息,100元现金充值到账户,但扣除积分操作失败了,导致白白损失了100积分
  • 积分也扣除了,消息也发送了,但是消息消费者出现问题,导致100元现金没有充值到账户

1.2、什么是事物消息

RocketMQ已成为Apache官方的顶级项目:

It can be thought of as a two-phase commit message implementation to ensure eventual consistency in distributed system. Transactional message ensures that the execution of local transaction and the sending of message can be performed atomically.

RocketMQ官方将其视为两阶段提交消息实现,以确保分布式系统中的最终一致性。事务性消息确保可以原子方式执行本地事务的执行和消息的发送。

1.2.1、事物消息状态

事务消息有3种状态:

  • TransactionStatus.CommitTransaction,提交事务,表示允许消费者消费(使用)这条消息
  • TransactionStatus.RollbackTransaction,回滚事务,表示消息将被删除,不允许使用
  • TransactionStatus.Unknown,中间状态,表示需要MQ向消息发送方进行检查以确定状态

1.2.2、发送事物消息

RocketMQ已经把事物消息的发送方式封装得非常优雅,创建事务消息生产者和实现TransactionListener接口。

  • 创建事务消息生产者
    使用TransactionMqProducer类创建消息生产客户端,并指定唯一的ProducerGroup
    TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup");
    producer.setNamesrvAddr("127.0.0.1:9876");
    producer.start();
    //设置TransactionListener实现
    producer.setTransactionListener(transactionListener);
    //发送事务消息
    SendResult sendResult = producer.sendMessageInTransaction(msg, null);
  • 实现TransactionListener接口
public interface TransactionListener {
    /**
     * When send transactional prepare(half) message succeed, this method will be invoked to execute local transaction.
     *
     * @param msg Half(prepare) message
     * @param arg Custom business parameter
     * @return Transaction state
     */
    LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);

    /**
     * When no response to prepare(half) message. broker will send check message to check the transaction status, and this
     * method will be invoked to get local transaction status.
     *
     * @param msg Check message
     * @return Transaction state
     */
    LocalTransactionState checkLocalTransaction(final MessageExt msg);
}

1.3、RocketMQ事务消息实现原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tR9zZtRk-1600677305066)
在这里插入图片描述

事务发起方(即消息发送者)首先发送 prepare 消息到 MQ。

事务发起方(即消息发送者)在发送 prepare 消息成功后执行本地事务。

根据本地事务执行结果发送 commit 或者是 rollback 给 MQ。

如果消息是 rollback,MQ 将删除该 prepare 消息不进行下发。

如果消息是 commit,MQ 将会把这个消息发送给 consumer 端。

如果执行本地事务过程中,执行端挂掉,或者超时,导致 MQ 收不到任何的消息(不知道是该 commit 还是该 rollback),RocketMQ 会定期扫描消息集群中的事务消息,这时候发现了某个 prepare 消息还不知道该怎么处理,它会向消息发送者确认,所以消息发送者需要实现一个 check 接口,RocketMQ 会根据消息发送者设置的策略来决定是 rollback 还是继续 commit。这样就保证了消息发送与本地事务同时成功或同时失败。

Consumer 端的消费成功机制由 MQ 保证。

1.4、源码分析

    public TransactionSendResult sendMessageInTransaction(final Message msg,
                                                          final LocalTransactionExecuter localTransactionExecuter, final Object arg)
        throws MQClientException {
        TransactionListener transactionListener = getCheckListener();
        if (null == localTransactionExecuter && null == transactionListener) {
            throw new MQClientException("tranExecutor is null", null);
        }
        Validators.checkMessage(msg, this.defaultMQProducer);

        SendResult sendResult = null;
        MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
        MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
        try {
            sendResult = this.send(msg);
        } catch (Exception e) {
            throw new MQClientException("send message Exception", e);
        }

        LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
        Throwable localException = null;
        switch (sendResult.getSendStatus()) {
            case SEND_OK: {
                try {
                    if (sendResult.getTransactionId() != null) {
                        msg.putUserProperty("__transactionId__", sendResult.getTransactionId());
                    }
                    String transactionId = msg.getProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX);
                    if (null != transactionId && !"".equals(transactionId)) {
                        msg.setTransactionId(transactionId);
                    }
                    if (null != localTransactionExecuter) {
                        localTransactionState = localTransactionExecuter.executeLocalTransactionBranch(msg, arg);
                    } else if (transactionListener != null) {
                        log.debug("Used new transaction API");
                        localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
                    }
                    if (null == localTransactionState) {
                        localTransactionState = LocalTransactionState.UNKNOW;
                    }

                    if (localTransactionState != LocalTransactionState.COMMIT_MESSAGE) {
                        log.info("executeLocalTransactionBranch return {}", localTransactionState);
                        log.info(msg.toString());
                    }
                } catch (Throwable e) {
                    log.info("executeLocalTransactionBranch exception", e);
                    log.info(msg.toString());
                    localException = e;
                }
            }
            break;
            case FLUSH_DISK_TIMEOUT:
            case FLUSH_SLAVE_TIMEOUT:
            case SLAVE_NOT_AVAILABLE:
                localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
                break;
            default:
                break;
        }

        try {
            this.endTransaction(sendResult, localTransactionState, localException);
        } catch (Exception e) {
            log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
        }

        TransactionSendResult transactionSendResult = new TransactionSendResult();
        transactionSendResult.setSendStatus(sendResult.getSendStatus());
        transactionSendResult.setMessageQueue(sendResult.getMessageQueue());
        transactionSendResult.setMsgId(sendResult.getMsgId());
        transactionSendResult.setQueueOffset(sendResult.getQueueOffset());
        transactionSendResult.setTransactionId(sendResult.getTransactionId());
        transactionSendResult.setLocalTransactionState(localTransactionState);
        return transactionSendResult;
    }
  • 分布式事务等于事务消息吗?

两者并没有关系,事务消息仅仅保证本地事务和MQ消息发送形成整体的原子性,而投递到MQ服务器后,消费者是否能一定消费成功是无法保证的。

1.5、参考资料

  • RocketMQ支持事务消息机制
    https://www.jianshu.com/p/cc5c10221aa1

  • SpringCloud集成RocketMQ实现事务消息方案
    https://blog.csdn.net/weixin_44062339/article/details/100180487

  • 分布式事务利器——RocketMQ事务消息的启示
    http://blog.itpub.net/31556438/viewspace-2649246/

  • RocketMQ事务消息学习及刨坑过程
    https://www.cnblogs.com/huangying2124/p/11702761.html

  • 源码分析
    https://blog.csdn.net/prestigeding/article/details/81263833

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值