RocketMQ使用总结

消息发送

普通消息

生产者多次循环发送该消息,broker收到消息顺序会不一致,因为convertAndSend并没有等上一条信息发送完并收到响应再发送下一条消息

@GetMapping("/send")
public String send() {
	rocketMQTemplate.convertAndSend("topic","普通消息");
	return "发送成功";
}

同步消息

producer向 broker 发送消息后同步等待, 直到broker 服务器返回发送结果

 @GetMapping("/sync")
public String sync() {
    SendResult sendResult = rocketMQTemplate.syncSend("topic:sync", "同步消息");
    if (sendResult.getSendStatus() == SendStatus.SEND_OK) {
        return "同步发送成功";
    }
    return "同步发送失败";
}

SendStatus中有四种状态

状态意义
SendStatus.SEND_OK消息发送成功,需要将消息刷盘和消息复制到 slave 节点变为同步才是真正发送成功
SendStatus. FLUSH_DISK_TIMEOUT消息发送成功但消息刷盘超时
SendStatus.FLUSH_SLAVE_TIMEOUT消息发送成功但是消息同步到slave节点超时
SendStatus.SLAVE_NOT_AVAILABLE消息发送成功但是broker的slave节点不可用

配置消息刷盘同步,配置消息复制到slave为同步,将flushDiskType刷盘方式设为同步刷盘,将brokerRole角色设为同步双写
/conf/broker.conf

flushDiskType=SYNC_FLUSH #ASYNC_FLUSH 异步刷盘; SYNC_FLUSH 同步刷盘
brokerRole=SYNC_MASTER # Broker的角色: ASYNC_MASTER 异步复制; SYNC_MASTER 同步双写; SLAVE 努力

异步消息

producer向 broker 发送消息注册回调方法,调用 API 后立即返回,消息发送成功或失败的回调任务在一个新的线程中执行

@GetMapping("/async")
public String async() {
    rocketMQTemplate.asyncSend("topic:async", "异步消息", new SendCallback() {
        @Override
        public void onSuccess(SendResult sendResult) {
            System.out.println("异步消息发送成功");
        }

        @Override
        public void onException(Throwable throwable) {
            System.out.println("异步消息发送失败");
        }
    });

    return "发送异步消息";
}

topic:async中的topic是主题,async是tag,用于区分一个主题下的不同tag,之间用冒号隔开,在写消费者时,topic写主题,selectorExpression写tag的表达式,支持 * || 这些匹配逻辑

@Component
@RocketMQMessageListener(topic = "topic",
selectorExpression = "async",consumerGroup = "my-consumer-group")
public class Consumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println(message);
    }
}

单向消息

producer向 broker 发送消息,执行 API 时直接返回,不等待broker 服务器的结果 ,也不注册回调函数

@GetMapping("/oneWay")
public String oneWay() {
    rocketMQTemplate.sendOneWay("oneWay", "单向消息");
    return "单向消息发送成功";
}

顺序消息

RocketMQ的一个主题topic下会有多个消息队列,使用setMessageQueueSelector将信息发送指定的消息队列,如list.get(1)就是指定1这个队列,生产环境可以将id根据队列数list.size()取模,则如果发送多次消息,这多次消息都在该队列中排队即有序,前面完成了有序存储,后面需要消费者有序消费

@RequestMapping("/orderly")
public String orderly() {
    rocketMQTemplate.setMessageQueueSelector(new MessageQueueSelector() {
        @Override
        public MessageQueue select(List<MessageQueue> list, org.apache.rocketmq.common.message.Message message, Object o) {
            return list.get(1);
        }
    });
    rocketMQTemplate.syncSendOrderly("orderly", "顺序消息", "123");
    return "发送顺序消息";
}

消费者有序消费需要在注解上加上consumeMode = ConsumeMode.ORDERLY,表示有序消费,consumeThreadMax控制消费者线程为1,否则就算消息在一个队列有序存储也不会有序

@Component
@RocketMQMessageListener(topic = "topic",selectorExpression = "*",
consumerGroup = "my-consumer-group",
consumeMode = ConsumeMode.ORDERLY,
consumeThreadMax = 1)
public class Consumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println(message);
    }
}

总结一下,顺序消息需要一个生产者往一个队列中生产消息,消费该队列的消费者只能有一个,且需要是顺序消费模式,且消费线程为1

批量消息

其实就算发送一个消息列表,将多个消息聚合成一个列表,然后通过一次同步发送发送出去,但是每次消息发送会有长度限制,普通和顺序消息最长4 MB,所以如果需要发送的消息太多,需要分片

@RequestMapping("/batch")
public String pushBatchMessage() {
    // 将消息聚合在一起,再同步发送
    List<Message> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add( MessageBuilder.withPayload("批量消息"+i)
                .setHeader(RocketMQHeaders.KEYS, i)
                .build());
    }
    rocketMQTemplate.syncSend("topic:batch", list);
    return "发送批量消息";
}

sql过滤

生产者代码,可以看到其实sql过滤也是使用同步发送,只不过在构建消息添加了一个字段value,就可以消费者可以根据该value选择性消费

@RequestMapping("/sql")
public String pushSqlMessage() {
    List<Message> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add( MessageBuilder.withPayload("sql过滤消息"+i)
                .setHeader(RocketMQHeaders.KEYS, i)
                .setHeader("value", i)
                .build());
    }
    rocketMQTemplate.syncSend("sql", list);
    return "sql过滤消息";
}

消费者代码
使用selectorType = SelectorType.SQL92,必须修改配置文件,否则启动会报错, SelectorType.SQL92是启动sql过滤,默认的SelectorType.Tag,即通过标签选择消息

@Component
@RocketMQMessageListener(topic = "sql",
 		selectorType = SelectorType.SQL92,
        selectorExpression = "value > 4",
        consumerGroup = "my-consumer-group"
 )
public class Consumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println(message);
    }
}

修改配置文件,修改配置文件后要根据配置文件启动,如sh ./bin/mqbroker -c ./conf/broker.conf ,-c参数就是根据配置文件启动
/conf/broker.conf

autoCreateTopicEnable=true #是否允许 Broker 自动创建Topic
autoCreateSubscriptionGroup=true #是否允许 Broker 自动创建订阅组
namesrvAddr=127.0.0.1:9876 #nameServer地址
enablePropertyFilter=true #允许sql过滤

事务消息

事务消息就是,先发送个信息给broker,表明发送了消息,然后执行本地事务,如果本地事务成功执行了,则发送commit给broker,则该消息可以被消费者消费了,如果发送的是rollback,则该消息不会被消费

@RequestMapping("/transaction")
public String transaction() {
    Message<String> message = MessageBuilder.withPayload("事务消息")
            .setHeader(RocketMQHeaders.KEYS, 1)
            .setHeader("money", 10)
            .setHeader(RocketMQHeaders.TRANSACTION_ID, 100)
            .build();
    TransactionSendResult transactionSendResult = rocketMQTemplate.sendMessageInTransaction("transaction", message, null);
    return "事务消息";
}

该代码也是写在消息生产者中,executeLocalTransaction即执行本地事务的代码,如果返回的是RocketMQLocalTransactionState.COMMIT,可以在消费者看到消息被消费,如果执行的是RocketMQLocalTransactionState.ROLLBACK可以看到消息不会被消费

@RocketMQTransactionListener
public class TransactionListener implements RocketMQLocalTransactionListener {
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
        System.out.println("运行executeLocalTransaction");
        return RocketMQLocalTransactionState.COMMIT;
      //  return RocketMQLocalTransactionState.ROLLBACK;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        System.out.println("checkLocalTransaction");
        return RocketMQLocalTransactionState.COMMIT;
    }
}

重复消费问题

@Component
@RocketMQMessageListener(topic = "topic",selectorExpression = "*",
consumerGroup = "my-consumer-group",
consumeMode = ConsumeMode.ORDERLY,
messageModel = MessageModel.CLUSTERING)
public class Consumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println(message);
    }
}

配置messageModel,messageModel有两种模式BROADCASTING 和 CLUSTERING,

  1. 在BROADCASTING模式下所有注册的消费者都会消费
  2. 在CLUSTERING模式下,如果一个topic被多个consumerGroup消费,也会重复消费
  3. 在CLUSTERING模式下,同一个consumerGroup消费者组,一个队列只会分配给一个消费者,但是在消费者上线和下线时,会重新负载均衡,更换队列对应的消费者。一个队列所对应的新的消费者要获取之前消费的offset,此时之前的消费者可能已经消费了一条消息,但是并没有把offset提交给broker,那么新的消费者可能会重新消费一次。
  4. orderly是前一个消费者先解锁,后一个消费者加锁再消费的模式,比起concurrently要严格了,但是加锁的线程和提交offset的线程不是同一个,所以还是会出现极端情况下的重复消费
  5. orderly模式是一批中有一条消费失败,一批统一重新消费,直到达到最大消费次数的限制,发送到死信队列。而concurrently情况下,在返回成功(CONSUME_SUCCESS)的前提下,有个ackIndex可以分隔成功和失败的消息,失败的、没有消费的消息发送到retry队列,不会造成重复消费,而如果返回的是(RECONSUME_LATER),仍然是和orderly一样的同批次全部重新消费
  6. 消费者pullRequest发出去,如果长时间收不到请求,是会被取出来重新再放入队列再请求一次的,所以也是会重复拉取消息的

综上所述,rocketMQ是无法避免消息重复消费的,只能使用幂等方案

防止消息丢失

生产者防止消息丢失

  1. 同步发送信息,同步发送后会返回状态码,根据状态码决定是否执行消息重发逻辑
状态意义
SendStatus.SEND_OK消息发送成功,需要将消息刷盘和消息复制到 slave 节点变为同步才是真正发送成功
SendStatus. FLUSH_DISK_TIMEOUT消息发送成功但消息刷盘超时
SendStatus.FLUSH_SLAVE_TIMEOUT消息发送成功但是消息同步到slave节点超时
SendStatus.SLAVE_NOT_AVAILABLE消息发送成功但是broker的slave节点不可用
  1. 异步发送,可以在onException中执行消息重发逻辑
@GetMapping("/async")
public String async() {
    rocketMQTemplate.asyncSend("topic:async", "异步消息", new SendCallback() {
        @Override
        public void onSuccess(SendResult sendResult) {
            System.out.println("异步消息发送成功");
        }

        @Override
        public void onException(Throwable throwable) {
            System.out.println("异步消息发送失败");
        }
    });

    return "发送异步消息";
}
  1. 使用事务消息,Producer先发送half消息,Broker会把消息写入队列后给Producer返回成功,Producer再执行本地事务,成功后给Broker发送commit命令,失败则发送rollback。通过这种确认机制也可以防止消息丢失
  2. 使用消息索引,在构建消息时会有一个KEYS,可以发送消息后,生产者根据该KEYS去查询该消息,使用消费者的queryMessage方法
 @GetMapping("/sync")
public String sync() throws MQClientException, InterruptedException {
         Message<String> message = MessageBuilder.withPayload("同步消息")
                .setHeader(RocketMQHeaders.KEYS, "key")
                .setHeader("money", 10)
                .build();
  
        rocketMQTemplate.syncSend("topic:sync", message);
        DefaultMQProducer producer = rocketMQTemplate.getProducer();
        producer.queryMessage("topic", "key", 1, 10, 100);

        return "同步消息";
    }
public QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end) throws MQClientException, InterruptedException {
        return this.defaultMQProducerImpl.queryMessage(this.withNamespace(topic), key, maxNum, begin, end);
    }

broker消息存储者防止消息丢失

  1. 将刷盘策略设为同步刷盘
策略名作用
异步刷盘默认。消息写入 CommitLog 时,并不会直接写入磁盘,而是先写入 PageCache 缓存后返回成功,然后用后台线程异步把消息刷入磁盘。异步刷盘提高了消息吞吐量,但是可能会有消息丢失的情况,比如断点导致机器停机,PageCache 中没来得及刷盘的消息就会丢失
同步刷盘消息写入内存后,立刻请求刷盘线程进行刷盘,如果消息未在约定的时间内刷盘成功,就返回 FLUSH_DISK_TIMEOUT,Producer 收到这个响应后,可以进行重试。同步刷盘策略保证了消息的可靠性,同时降低了吞吐量,增加了延迟
  1. 将主从复制策略改为同步复制,部署broker集群时为一主多从,且主从复制默认是异步的,master 收到消息后,不等 slave 节点复制消息就直接给 Producer 返回成功,这样如果 master 宕机了,进行主备切换后就会有消息丢失,因为当原先的主机重新启动,它已经变为从机了,而此时的主机(也就是原先从机)不会对此时从机的数据进行同步,因为主从复制都是把数据从主复制到从,而不是从从复制到主

消费者防止消息丢失

  1. 一般我们消费需要进行确认机制,即使用try-catch,成功了则将ACK发回给broker,出异常了则在catch块中发送失败状态码给broker。但是在使用注解实现消费者时,是看不到这些的,但是其实是封装了这种逻辑的,在消息消费出异常会进行重新消费
@Component
@RocketMQMessageListener(topic = "topic",
selectorExpression = "async",consumerGroup = "my-consumer-group")
public class Consumer implements RocketMQListener<String> {
    @Override
    public void onMessage(String message) {
        System.out.println(message);
    }
}
  1. 在DefaultRocketMQListenerContainer类中有两个内部类,DefaultMessageListenerOrderly和DefaultMessageListenerConcurrently,它们分别实现接口MessageListenerOrderly 和 MessageListenerConcurrently,可以看到这两个接口的consumeMessage方法都是有一个枚举类的返回值的,使用try-catch块,在成功时返回成功状态码,在异常时返回异常状态码。这两种实现类就对应着注解@RocketMQMessageListener的属性onsumeMode消费模式中的两个选项ConsumeMode.CONCURRENTLY 并行处理,ConsumeMode.ORDERLY 按顺序处理
public class DefaultMessageListenerOrderly implements MessageListenerOrderly {
        public DefaultMessageListenerOrderly() {
        }

        public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
            Iterator var3 = msgs.iterator();

            while(var3.hasNext()) {
                MessageExt messageExt = (MessageExt)var3.next();
                DefaultRocketMQListenerContainer.log.debug("received msg: {}", messageExt);

                try {
                    long now = System.currentTimeMillis();
                    DefaultRocketMQListenerContainer.this.handleMessage(messageExt);
                    long costTime = System.currentTimeMillis() - now;
                    DefaultRocketMQListenerContainer.log.info("consume {} cost: {} ms", messageExt.getMsgId(), costTime);
                } catch (Exception var9) {
                    DefaultRocketMQListenerContainer.log.warn("consume message failed. messageExt:{}", messageExt, var9);
                    context.setSuspendCurrentQueueTimeMillis(DefaultRocketMQListenerContainer.this.suspendCurrentQueueTimeMillis);
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }
            }

            return ConsumeOrderlyStatus.SUCCESS;
        }
    }
    
 public class DefaultMessageListenerConcurrently implements MessageListenerConcurrently {
        public DefaultMessageListenerConcurrently() {
        }

        public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
            Iterator var3 = msgs.iterator();

            while(var3.hasNext()) {
                MessageExt messageExt = (MessageExt)var3.next();
                DefaultRocketMQListenerContainer.log.debug("received msg: {}", messageExt);

                try {
                    long now = System.currentTimeMillis();
                    DefaultRocketMQListenerContainer.this.handleMessage(messageExt);
                    long costTime = System.currentTimeMillis() - now;
                    DefaultRocketMQListenerContainer.log.debug("consume {} cost: {} ms", messageExt.getMsgId(), costTime);
                } catch (Exception var9) {
                    DefaultRocketMQListenerContainer.log.warn("consume message failed. messageExt:{}, error:{}", messageExt, var9);
                    context.setDelayLevelWhenNextConsume(DefaultRocketMQListenerContainer.this.delayLevelWhenNextConsume);
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
            }

            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }
    }
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lolxxs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值