RocketMQ 原生API使用

普通消息

生产者

单向发送

// 创建生产者,指定生产者组(oneway-producer-group)
DefaultMQProducer producer = new DefaultMQProducer("oneway-producer-group"); 
// 指定 NameServer
producer.setNamesrvAddr("127.0.0.1:9876"); 
// 启动生产者
producer.start(); 
// 构建消息,指定 Topic(onewayTopic)
Message message = new Message("onewayTopic", "我是一个单向消息".getBytes()); 
// 单向发送,没有返回值,不知道成功与否
producer.sendOneway(message);
// 关闭生产者
producer.shutdown();

同步发送

DefaultMQProducer producer = new DefaultMQProducer("test-producer-group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
// 发送 10 个消息,这10个消息将分摊到4个队列(2-2-3-3)
for (int i = 0; i < 10; i++) {
    Message message = new Message("testTopic", "我是一个简单的消息".getBytes());
  	// 同步发送,有返回值(返回值中有消息ID,发送到哪个队列了,位点等信息)
    SendResult sendResult = producer.send(message);
    System.out.println("消息发送状态:" + sendResult.getSendStatus());
}
producer.shutdown();

异步发送

DefaultMQProducer producer = new DefaultMQProducer("async-producer-group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
Message message = new Message("asyncTopic", "我是一个异步消息".getBytes());
// 回调方法接收服务器响应,是异步的,不阻塞当前
producer.send(message, new SendCallback() {
    @Override
    public void onSuccess(SendResult sendResult) {
        System.out.println("发送成功");
    }

    @Override
    public void onException(Throwable e) {
        System.err.println("发送失败:" + e.getMessage());
    }
});
producer.shutdown();

消费者

// 创建一个消费者(消费者组为 test-consumer-group)
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-consumer-group");
// 连接 name server
consumer.setNamesrvAddr("127.0.0.1:9876");
// 订阅一个主题  * 标识订阅这个主题中所有的消息(可以根据 Tag 过滤)
consumer.subscribe("testTopic", "*");
// 设置一个监听器(MessageListenerConcurrently:并发模式)
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        // 全部内容有很多,包括消息头和消息体,消息体就是生产者发送的内容
        System.out.println("消息全部内容" + msgs.get(0).toString());
        System.out.println("消息体:" + new String(msgs.get(0).getBody()));
        System.out.println("消费上下文:" + context);
        // 返回值 CONSUME_SUCCESS 成功,消息会从 mq 出队
        // 返回值 RECONSUME_LATER 失败, 消息会重新回到队列(默认重试16次)
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});
// 启动
consumer.start();

批量消息

  1. 生产者把一批消息一起发送,只发送一次
  2. 消费者还是一条一条的来消费

生产者

DefaultMQProducer producer = new DefaultMQProducer("batch-producer-group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
// 构建消息
List<Message> msgs = Arrays.asList(
        new Message("batchTopic", "我是一组消息的A消息".getBytes()),
        new Message("batchTopic", "我是一组消息的B消息".getBytes()),
        new Message("batchTopic", "我是一组消息的C消息".getBytes())
);
// send 被重载过,支持传入一个 List<Message>,虽然多个消息,但是是一次发送,所以这批消息将进入同一个队列中
SendResult send = producer.send(msgs);
System.out.println(send);
producer.shutdown();

消费者

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("batch-consumer-group");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.subscribe("batchTopic", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        // 会打印 3 次(一次发送多个消息,消费时还是一条一条的来消费)
        System.out.println("收到消息了" + new Date());
        System.out.println(msgs.size());
        System.out.println(new String(msgs.get(0).getBody()));
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});
consumer.start();

延迟消息

  1. 4.x 只支持级别,不同级别对应不同延迟时间,这个可以改,要到配置文件 conf 里改;
  2. 5.x 可以指定时间了
  3. 消费者没有需要注意的地方,和前面的示例一样即可
DefaultMQProducer producer = new DefaultMQProducer("ms-producer-group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
Message message = new Message("orderMsTopic", "我是一个延迟消息".getBytes());
// 给消息设置一个延迟时间,不同的延迟级别表示不同的延迟时间
// messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
message.setDelayTimeLevel(3);
producer.send(message);
System.out.println("发送时间" + new Date());
producer.shutdown();

事务消息

RocketMQ原生API事务消息示例及原理

顺序消息

  1. 一个消费者对应一个队列,所以消息不能投递到不同的队列,所以在发消息时,同一组顺序消息要发送到相同的队列中(定义算法来选择队列实现)
  2. 消费者默认是并发模式(多线程),因为多线程天生保证不了顺序,所以不能使用并发模式,要使用顺序模式(单线程+锁实现)

生产者

DefaultMQProducer producer = new DefaultMQProducer("ordered_producer_group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();

// 3 个订单
String[] orderIds = {"ORDER_001", "ORDER_002", "ORDER_003"};

// 每个订单 4 个步骤,顺序执行(创建、支付、发货、完成)
String[] orderSteps = {"CREATE", "PAY", "SHIP", "COMPLETE", "CANCEL"};

for (String orderId : orderIds) { // 订单
    for (String step : orderSteps) { // 步骤
      
        // 创建消息,指定Topic、Tag和消息体
        Message msg = new Message("OrderTopic", "Order", (orderId + ":" + step).getBytes());

        // 发送顺序消息(是 MessageQueueSelector,而不是 MessageListenerConcurrently )
        producer.send(msg, new MessageQueueSelector() {
          	// 这个方法就是在选择队列(arg 是外部 send() 的第三个参数 orderId)
            @Override
            public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                String orderId = (String) arg;
                int index = Math.abs(orderId.hashCode()) % mqs.size();
                return mqs.get(index);
            }
        }, orderId); // 使用orderId作为选择队列的参数,会传到 select() 方法里

        System.out.printf("Send ordered message: %s%n", new String(msg.getBody()));

        // 模拟处理间隔
        Thread.sleep(500);
    }
}

// 关闭生产者
producer.shutdown();

消费者

RocketMQ 内部是给队列上锁,消费者处理消息前要先获取锁,处理完后要释放锁,持有锁期间别的线程

已经是单线程了,为什么还需要锁?消费者是单线程,但是可能多个消费者消费同一个队列,还是会出现并发

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ordered_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("OrderTopic", "Order");

// MessageListenerConcurrently 并发模式(多线程消费,消费失败默认重试16次)
// MessageListenerOrderly 顺序模式(单线程的,消费失败默认重试次数为 Integer.Max_Value)
consumer.registerMessageListener(new MessageListenerOrderly() {
    @Override
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
        for (MessageExt msg : msgs) {
            String message = new String(msg.getBody());
            System.out.printf(new String(msg.get(0).getBody()));
        }
        return ConsumeOrderlyStatus.SUCCESS;
    }
});

consumer.start();
System.out.println("Ordered consumer started.");

消息 Tag 过滤

发消息时:topic 用来确定队列,tag 不参与队列的选择,tag 会作为消息属性与消息一起存储在队列上

消费消息时:borker 会把队列中的消息根据 tag 确定出要推送的消费者

生产者

发送两条消息,一条消息 tag 为 vip1,一条消息 tag 为 vip2

DefaultMQProducer producer = new DefaultMQProducer("tag-producer-group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
Message message = new Message("tagTopic", "vip1", "vip1消息".getBytes());
Message message2 = new Message("tagTopic", "vip2", "vip2消息".getBytes());
producer.send(message);
producer.send(message2);
producer.shutdown();

消费者1

borker 会把 tag 是 vip1 的消息推送给当前消费者

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("tag-consumer-group-a");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.subscribe("tagTopic", "vip1");
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        System.out.println("消费者1:" + new String(msgs.get(0).getBody()));
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});
consumer.start();

消费者2

borker 会把两条消息(tag 是 vip1vip2)都推送给当前消费者

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("tag-consumer-group-b");
consumer.setNamesrvAddr("127.0.0.1:9876");
// 订阅 tagTopic,接收 tag 为 vip1 或 vip2 的消息(两个消息都会接收)
consumer.subscribe("tagTopic", "vip1 || vip2");
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        System.out.println("消费者2:" + new String(msgs.get(0).getBody()));
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});
consumer.start();

消息指定 Key

生产者

DefaultMQProducer producer = new DefaultMQProducer("key-producer-group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
String key = UUID.randomUUID().toString();
// topic、tag、key、msg
Message message = new Message("keyTopic", "vip1", key, "我是带key的消息".getBytes());
producer.send(message);
producer.shutdown();

消费者

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("key-consumer-group");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.subscribe("keyTopic", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        MessageExt messageExt = msgs.get(0);
        System.out.println("消息主体:" + new String(messageExt.getBody()));
        System.out.println("消息key:" + messageExt.getKeys());
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});
consumer.start();

重试机制

生产者

DefaultMQProducer producer = new DefaultMQProducer("retry-producer-group");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
// 发送失败重试次数
producer.setRetryTimesWhenSendFailed(2);
producer.setRetryTimesWhenSendAsyncFailed(2);
String key = UUID.randomUUID().toString();
Message message = new Message("retryTopic", "vip1", "我是一条重试消息".getBytes());
producer.send(message);
producer.shutdown();

消费者

并发模式 MessageListenerConcurrently

  • 最重试次数:16(默认)
  • 重试间隔:10s, 30s, 1m, 2m, 3m, 4m, 5m, 6m, 7m, 8m, 9m, 10m, 20m, 30m, 1h, 2h
  • 消费失败,未超过最大重试次数时,消息进入重试队列,队列的 Topic 为 %RETRY%消费者组名称
    这时可以通过 getReconsumeTimes() 获取到重试次数
  • 消费失败,超过最大重试次数时,消息进入死信队列,队列的 Topic 为 %DLQ%消费者组名称
  • 进入死信队列的消息处理:创建订阅这个死信队列 Topic 的消费者

业务中可以在重试达到多少次的时候记录下来,人工进行干预,避免进入死信队列

/**
 * 如果进入死信队列,这个队列的 Topic 为:%DLQ%retry-consumer-group
 */
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-consumer-group");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.subscribe("retryTopic", "*");
// 并发模式:设置最大重试次数
consumer.setMaxReconsumeTimes(10); 
// 并发模式:设置重试队列的延迟级别
consumer.setDelayLevelWhenNextConsume(3); 
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        MessageExt messageExt = msgs.get(0);
        // 重试次数(并发模式下才会重试,并发模式下才能通过此方法获取当前消息的重试次数)
        int times = messageExt.getReconsumeTimes();
        if (times > 3){
            // 3次都消费失败,记录下来,返回成功
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
        // 业务处理,报错会视为消费失败消息重新回到队列
		...
    }
});
consumer.start();

顺序模式 MessageListenerOrderly

  • 最大重试次数:Integer.MAX_VALUE
  • 重试间隔:默认 1000ms(1秒)
  • 和并发模式不一样:重试时是在原始队列重试(不会进入专门的重试队列)、不会进入死信队列,会无限次重试,直至成功(会阻塞当前队列)
// 为每个队列维护错误计数器
private ConcurrentMap<Integer, AtomicInteger> queueErrorCounters = new ConcurrentHashMap<>();

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("retry-consumer-group");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.subscribe("retryTopic", "*");
// 顺序模式:设置顺序消费挂起时间(毫秒)
consumer.setSuspendCurrentQueueTimeMillis(5000); 
consumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
        consumeMessage(msgs, context);
    }
});
consumer.start();

@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
    MessageExt msg = msgs.get(0);
    int queueId = msg.getQueueId();
    
    try {
        // 业务处理
        queueErrorCounters.remove(queueId); // 成功则清除计数器
        return ConsumeOrderlyStatus.SUCCESS;
    } catch (Exception e) {
        // 错误计数+1
        AtomicInteger counter = queueErrorCounters.computeIfAbsent(queueId, k -> new AtomicInteger(0));
        int retryCount = counter.incrementAndGet();
        
        // 超过阈值则跳过(需记录告警)
        if(retryCount > MAX_RETRY){
            log.error("消息[{}]重试超过{}次,人工介入", msg.getMsgId(), MAX_RETRY);
            return ConsumeOrderlyStatus.SUCCESS; // 慎用!可能造成数据不一致
        }
        return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
    }
}
posted @ 2024-10-11 12:49  CyrusHuang  阅读(243)  评论(0)    收藏  举报