上一篇我们大概了解了RocketMQ的架构,这篇具体使用一下。
1. 启动 RocketMQ
安装 NameServer。
docker run -d -p 9876:9876 --name rmqnamesrv foxiswho/rocketmq:server-4.5.1
安装 Brocker。
1)新建配置目录。
如果是 Windows 需要替换为 Windows 的电脑路径,和 Linux 还是有点差异。
mkdir -p ${HOME}/docker/software/rocketmq/conf
2)新建配置文件 broker.conf。
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0
deleteWhen = 04
fileReservedTime = 48
brokerRole = ASYNC_MASTER
flushDiskType = ASYNC_FLUSH
# 此处为本地ip, 如果部署服务器, 需要填写服务器外网ip
brokerIP1 = xx.xx.xx.xx
3)创建容器。
docker run -d \
-p 10911:10911 \
-p 10909:10909 \
--name rmqbroker \
--link rmqnamesrv:namesrv \
-v ${HOME}/docker/software/rocketmq/conf/broker.conf:/etc/rocketmq/broker.conf \
-e "NAMESRV_ADDR=namesrv:9876" \
-e "JAVA_OPTS=-Duser.home=/opt" \
-e "JAVA_OPT_EXT=-server -Xms512m -Xmx512m" \
foxiswho/rocketmq:broker-4.5.1
安装 RocketMQ 控制台。
docker pull pangliang/rocketmq-console-ng
docker run -d \
--link rmqnamesrv:namesrv \
-e "JAVA_OPTS=-Drocketmq.config.namesrvAddr=namesrv:9876 -Drocketmq.config.isVIPChannel=false" \
--name rmqconsole \
-p 8088:8080 \
-t pangliang/rocketmq-console-ng
运行成功,稍等几秒启动时间,浏览器输入 localhost:8088
查看控制台。
2. 发送普通消息
首先介绍一下消息发送的大致流程,当我们调用消息发送方法时该方法会先对待发送消息进行前置验证,如果消息主题和消息内容均没有问题的话,就会根据消息主题(Topic)去获取路由信息,即消息主题对应的队列,broker,broker的ip和端口信息,然后选择一条队列发送消息,成功的话返回发送成功,失败的话会根据我们设置的重试次数进行重新发送,单向消息发送不会进行失败重试!
2.1 引入 RocketMQ 依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
2.2 启动自动装配
如果 SpringBoot2 版本,就不需要执行这一步。
resources 目录下创建 META-INF/spring 目录,并创建org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件。
# RocketMQ 2.2.3 version does not adapt to SpringBoot3
org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration
2.3 消息生产者
配置文件中引入 RocketMQ 相关配置定义,比如连接 NameServer 地址等。
server:
port: 6060
rocketmq:
name-server: 127.0.0.1:9876 # NameServer 地址
producer:
group: rocketmq-4x-service_common-message-execute_pg # 全局发送者组定义
(2)消息生产者初始化
/***************************消息生产者***************************/
@Autowired
private MQTransactionListener mqTransactionListener; //TODO 事务消息监听器
//TODO 消息生产者配置信息
@Value("${rocketmq.producer.namesrvAddr:127.0.0.1:9876}")
private String pNamesrvAddr; //TODO 生产者nameservice地址
@Value("${rocketmq.producer.maxMessageSize:4096}")
private Integer maxMessageSize ; //TODO 消息最大大小,默认4M
@Value("${rocketmq.producer.sendMsgTimeout:30000}")
private Integer sendMsgTimeout; //TODO 消息发送超时时间,默认3秒
@Value("${rocketmq.producer.retryTimesWhenSendFailed:2}")
private Integer retryTimesWhenSendFailed; //TODO 消息发送失败重试次数,默认2次
private static ExecutorService executor = ThreadUtil.newExecutor(32);//TODO 执行任务的线程池
//普通消息生产者
@Bean("default")
public DefaultMQProducer getDefaultMQProducer() {
DefaultMQProducer producer = new DefaultMQProducer(this.groupName);
producer.setNamesrvAddr(this.pNamesrvAddr);
producer.setMaxMessageSize(this.maxMessageSize);
producer.setSendMsgTimeout(this.sendMsgTimeout);
producer.setRetryTimesWhenSendFailed(this.retryTimesWhenSendFailed);
try {
producer.start();
} catch (MQClientException e) {
System.out.println(e.getErrorMessage());
}
return producer;
}
//事务消息生产者(rocketmq支持柔性事务)
@Bean("transaction")
public TransactionMQProducer getTransactionMQProducer() {
//初始化事务消息基本与普通消息生产者一致
TransactionMQProducer producer = new TransactionMQProducer("transaction_" + this.groupName);
producer.setNamesrvAddr(this.pNamesrvAddr);
producer.setMaxMessageSize(this.maxMessageSize);
producer.setSendMsgTimeout(this.sendMsgTimeout);
producer.setRetryTimesWhenSendFailed(this.retryTimesWhenSendFailed);
//添加事务消息处理线程池
producer.setExecutorService(executor);
//添加事务消息监听
producer.setTransactionListener(mqTransactionListener);
try {
producer.start();
} catch (MQClientException e) {
System.out.println(e.getErrorMessage());
}
return producer;
}
如果是要使用事务消息的话如要设置事务消息处理线程池和事务消息监听器,监听器和消费者监听类似,后面会在消费者介绍中说明;
(3)发送消息
1)、消息发送根据消息功能主要分为普通消息、事务消息、顺序消息、延时消息等!特别说明一下事务消息多用于保证多服务模块间的事务一致性!事务消息发送后并不会直接通知消费者消费消息,而是会先生成一个半消息,会先进入事务消息监听器中,确保该消息事务提交成功后才会向broker发送消息,从而被消费者获取并进行消费;
2)、根据发送方式可以分为同步消息,异步消息和单向消息等,其中同步消息常用于比较重要的消息发送,需要等待broker响应告知消息发送状态;异步消息的话常用于对想用时间敏感,需要快速返回的模块,我们会设置一个回调代码块去异步监听Borker的响应;单向消息的话主要用于对发送结果不敏感,不会影响业务的模块,无需监听broker响应,常用于日志发送等模块。示例如下:
/**
* 添加订单(发送消息积分模块同步添加积分)
* zlx
* 12:09 2021/6/4
* @param order 订单信息
* @return org.apache.rocketmq.client.producer.TransactionSendResult
**/
@Override
public Order addOder(Order order) {
order.setOrderId(SonwflakeUtils.get().id());
if (order.getMessageType() == 1) {
//普通消息
this.save(order);
Message message = new Message("points","default", JSON.toJSONString(order).getBytes());
try {
SendResult sendResult = producer.send(message);//同步消息
System.out.println("发送状态:" + sendResult.getSendStatus() +
",消息ID:" + sendResult.getMsgId() +
",队列:" + sendResult.getMessageQueue().getQueueId());
// producer.sendOneway(message);//单向消息
// producer.send(message, new SendCallback() {//异步消息
// @Override
// public void onSuccess(SendResult sendResult) {
//
// }
//
// @Override
// public void onException(Throwable throwable) {
//
// }
// });
} catch (RemotingException | MQBrokerException | InterruptedException | MQClientException e) {
e.printStackTrace();
}
} else {
//事务消息
Message message = new Message("points","transaction", JSON.toJSONString(order).getBytes());
try {
transactionMQProducer.sendMessageInTransaction(message, null);
} catch (MQClientException e) {
e.printStackTrace();
}
}
return order;
}
3.消息消费
消息监听初始化
@Value("${spring.application.name:application}")
private String groupName;//集群名称,这边以应用名称作为集群名称
/***************************消息生产者***************************/
@Autowired
private Map<String, MQHandler> mqHandlerMap;
//TODO 消息消费者配置信息
@Value("${rocketmq.consumer.namesrvAddr:127.0.0.1:9876}")
private String cNamesrvAddr; //TODO 消费者nameservice地址
@Value("${rocketmq.consumer.consumeThreadMin:20}")
private int consumeThreadMin; //TODO 最小线程数
@Value("${rocketmq.consumer.consumeThreadMax:64}")
private int consumeThreadMax; //TODO 最大线程数
@Value("${rocketmq.consumer.topics:test~*}")
private String topics; //TODO 消费者监听主题,多个主题以分号隔开(topic~tag;topic~tag)
@Value("${rocketmq.consumer.consumeMessageBatchMaxSize:1}")
private int consumeMessageBatchMaxSize; //TODO 一次消费消息的条数,默认为1条
@Bean
public DefaultMQPushConsumer getRocketMQConsumer() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
consumer.setNamesrvAddr(cNamesrvAddr);
consumer.setConsumeThreadMin(consumeThreadMin);
consumer.setConsumeThreadMax(consumeThreadMax);
consumer.registerMessageListener(getMessageListenerConcurrently());
//TODO 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费,如果非第一次启动,那么按照上次消费的位置继续消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
//TODO 设置消费模型,集群还是广播,默认为集群
//consumer.setMessageModel(MessageModel.CLUSTERING);
//TODO 设置一次消费消息的条数,默认为1条
consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize);
try {
//TODO 设置该消费者订阅的主题和tag,如果是订阅该主题下的所有tag,则tag使用*;如果需要指定订阅该主题下的某些tag,则使用||分割,例如tag1||tag2||tag3
String[] topicTagsArr = topics.split(";");
for (String topicTags : topicTagsArr) {
String[] topicTag = topicTags.split("~");
consumer.subscribe(topicTag[0],topicTag[1]);
}
consumer.start();
}catch (Exception e){
throw new Exception(e);
}
return consumer;
}
//TODO 并发消息侦听器(如果对顺序消费有需求则使用MessageListenerOrderly 有序消息侦听器)
@Bean
public MessageListenerConcurrently getMessageListenerConcurrently() {
return new MQListenerConcurrently(mqHandlerMap);
}
这边对MessageListenerConcurrently有进行一定封装,主要是为了在消息处理时通过注解定位消息Topic和tag而自动选择对应的消息处理类进行业务处理;封装代码如下:
/**
* 并发消息监听器
*/
public class MQListenerConcurrently implements MessageListenerConcurrently {
@Autowired
private Map<String, MQHandler> mqHandlerMap;
public MQListenerConcurrently(Map<String, MQHandler> mqHandlerMap) {
this.mqHandlerMap = mqHandlerMap;
}
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
if(CollectionUtils.isEmpty(list)){
System.out.println("接受到的消息为空,不处理,直接返回成功");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
MessageExt messageExt = list.get(0);
//TODO 判断该消息是否重复消费(RocketMQ不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重)
//TODO 获取该消息重试次数
int reconsume = messageExt.getReconsumeTimes();
if(reconsume ==3){//消息已经重试了3次,需做告警处理,已经相关日志
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
//TODO 处理对应的业务逻辑
String topic = messageExt.getTopic();
String tags = messageExt.getTags();
System.out.println("接受到的消息主题为:" + topic + "; tag为:" + tags);
MQHandler mqMsgHandler = null;
//获取消息处理类中的topic和tag注解,根据topic和tag进行策略分发出来具体业务
for (Map.Entry<String, MQHandler> entry : mqHandlerMap.entrySet()) {
MQHandlerActualizer msgHandlerActualizer = entry.getValue().getClass().getAnnotation(MQHandlerActualizer.class);
if (msgHandlerActualizer == null) {
//非消息处理类
continue;
}
String annotationTopic = msgHandlerActualizer.topic();
if (!StrUtil.equals(topic,annotationTopic)) {
//非该主题处理类
continue;
}
String[] annotationTags = msgHandlerActualizer.tags();
if(StrUtil.equals(annotationTags[0],"*")){
//获取该实例
mqMsgHandler = entry.getValue();
break;
}
boolean isContains = Arrays.asList(annotationTags).contains(tags);
if(isContains){
//注解类中包含tag则获取该实例
mqMsgHandler = entry.getValue();
break;
}
}
if (mqMsgHandler == null) {
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
ConsumeConcurrentlyStatus status = mqMsgHandler.handle(tags,messageExt);
// 如果没有return success,consumer会重新消费该消息,直到return success
return status;
}
消息消费
使用注解@MQHandlerActualizer标明该消息处理类的主题,默认监听所有tag,如果需要对tag监听进行分类,后面加上tag即可!消息监听器在收到消息后会自动调用主题对应的处理类进行业务处理,示例如下:
@MQHandlerActualizer(topic = "points")
public class PointsMQHandler implements MQHandler {
@Autowired
private PointsService pointsService;
@Override
public ConsumeConcurrentlyStatus handle(String tag, MessageExt messageExt) {
//消息监听
String messageStr = new String(messageExt.getBody());
Map orderMap = (Map) JSON.parse(messageStr);
Points points = new Points();
Long orderId = (Long) orderMap.get("orderId");
System.out.println("消息tag为:" + tag);
System.out.println("消息监听:" + "为订单" + orderId + "添加积分");
//查询该订单是否已经生成对应积分(rocketMQ可能会重复发送消息,需实现幂等)
QueryWrapper<Points> pointsQueryWrapper = new QueryWrapper<>();
pointsQueryWrapper.lambda().eq(Points::getOrderId,orderId);
Points tempPoints = pointsService.getOne(pointsQueryWrapper);
if (tempPoints != null) {
//该订单已经生成积分
System.out.println(orderId + "已经生成积分");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
points.setPointsId(SonwflakeUtils.get().id());
points.setOrderId(orderId);
Integer orderAmout = (Integer) orderMap.get("orderAmout");
points.setPoints(orderAmout * 10);
pointsService.save(points);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}