目录
背景
准备工作
加依赖
添配置
生产者
消费者
事务消息
消息发送
事务消息 处理
总结
上面几篇博文,从RocketMq 的概念,特性,架构等方面详细描述了RocketMq 的基础知识和架构原理;下面我们研究下工作中在什么样的场景应该使用RocketMq什么样的属性;
开发环境: jdk1.8 + RocketMq 4.8.0 + springboot 2.3.2.release
org.apache.rocketmq
rocketmq-spring-boot-starter
2.0.3
# rocketmq 配置
rocketmq:
name-server: 127.0.0.1:9876
producer:
# 注意:必须制定group
group: test-group
package com.springcloud.alibaba.controller;
import com.alibaba.fastjson.JSONObject;
import com.springcloud.alibaba.domain.message.MqMsgDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.messaging.support.MessageBuilder;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* @Author corn
* @Date 2021/2/27 20:31
*/
@SpringBootTest
@Slf4j
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
public class MqSendTest {
private final RocketMQTemplate rocketMQTemplate;
/**
*@描述 消息同步发送,常用作比较重要的消息发送,比如邮件短信的发送 ;优点: 保证消息的同步传输 缺点: 并发比较高时,性能下降
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/2/28
*/
@Test
public void SyncSend() throws UnsupportedEncodingException {
MqMsgDto ayncSendMsg = MqMsgDto.builder()
.msgId(new Random().nextInt())
.msgVal("同步发送")
.build();
// 入参: topic 和message
SendResult sendResult = rocketMQTemplate.syncSend("add-mq", ayncSendMsg);
log.info(JSONObject.toJSONString(sendResult));
}
/**
*@描述 消息异步发送,通常用作业务对时间比较敏感的操作,比如注册成功后,发送邮件通知
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/2/28
*/
@Test
public void asyncSend(){
MqMsgDto asyncSend = MqMsgDto.builder()
.msgVal("异步发送")
.msgId(new Random().nextInt())
.build();
// 入参: topic ,message ,SendCallback 回调事件
rocketMQTemplate.asyncSend("add-mq", asyncSend, new SendCallback() {
// 消息发送成功后的回调事件
@Override
public void onSuccess(SendResult sendResult) {
log.info("异步消息发送成功,返回result:{}",JSONObject.toJSONString(sendResult));
}
// 消息发送异常的消息回调事件,可以打印出异常的原因
@Override
public void onException(Throwable throwable) {
log.info("异步消息发送失败,异常状态:{}",throwable.getMessage());
}
});
}
/**
*@描述 单项发送,无需等待broker 的响应,通常用作对数据可靠性要求不高的场景。比如日志的收集等
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/2/28
*/
@Test
public void sendOneway(){
MqMsgDto sendoneway = MqMsgDto.builder()
.msgVal("单项发送")
.msgId(new Random().nextInt())
.build();
// 入参: topic ,message
rocketMQTemplate.sendOneWay("add-mq",sendoneway);
log.info("消息单项发送");
}
/**
*@描述 消息有序发送,通常用作对消息顺序有严格要求的场景,比如订单系统等。
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/2/28
*/
@Test
public void sendOrder(){
List orderbyObject = getOrderbyObject();
for (MqMsgDto mqMsgDto : orderbyObject) {
// 参数: topic 、message 、唯一id,通过改id 可以将相同id的消息分配到一个consumerQueue 中,从而来保证消息的有序性
rocketMQTemplate.syncSendOrderly("add-order-mq",mqMsgDto,String.valueOf(mqMsgDto.getMsgId()));
}
}
public List getOrderbyObject(){
List list = new ArrayList<>();
// 创建订单
list.add(MqMsgDto.builder()
.msgId(10001)
.msgVal("创建订单")
.build());
list.add(MqMsgDto.builder()
.msgId(10002)
.msgVal("创建订单2")
.build());
list.add(MqMsgDto.builder()
.msgId(10001)
.msgVal("付款")
.build());
list.add(MqMsgDto.builder()
.msgId(10002)
.msgVal("付款2")
.build());
list.add(MqMsgDto.builder()
.msgId(10001)
.msgVal("完成")
.build());
list.add(MqMsgDto.builder()
.msgId(10002)
.msgVal("完成2")
.build());
return list;
}
/**
*@描述 延时发送消息
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/2/28
*/
@Test
public void delayTimeSend(){
MqMsgDto delayTime = MqMsgDto.builder().msgId(101)
.msgVal("延时消息发送")
.build();
// 参数: topic 、message、消息发送等待时间、延时级别,延时级别分为18个级别,数字分别对应相关的延迟级别,具体的对应关系可以查找rocketMq 官网
rocketMQTemplate.syncSend("add-mq", MessageBuilder.withPayload(delayTime).build(),2000,10);
log.info("延时消息发送成功");
}
/**
*@描述 消息筛选,RocketMq 消息筛选支持tag 消息筛选,和sql92 的方式
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/2/28
*/
@Test
public void searchTag(){
// 通过RocketMQ 源码得知,设置tag的方法是 在topic:tag 的方式设置的,及topic 冒号后即为tag
SendResult tag = rocketMQTemplate.syncSend("add-mq:tag", MqMsgDto.builder().msgId(1).msgVal("标签为tag 的消息").build());
SendResult tag2 = rocketMQTemplate.syncSend("add-mq:tag1", MqMsgDto.builder().msgId(2).msgVal("标签为tag1 的消息").build());
// MessageBuilder.withPayload(T) 是将相关泛型转换成RocketMq 的MessageBuilder 消息体。并且可以在header 属性中设置相关的参数信息,用来做参数的传递或者是sql92 方式的筛选
rocketMQTemplate.syncSend("add-mq",MessageBuilder.withPayload(MqMsgDto.builder().msgId(3).msgVal("设置属性num=2").build()).setHeader("num",2).build());
rocketMQTemplate.syncSend("add-mq",MessageBuilder.withPayload(MqMsgDto.builder().msgId(4).msgVal("设置属性num=6").build()).setHeader("num",6).build());
rocketMQTemplate.syncSend("add-mq",MessageBuilder.withPayload(MqMsgDto.builder().msgId(5).msgVal("设置属性name=test").build()).setHeader("name","test").build());
rocketMQTemplate.syncSend("add-mq",MessageBuilder.withPayload(MqMsgDto.builder().msgId(5).msgVal("设置属性name=test11").build()).setHeader("name","test111").build());
}
}
package com.springcloud.alibaba.consumer.rocketMq;
import com.alibaba.fastjson.JSONObject;
import com.springcloud.alibaba.consumer.domain.message.MqMsgDto;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Service;
/**
* 描述消费者之前,首先详细了解下@RocketMqMessageListener 注解中的常用属性及含义
* consumerGroup: 消费组名称,具有相同角色的使用者、具有完全相同的订阅和consumerGroup,才能正确实现负载平衡。它是必需的,并且必须是全局唯一的。
* topic: 订阅所要消费消息的主题
* selectorType: 设置筛选消息的方式,默认是tag ,支持设置sql92
* selectorExpression: 设置筛选消息的规则,并且支持多种规则,规则如下:
* 数值比较,比如:>,>=,<,<=,BETWEEN,=;
* 字符比较,比如:=,<>,IN;
* IS NULL 或者 IS NOT NULL;
* 逻辑符号 AND,OR,NOT;
* 常量支持类型为:
* 数值,比如:123,3.1415;
* 字符,比如:'abc',必须用单引号包裹起来;
* NULL,特殊的常量
* 布尔值,TRUE 或 FALSE
*consumeMode: 设置消费消息的模式,默认为同时消费,也可以设置成顺序消费
* messageModel: 设置消息消费的模式,默认是集群模式,可以设置成广播模式
* consumeThreadMax: 设置消费者最大消费线程数,默认64
* consumeTimeout: 设置消费者最大的消费时间,默认是30s
*
*
* @Author corn
* @Date 2021/2/24 22:04
*/
@Service
@Slf4j
@RocketMQMessageListener(consumerGroup = "consumerGroup",topic = "transaction-topic")
public class RocketMqTestConsumer implements RocketMQListener {
@Override
public void onMessage(MqMsgDto mqMsgDto) {
// 接收到mq 消息时执行的方法
log.info("mq消费消息内容为:{}",JSONObject.toJSONString(mqMsgDto));
}
}
/**
* 根据tag 做消息筛选
* @Author corn
* @Date 2021/2/24 22:04
*/
@Service
@Slf4j
@RocketMQMessageListener(consumerGroup = "consumerGroup6",topic = "add-mq",selectorExpression = "tag1")
public class RocketMqTag2TestConsumer implements RocketMQListener {
@Override
public void onMessage(MqMsgDto mqMsgDto) {
// 接收到mq 消息时执行的方法
log.info("mq消费消息内容为:{}",JSONObject.toJSONString(mqMsgDto));
}
}
/**
* 顺序消费
* @Author corn
* @Date 2021/2/24 22:04
*/
@Service
@Slf4j
@RocketMQMessageListener(consumerGroup = "consumerOrderGroup",topic = "add-order-mq",consumeMode = ConsumeMode.ORDERLY)
public class RocketMqOrderlyTestConsumer implements RocketMQListener {
@Override
public void onMessage(MqMsgDto mqMsgDto) {
// 接收到mq 消息时执行的方法
log.info("mq消费消息内容为:{}",JSONObject.toJSONString(mqMsgDto));
}
}
/**
* 根据属性num 进行筛选,只有selectType = Sql92 时
* @Author corn
* @Date 2021/2/24 22:04
*/
@Service
@Slf4j
@RocketMQMessageListener(consumerGroup = "consumerGroup9",topic = "add-mq",selectorType = SelectorType.SQL92,selectorExpression = "num=2",messageModel = MessageModel.BROADCASTING)
public class RocketMqNum1TestConsumer implements RocketMQListener {
@Override
public void onMessage(MqMsgDto mqMsgDto) {
// 接收到mq 消息时执行的方法
log.info("mq消费消息内容为:{}",JSONObject.toJSONString(mqMsgDto));
}
}
事务消息的原理在前面的设计中,已经详细阐述了RocketMq 事务消息的工作机制。事务消息的目的主要是保证本地事务和Mq消息发送的一致性,所以编码主要体现在生产者端;详细编码如下:
// 发送mq
this.rocketMQTemplate.sendMessageInTransaction(
/**
*用于将回调事件与侦听器相关联的txProducerGroup,rocketMQTemplate必须发送带有声明的txProducerGroup的事务性消息;
* 需要注意事务回调txProduceGroup 要和此处一致
*/
"tx-add-bouns-group",
// topic
"transaction-topic",
// 消息内容,setHeader 主要传递参数,供事务回调使用
MessageBuilder.withPayload(msg)
.setHeader(RocketMQHeaders.TRANSACTION_ID,transactionId)
.setHeader("share_id",3)
.build(),
// args 参数
user
);
package com.springcloud.alibaba.rocketmq;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.springcloud.alibaba.mapper.RocketmqTranscationLogMapper;
import com.springcloud.alibaba.model.CanalUser;
import com.springcloud.alibaba.model.RocketmqTranscationLog;
import com.springcloud.alibaba.service.CanalUserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.CollectionUtils;
import java.util.List;
/**
* 事务消息回调事件
* @Author corn
* @Date 2021/2/28 21:58
*/
@RocketMQTransactionListener(txProducerGroup = "tx-add-bouns-group")
@RequiredArgsConstructor(onConstructor = @_(@Autowired))
@Slf4j
public class TransactionListener implements RocketMQLocalTransactionListener {
private final CanalUserService canalUserService;
private final RocketmqTranscationLogMapper transcationLogMapper;
/**
*@描述 执行本地事务方法
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/16
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
MessageHeaders headers = message.getHeaders();
String transactionId = (String)headers.get(RocketMQHeaders.TRANSACTION_ID);
try {
// 调用本地方法事务逻辑,如果执行成功,则提交事务,反之回滚事务
this.canalUserService.saveTransactionLog((CanalUser) o,transactionId);
Thread.sleep(500000);
return RocketMQLocalTransactionState.COMMIT;
}catch (Exception e){
log.error("异常:{}",e.getMessage());
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
*@描述 事务回调方法,发生在生产者没有告诉broker事务消息的执行状态,broker会调用回查方法查询本地事务执行状态;
* 需要注意的是,broker 并非无休止的轮查,默认回查15次,如果还未查到消息的状态,那么就会回滚该条事务消息;
* 注意: RocketMq 3.1.2之前的有事务消息回查功能的版本。消息回查功能基于文件系统,回查后得到的结果以及正常的处理结果Commit/Rollback都会修改CommitLog里PREPARED消息的状态
* ,这会导致内存中脏页过多,有隐患。在之后的版本移除了基于文件系统的状态修改机制,对事务消息的处理流程进行重做,移除了消息回查功能。
*@参数
*@返回值
*@创建人 corn
*@创建时间 2021/3/16
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
MessageHeaders headers = message.getHeaders();
String transactionId =(String) headers.get(RocketMQHeaders.TRANSACTION_ID);
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("transaction_id",transactionId);
List rocketmqTranscationLogs = transcationLogMapper.selectList(queryWrapper);
if (CollectionUtils.isEmpty(rocketmqTranscationLogs)){
return RocketMQLocalTransactionState.ROLLBACK;
}else {
return RocketMQLocalTransactionState.COMMIT;
}
}
}
上述中的几种RocketMq 发送消息和消费消息的简单实现demo ,够满足日常工作中简单的一些场景。对于消息发送的流程、消息如何落盘的、以及消息是如何消费的详细的设计,还是需要阅读源码。mark 一下,可以参考芋道源码中的RocketMq 源码进行阅读,了解mq 的底层实现原理