RocketMq发送消息

发送类型

Rocketmq提供三种方式可以发送普通消息:同步、异步、和单向发送。

  • 同步:发送方发送消息后,收到服务端响应后才发送下一条消息
  • 异步:发送一条消息后,不等服务端返回就可以继续发送消息或者后续任务处理。发送方通过回调接口接收服务端响应,并处理响应结果。
  • OneWay:发送方发送消息,不等待服务端返回响应且没有回调函数触发,即只发送请求不需要应答。

发送方式对比:发送吞吐量,单向>异步>同步。但单向发送可靠性差存在丢失消息可能,选型根据实际需求确定。

Demo演示

package com.company.mq;

@Slf4j
@RestController
public class Controller {
    /**
     * 生产者组
     */
    private final static String PRODUCE_RGROUP = "test_producer";
    /**
     * 创建生产者对象
     */
    private static DefaultMQProducer producer = null;

    static {
        producer = new DefaultMQProducer(PRODUCE_RGROUP);
        //不开启vip通道 开通口端口会减2
        producer.setVipChannelEnabled(false);
        //绑定name server
        producer.setNamesrvAddr("127.0.0.1:9876");
        try {
            producer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }

    }

    @GetMapping("/message")
    public void message() throws Exception {
        //1、同步
        sync();
        //2、异步
        async();
        //3、单项发送
        oneWay();
    }

    /**
     * 1、同步发送消息
     */
    private void sync() throws Exception {
        //创建消息
        Message message = new Message("topic_family", ("  同步发送  ").getBytes());
        //同步发送消息
        SendResult sendResult = producer.send(message);
        log.info("Product-同步发送-Product信息={}", sendResult);
    }

    /**
     * 2、异步发送消息
     */
    private void async() throws Exception {
        //创建消息
        Message message = new Message("topic_family", ("  异步发送  ").getBytes());
        //异步发送消息
        producer.send(message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("Product-异步发送-输出信息={}", sendResult);
            }

            @Override
            public void onException(Throwable e) {
                e.printStackTrace();
                //补偿机制,根据业务情况进行使用,看是否进行重试
            }
        });
    }

    /**
     * 3、单项发送消息
     */
    private void oneWay() throws Exception {
        //创建消息
        Message message = new Message("topic_family", (" 单项发送 ").getBytes());
        //同步发送消息
        producer.sendOneway(message);
    }
}

消息发送高可用设计

在rocketmq topic的创建机制中,一个topic对应有多个消息队列,那么我们在发送消息时,是如何选择消息队列进行发送的?假如这时有broker宕机了,rocketmq是如何规避故障broker的?

rocketmq在发送消息时,由于nameserver检测broker是否还存活是有延迟的,在选择消息队列时难免会遇到已经宕机的broker,又或者因为网络原因发送失败的,因此rocketmq采取了一些高可用设计的方案,主要通过两个手段:重试与Broker规避

重试

在client端,发送消息的方式有:同步(SYNC)、异步(ASYNC)、单向(ONEWAY)。

那么可以知道,retryTimesWhenSendFailed决定同步方法重试次数,默认重试次数为3次。

重试机制提高了消息发送的成功率。

现在有个由两个broker节点组成的集群,有topic1,默认在每个broker上创建4个队列,分别是:master-a(q0,q1,q2,q3)、master-b(q0,q1,q2,q3),上一次发送消息到master-a的q0队列,此时master-a宕机了,如果继续发送topic1消息,rocketmq如果避免再次发送到master-a。

RocketMq选择队列有两种方式,通过sendLatencyFaultEnable的值来控制,默认值为false,不启动broker故障延迟机制,值为true时启用broker故障延迟机制。

默认机制

sendLatencyFaultEnable=false,消息发送选择队列调用以下方法:

TopicPublishInfo

public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
        if (lastBrokerName == null) {
            return selectOneMessageQueue();
        } else {
            for (int i = 0; i < this.messageQueueList.size(); i++) {
                int index = this.sendWhichQueue.getAndIncrement();
                int pos = Math.abs(index) % this.messageQueueList.size();
                if (pos < 0)
                    pos = 0;
                MessageQueue mq = this.messageQueueList.get(pos);
                if (!mq.getBrokerName().equals(lastBrokerName)) {
                    return mq;
                }
            }
            return selectOneMessageQueue();
        }
    }

lastBrokerName指的是上一次执行消息发送时选择失败的broker,在重试机制下,第一次执行消息发送时,lastBrokerName=null,直接选择以下方法:

public MessageQueue selectOneMessageQueue() {
        int index = this.sendWhichQueue.getAndIncrement();
        int pos = Math.abs(index) % this.messageQueueList.size();
        if (pos < 0)
            pos = 0;
        return this.messageQueueList.get(pos);
    }

sendWhichQueue是一个利用ThreadLocal本地线程存储自增值的一个类,自增值第一次使用Random类随机取值,此后如果消息发送出发重试机制,那么每次自增取值。

public class ThreadLocalIndex {
    private final ThreadLocal threadLocalIndex = new ThreadLocal();
    private final Random random = new Random();

    public int getAndIncrement() {
        Integer index = this.threadLocalIndex.get();
        if (null == index) {
            index = Math.abs(random.nextInt());
            this.threadLocalIndex.set(index);
        }

        index = Math.abs(index + 1);
        this.threadLocalIndex.set(index);
        return index;
    }

    @Override
    public String toString() {
        return "ThreadLocalIndex{" +
            "threadLocalIndex=" + threadLocalIndex.get() +
            '}';
    }
}

此方法直接用sendWhichQueue自增获取值,再与消息队列的长度进行取模运算,取模目的是为了循环选择消息队列。

如果此时选择的队列发送消息失败了,此时重试机制在再次选择队列时lastBrokerName不为空,回到最开始的那个方法,还是利用sendWhichQueue自增获取值,但这里多了一个步骤,与消息队列的长度进行取模运算后,如果此时选择的队列所在的broker还是上一次选择失败的broker,则继续选择下一个队列。

我们再细想一下,如果此时有broker宕机了,在默认机制下很可能下一次选择的队列还是在已经宕机的broker,没有办法规避故障的broker,因此消息发送很可能会再次失败,重试发送造成了不必要的性能损失。

所以rocketmq采用Broker故障延迟机制来规避故障的broker。

Broker规避

sendLatencyFaultEnable=true,消息发送选择队列调用MQFaultStrategy#selectOneMessageQueue:

public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
        if (this.sendLatencyFaultEnable) {
            try {
                int index = tpInfo.getSendWhichQueue().getAndIncrement();
                for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
                    // 队列位置值取模
                    int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
                    if (pos < 0)
                        pos = 0;
                    MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
                    // 校验队列是否可用
                    if (latencyFaultTolerance.isAvailable(mq.getBrokerName()))
                        return mq;
                }

                // 尝试从失败的broker列表中选择一个可用的broker
                final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
                int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
                if (writeQueueNums > 0) {
                    final MessageQueue mq = tpInfo.selectOneMessageQueue();
                    if (notBestBroker != null) {
                        mq.setBrokerName(notBestBroker);
                        mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
                    }
                    return mq;
                } else {
                    // 从失败条目中移除已经恢复的broker
                    latencyFaultTolerance.remove(notBestBroker);
                }
            } catch (Exception e) {
                log.error("Error occurred when selecting message queue", e);
            }

            return tpInfo.selectOneMessageQueue();
        }

        // 默认机制
        return tpInfo.selectOneMessageQueue(lastBrokerName);
    }

消息类型

消息客户端提供多种SDK: 普通、顺序、事务、延时消息

普通消息

MQ生产者客户端对象是线程安全的,可以在多线程之间共享使用。同时也可以用多线程并发发送消息可以增加消息TPS,一般项目中创建一个Peoducer实例就好。

 DefaultMQProducer producer = new DefaultMQProducer("arch-rocketmq");
 producer.setNamesrvAddr("localhost:9876");
 producer.start();   
 try {
     Message msg = new Message("mq-test4","*", ("Hello RocketMQ").getBytes("UTF-8"));
     SendResult sendResult = producer.send(msg);
     System.out.printf("%s%n", sendResult);
 }  

顺序消息

提供一种严格按照顺序来发送和消费的消息类型。指定一个topic后,所有消息Message根据sharding key进行分区,相同key的消息在同一个分区内完全按照FIFO进行发送和消费。

MQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
producer.start();
for (int i = 0; i < 100; i++) {
    int orderId = i % 10;
    Message msg = new Message("Topic-test", "*", "KEY" + i,("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
    SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
        @Override
        public MessageQueue select(List mqs, Message msg, Object arg) {
           Integer id = (Integer) arg;
          int index = id % mqs.size();
          return mqs.get(index);
       }
    }, orderId);
    System.out.printf("%s%n", sendResult);
}

延时消息

用于指定消息发送到消息队列后,延时一段时间才会被客户端进行消费,使用于任务延时场景。开源项目支持18个延迟级别 delayTimeLevel: 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。

 DefaultMQProducer producer = new DefaultMQProducer("arch-rocketmq");
 producer.setNamesrvAddr("localhost:9876");
 producer.start();   
 try {
     Message msg = new Message("mq-test4","*", ("Hello RocketMQ").getBytes("UTF-8"));
     msg.setDelayTimeLevel(3);  // 延迟10s
     SendResult sendResult = producer.send(msg);
     System.out.printf("%s%n", sendResult);
 } catch (Exception e) { }

RocketMq发送消息_第1张图片

  1. producer要将一个延迟消息发送到某个Topic中
  2. Broker判断这是一个延迟消息后,将其通过临时存储进行暂存。
  3. Broker内部通过一个延迟服务(delay service)检查消息是否到期,将到期的消息投递到目标Topic中。这个的延迟服务名字为delay service,不同消息中间件的延迟服务模块名称可能不同。
  4. 消费者消费目标topic中的延迟投递的消息

发送延时消息时先把消息按照延迟时间段发送到指定的队列中(rocketmq把每种延迟时间段的消息都存放到同一个队列中)然后通过一个定时器进行轮训这些队列,查看消息是否到期,如果到期就把这个消息发送到指定topic的队列中,这样的好处是同一队列中的消息延时时间是一致的,还有一个好处是这个队列中的消息时按照消息到期时间进行递增排序的,说的简单直白就是队列中消息越靠前的到期时间越早。

具体实现在 ScheduleMessageService这个类

public void start() {
        if (started.compareAndSet(false, true)) {
            this.timer = new Timer("ScheduleMessageTimerThread", true);
            for (Map.Entry entry : this.delayLevelTable.entrySet()) {
                Integer level = entry.getKey();
                Long timeDelay = entry.getValue();
                Long offset = this.offsetTable.get(level);
                if (null == offset) {
                    offset = 0L;
                }

                // 启动定时任务
                if (timeDelay != null) {
                    this.timer.schedule(new DeliverDelayedMessageTimerTask(level, offset), FIRST_DELAY_TIME);
                }
            }

            this.timer.scheduleAtFixedRate(new TimerTask() {

                @Override
                public void run() {
                    try {
                        if (started.get()) ScheduleMessageService.this.persist();
                    } catch (Throwable e) {
                        log.error("scheduleAtFixedRate flush exception", e);
                    }
                }
            }, 10000, this.defaultMessageStore.getMessageStoreConfig().getFlushDelayOffsetInterval());
        }
    }

 

class DeliverDelayedMessageTimerTask extends TimerTask {
        private final int delayLevel;
        private final long offset;

        public DeliverDelayedMessageTimerTask(int delayLevel, long offset) {
            this.delayLevel = delayLevel;
            this.offset = offset;
        }

        @Override
        public void run() {
            try {
                if (isStarted()) {
                    this.executeOnTimeup();
                }
            } catch (Exception e) {
                // XXX: warn and notify me
                log.error("ScheduleMessageService, executeOnTimeup exception", e);
                ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
                    this.delayLevel, this.offset), DELAY_FOR_A_PERIOD);
            }
        }

        /**
         * @return
         */
        private long correctDeliverTimestamp(final long now, final long deliverTimestamp) {

            long result = deliverTimestamp;

            long maxTimestamp = now + ScheduleMessageService.this.delayLevelTable.get(this.delayLevel);
            if (deliverTimestamp > maxTimestamp) {
                result = now;
            }

            return result;
        }

        public void executeOnTimeup() {
            // 获取consumerQuery
            ConsumeQueue cq =
                ScheduleMessageService.this.defaultMessageStore.findConsumeQueue(TopicValidator.RMQ_SYS_SCHEDULE_TOPIC,
                    delayLevel2QueueId(delayLevel));

            long failScheduleOffset = offset;

            if (cq != null) {
                // 获取延时队列里的消息
                SelectMappedBufferResult bufferCQ = cq.getIndexBuffer(this.offset);
                if (bufferCQ != null) {
                    try {
                        long nextOffset = offset;
                        int i = 0;
                        ConsumeQueueExt.CqExtUnit cqExtUnit = new ConsumeQueueExt.CqExtUnit();
                        // 循环处理
                        for (; i < bufferCQ.getSize(); i += ConsumeQueue.CQ_STORE_UNIT_SIZE) {
                            long offsetPy = bufferCQ.getByteBuffer().getLong();
                            int sizePy = bufferCQ.getByteBuffer().getInt();
                            long tagsCode = bufferCQ.getByteBuffer().getLong();

                            if (cq.isExtAddr(tagsCode)) {
                                if (cq.getExt(tagsCode, cqExtUnit)) {
                                    tagsCode = cqExtUnit.getTagsCode();
                                } else {
                                    //can't find ext content.So re compute tags code.
                                    log.error("[BUG] can't find consume queue extend file content!addr={}, offsetPy={}, sizePy={}",
                                        tagsCode, offsetPy, sizePy);
                                    long msgStoreTime = defaultMessageStore.getCommitLog().pickupStoreTimestamp(offsetPy, sizePy);
                                    tagsCode = computeDeliverTimestamp(delayLevel, msgStoreTime);
                                }
                            }

                            long now = System.currentTimeMillis();
                            long deliverTimestamp = this.correctDeliverTimestamp(now, tagsCode);

                            nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);

                            long countdown = deliverTimestamp - now;

                            if (countdown <= 0) {
                                MessageExt msgExt =
                                    ScheduleMessageService.this.defaultMessageStore.lookMessageByOffset(
                                        offsetPy, sizePy);

                                if (msgExt != null) {
                                    try {
                                        MessageExtBrokerInner msgInner = this.messageTimeup(msgExt);
                                        if (TopicValidator.RMQ_SYS_TRANS_HALF_TOPIC.equals(msgInner.getTopic())) {
                                            log.error("[BUG] the real topic of schedule msg is {}, discard the msg. msg={}",
                                                    msgInner.getTopic(), msgInner);
                                            continue;
                                        }
                                        PutMessageResult putMessageResult =
                                            ScheduleMessageService.this.writeMessageStore
                                                .putMessage(msgInner);

                                        if (putMessageResult != null
                                            && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
                                            continue;
                                        } else {
                                            // XXX: warn and notify me
                                            log.error(
                                                "ScheduleMessageService, a message time up, but reput it failed, topic: {} msgId {}",
                                                msgExt.getTopic(), msgExt.getMsgId());
                                            ScheduleMessageService.this.timer.schedule(
                                                new DeliverDelayedMessageTimerTask(this.delayLevel,
                                                    nextOffset), DELAY_FOR_A_PERIOD);
                                            ScheduleMessageService.this.updateOffset(this.delayLevel,
                                                nextOffset);
                                            return;
                                        }
                                    } catch (Exception e) {
                                        /*
                                         * XXX: warn and notify me



                                         */
                                        log.error(
                                            "ScheduleMessageService, messageTimeup execute error, drop it. msgExt="
                                                + msgExt + ", nextOffset=" + nextOffset + ",offsetPy="
                                                + offsetPy + ",sizePy=" + sizePy, e);
                                    }
                                }
                            } else {
                                ScheduleMessageService.this.timer.schedule(
                                    new DeliverDelayedMessageTimerTask(this.delayLevel, nextOffset),
                                    countdown);
                                ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
                                return;
                            }
                        } // end of for

                        nextOffset = offset + (i / ConsumeQueue.CQ_STORE_UNIT_SIZE);
                        ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(
                            this.delayLevel, nextOffset), DELAY_FOR_A_WHILE);
                        ScheduleMessageService.this.updateOffset(this.delayLevel, nextOffset);
                        return;
                    } finally {

                        bufferCQ.release();
                    }
                } // end of if (bufferCQ != null)
                else {

                    long cqMinOffset = cq.getMinOffsetInQueue();
                    if (offset < cqMinOffset) {
                        failScheduleOffset = cqMinOffset;
                        log.error("schedule CQ offset invalid. offset=" + offset + ", cqMinOffset="
                            + cqMinOffset + ", queueId=" + cq.getQueueId());
                    }
                }
            } // end of if (cq != null)

            ScheduleMessageService.this.timer.schedule(new DeliverDelayedMessageTimerTask(this.delayLevel,
                failScheduleOffset), DELAY_FOR_A_WHILE);
        }
}

具体思路

1.producer端设置消息delayLevel延迟级别,消息属性DELAY中存储了对应了延时级别

2.broker端收到消息后,判断延时消息延迟级别,如果大于0,则备份消息原始topic,queueId,并将消息topic改为延时消息队列特定topic(SCHEDULE_TOPIC),queueId改为延时级别-1

3.mq服务端ScheduleMessageService中,为每一个延迟级别单独设置一个定时器,定时(每隔1秒)拉取对应延迟级别的消费队列

4.根据消费偏移量offset从commitLog中解析出对应消息

5.从消息tagsCode中解析出消息应当被投递的时间,与当前时间做比较,判断是否应该进行投递

6.若到达了投递时间,则构建一个新的消息,并从消息属性中恢复出原始的topic,queueId,并清除消息延迟属性,从新进行消息投递

优点:设计简单,把所有相同延迟时间的消息都先放到一个队列中,定时扫描,可以保证消息消费的有序性

缺点:定时器采用了timer,timer是单线程运行,如果延迟消息数量很大的情况下,可能单线程处理不过来,造成消息到期后也没有发送出去的情况。同时将相同的延时消息放入一个队列中不能实现任意级别的时延。目前正在整理一个任意时延的消息队列,完成时会将git献上。


事务消息

RocketMQ提供类似XA分布式事务能力(XA则是一种分布式事务协议,包含事务管理器和本地资源两部分),主要解决了消息发送和数据库事务带来不一致问题。整个事务消息交互如下图

RocketMq发送消息_第2张图片

RocketMq发送消息_第3张图片

 

RocketMQ在其消息定义的基础上,对事务消息扩展了两个相关的概念:

  1. Half(Prepare) Message——半消息(预处理消息) 

半消息是一种特殊的消息类型,该状态的消息暂时不能被Consumer消费。当一条事务消息被成功投递到Broker上,但是Broker并没有接收到Producer发出的二次确认时,该事务消息就处于"暂时不可被消费"状态,该状态的事务消息被称为半消息。

  1. Message Status Check——消息状态回查

由于网络抖动、Producer重启等原因,可能导致Producer向Broker发送的二次确认消息没有成功送达。如果Broker检测到某条事务消息长时间处于半消息状态,则会主动向Producer端发起回查操作,查询该事务消息在Producer端的事务状态(Commit 或 Rollback)。可以看出,Message Status Check主要用来解决分布式事务中的超时问题。

半消息发送流程

在本地应用发送事务消息的核心类是TransactionMQProducer,该类通过继承DefaultMQProducer来复用大部分发送消息相关的逻辑

@Override
public TransactionSendResult sendMessageInTransaction(final Message msg,
    final Object arg) throws MQClientException {
    if (null == this.transactionListener) {
        throw new MQClientException("TransactionListener is null", null);
    }

    return this.defaultMQProducerImpl.sendMessageInTransaction(msg, null, arg);
}
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);
}

DefaultMQProducer类的sendMessageInTransaction方法源码

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

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

        Validators.checkMessage(msg, this.defaultMQProducer);

        // 给消息打上事务消息相关的标记,用于MQ服务端区分普通消息和事务消息
        SendResult sendResult = null;
        MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true");
        MessageAccessor.putProperty(msg, MessageConst.PROPERTY_PRODUCER_GROUP, this.defaultMQProducer.getProducerGroup());
        try {

            // 发送半消息(half message)
            sendResult = this.send(msg);
        } catch (Exception e) {
            throw new MQClientException("send message Exception", e);
        }

        LocalTransactionState localTransactionState = LocalTransactionState.UNKNOW;
        Throwable localException = null;
        switch (sendResult.getSendStatus()) {

            // 发送成功则由transactionListener执行本地事务
            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;
        }


        // 执行endTransaction方法,如果半消息发送失败或本地事务执行失败告诉服务端是删除半消息,
        // 半消息发送成功且本地事务执行成功则告诉服务端生效半消息。
        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;
    }

主要流程

  1. 给消息打上事务消息相关的标记,用于MQ服务端区分普通消息和事务消息
  2. 发送半消息(half message)
  3. 发送成功则由transactionListener执行本地事务
  4. 执行endTransaction方法,如果半消息发送失败本地事务执行失败告诉服务端是删除半消息,半消息发送成功本地事务执行成功则告诉服务端生效半消息。

RocketMQ Server

Server在接收到消息过后会进行一些领域对象的转化和是否支持事务消息的权限校验,设置事务TOPIC:RMQ_SYS_TRANS_OP_HALF_TOPIC

半消息存储处理:TransactionalMessageBridge.putHalfMessage

public PutMessageResult putHalfMessage(MessageExtBrokerInner messageInner) {
        return store.putMessage(parseHalfMessageInner(messageInner));
    }


private MessageExtBrokerInner parseHalfMessageInner(MessageExtBrokerInner msgInner) {
        MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_TOPIC, msgInner.getTopic());
        MessageAccessor.putProperty(msgInner, MessageConst.PROPERTY_REAL_QUEUE_ID,
            String.valueOf(msgInner.getQueueId()));
        msgInner.setSysFlag(
            MessageSysFlag.resetTransactionValue(msgInner.getSysFlag(), MessageSysFlag.TRANSACTION_NOT_TYPE));
        msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic());
        msgInner.setQueueId(0);
        msgInner.setPropertiesString(MessageDecoder.messageProperties2String(msgInner.getProperties()));
        return msgInner;
    }

主要流程

  1. 将消息的topic,queueId放进消息体自身的map里进行缓存
  2. 将消息的topic 设置为“RMQ_SYS_TRANS_OP_HALF_TOPIC”,queueId设置为0
  3. 将消息写入磁盘持久化

所有的事务半消息都会被放进同一个topic的同一个queue里面,通过对topic的区分,从而避免了半消息被consumer给消费到。Server将半消息持久化后然后会发送结果给我们本地的应用程序。

定时任务回查

TransactionalMessageService,check方法和putBackHalfMsgQueue

@Override
public void check(long transactionTimeout, int transactionCheckMax,
    AbstractTransactionalMessageCheckListener listener) {
                  ...
     if (!putBackHalfMsgQueue(msgExt, i)) {
        continue;
     }
       listener.resolveHalfMsg(msgExt);
   } 
                                    ...
}
private boolean putBackHalfMsgQueue(MessageExt msgExt, long offset) {
        PutMessageResult putMessageResult = putBackToHalfQueueReturnResult(msgExt);
        if (putMessageResult != null
            && putMessageResult.getPutMessageStatus() == PutMessageStatus.PUT_OK) {
            msgExt.setQueueOffset(
                putMessageResult.getAppendMessageResult().getLogicsOffset());
            msgExt.setCommitLogOffset(
                putMessageResult.getAppendMessageResult().getWroteOffset());
            msgExt.setMsgId(putMessageResult.getAppendMessageResult().getMsgId());
            log.debug(
                "Send check message, the offset={} restored in queueOffset={} "
                    + "commitLogOffset={} "
                    + "newMsgId={} realMsgId={} topic={}",
                offset, msgExt.getQueueOffset(), msgExt.getCommitLogOffset(), msgExt.getMsgId(),
                msgExt.getUserProperty(MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX),
                msgExt.getTopic());
            return true;
        } else {
            log.error(
                "PutBackToHalfQueueReturnResult write failed, topic: {}, queueId: {}, "
                    + "msgId: {}",
                msgExt.getTopic(), msgExt.getQueueId(), msgExt.getMsgId());
            return false;
        }
    }

check方法主要是对半消息进行过滤

putBackHalfMsgQueue方法,每次把半消息从磁盘拉到内存里进行处理都会对其属性进行改变(例如TRANSACTION_CHECK_TIMES,这是是否丢弃事务消息的关键信息),所以在发送回查消息之前需要对半消息再次放进磁盘。RocketMQ采取的方法是基于最新的物理偏移量重新写入,而不是对原有的半消息进行修改,其中的目的就是RocketMQ的存储设计采用顺序写,如果去修改消息 ,无法做到高性能。

半消息处理,AbstractTransactionalMessageCheckListener.resolveHalfMsg,主要目的是开启一个线程然后发送check消息。

public void resolveHalfMsg(final MessageExt msgExt) {
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    sendCheckMessage(msgExt);
                } catch (Exception e) {
                    LOGGER.error("Send check message error!", e);
                }
            }
        });
    }

本地应用消息处理

DefaultMQProducerImpl.checkTransactionState,是本地应用对回查消息的处理逻辑

@Override
public void checkTransactionState(final String addr, final MessageExt msg,
    final CheckTransactionStateRequestHeader header) {
    Runnable request = new Runnable() {
        ...
        @Override
        public void run() {
            ...
     TransactionListener transactionListener = getCheckListener();
            ...
     localTransactionState = transactionListener.checkLocalTransaction(message);
               ...
                 
      this.processTransactionState(
                    localTransactionState,
                    group,
                    exception);        
        }
      
        private void processTransactionState(
           ...
 DefaultMQProducerImpl.this.mQClientFactory.getMQClientAPIImpl().endTransactionOneway(brokerAddr, thisHeader, remark,
                    3000);
           ...
        }
    };
    this.checkExecutor.submit(request);
}
  • 开启一个线程来执行回查的逻辑
  • 执行transactionListener的checkLocalTransaction方法来获取本地事务执行的结果

link

RocketMq技术内幕

RocketMQ系列3-消息发送流程 - 简书

【分布式技术专题】RocketMQ延迟消息实现原理和源码分析 - 掘金

https://segmentfault.com/a/1190000021822374

https://segmentfault.com/a/1190000019755235

你可能感兴趣的:(mq,java,开发语言,后端)