本文主要涉及的内容有发送消息时:

  • 顺序消息之队列选择机制
  • RocketMQ key
  • RocketMQ tag
  • RocketMQ msgId

顺序消息之队列选择机制

很多业务场景下需要保证消息的顺序处理,比如订单流转到不同状态都会向同一个topic发送消息,但消费者在进行消费时希望按照订单的的变化顺序进行处理,如果不控制的话消息会发送到topic里的不同队列里去,这样消费者就没办法进行顺序消费了。本文先只分析Producer是如何发送顺序消息的,至于Consumer的处理以后再进行分析。
我们知道RocketMQ是支持队列级别的顺序消息的,那么在发送消息的时候只要做到将需要顺序消费的消息按顺序都发送到一个队列里就可以了。RocketMQ在消息发送时提供了自定义的队列负载机制,消息发送的默认队列负载机制为轮询,那如何进行队列选择呢?RocketMQ 提供了如下 API(这里只举了其中一个API的例子):

SendResult send(final Message msg, final MessageQueueSelector selector, final Object arg)
        throws MQClientException, RemotingException, MQBrokerException, InterruptedException;

使用示例:

public static void main(String[] args) throws UnsupportedEncodingException {
        try {
            MQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
            producer.start();

            String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"};
            for (int i = 0; i < 100; i++) {
                int orderId = i % 10;
                Message msg =
                    new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i,
                        ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
                SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(List<MessageQueue> 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);
            }

            producer.shutdown();
        } catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
            e.printStackTrace();
        }
    }

在发送消息的时候,我们指定了选择队列的实现,传入参考arg(可以是orderId或userId等)然后对整个队列取模,这样就可以做到同一个arg都会发到同一个队列里了。

关于顺序消息下面几点需要特别注意:

  1. 如果我们使用了MessageQueueSelector,那消息发送的重试机制将失效,即 RocketMQ 客户端并不会重试,消息发送的高可用需要由业务方来保证,一个办法就是消息发送失败后存在数据库中,然后定时调度,最终将消息发送到 MQ。
  2. 当使用的是异步发送的方式且进行了重试(异步时RocketMQ本身不会重试,业务方可能会进行重试),比如有1,2,3三条消息,但消息2失败了再进行重发,那它可能会在消息3的后面重发成功,像这种情形也就做不到消息的有序性了。所以我们在使用顺序消息时不要使用异步发送而要采用同步发送方式。
  3. 当 Broker 宕机重启,由于分区会发生重平衡动作,此时生产端根据 key 哈希取模得到的分区发生变化,这时会发生短暂消息顺序不一致的现象。针对这一问题,如果业务方不能容忍短时的顺序一致性,要么集群出现故障后集群立马不可用,要么主题做成单分区,但这么做大大牺牲了集群的高可用,单分区也会另集群性能大大降低

RocketMQ key的使用

RocketMQ 提供了丰富的消息查询机制,例如使用消息偏移量、消息全局唯一 msgId、消息 Key。

RocketMQ 在消息发送的时候,可以为一条消息设置索引建,例如上面示例中我们指定了"KEY"+序号作为消息的 Key,这样我们可以通过该索引 Key 进行查询消息。

如果需要为消息指定 Key,只需要在构建 Message 的时候传入 Key 参数即可,例如下面的 API:

public Message(String topic, String tags, String keys, byte[] body)

RocketMQ tag的使用

RocketMQ 可以为 Topic 设置 Tag(标签),这样消费端可以对 Topic 中的消息基于 Tag 进行过滤,即选择性的对 Topic 中的消息进行处理。

例如一个订单的全生命流程:创建订单、待支付、支付完成、商家审核,商家发货、买家发货,订单每一个状态的变更都会向同一个主题 order_topic 发送消息,但不同下游系统只关注订单流中某几个阶段的消息,并不是需要处理所有消息。我们就可以对上面每个状态指定不同的tag,消费端在订阅消息的时候也指定相应的tag,这样消费者就可以只消费自己指定tag的消息了。API与指定message key一样相同:

public Message(String topic, String tags, String keys, byte[] body)

消费端订阅时指定tag API:

void subscribe(final String topic, final String subExpression) throws MQClientException;

在控制台我们可以看到不符合订阅的 Tag,其消费状态显示为 CONSUMED_BUT_FILTERED(消费但被过滤掉)。

RocketMQ msgId

当我们用RocketMQ发送信息的时候通常都会返回如下信息:

SendResult [sendStatus=SEND_OK, msgId=0A42333A0DC818B4AAC246C290FD0000, offsetMsgId=0A42333A00002A9F000000000134F1F5, messageQueue=MessageQueue [topic=topicTest1, brokerName=mac.local, queueId=3], queueOffset=4]

对于客户端来说msgId是由客户端producer自己生成的,offsetMsgId是由服务端broker生成的,其中offsetMsgId就是我们在rocketMQ控制台直接输入查询的那个messageId。

我们先来看下生成msgId的代码:

    public static String createUniqID() {
        StringBuilder sb = new StringBuilder(LEN * 2);
        sb.append(FIX_STRING);
        sb.append(UtilAll.bytes2string(createUniqIDBuffer()));
        return sb.toString();
    }

    public static String createUniqID() {
        StringBuilder sb = new StringBuilder(LEN * 2);
        sb.append(FIX_STRING);
        sb.append(UtilAll.bytes2string(createUniqIDBuffer()));
        return sb.toString();
    }

    private static byte[] createUniqIDBuffer() {
        ByteBuffer buffer = ByteBuffer.allocate(4 + 2);
        long current = System.currentTimeMillis();
        if (current >= nextStartTime) {
            setStartTime(current);
        }
        buffer.position(0);
        buffer.putInt((int) (System.currentTimeMillis() - startTime));
        buffer.putShort((short) COUNTER.getAndIncrement());
        return buffer.array();
    }

FIX_STRING是什么内容呢?在MessageClientIDSetter类的静态代码块里有:

    static {
        LEN = 4 + 2 + 4 + 4 + 2;
        ByteBuffer tempBuffer = ByteBuffer.allocate(10);
        tempBuffer.position(2);
        tempBuffer.putInt(UtilAll.getPid());
        tempBuffer.position(0);
        try {
            tempBuffer.put(UtilAll.getIP());
        } catch (Exception e) {
            tempBuffer.put(createFakeIP());
        }
        tempBuffer.position(6);
        tempBuffer.putInt(MessageClientIDSetter.class.getClassLoader().hashCode());
        FIX_STRING = UtilAll.bytes2string(tempBuffer.array());
        setStartTime(System.currentTimeMillis());
        COUNTER = new AtomicInteger(0);
    }

组成成份有:

  • 客户端发送 IP,支持 IPV4 和 IPV6
  • 进程 PID(2 字节)
  • 类加载器的 hashcode(4 字节)
  • 当前系统时间戳与启动时间戳的差值(4 字节)
  • 自增序列(2 字节)

对于每个producer实例来说ip都是唯一的,所以不同producer生成的msgId是不会重复的。对于producer单个实例来说的区分因子是:time + counter。应用不重启的情况下msgId是可以保证唯一性的,应用重启了只要系统的时钟不变msgId也是唯一的。所以只要系统的时钟不回拨我们就可以保证msgId的全局唯一。

上面组成部分里的时间戳差值是当前时间戳与上个月时间戳的差值,那应用运行了一个月再进行重启msgId就会重复了。从生成算法上来说是的!但是MQ的message是有时效性的,有效期是72小时也就是3天。每天的凌晨4点rocketMQ会把过期的message清除掉。所以msgId也是保证全局唯一的。

最后再提一下offsetMsgId.
offsetMsgId指的是消息所在 Broker 的物理偏移量,即在 commitlog 文件中的偏移量,其组成如下两部分组成:

  • Broker 的 IP 与端口号
  • commitlog 中的物理偏移量
    public static String createMessageId(final ByteBuffer input, final ByteBuffer addr, final long offset) {
        input.flip();
        input.limit(MessageDecoder.MSG_ID_LENGTH);

        input.put(addr);
        input.putLong(offset);

        return UtilAll.bytes2string(input.array());
    }

我们可以根据 offsetMsgId 即可以定位到具体的消息,无需知道该消息的 Topic 等其他一切信息。


步履不停
38 声望13 粉丝

好走的都是下坡路