1

最近项目使用开源版本的rocketmq
发现了一些特殊场景,需要自己动手改造一下

  1. 延时消息 无法随机延时之后消费
  2. 事务消息 无法在事务完成之后发送消息

因为网上有较多rocketmq的延时消息实现,所以本文主要介绍事务消息相关改造

消息的困扰

项目经常会发生,消息发送之后,事务没有执行完毕,消费者在第一次消费的时候出现无法找到发送消息事务里面新增修改的数据

官方事务消息

发现官方的事务消息与目前项目所需要的“事务消息”有所不同

  • 官方的事务消息是发送消息之后立马执行executeLocalTransaction,然后获得消息的state
  1. 如果executeLocalTransaction返回的是COMMIT,则会立马投递消息,此时事务并没执行有完毕,还是会出现上面的问题。
  2. 而如果在executeLocalTransaction中返回UNKNOW,则意味着消息需要等回查checkLocalTransaction执行完毕后才能消费,而这个过程默认是需要10秒,简单一点:发送消息后需要等10秒才能消费....
备注

官方的事务消息虽然不是我们想要的,但是有一套完整的事务消息流程

  • 保证了已经发送的消息不会丢失
  • 保证了发送的消息必须被COMMIT才能被投递和消费

当发生异常情况(网络问题、系统崩溃、进程被kill等等),后期broker都会进行回查

由此催生了改造的想法

改造原理

把COMMIT操作推后到事务结束的时候才提交
而如果事务失败了,则会走checkLocalTransaction

改造TransactionMQProducer

创建一个TransactionMQProducer继承官方的TransactionMQProducer

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.Validators;
import org.apache.rocketmq.client.exception.MQBrokerException;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageAccessor;
import org.apache.rocketmq.common.message.MessageConst;
import org.apache.rocketmq.common.protocol.NamespaceUtil;
import org.apache.rocketmq.remoting.RPCHook;
import org.apache.rocketmq.remoting.exception.RemotingException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Lazy;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

public class TransactionMQProducer extends org.apache.rocketmq.client.producer.TransactionMQProducer {

    @Lazy
    @Resource
    private ApplicationContext applicationContext;

    public TransactionMQProducer() {
    }

    public TransactionMQProducer(String producerGroup) {
        super(producerGroup);
    }

    public TransactionMQProducer(String namespace, String producerGroup) {
        super(namespace, producerGroup);
    }

    public TransactionMQProducer(String producerGroup, RPCHook rpcHook) {
        super(producerGroup, rpcHook);
    }

    public TransactionMQProducer(String namespace, String producerGroup, RPCHook rpcHook) {
        super(namespace, producerGroup, rpcHook);
    }

    /**
     * 发送事务消息
     *
     * @param msg 消息
     * @param arg 参数
     * @return 结果
     * @throws MQClientException
     */
    public TransactionSendResult sendMessageInTransaction(final Message msg, final Object arg) throws MQClientException {

        msg.setTopic(NamespaceUtil.wrapNamespace(this.getNamespace(), msg.getTopic()));

        TransactionListener transactionListener = this.defaultMQProducerImpl.getCheckListener();
        if (null == transactionListener) {
            throw new MQClientException("tranExecutor is null", null);
        }

        // ignore DelayTimeLevel parameter
        if (msg.getDelayTimeLevel() != 0) {
            MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_DELAY_TIME_LEVEL);
        }

        Validators.checkMessage(msg, this);

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

        LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;

        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);
                    }
                    log.debug("Used new transaction API");
                    localTransactionState = transactionListener.executeLocalTransaction(msg, arg);
                    if (null == localTransactionState) {
                        localTransactionState = LocalTransactionState.UNKNOW;
                    }

                } catch (Throwable e) {
                    log.info("executeLocalTransactionBranch exception", e);
                    log.info(msg.toString());
                    throw e;
                }
                // 把发送成功的消息添加到待处理列表
                applicationContext.publishEvent(new TxnMessageEvent(this, sendResult));
                break;
            case FLUSH_DISK_TIMEOUT:
            case FLUSH_SLAVE_TIMEOUT:
            case SLAVE_NOT_AVAILABLE:
                // 只有在消息发送失败,才会立马rollback
                localTransactionState = LocalTransactionState.ROLLBACK_MESSAGE;
                try {
                    this.defaultMQProducerImpl.endTransaction(sendResult, localTransactionState, null);
                } catch (Exception e) {
                    log.warn("local transaction execute " + localTransactionState + ", but end broker transaction failed", e);
                }
                break;
            default:
                break;
        }

        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;
    }

    /**
     * 发送确认消息
     *
     * @param sendResult            消息结果
     * @param localTransactionState 状态
     * @param localException        异常
     * @throws InterruptedException
     * @throws UnknownHostException
     * @throws RemotingException
     * @throws MQBrokerException
     */
    public void endTransaction(
            final SendResult sendResult,
            final LocalTransactionState localTransactionState,
            final Throwable localException) throws InterruptedException, UnknownHostException, RemotingException, MQBrokerException {
        this.defaultMQProducerImpl.endTransaction(sendResult, localTransactionState, localException);
    }

    /**
     * 当事务成功的时候-发送COMMIT消息
     *
     * @param txnMessageEvent 事件
     * @throws InterruptedException
     * @throws RemotingException
     * @throws MQBrokerException
     * @throws UnknownHostException
     */
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void afterCommit(TxnMessageEvent txnMessageEvent) throws InterruptedException, RemotingException, MQBrokerException, UnknownHostException {
        SendResult sendResult = txnMessageEvent.getSendResult();
        LocalTransactionState transactionState = LocalTransactionState.COMMIT_MESSAGE;
        endTransaction(sendResult, transactionState, null);
        log.info("Send Transaction {} {}", transactionState, sendResult.getMsgId());
    }

    /**
     * 当事务失败的时候-发送ROLLBACK消息
     *
     * @param txnMessageEvent 事件
     * @throws InterruptedException
     * @throws RemotingException
     * @throws MQBrokerException
     * @throws UnknownHostException
     */
    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void afterRollback(TxnMessageEvent txnMessageEvent) throws InterruptedException, RemotingException, MQBrokerException, UnknownHostException {
        SendResult sendResult = txnMessageEvent.getSendResult();
        LocalTransactionState transactionState = LocalTransactionState.ROLLBACK_MESSAGE;
        endTransaction(sendResult, transactionState, new RuntimeException("事务执行失败"));
        log.info("Send Transaction {} {}", transactionState, sendResult.getMsgId());
    }

TxnMessageEvent源码

import lombok.EqualsAndHashCode;
import org.apache.rocketmq.client.producer.SendResult;
import org.springframework.context.ApplicationEvent;

@EqualsAndHashCode(callSuper = true)
public class TxnMessageEvent extends ApplicationEvent {

    private SendResult sendResult;

    /**
     * Create a new {@code ApplicationEvent}.
     *
     * @param source the object on which the event initially occurred or with
     *               which the event is associated (never {@code null})
     */
    public TxnMessageEvent(Object source, SendResult sendResult) {
        super(source);
        this.sendResult = sendResult;
    }

    public SendResult getSendResult() {
        return sendResult;
    }

}

TransactionListener 定义

@Component
public class ExtTransactionListener implements TransactionListener {

    private ConcurrentHashMap<String, Integer> countHashMap = new ConcurrentHashMap<>();

    private static int MAX_COUNT = 5;

    @Resource
    private MessageTransactionManager messageTransactionManager;

    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        String transactionId = msg.getTransactionId();
        MessageTransaction transaction = messageTransactionManager.get(transactionId);
        if (transaction != null) {
            return LocalTransactionState.UNKNOW;
        }
        transaction = new MessageTransaction();
        transaction.setId(transactionId);
        messageTransactionManager.add(transaction);
        return LocalTransactionState.COMMIT_MESSAGE;
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        String id = msg.getTransactionId();

        MessageTransaction transaction = messageTransactionManager.get(id);
        if (transaction != null) {
            countHashMap.remove(id);
            return LocalTransactionState.COMMIT_MESSAGE;
        }

        Integer num = countHashMap.get(id);
        if (num == null) {
            num = 1;
        }

        if (++num > MAX_COUNT) {
            countHashMap.remove(id);
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }

        countHashMap.put(id, num);
        return LocalTransactionState.UNKNOW;
    }

}

注意messageTransactionManager这个需要改为自己的消息事务存储的Service或者Dao层即可
简单理解
发送消息的时候同时跟随事务保存一条消息事务记录,ID为事务ID(默认为消息的ID,唯一,可自行优化为一个事务只有一个事务ID)
作用

  1. 如果事务正常提交了,则此消息事务记录就没有用
  2. 如果事务正常提交了,但是mq没有收到COMMIT消息(网络故障等),后面就会执行checkLocalTransaction,如果根据里面逻辑判断,如果消息能够在库里面查询到,则意味着事务成功提交了,此时返回COMMIT,进而投递消息
  3. 如果事务失败,MQ没有收到ROLLBACK消息,就会执行checkLocalTransaction,根据尝试最多5次,如果都查询不到,就返ROLLBACK消息,表示消息回滚了

定义MqProderBean

@Data
@Configuration
@ConfigurationProperties(prefix = "rocketmq.producer")
public class MqProducerConfig {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private String groupName;
    private String namesrvAddr;
    private String instanceName;
    private int maxMessageSize; // 4M
    private int sendMsgTimeout;

    @Resource
    private TransactionListener transactionListener;

    private ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("client-transaction-msg-check-thread");
            return thread;
        }
    });

    @Bean
    public TransactionMQProducer transactionMQProducer() {
        if (StringUtils.isBlank(this.groupName)) {
            throw new BaseException("groupName is blank");
        }
        if (StringUtils.isBlank(this.namesrvAddr)) {
            throw new BaseException("nameServerAddr is blank");
        }
        if (StringUtils.isBlank(this.instanceName)) {
            throw new BaseException("instanceName is blank");
        }
        TransactionMQProducer producer = new TransactionMQProducer(this.groupName);
        producer.setNamesrvAddr(this.namesrvAddr);
        producer.setInstanceName(String.format("%s#%d", instanceName, UtilAll.getPid()));
        producer.setMaxMessageSize(this.maxMessageSize);
        producer.setSendMsgTimeout(this.sendMsgTimeout);
        producer.setTransactionListener(transactionListener);
        producer.setExecutorService(executorService);

        try {
            producer.start();
            logger.info(String.format("producer is start ! groupName:[%s],namesrvAddr:[%s]", this.groupName,
                    this.namesrvAddr));
        } catch (MQClientException e) {
            logger.error(String.format("producer is error %s", e.getMessage()), e);
            throw new BaseException(e);
        }
        return producer;
    }

}

注意导入的TransactionMQProducer 为自定义的Producer

对应的配置文件

rocketmq.producer.groupName=xxx
rocketmq.producer.namesrvAddr=x.x.x.x:9876
rocketmq.producer.instanceName=xxx
rocketmq.producer.maxMessageSize=131072
rocketmq.producer.sendMsgTimeout=10000

发送事务消息

@Resource
private TransactionMQProducer mqProducer;
mqProducer.sendMessageInTransaction(message, null);

其中null是传递给TransactionListener的参数,可选

本文事务消息(事务结束后发送消息)场景的解决方案,是建立在rocketmq本身事务消息为基础而实现的


岁月安然
27 声望4 粉丝

随遇而安