系列文章目录
rocketmq-安装篇(一)
rocketmq-手把手搭建集群模式(二)
rocketmq-各类型消息实战(三)
rocketmq-名词解释(四)
rocketmq-消息存储(五)
文章目录
前言
通过实战代码展示发送各种类型的消息。废话不多说,直接上干货。
一消费模式
两种模式:集群模式、广播模式
1.1集群模式
默认模式-负载均衡
- 同组消费者同一条信息只被消费一遍;
- 集群模式的消费进度是保存在Broker端的,所以即使应用崩溃,消费进度也不会出错
- 场景: 异步通信、削峰等对消息没有顺序要求的场景都适合集群消费
1.2广播模式
- 所有消费者组(即使同组)中的全部消费者实例将消费整个 Topic 的全部消息;组名失效
- 消费进度:广播消费的消费进度保存在客户端机器的文件中。如果文件弄丢了,那么消费进度就丢失了,可能会导致部分消息没有消费
- 场景:广播消费比较适合各个消费者实例都需要通知的场景,比如刷新应用服务器中的缓存
1.3指定模式
/**
* 设置消费模型,集群还是广播,默认为集群
* MessageModel.CLUSTERING、MessageModel.BROADCASTING、
*/
consumer.setMessageModel(MessageModel.CLUSTERING);
二项目代码
此处只展示部分核心代码,源代码见:这里 查看
spring-boot-rocketmq、spring-boot-rocketmq-consumer两个模块即可。
2.1生产者
spring-boot-rocketmq,步骤如下 :
- 在application.properties 配置参数
- 定义配置类读取参数,MQProducerConfigure
- 编写controller,注入defaultMQProducer,在接口中直接发送消息即可
config:
@ConfigurationProperties(prefix = "rocketmq.producer")
public class MQProducerConfigure {
public static final Logger LOGGER = LoggerFactory.getLogger(MQProducerConfigure.class);
private String groupName;
private String namesrvAddr;
// 消息最大值
private Integer maxMessageSize;
// 消息发送超时时间
private Integer sendMsgTimeOut;
// 失败重试次数
private Integer retryTimesWhenSendFailed;
/**
* mq 生成者配置
* @return
* @throws MQClientException
*/
@Bean
@ConditionalOnProperty(prefix = "rocketmq.producer", value = "isOnOff", havingValue = "on")
public DefaultMQProducer defaultProducer() throws MQClientException {
LOGGER.info("defaultProducer 正在创建---------------------------------------");
DefaultMQProducer producer = new DefaultMQProducer(groupName);
producer.setNamesrvAddr(namesrvAddr);
producer.setVipChannelEnabled(false);
producer.setMaxMessageSize(maxMessageSize);
producer.setSendMsgTimeout(sendMsgTimeOut);
producer.setRetryTimesWhenSendAsyncFailed(retryTimesWhenSendFailed);
producer.start();
LOGGER.info("rocketmq producer server 开启成功----------------------------------");
return producer;
}
}
controller:
/**
* @author:
* @Date: 2020/4/21 11:17
* @Description:
*/
@RestController
@RequestMapping("/mqProducer")
public class MQProducerController {
public static final Logger LOGGER = LoggerFactory.getLogger(MQProducerController.class);
@Autowired
DefaultMQProducer defaultMQProducer;
@Autowired
RocketmqMessageLogMapper rocketmqMessageLogMapper;
@Autowired
private UidGenService uidGenService;
/**
* 同步发送简单的MQ消息
* @param msg
* @return
*/
@RequestMapping("/sendSync")
public BaseResponse sendSync(String msg) throws Exception {
if (StringUtils.isEmpty(msg)) {
return new BaseResponse().succeed();
}
LOGGER.info("发送MQ消息内容:" + msg);
Message sendMsg = new Message("TestTopic", "TestTag", UUID.randomUUID().toString(), msg.getBytes());
// 默认3秒超时 ,同步发送消息
SendResult sendResult = defaultMQProducer.send(sendMsg);
LOGGER.info("消息发送响应:" + sendResult.toString());
BaseResponse response = new BaseResponse().succeed();
response.setData(sendResult);
return response;
}
/**
* 异步发送简单的MQ消息
* @param
* @return
*/
@RequestMapping("/sendAsync")
public BaseResponse sendAsync(@RequestParam Map<String,String> map) throws Exception {
/*if (StringUtils.isEmpty(timestamp)) {
return new BaseResponse().succeed();
}*/
String topic = "HelloTopic";
String tags = "TestTag";
map.put(PublicConstants.TRACE_ID,MDC.get(PublicConstants.TRACE_ID));
String msg = JSONObject.toJSONString(map);
LOGGER.info("发送MQ消息内容:" + msg);
Message message = new Message(topic, tags, UUID.randomUUID().toString(), msg.getBytes());
// 默认3秒超时 ,异步发送消息
defaultMQProducer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
LOGGER.info("发送MQ消息成功:{}" ,sendResult);
System.out.println(sendResult);
//保存消费消息
RocketmqMessageLog rocketmqMessageLog = new RocketmqMessageLog();
rocketmqMessageLog.setId(uidGenService.getUid());
rocketmqMessageLog.setMsgBody(msg);
rocketmqMessageLog.setMsgId(sendResult.getMsgId());
rocketmqMessageLog.setQueueId(sendResult.getMessageQueue().getQueueId());
rocketmqMessageLog.setTopic(topic);
rocketmqMessageLog.setTags(tags);
rocketmqMessageLogMapper.insert(rocketmqMessageLog);
}
@Override
public void onException(Throwable e) {
LOGGER.info("发送MQ消息异常:",e);
System.out.println(e.getMessage());
}
});
LOGGER.info("消息发送完成" );
BaseResponse response = new BaseResponse().succeed();
return response;
}
}
application.properties:
spring.application.name=spring-boot-rocketmq
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
######################rocketmq 生产者配置
# 是否开启自动配置
rocketmq.producer.isOnOff=on
# 发送同一类消息设置为同一个group,保证唯一默认不需要设置,rocketmq会使用ip@pid(pid代表jvm名字)作为唯一标识
rocketmq.producer.groupName= springbootRocketmqProducer
# mq的nameserver地址
rocketmq.producer.namesrvAddr=192.168.149.128:9876
# 消息最大长度 默认 1024 * 4 (4M)
rocketmq.producer.maxMessageSize = 4096
# 发送消息超时时间,默认 3000ms
rocketmq.producer.sendMsgTimeOut=3000
# 发送消息失败重试次数,默认2
rocketmq.producer.retryTimesWhenSendFailed=2
2.2消费者
spring-boot-rocketmq-consumer,步骤如下 :
- 在application.properties 配置参数
- 定义配置类读取参数,MQConsumerConfigure
- 编写监听器,MQConsumeConcurrentListener,针对业务逻辑处理接收到的信息即可。
MQConsumerConfigure:
/**
* @author
* @date 2020年10月19日 14:55
* @Description: mq消费者配置
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "rocketmq.consumer")
public class MQConsumerConfigure {
public static final Logger LOGGER = LoggerFactory.getLogger(MQConsumerConfigure.class);
private String groupName;
private String namesrvAddr;
private String topics;
// 消费者线程数据量
private Integer consumeThreadMin;
private Integer consumeThreadMax;
private Integer consumeMessageBatchMaxSize;
@Autowired
private MQConsumeConcurrentListener mqConsumeSequenceListener;
/**
* mq 消费者配置
* @return
* @throws MQClientException
*/
@Bean
@ConditionalOnProperty(prefix = "rocketmq.consumer", value = "isOnOff", havingValue = "on")
public DefaultMQPushConsumer defaultConsumer() throws MQClientException {
LOGGER.info("defaultConsumer 正在创建---------------------------------------");
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
consumer.setNamesrvAddr(namesrvAddr);
consumer.setConsumeThreadMin(consumeThreadMin);
consumer.setConsumeThreadMax(consumeThreadMax);
consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize);
// 设置监听
consumer.registerMessageListener(mqConsumeSequenceListener);
/**
* 设置consumer第一次启动是从队列头部开始还是队列尾部开始
* 如果不是第一次启动,那么按照上次消费的位置继续消费
*/
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
/**
* 设置消费模型,集群还是广播,默认为集群
*/
//consumer.setMessageModel(MessageModel.CLUSTERING);
try {
// 设置该消费者订阅的主题和tag,如果订阅该主题下的所有tag,则使用*,
String[] topicArr = topics.split(";");
for (String tag : topicArr) {
String[] tagArr = tag.split("~");
consumer.subscribe(tagArr[0], tagArr[1]);
}
consumer.start();
LOGGER.info("consumer 创建成功 groupName={}, topics={}, namesrvAddr={}",groupName,topics,namesrvAddr);
} catch (MQClientException e) {
LOGGER.error("consumer 创建失败!");
}
return consumer;
}
}
MQConsumeMsgListener:
/**
* @author:
* @Date: 2020/4/21 11:05
* @Description: 消费者监听
*/
@Component
public class MQConsumeConcurrentListener implements MessageListenerConcurrently {
public static final Logger LOGGER = LoggerFactory.getLogger(MQConsumeConcurrentListener.class);
/**
* 默认msg里只有一条消息,可以通过设置consumeMessageBatchMaxSize参数来批量接收消息
* 不要抛异常,如果没有return CONSUME_SUCCESS ,consumer会重新消费该消息,直到return CONSUME_SUCCESS
* @param msgList
* @param consumeConcurrentlyContext
* @return
*/
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgList, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
if (CollectionUtils.isEmpty(msgList)) {
LOGGER.info("MQ接收消息为空,直接返回成功");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
MessageExt messageExt = msgList.get(0);
LOGGER.info("MQ接收到的消息为:" + messageExt.toString());
try {
String topic = messageExt.getTopic();
String tags = messageExt.getTags();
String body = new String(messageExt.getBody(), "utf-8");
//JSONObject jsonObject = JSONObject.parseObject(body);
//LOGGER.info("MQ消息traceId={},topic={}, tags={}, 消息内容={}",jsonObject.getString("traceId"), topic,tags,body);
LOGGER.info("MQ消息,topic={}, tags={}, 消息内容={}", topic,tags,body);
} catch (Exception e) {
LOGGER.error("获取MQ消息内容异常{}",e);
}
// TODO 处理业务逻辑
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}
application.properties:
spring.application.name=spring-boot-rocketmq
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
######################rocketmq 消费者配置
# 是否开启自动配置
rocketmq.consumer.isOnOff=on
# 发送同一类消息设置为同一个group,保证唯一默认不需要设置,rocketmq会使用ip@pid(pid代表jvm名字)作为唯一标识
rocketmq.consumer.groupName=springbootRocketmqConsumer
# mq的nameserver地址
rocketmq.consumer.namesrvAddr=192.168.149.128:9876
# 消费者订阅的主题topic和tags(*标识订阅该主题下所有的tags),格式: topic~tag1||tag2||tags3;
# rocketmq.consumer.topics=TestTopic~TestTag;TestTopic~HelloTag;HelloTopic~HelloTag;MyTopic~*
rocketmq.consumer.topics=TestTopic~*
# 消费者线程数据量
rocketmq.consumer.consumeThreadMin=5
rocketmq.consumer.consumeThreadMax=32
# 设置一次消费信息的条数,默认1
rocketmq.consumer.consumeMessageBatchMaxSize=1
2.3搞一下
自己把代码下载下来,修改下:application.properties对应地址
rocketmq.consumer.namesrvAddr=192.168.149.128:9876
不知道怎么搞nameserver的兄弟,可以看下之前文章,手把手的教你搭建rocketmq服务。
发送端:
//spring-boot-rocketmq 项目跑一下启动类SpringBootRocketmqApplication
//直接访问 MQProducerController 里面的rest接口就搞定了
//此处是:
localhost:8004/mqProducer/sendSync?msg=小兄弟你好呀
消费端:
//spring-boot-rocketmq-Consumer 项目跑一下启动类SpringBootRocketmqConsumerApplication
//注意看MQConsumeConcurrentListener这个类,会打印接收到的消息日志:
MQ消息,topic=TestTopic, tags=TestTag, 消息内容=小兄弟你好呀
三基本消息类型
3.1同步消息
同步等待:broker返回了响应结果,再往后执行。
//发送消息到一个Broker
SendResult sendResult=producer.send(msg);
可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。
3.2异步消息
给broker发送了消息就执行往后执行,等broker返回了执行结果,会回调相应的预定义的方法。
// 默认3秒超时 ,异步发送消息
defaultMQProducer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
LOGGER.info("发送MQ消息成功:{}" ,sendResult);
System.out.println(sendResult);
//保存消费消息
RocketmqMessageLog rocketmqMessageLog = new RocketmqMessageLog();
rocketmqMessageLog.setId(uidGenService.getUid());
rocketmqMessageLog.setMsgBody(msg);
rocketmqMessageLog.setMsgId(sendResult.getMsgId());
rocketmqMessageLog.setQueueId(sendResult.getMessageQueue().getQueueId());
rocketmqMessageLog.setTopic(topic);
rocketmqMessageLog.setTags(tags);
rocketmqMessageLogMapper.insert(rocketmqMessageLog);
}
@Override
public void onException(Throwable e) {
LOGGER.info("发送MQ消息异常:",e);
System.out.println(e.getMessage());
}
});
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。
3.3单向消息
这种方式主要用在不特别关心发送结果的场景,例如日志发送。
/**
* 发送单向的MQ消息
* @param msg
* @return
*/
@RequestMapping("/sendOneWay")
public BaseResponse sendOneWay(String msg) throws Exception {
if (StringUtils.isEmpty(msg)) {
return new BaseResponse().succeed();
}
LOGGER.info("发送MQ消息内容:" + msg);
Message sendMsg = new Message("TestTopic", "TestTag", UUID.randomUUID().toString(), msg.getBytes());
// 发送消息
defaultMQProducer.sendOneway(sendMsg);
BaseResponse response = new BaseResponse().succeed();
response.setData("successed");
return response;
}
四场景化消息类型
4.1顺序消息
- 消息有序指的是可以按照消息的发送顺序来消费。
- 顺序消费的原理解析: 在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列),而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上单线程依次拉取,则就保证了顺序。
- RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。
多线程拉取消息?
如果采用 实现MessageListenerConcurrently的方式,此时是多线程同时从一个队列中获取消息,无法保证有序;采用MessageListenerOrderly 是通过单线程从一个队列中获取消息,保证有序。
下面用订单进行分区有序的的示例,一个订单顺序流程:创建订单、锁定库存、推送订单通知,订单号相同的消息会被先后发送到同一个队列中,消费时,同一个order对应的信息肯定在同一个队列。
发送端:
/**
* 发送顺序消息
* @param
* @return
*/
@RequestMapping("/sendSequence")
public BaseResponse sendSequence() throws Exception {
/**
* 造数据
*/
List<OrderStep> orders = getOrders();
orders.stream().forEach(order -> {
String jsonString = JSONObject.toJSONString(order);
LOGGER.info("sendSequence-发送MQ消息内容:" + jsonString);
Message message = new Message("TestTopic", "TestTag", UUID.randomUUID().toString(), jsonString.getBytes());
// 发送消息
try {
/**
* 参数1:消息对象
* 参数2:队列选择器
* 参数3:选择队列的业务标识-订单id
*/
defaultMQProducer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Long id = (Long) arg;
//根据订单id选择发送哪个queue
long index = id % mqs.size();
LOGGER.info("sendSequence-订单id={},队列={}", id, index);
return mqs.get((int) index);
}
}, order.getOrderId());
} catch (Exception e) {
LOGGER.info("sendSequence-发送消息异常:{}",e);
}
}
);
BaseResponse response = new BaseResponse().succeed();
response.setData("successed");
return response;
}
消费端:
/**
* @author:
* @Date: 2020/4/21 11:05
* @Description: 消费者监听--多线程获取消息,
*/
@Component
public class MQConsumeSequenceListener implements MessageListenerOrderly {
public static final Logger LOGGER = LoggerFactory.getLogger(MQConsumeSequenceListener.class);
/**
* 默认msg里只有一条消息,可以通过设置consumeMessageBatchMaxSize参数来批量接收消息
* 不要抛异常,如果没有return SUCCESS ,consumer会重新消费该消息,直到return SUCCESS
* @param msgList
* @param consumeOrderlyContext
* @return
*/
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgList, ConsumeOrderlyContext consumeOrderlyContext) {
if (CollectionUtils.isEmpty(msgList)) {
LOGGER.info("MQ接收消息为空,直接返回成功");
return ConsumeOrderlyStatus.SUCCESS;
}
try {
LOGGER.info("MQ接收到的消息msgSize=[{}]", msgList.size());
for (MessageExt messageExt : msgList) {
if (null == messageExt) {
LOGGER.info("mq消息msg为空,直接处理下一条消息");
continue;
}
String topic = messageExt.getTopic();
String tags = messageExt.getTags();
String body = new String(messageExt.getBody(), "utf-8");
//JSONObject jsonObject = JSONObject.parseObject(body);
//LOGGER.info("MQ消息traceId={},topic={}, tags={}, 消息内容={}",jsonObject.getString("traceId"), topic,tags,body);
LOGGER.info("MQ消息,topic={}, tags={}, 消息内容={}", topic,tags,body);
}
} catch (Exception e) {
LOGGER.error("获取MQ消息内容异常{}",e);
}
// TODO 处理业务逻辑
return ConsumeOrderlyStatus.SUCCESS;
}
}
4.2延时消息
- 消费者延时消费消息。比如电商里,提交了一个订单,当时就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
它的实现与普通消息的发送和消费没多大区别,只多了一句话:
message.setDelayTimeLevel()
使用限制
现在RocketMq并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h分别对应着等级1到18
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
发送端:
/**
* 发送延时消息
* @param msg
* @return
*/
@RequestMapping("/sendDelay")
public BaseResponse sendDelay(String msg) throws Exception {
if (StringUtils.isEmpty(msg)) {
return new BaseResponse().succeed();
}
LOGGER.info("发送MQ消息内容:" + msg);
Message message = new Message("delayTopic", "TestTag", UUID.randomUUID().toString(), msg.getBytes());
// 设置延时等级2,这个消息将在5s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
message.setDelayTimeLevel(2);
// 发送消息
SendResult sendResult = defaultMQProducer.send(message);
// 打印结果
System.out.println(String.format("SendResult status:%s, queueId:%d",
sendResult.getSendStatus(),
sendResult.getMessageQueue().getQueueId()));
BaseResponse response = new BaseResponse().succeed();
response.setData("successed");
return response;
}
消费端: 与普通消费一致
4.3批量消息
- 批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的 topic,相同的waitStoreMsgOK,而且不能是延时消息。这一批消息的总大小不应超过4MB。
发送端:
/**
* 发送批量消息
*/
@RequestMapping("/sendBatch")
public BaseResponse sendBatch() throws Exception {
// 创建批量消息
List<Message> messageList = new ArrayList<>();
Message message1 = new Message("batchTopic", "batchTag", ("Hello Batch message-1").getBytes());
Message message2 = new Message("batchTopic", "batchTag", ("Hello Batch message-2").getBytes());
Message message3 = new Message("batchTopic", "batchTag", ("Hello Batch message-3").getBytes());
messageList.add(message1);
messageList.add(message2);
messageList.add(message3);
LOGGER.info("发送MQ消息内容:" + JSONObject.toJSONString(messageList));
// 发送消息
SendResult sendResult = defaultMQProducer.send(messageList);
// 打印结果
System.out.println(String.format("SendResult status:%s, queueId:%d",
sendResult.getSendStatus(),
sendResult.getMessageQueue().getQueueId()));
BaseResponse response = new BaseResponse().succeed();
response.setData("successed");
return response;
}
大于4MB的批量消息发送
要发送大于4MB的消息,我们只需要把大的消息分裂成若干个小的消息。首先创建一个拆分消息的工具类
public class ListSplitter implements Iterator<List<Message>> {
private final int SIZE_LIMIT = 1024 * 1024 * 4;
private final List<Message> messages;
private int currIndex;
public ListSplitter(List<Message> messages) {
this.messages = messages;
}
@Override
public boolean hasNext() {
return currIndex < messages.size();
}
@Override
public List<Message> next() {
int nextIndex = currIndex;
int totalSize = 0;
for (; nextIndex < messages.size(); nextIndex++) {
Message message = messages.get(nextIndex);
int tmpSize = message.getTopic().length() + message.getBody().length;
Map<String, String> properties = message.getProperties();
for (Map.Entry<String, String> entry : properties.entrySet()) {
tmpSize += entry.getKey().length() + entry.getValue().length();
}
// 增加日志的开销20字节
tmpSize = tmpSize + 20;
if (tmpSize > SIZE_LIMIT) {
//单个消息超过了最大的限制
//忽略,否则会阻塞分裂的进程
if (nextIndex - currIndex == 0) {
//假如下一个子列表没有元素,则添加这个子列表然后退出循环,否则只是退出循环
nextIndex++;
}
break;
}
if (tmpSize + totalSize > SIZE_LIMIT) {
break;
} else {
totalSize += tmpSize;
}
}
List<Message> subList = messages.subList(currIndex, nextIndex);
currIndex = nextIndex;
return subList;
}
}
然后在实现发送消息,和上面的区别在于在发送消息前,使用了工具类分隔消息
/**
* 发送批量消息
*/
@RequestMapping("/sendBatch")
public BaseResponse sendBatch() throws Exception {
// 创建批量消息
List<Message> messageList = new ArrayList<>();
Message message1 = new Message("batchTopic", "batchTag", ("Hello Batch message-1").getBytes());
Message message2 = new Message("batchTopic", "batchTag", ("Hello Batch message-2").getBytes());
Message message3 = new Message("batchTopic", "batchTag", ("Hello Batch message-3").getBytes());
messageList.add(message1);
messageList.add(message2);
messageList.add(message3);
LOGGER.info("发送MQ消息内容:" + JSONObject.toJSONString(messageList));
//发送批量消息:把大的消息分裂成若干个小的消息
ListSplitter splitter = new ListSplitter(messageList);
while (splitter.hasNext()) {
try {
List<Message> listItem = splitter.next();
SendResult result = defaultMQProducer.send(listItem);
System.out.println(String.format("SendResult status:%s, queueId:%d",
result.getSendStatus(),
result.getMessageQueue().getQueueId()));
} catch (Exception e) {
e.printStackTrace();
//处理error
}
}
BaseResponse response = new BaseResponse().succeed();
response.setData("successed");
return response;
}
消费端: 与普通消费一致
4.4过滤消息
- 方式1: tag
在消费端订阅感兴趣的tag即可,
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("defultGroup");
//消费者将接收包含 TagA 或 TagB 或 TagC 的消息,但是限制是一个消息只能有一个标签。
consumer.subscribe("Topic", "TagA || TagB || TagC");
- 方式2: 根据msg自定义属性值 通过SQL92表达式进行筛选,
例如:
messages.putUserProperty("index", String.valueOf(i));
SQL92基本语法
RocketMQ只定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。
- 数值比较,比如:>,>=,<,<=,BETWEEN,=;
- 字符比较,比如:=,<>,IN;
- IS NULL 或者 IS NOT NULL;
- 逻辑符号 AND,OR,NOT;
常量支持类型为:
- 数值,比如:123,3.1415;
- 字符,比如:‘abc’,必须用单引号包裹起来;
- NULL,特殊的常量
- 布尔值,TRUE 或 FALSE
使用限制
只有push模式的消费者支持使用 SQL92 进行消息过滤:
public void subscribe(finalString topic, final MessageSelector messageSelector)
发送端:
/**
* 发送过滤消息
*/
@RequestMapping("/sendFilter")
public BaseResponse sendFilter() throws Exception {
// 创建批量消息
List<Message> messageList = new ArrayList<>();
Message message1 = new Message("batchTopic", "batchTag", ("Hello Batch message-1").getBytes());
Message message2 = new Message("batchTopic", "batchTag", ("Hello Batch message-2").getBytes());
Message message3 = new Message("batchTopic", "batchTag", ("Hello Batch message-3").getBytes());
messageList.add(message1);
messageList.add(message2);
messageList.add(message3);
LOGGER.info("发送MQ消息内容:" + JSONObject.toJSONString(messageList));
for (int i = 0; i < 10; i++) {
Message message = new Message("filterTopic", "TagA", ("Hello filter message-" + i).getBytes());
// 发送消息时,通过`putUserProperty`来设置消息的属性,这个属性在消费的时候会拿来写SQL语句
message.putUserProperty("num", String.valueOf(i));
SendResult sendResult = defaultMQProducer.send(message);
System.out.println(String.format("SendResult status:%s, queueId:%d",
sendResult.getSendStatus(),
sendResult.getMessageQueue().getQueueId()));
}
BaseResponse response = new BaseResponse().succeed();
response.setData("successed");
return response;
}
消费端:
MQConsumerConfigure 需要调整订阅topic 部分
try {
// 设置该消费者订阅的主题和tag,如果订阅该主题下的所有tag,则使用*,
String[] topicArr = topics.split(";");
for (String tag : topicArr) {
String[] tagArr = tag.split("~");
consumer.subscribe(tagArr[0], tagArr[1]);
}
//测试过滤消息
consumer.subscribe("filterTopic", MessageSelector.bySql("TAGS = 'TagA' and num between 2 and 5"));
consumer.start();
LOGGER.info("consumer 创建成功 groupName={}, topics={}, namesrvAddr={}",groupName,topics,namesrvAddr);
} catch (MQClientException e) {
LOGGER.error("consumer 创建失败!");
}
消息订阅的时候使用了 MessageSelector.bySql(); 表明使用 SQL92 的过滤模式,然后传入的字符串是符合 SQL92 语法的语句。
“TAGS”是关键字,为了兼容tag过滤,在SQL92过滤中,使用TAGS替代tag,然后“num”是消息生产时 message.putUserProperty(“num”, String.valueOf(i));设置的属性。
如果我的生产者有多个tag:
String[] tags = new String[] {"TagA", "TagB", "TagC"};
// 依次选择 tag
Message message = new Message("filterTopic", tags[i % tags.length], ("Hello filter message-" + i).getBytes());
// 发送消息时,通过`putUserProperty`来设置消息的属性
message.putUserProperty("num", String.valueOf(i));
那么消费者可以这样过滤:
// 订阅 Topic
consumer.subscribe("filterTopic", MessageSelector.bySql("TAGS in ('TagA', 'TagB') and num between 2 and 5"));
需要注意的是,使用 MessageSelector.bySql
时,需要在broker.conf中配置enablePropertyFilter=true
,否则会报如下错误:
Exception in thread "main" org.apache.rocketmq.client.exception.MQClientException: CODE: 1 DESC: The broker does not support consumer to filter message by SQL92
For more information, please visit the url, http://rocketmq.apache.org/docs/faq/
最后再重启 broker 就行了。
4.5事物消息
事务消息的大致方案,其中分为两个流程:正常事务消息的发送及提交、事务消息的补偿流程。
- 正常事务消息的发送及提交 (黑线)
- 事务消息的补偿流程 (红线)。
正常事物流程
- 发送消息(half 消息)
- MQ服务端响应消息发送结果
- 根据发送结果执行本地事务(如果发送失败,此时half消息对业务不可见,本地事务逻辑不执行)。
- 根据本地事务状态执行Commit(Commit操作生成消息索引,消息对消费者可见)或者Rollback(直接删掉消息)
事务消息的补偿流程
- 对没有Commit/Rollback的事务消息(pending状态的消息),启动定时任务回查事务状态(最多重试15次,超过了默认丢弃此消息)
- Producer收到回查消息,检查回查消息对应的本地事务的状态。
- 根据本地事务状态,重新Commit或者Rollback
补偿阶段用于解决消息Commit或者Rollback发生超时或者失败的情况.
事务消息的状态
事务消息共有三种状态:提交状态、回滚状态、中间状态。
- LocalTransactionState.COMMIT_MESSAGE:: 提交事务,它允许消费者消费此消息。
- LocalTransactionState.ROLLBACK_MESSAGE: 回滚事务,它代表该消息将被删除,不允许被消费。
- LocalTransactionState.UNKNOW: 中间状态,它代表需要检查消息队列来确定状态。
发生端: 这个单独写的测试代码
/**
* 发送事务消息1
*/
@RequestMapping("/sendTransaction2")
public BaseResponse sendTransaction2() throws Exception {
// 创建 TransactionMQProducer 实例,并设置生产者组名
TransactionMQProducer producer = new TransactionMQProducer("transactionGroup");
// 设置 NameServer 地址
producer.setNamesrvAddr("192.168.149.128:9876");
// 设置 Tags
String[] tags = {"TagA", "TagB", "TagC"};
// 添加事务监听器
producer.setTransactionListener(new TransactionListener() {
/**
* 执行本地事务的方法
*/
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
if (StringUtils.equals("TagA", message.getTags())) { // 如果是 TagA 的消息,就提交事务
return LocalTransactionState.COMMIT_MESSAGE;
} else if (StringUtils.equals("TagB", message.getTags())) { // 如果是 TagB 的消息,就回滚事务
return LocalTransactionState.ROLLBACK_MESSAGE;
} else if (StringUtils.equals("TagC", message.getTags())) { // 如果是 TagC 的消息,就把消息设置为 中间状态 ,使其待会儿进行回查本地事物
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.UNKNOW;
}
/**
* 消息回查执行的方法
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
System.out.println("消息的Tag:" + messageExt.getTags());
// 消息来回查的时候,进行提交事务
return LocalTransactionState.COMMIT_MESSAGE;
}
});
// 启动producer
producer.start();
// 分别给每一个 Tag 绑定一条消息
for (int i = 0; i < tags.length; i++) {
// 创建消息对象,指定主题Topic、Tag和消息体
Message msg = new Message("TransactionTopic", tags[i], ("Hello Transaction" + i).getBytes());
// 发送消息
SendResult result = producer.sendMessageInTransaction(msg, null);
// 打印发送结果
System.out.println("发送结果:" + result.getSendStatus());
}
BaseResponse response = new BaseResponse().succeed();
response.setData("successed");
return response;
}
注意: 在创建 Producer 时,是带事务的Producer: TransactionMQProducer
我们使用了 producer.setTransactionListener() 方法注册了一个事务监听器,使用的是 TransactionListener 接口,里面有两个方法:
- LocalTransactionState executeLocalTransaction(Message var1, Object var2): 该方法是用作执行本地事务。在上面代码中,分别对TagA、TagB和TagC做了不同的事务操作。
- LocalTransactionState checkLocalTransaction(MessageExt var1): 该方法用于检查本地事务状态,并回应消息队列的检查请求,在上面代码中,处于中间状态的 TagC 将会进行回查,就会执行该方法。
消费端: 与普通消费一致
最后执行的结果应该是只有 TagA 和 TagC 绑定的消息能够被正常消费,TagB 绑定的消息在事务中被 Rollback ,所以被 MQServer 删除了。
使用限制
- 事务消息不支持 延时消息 和 批量消息。
- 为了避免单个消息被check多次从而引起大部分队列消息的堆积,我们限制单个消息的check次数默认为15次,用户可以通过配置transactionCheckMax参数更改。如果一个消息checked超过了“transactionCheckMax”,这个消息就会被丢弃,同时默认打印一条error log。用户可以通过重写AbstractTransactionCheckListener来自定义处理方式。
- 根据配置文件中的transactionTimeout设置,事务性消息经过“transactionTimeout”时间后会被checked。用户可以在发送事务性消息时设置CHECK_IMMUNITY_TIME_IN_SECONDS属性,该属性的优先级高于transactionMsgTimeout。
- 一个事务性消息可能会checked 检查或consumed消费多次
- 已经commited的消息再次发送到用户的目标topic可能会失败。目前依靠日志记录(log record)。RocketMQ本身的高可用机制确保了高可用性。如果想确保事务性消息没有丢失并且保证了事务的完整性,可以使用同步双写机制。
- 事务性消息的Producer IDs不能与其他类型消息的Producer IDs共享。与其他类型的消息不同,事务性消息允许反向查询,通过Producer ID查询client。
五问题
5.1 使用sql92的的方式订阅消息时,及时在broker.conf 增加了配置,还是无法接收到
答:检查一下是否修改的是配置文件是启动时指定的问题。
5.2发送事务消失时,发送tagc的消息时发送的是UNKOWN,但是producer没有接收到事务确认回调,怎么回事?
答:添加事务监听器 此处需要注意rocketmq 从 4.3.0开始才支持事务,所以按照rocketmq的时候需要安装高版本的。–例子中的版本为4.2.0 不支持事务。为了测试事务消息,rocketmq服务及代码升级为了4.6.1
5.3 MessageListenerOrderly vs MessageListenerConcurrently(只设置一个线程) 有啥区别?
答:todo
5.4 两个消费组,一个设置为集群模式,一个设置为广播是什么效果?
消费记录哪里存储,消息消费次数?
答:todo
源代码见:这里
我向你奔赴而来,你就是星辰大海