为什么是RocketMQ
RocketMQ主要是针对online服务,提供可靠存储的消息中间件
持久化 & 多副本
RocketMQ可配置为 3副本、异步复制、异步刷盘,类比kafka的的replica_factor=3, acks=leader;在这种配置之下,可靠性99.99%,可用性99.95%。同时可以针对特殊场景,提供同步刷盘、同步复制的集群,提供更高的可靠性,在6副本、同步复制、同步刷盘的配置下,消息可靠性可达9个9。相应的,写入延时会升高,qps、availability会降低。RocketMQ的消息默认存储时间也可配置。
消息重试(Requeue)
kafka设计之初主要作为 log (特指类似binlog这种WAL)的传输总线,同一个partition内的有序是默认需求,这意味着消费失败的消息是不能重入的,否则消息的顺序性就被破坏了;用户需要自己打日志或者转储来处理消费失败的消息。
而在MQ领域,可能大多数场景下其实并不需要严格的顺序,对于消费失败的消息(可能是消费者下游短时间不可用、load高等),这种情况下用户希望消息能够重新投递到MQ,可以不用额外写代码处理错误的消息。RocketMQ 在非有序消费模式下,支持消息重试,类比NSQ的requeue机制,并且重试的消息具有和普通消息一样的持久化支持。
死信队列 (Dead Letter Queue)
当消息多次重试仍然消费失败,这种情况多半是消费者逻辑有问题,比如对于消息的某些字段没有兼容等;需要有地方能够转储这一批消息。RocketMQ提供了死信队列,消费者可以指定消息最大重试次数,当消息重试超过该次数,消息将会发往死信队列。待消费者的问题解决之后,可以从死信队列拉取这些消息统一处理。
延时消息
一些场景下,生产者发送消息成功后,希望delay一段时间消息再投递给消费者,比如创建一个订单之后30分钟内未支付则取消订单的场景。
NSQ支持ms级别精度的延时消息,其实现为内存里的一个priority queue,没有持久化;kafka不支持延时消息;RocketMQ支持分级的时延,比如支持10s、30s、1min、5min、10min、15min、20min、30min等多个级别时延,足够覆盖大部分场景,并且有持久化。
需要任意时延的延迟消息用户也可以支持。
写入延迟保障
RocketMQ和kafka都是顺序写盘,充分利用page cache以获得低时延和吞吐。kafka的存储模型为每个partition都由若干segment(物理文件)组成的逻辑文件,当kafka集群上的topic/partition数量多了之后,kafka性能会快速劣化,顺序写也可能会逐渐退化到随机写(简单来水就是,kafka对于单个partition是append only的,但是一块盘上可能有若干partition,多个partition整体看不是append only)(参考:如何缓解Kafka集群在有大量topic时性能快速劣化的问题?),写入延迟上涨;只能拆分集群,减少单集群上topic/partition数量。
而RocketMQ的存储模型与kafka不同,所有消息都写到一个CommitLog中即返回,再异步将offset、length信息dispatch到各Consume Queue中,所以是严格的顺序写,在topic数量很多的时候,page fault仍然维持在低水平,写入时延较为稳定。
在异步复制、异步刷盘下,写入延时的avg为10ms,pct99为20ms
基于时间回溯
比如30分钟前消费者下游故障,期间消费者全部异常,用户希望将消费进度回拨到30min前,达到”补"消息的效果。kafka需要集群版本在0.10以上才能支持,RocketMQ默认支持。
顺序/乱序消费
RocketMQ同时提供了顺序和乱序消费。 顺序的保障是,消息写到broker上的一个Consume Queue的顺序和消费者从该Consume Queue读取的顺序是一致的,多个producer并发写到一个Consume Queue的状况顺序没有保障,实际的顺序以broker收到的顺序为准。
一般有序消息都是同步发送的,也即msg n发送成功并且client收到写入成功的结果之后才会发送msg n+1,异步/并发的有序消息是个伪需求。
对于消息没有顺序要求的场景,可以使用乱序消费,并发度为消费者实例数 * 线程/协程数。
广播消费(TBD)
即同一个consumer group内的所有消费者每一个实例都能收到全量消息;可以通过每个消费者使用独立的Consumer group达到效果。
事务消息 (TBD)
事务消息指:Producer发消息和Producer的其他操作(如写DB)形成事务,如果其他操作Commit,则消息Consumer可见;如果其他操作Rollback,则消息也不会投递给Consumer。
原生的RocketMQ的java SDK支持事务消息,现阶段其他语言暂不支持,后续可能会支持。
Spring Boot集成RocketMQ
- POM依赖
- 生产者(子Topic区分不同消息类型)
- 消费者(启动多个消费者,订阅不同的子Topic)
代码
https://github.com/zc-zangchao/multiple-data-source
POM依赖
<springboot.version>2.2.0.RELEASE</springboot.version>
<rocketmq-client.version>4.5.1</rocketmq-client.version>
<rocketmq.version>2.0.2</rocketmq.version>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${springboot.version}</version>
</dependency>
<!-- rocket mq -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>${rocketmq-client.version}</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>${rocketmq.version}</version>
</dependency>
生产者
package com.springboot.demo.mq.mq;
import com.springboot.demo.mq.busi.type.EnumBusinessType;
import com.springboot.demo.mq.mq.constants.Constants;
import com.springboot.demo.mq.mq.utils.MqUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.common.message.Message;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/**
* 异步消息发送
**/
@Service
@Slf4j
public class MqProducer {
private static final String PRODUCER_INSTANCE_NAME = Constants.MQ_PRODUCER_INSTANCE;
private static final String MSG_KEY_CONNECT = "-";
private static DefaultMQProducer producer;
private String rocketMQNameServer = Constants.MQ_SERVER;
private String producerGroupName = Constants.MQ_PRODUCER_GROUP;
private String topic = Constants.MQ_TOPIC;
@PostConstruct
public void init() {
try {
log.info("Start mq producer begin");
producer = new DefaultMQProducer();
producer.setProducerGroup(producerGroupName); // 设置MQ生产者组名
log.info("Mq.name.server {}", rocketMQNameServer);
producer.setNamesrvAddr(rocketMQNameServer);
producer.setInstanceName(PRODUCER_INSTANCE_NAME);
producer.start();
log.info("Start mq producer end");
} catch (Exception e) {
log.error("Mq loading error!", e);
throw new RuntimeException("Rocket mq producer start failed");
}
}
@PreDestroy
public void stop() {
log.info("Shutdown mq producer!");
producer.shutdown();
}
/**
* 发送不保证顺序性的消息
*
* @param key 消息的key(用来判重)
* @param msg 消息的内容
* @param delayTimeLevel 时间延迟级别
* @return true:处理成功,不需要重发 false:处理失败, 暂不重发
*/
public boolean sendMessage(EnumBusinessType msgTag, String key, String msg, int delayTimeLevel) {
log.info("Send mq[topic={} msgTag={} key={} msg={} delayTimeLevel={}]", topic, msgTag, key, msg, delayTimeLevel);
byte[] msgBytes = MqUtils.convertStrToByteArr(msg);
boolean result = sendMessage(topic, msgTag, key, msgBytes, delayTimeLevel);
if (!result) {
log.error("[WARN]send mq-msg fail!, topic:{}, tag:{}, key:{}, serialize msg:{}", topic, msgTag, key, msg);
}
return result;
}
/**
* 发送不保证顺序性的消息
*
* @param topic 消息的topic
* @param msgTag 消息的tag
* @param key 消息的key(用来判重)
* @param msgBytes 消息的内容
* @return true:处理成功 false:处理失败,暂不考虑重发
*/
private boolean sendMessage(String topic, EnumBusinessType msgTag, String key, byte[] msgBytes, int delayTimeLevel) {
String msgKey = topic + MSG_KEY_CONNECT + key;
try {
Message rocketMsg = new Message(topic, String.valueOf(msgTag), msgKey, msgBytes);
rocketMsg.setDelayTimeLevel(delayTimeLevel);
SendResult sendResult = producer.send(rocketMsg);
if (sendResult.getSendStatus() == SendStatus.SEND_OK) {
log.info("Send msg status success! topic:{}, tag:{}, key:{}", topic, msgTag, msgKey);
} else {
log.warn("Send msg status fail! status:{}, topic:{}, tag:{}, key:{}, msgID:{}", sendResult.getSendStatus(),
topic, msgTag, key, sendResult.getMsgId());
return false;
}
} catch (Exception e) {
log.error("Send msg occurs exception! topic:{}, tag:{}, key:{}", topic, msgTag, key, e);
return false;
}
return true;
}
}
消息类型(子Topic)
package com.springboot.demo.mq.busi.type;
public enum EnumBusinessType {
LOGIN,
LOGIN_OUT;
}
常量类
package com.springboot.demo.mq.mq.constants;
public final class Constants {
public static final String MQ_SERVER = "127.0.0.1:9876";
public static final String MQ_PRODUCER_INSTANCE = "PRODUCER_INSTANCE_DEMO";
public static final String MQ_PRODUCER_GROUP = "PRODUCER_GROUP_DEMO";
public static final String MQ_TOPIC = "TOPIC_DEMO";
public static final String MQ_CONSUMER_PREFIX = "CONSUMER_DEMO_";
private Constants() {}
}
工具类
package com.springboot.demo.mq.mq.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.UnsupportedEncodingException;
@Slf4j
public final class MqUtils {
private static final String DEFAULT_CHARSET = "UTF-8";
private MqUtils() {
}
// 字符串转byte[]
public static byte[] convertStrToByteArr(String str) {
try {
return str == null ? new byte[0] : str.getBytes(DEFAULT_CHARSET);
} catch (UnsupportedEncodingException e) {
log.error("Convert string to byte array fail, str is {}", str, e);
}
return new byte[0];
}
// byte[]转字符串
public static String convertByteArrToStr(byte[] bytes) {
return bytes == null ? "" : convertByteArrNotNull(bytes);
}
private static String convertByteArrNotNull(byte[] bytes) {
try {
return new String(bytes, DEFAULT_CHARSET);
} catch (UnsupportedEncodingException e) {
log.error("Convert byte array to string fail.", e);
}
return "";
}
}
消费者
package com.springboot.demo.mq.mq;
import com.springboot.demo.mq.busi.type.EnumBusinessType;
import com.springboot.demo.mq.mq.constants.Constants;
import com.springboot.demo.mq.mq.listener.AbstractMqListener;
import com.springboot.demo.mq.mq.listener.impl.UserLoginListener;
import com.springboot.demo.mq.mq.listener.impl.UserLoginOutListener;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.MQPushConsumer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
/**
* 消息消费
**/
@Service
@Slf4j
public class MqConsumer {
private static List<MQPushConsumer> consumers = new ArrayList<>();
private String rocketMQNameServer = Constants.MQ_SERVER;
private String consumerGroupNamePrefix = Constants.MQ_CONSUMER_PREFIX;
private String topic = Constants.MQ_TOPIC;
/**
* 需要确保依赖的所有的子类Listener在本类初始化之前初始化完成
*/
@Autowired
private UserLoginListener userLoginListener;
@Autowired
private UserLoginOutListener userLoginOutListener;
@PostConstruct
public void init() {
try {
log.info("Start mq consumers begin, Mq.name.server {}.", rocketMQNameServer);
// 启动多个consumer(对应多个tag)
for (EnumBusinessType msgType : EnumBusinessType.values()) {
startMqConsumer(msgType);
}
log.info("Start mq consumers end");
} catch (Exception e) {
log.error("Mq consumer loading error!", e);
throw new RuntimeException("Rocket mq consumer start failed");
}
}
private void startMqConsumer(EnumBusinessType msgTag) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
consumer.setConsumerGroup(consumerGroupNamePrefix + msgTag);
consumer.setNamesrvAddr(rocketMQNameServer);
// Subscribe topic with tag
consumer.subscribe(topic, String.valueOf(msgTag));
consumer.registerMessageListener(getMqListenerInstance(msgTag));
//Launch the consumer instance.
consumer.start();
// add to list
consumers.add(consumer);
log.info("Start mq consumer success, subscribe topic: {}, tag: {}", topic, msgTag);
}
private AbstractMqListener getMqListenerInstance(EnumBusinessType type) {
if (EnumBusinessType.LOGIN == type) {
return userLoginListener;
} else {
return userLoginOutListener;
}
}
@PreDestroy
public void stop() {
for (MQPushConsumer consumer : consumers) {
consumer.shutdown();
}
log.info("Shutdown mq consumers!");
}
}
Listener
package com.springboot.demo.mq.mq.listener;
import com.springboot.demo.mq.mq.constants.Constants;
import com.springboot.demo.mq.mq.utils.MqUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
/**
* 异步消息的消费
**/
@Slf4j
public abstract class AbstractMqListener implements MessageListenerConcurrently, IReadMessage {
private String topic = Constants.MQ_TOPIC;
@Override
public final ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgExtList,
ConsumeConcurrentlyContext consumeConcurrentlyContext) {
if (msgExtList == null || msgExtList.size() == 0) {
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
log.info("Receive msg ext list, size is:{}", msgExtList.size());
boolean result = true;
for (MessageExt msg : msgExtList) {
try {
if (!msg.getTopic().equals(topic)){
continue;
}
UnifiedMessage unifiedMessage = new UnifiedMessage(msg.getMsgId(), msg.getTags(), MqUtils.convertByteArrToStr(msg.getBody()));
// 调用具体业务类处理消息,返回处理成功或失败
result = readMessage(unifiedMessage);
if (!result) {
log.warn("Read result fail, unified message: {}", unifiedMessage);
} else {
log.info("Read result success, unified message: {}", unifiedMessage);
}
} catch (Exception ex) {
result = false;
log.error("Occurs exception", ex);
}
}
return result ? ConsumeConcurrentlyStatus.CONSUME_SUCCESS : ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
接口类及模型
package com.springboot.demo.mq.mq.listener;
public interface IReadMessage {
/**
* 具体业务逻辑重写这个方法就可以啦
* @param unifiedMessage 读取到的消息统一模型
* @return 读取数据是否成功
*/
boolean readMessage(UnifiedMessage unifiedMessage);
}
package com.springboot.demo.mq.mq.listener;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class UnifiedMessage {
private String msgId;
private String msgTags;
private String msgBody;
}
实现类
package com.springboot.demo.mq.mq.listener.impl;
import com.springboot.demo.mq.mq.listener.AbstractMqListener;
import com.springboot.demo.mq.mq.listener.UnifiedMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class UserLoginListener extends AbstractMqListener {
@Override
public boolean readMessage(UnifiedMessage unifiedMessage) {
log.info(unifiedMessage.getMsgBody());
return true;
}
}
package com.springboot.demo.mq.mq.listener.impl;
import com.springboot.demo.mq.mq.listener.AbstractMqListener;
import com.springboot.demo.mq.mq.listener.UnifiedMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class UserLoginOutListener extends AbstractMqListener {
@Override
public boolean readMessage(UnifiedMessage unifiedMessage) {
log.info(unifiedMessage.getMsgBody());
return true;
}
}
参考:
RocketMQ Producer使用方法
Apache RocketMQ Quick Start
RocketMQ 常见异常处理
RocketMQ 解决 No route info of this topic 异常步骤
rocketmq No route info of this topic错误(原因版本不一致) – fastjson jar包缺失或版本不对