RocketMQ-顺序消息

顺序消息的概念

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。

RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。

  • 局部有序:指发送同一个队列的消息有序,可以在发送消息时指定队列,在消费消息时也按顺序消费。
    示例场景:同一订单ID的消息要保证有序,不同订单ID的消息没有约束,相互不影响,且不同订单ID之间的消息是并行的。
  • 全局有序:设置 Topic只有一个队列可以实现全局有序,创建Topic时手动设置。此类场景极少,性能差,一般不推荐使用

顺序消费的原理

在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);

而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。

但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。

当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

最后我们从源码角度分析RocketMQ怎么实现发送顺序消息的。

RocketMQ通过轮询所有队列的方式来确定消息被发送到哪一个队列(负载均衡策略)。比如下面的示例中:

public SendResult syncSendOrderly(String destination, Message<?> message, String hashKey, long timeout) {
        if (!Objects.isNull(message) && !Objects.isNull(message.getPayload())) {
            try {
                long now = System.currentTimeMillis();
                org.apache.rocketmq.common.message.Message rocketMsg = this.createRocketMqMessage(destination, message);
                SendResult sendResult = this.producer.send(rocketMsg, this.messageQueueSelector, hashKey, timeout);
                long costTime = System.currentTimeMillis() - now;
                if (log.isDebugEnabled()) {
                    log.debug("send message cost: {} ms, msgId:{}", costTime, sendResult.getMsgId());
                }

                return sendResult;
            } catch (Exception var12) {
                log.error("syncSendOrderly failed. destination:{}, message:{} ", destination, message);
                throw new MessagingException(var12.getMessage(), var12);
            }
        } else {
            log.error("syncSendOrderly failed. destination:{}, message is null ", destination);
            throw new IllegalArgumentException("`message` and `message.payload` cannot be null");
        }
    }

根据hash值判断发送到那个消息队列:

public class SelectMessageQueueByHash implements MessageQueueSelector {
    public SelectMessageQueueByHash() {
    }
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        int value = arg.hashCode() % mqs.size();
        if (value < 0) {
            value = Math.abs(value);
        }
        return (MessageQueue)mqs.get(value);
    }
}

演示

下面用订单进行分区有序的示例。一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。

Producer


        try {
        List<OrderStep> orderList = buildOrders();
        //顺序消息
        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = sdf.format(date);
        for (int i = 0; i < 10; i++) {
            // 加个时间前缀
            String body = dateStr + " Hello RocketMQ " + i + " " ;
            Message msg = null;

            msg = new Message("add-amount", "tags"+i, "KEY" + i,
                body.getBytes(RemotingHelper.DEFAULT_CHARSET));
         // hashkey是为了确保这些消息被路由到同一个消息队列,这样消费者就能够按照顺序处理它们
            SendResult sendResult = rocketMQTemplate.syncSendOrderly("add-amount", msg, "syncOrderlyHashKey");
            System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s",
                sendResult.getSendStatus(),
                sendResult.getMessageQueue().getQueueId(),
                body));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 生成模拟订单数据
     */
    private static List<OrderStep> buildOrders() {
        List<OrderStep> orderList = new ArrayList<OrderStep>();

        OrderStep orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("购物车");
        orderList.add(orderDemo);

        return orderList;
    }

    /**
     * 订单的步骤
     */
    private static class OrderStep {
        private long orderId;
        private String desc;

        public long getOrderId() {
            return orderId;
        }

        public void setOrderId(long orderId) {
            this.orderId = orderId;
        }

        public String getDesc() {
            return desc;
        }

        public void setDesc(String desc) {
            this.desc = desc;
        }

        @Override
        public String toString() {
            return "OrderStep{" +
                "orderId=" + orderId +
                ", desc='" + desc + '\'' +
                '}';
        }
    }

日志:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Consumer

(为了提高消费能力这里我采用了批量顺序消费,加快消费速率)

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.apache.rocketmq.spring.core.RocketMQPushConsumerLifecycleListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@Service
@RocketMQMessageListener(topic = "add-amount",consumerGroup = "cloud-group",
    consumeMode = ConsumeMode.ORDERLY //顺序消费参数)
@RequiredArgsConstructor(onConstructor = @__(@Autowired) )
public class AccountListener implements RocketMQListener<String>, RocketMQPushConsumerLifecycleListener
{


    @Override
    public void onMessage(String  userAddMoneyDTO) {
        log.info("received message: {}",userAddMoneyDTO);
       // accountMapper.increaseAmount(userAddMoneyDTO.getUserCode(),userAddMoneyDTO.getAmount());
        log.info("add money success");
    }
    //设置批量消费参数
    @Override
    public void prepareStart(DefaultMQPushConsumer consumer) {
        consumer.setPullInterval(100);
        //消费者一次处理的消息的最大批次
        consumer.setConsumeMessageBatchMaxSize(100);
        //费者一次从消息队列中拉取的消息批次大小
        consumer.setPullBatchSize(100);
        // 为了看到效果,暂时只让一个线程拉取消息(mq默认20)
        consumer.setConsumeThreadMax(32);
        consumer.setConsumeThreadMin(10);
    }
}

截图在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值