一、相关类介绍:
1、生产者:发送消息,只启动一次即可。
(1)DefaultMQProducer:最常见的,可以用于大部分消息发送;
(2)TransactionMQProducer:事务消息生产者。
2、消息者:注册监听器以及设置监听器的参数,只启动一次即可。
(1)DefaultMQPushConsumer:最常见的;
(2)SimpleConsumer:官方推荐的批量消息的消费者,当然批量消息也可以使用DefaultMQPushConsumer。
3、监听器:真正的业务消费逻辑,有两种监听器模式
(1)MessageListenerConcurrently:并发消费模式,可以用大大部分消息的消费。拉取到新消息之后就提交到线程池去消费
public interface MessageListenerConcurrently extends MessageListener {
ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> var1, ConsumeConcurrentlyContext var2);
}
(2) MessageListenerOrderly:有序消费模式,用于消费顺序消息。实际上,每一个消费者的的消费端都是采用线程池实现多线程消费的模式,即消费端是多线程消费。虽然MessageListenerOrderly被称为有序消费模式,但是仍然是使用的线程池去消费消息。顺序消费模式使用3把锁来保证消费的顺序,通过加分布式锁和本地锁保证同时只有一条线程去消费一个队列上的数据。
1、broker端的分布式锁:
在负载均衡的处理新分配队列的updateProcessQueueTableInRebalance方法,以及ConsumeMessageOrderlyService服务启动时的start方法中,都会尝试向broker申请当前消费者客户端分配到的messageQueue的分布式锁。
broker端的分布式锁存储结构为ConcurrentMap<String/* group */, ConcurrentHashMap<MessageQueue, LockEntry>>,该分布式锁保证同一个consumerGroup下同一个messageQueue只会被分配给一个consumerClient。
获取到的broker端的分布式锁,在client端的表现形式为processQueue. locked属性为true,且该分布式锁在broker端默认60s过期,而在client端默认30s过期,因此ConsumeMessageOrderlyService#start会启动一个定时任务,每过20s向broker申请分布式锁,刷新过期时间。而负载均衡服务也是每20s进行一次负载均衡。
broker端的分布式锁最先被获取到,如果没有获取到,那么在负载均衡的时候就不会创建processQueue了也不会提交对应的消费请求了。
2、messageQueue的本地synchronized锁:
在执行消费任务的开头,便会获取该messageQueue的本地锁对象objLock,它是一个Object对象,然后通过synchronized实现锁定。
这个锁的锁对象存储在MessageQueueLock.mqLockTable属性中,结构为ConcurrentMap<MessageQueue, Object>,所以说,一个MessageQueue对应一个锁,不同的MessageQueue有不同的锁。
因为顺序消费也是通过线程池消费的,所以这个synchronized锁用来保证同一时刻对于同一个队列只有一个线程去消费它。
3、ProcessQueue的本地consumeLock:
在获取到broker端的分布式锁以及messageQueue的本地synchronized锁的之后,在执行真正的消息消费的逻辑messageListener#consumeMessage之前,会获取ProcessQueue的consumeLock,这个本地锁是一个ReentrantLock。
在负载均衡时,如果某个队列C被分配给了新的消费者,那么当前客户端消费者需要对该队列进行释放,它会调用removeUnnecessaryMessageQueue方法对该队列C请求broker端分布式锁的解锁。
而在请求broker分布式锁解锁的时候,一个重要的操作就是首先尝试获取这个messageQueue对应的ProcessQueue的本地consumeLock。只有获取了这个锁,才能尝试请求broker端对该messageQueue的分布式锁解锁。
如果consumeLock加锁失败,表示当前消息队列正在消息,不能解锁。那么本次就放弃解锁了,移除消息队列失败,只有等待下次重新分配消费队列时,再进行移除。
如果没有这把锁,假设该消息队列因为负载均衡而被分配给其他客户端B,但是由于客户端A正在对于拉取的一批消费消息进行消费,还没有提交消费点位,如果此时客户端A能够直接请求broker对该messageQueue解锁,这将导致客户端B获取该messageQueue的分布式锁,进而消费消息,而这些没有commit的消息将会发送重复消费。
所以说这把锁的作用,就是防止在消费消息的过程中,该消息队列因为发生负载均衡而被分配给其他客户端,进而导致的两个客户端重复消费消息的行为。
public interface MessageListenerOrderly extends MessageListener {
ConsumeOrderlyStatus consumeMessage(List<MessageExt> var1, ConsumeOrderlyContext var2);
}
二、demo:生产和消费没有项目的概念,可以在同一个项目中,也可以在不同的项目中,只要连接信息一致即可。这里为了清晰创建了两个工程。
1、生产者:
(1)pom:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>rocket-mq-test</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- rocketmq相关依赖 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.6</version>
</dependency>
</dependencies>
</project>
(2) 配置文件:
server.port = 8888
server.servlet.context-path=/mqProviderTest
#mq配置
mq.groupname = test-mq-groupname
mq.nameserveraddress = xxx.xx.xx.xx:9876
#用户topic
mq.user.topic = test-mq-user-topic
mq.user.tag = test-mq-user-tag
#学校topic
mq.school.topic = test-mq-school-topic
mq.school.tag = test-mq-school-tag
(3) mq发送配置类:
package com.demo.config;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description: mq配置类
*/
@Configuration
public class DefaultMQProducerConfig {
@Value("${mq.groupname}")
private String groupName;
@Value("${mq.nameserveraddress}")
private String nameserveraddress;
/**
* 通用mq
* @return
*/
@Bean(name="defaultMQProducer",initMethod = "start",destroyMethod = "shutdown")
public DefaultMQProducer defaultMQProducer(){
DefaultMQProducer producer = new DefaultMQProducer(groupName);
producer.setNamesrvAddr(nameserveraddress);
producer.setSendMsgTimeout(3000);
return producer;
}
}
(4)发送controller
package com.demo.controller;
import com.alibaba.fastjson.JSON;
import com.demo.dto.SchoolDTO;
import com.demo.dto.UserDTO;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
@RestController
@RequestMapping("/sendMessage")
public class SendMessageController {
private final Logger logger = LoggerFactory.getLogger(SendMessageController.class);
@Autowired
private DefaultMQProducer defaultMQProducer;
@Value("${mq.user.topic}")
private String userTopic;
@Value("${mq.user.tag}")
private String userTag;
@Value("${mq.school.topic}")
private String schoolTopic;
@Value("${mq.school.tag}")
private String schoolTag;
@RequestMapping("/sendUser")
public void sendUser(@RequestBody UserDTO userDTO){
try{
Message msg = new Message(userTopic,userTag, JSON.toJSONString(userDTO).getBytes(StandardCharsets.UTF_8));
defaultMQProducer.send(msg);
logger.info("发送用户消息成功");
}catch (Exception e){
logger.error("发送用户消息失败,errorMessage={}",e.getMessage(),e);
}
}
@RequestMapping("/sendSchool")
public void sendSchool(@RequestBody SchoolDTO schoolDTO){
try{
Message msg = new Message(schoolTopic,schoolTag, JSON.toJSONString(schoolDTO).getBytes(StandardCharsets.UTF_8));
defaultMQProducer.send(msg);
logger.info("发送学校消息成功");
}catch (Exception e){
logger.error("发送学校消息失败,errorMessage={}",e.getMessage(),e);
}
}
}
2、消费者:
(1)pom、配置文件mq相关配置同上
(2)监听
package com.demo.config;
import com.demo.listener.UserAndSchoolListener;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @Description: mq配置类
*/
@Component
public class DefaultMQConsumeListener {
private static Logger logger = LoggerFactory.getLogger(DefaultMQConsumeListener.class);
@Value("${mq.groupname}")
private String groupName;
@Value("${mq.nameserveraddress}")
private String nameserveraddress;
@Value("${mq.user.topic}")
private String userTopic;
@Value("${mq.school.topic}")
private String schoolTopic;
/**
* 订阅用户、学校mq
*/
@PostConstruct
public void defaultMQProducer(){
try{
logger.info("mq producer 配置 start");
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer (groupName);
consumer.setNamesrvAddr(nameserveraddress);
// 订阅一个或多个Topic,以及Tag来过滤需要消费的消息
//1、订阅用户消息
consumer.subscribe(userTopic,"*");
//consumer.registerMessageListener(new UserListener());
//2、订阅学校消息
consumer.subscribe(schoolTopic,"*");
//consumer.registerMessageListener(new SchoolListener());
consumer.registerMessageListener(new UserAndSchoolListener());
consumer.start();
logger.info("mq producer 配置 end");
}
catch (Exception e){
logger.error("mq consume启动失败,errorMsg={}",e.getMessage(),e);
}
}
}
package com.demo.listener;
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.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public class UserAndSchoolListener implements MessageListenerConcurrently {
private static Logger logger = LoggerFactory.getLogger(UserAndSchoolListener.class);
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
try{
logger.info("{} Receive New Messages:{} ", Thread.currentThread().getName(), list);
Message message = list.get(0);
String body = new String(message.getBody(), "UTF-8");
System.out.println("消息:"+body);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}catch (Exception e){
logger.error("接收消息异常{}",e.getMessage(),e);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
}
3、测试
分别访问http://localhost:8888/mqProviderTest/sendMessage/sendUser
和http://localhost:8888/mqProviderTest/sendMessage/sendSchool
控制台打印
2023-09-19 16:00:28.804 INFO 19172 --- [-mq-groupname_1] com.demo.listener.UserAndSchoolListener : ConsumeMessageThread_test-mq-groupname_1 Receive New Messages:[MessageExt [brokerName=i-8847E0CB, queueId=1, storeSize=249, queueOffset=8, sysFlag=0, bornTimestamp=1695110428758, bornHost=/172.16.12.131:52049, storeTimestamp=1695110412277, storeHost=/172.31.7.9:10911, msgId=AC1F070900002A9F000000000005E7F1, commitLogOffset=387057, bodyCRC=356242209, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='test-mq-user-topic', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=9, CONSUME_START_TIME=1695110428804, UNIQ_KEY=7F00000121F018B4AAC26021D0550000, CLUSTER=DefaultCluster, TAGS=test-mq-user-tag}, body=[123, 34, 97, 103, 101, 34, 58, 49, 44, 34, 117, 115, 101, 114, 65, 99, 99, 111, 117, 110, 116, 34, 58, 34, 122, 104, 97, 110, 103, 115, 97, 110, 34, 44, 34, 117, 115, 101, 114, 78, 97, 109, 101, 34, 58, 34, -27, -68, -96, -28, -72, -119, 34, 125], transactionId='null'}]]
消息:{"age":1,"userAccount":"zhangsan","userName":"张三"}
2023-09-19 16:00:30.610 INFO 19172 --- [-mq-groupname_2] com.demo.listener.UserAndSchoolListener : ConsumeMessageThread_test-mq-groupname_2 Receive New Messages:[MessageExt [brokerName=i-8847E0CB, queueId=1, storeSize=245, queueOffset=1, sysFlag=0, bornTimestamp=1695110430594, bornHost=/172.16.12.131:52049, storeTimestamp=1695110414094, storeHost=/172.31.7.9:10911, msgId=AC1F070900002A9F000000000005E8EA, commitLogOffset=387306, bodyCRC=1125999612, reconsumeTimes=0, preparedTransactionOffset=0, toString()=Message{topic='test-mq-school-topic', flag=0, properties={MIN_OFFSET=0, MAX_OFFSET=2, CONSUME_START_TIME=1695110430610, UNIQ_KEY=7F00000121F018B4AAC26021D7820001, CLUSTER=DefaultCluster, TAGS=test-mq-school-tag}, body=[123, 34, 115, 99, 104, 111, 111, 108, 73, 100, 34, 58, 34, 49, 50, 51, 52, 53, 54, 34, 44, 34, 115, 99, 104, 111, 111, 108, 78, 97, 109, 101, 34, 58, 34, -27, -83, -90, -26, -96, -95, -28, -72, -128, 34, 125], transactionId='null'}]]
消息:{"schoolId":"123456","schoolName":"学校一"}
注:以上是单个rocketmq,如果是集群模式,连接时以分号;分割:
mq.nameserveraddress = xxx.xx.xx1:9876;xxx.xx.xx2:9876