RabbitMQ
有段时间没有用过RabbitMQ了,所以练习一下,温故而知新,也分享给大家,共同学习。
简介
MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法。应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它们。消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术。排队指的是应用程序通过 队列来通信。队列的使用除去了接收和发送应用程序同时执行的要求。其中较为成熟的MQ产品有RocketMQ(阿里捐献给Apache), RabbitMQ,ActiveMQ(Apache),Kafka(Apache)。
概念
Exchange 交换机
交换机上可以绑定队列并创建绑定规则,生产者将消息发送到交换机,交换机通过绑定规则将消息推送到对应的队列,由队列存放消息。
交换机的类型:
- Direct : 点对点发送消息,通过routingKey匹配的形式发送消息。
- Fanout: 扇出模式,将消息发送到所有绑定的队列上。
- Topic: 交换机在绑定队列时通过通配符的形式设置routingKey,交换机通过通配符匹配来发送消息到对应的队列里面,#表示任意个词,*表示一个词汇: info.# 可以疲累 info.a.b.c.d…, info.* 只能匹配 info.a,info.b;
- Header:headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。
创建交换机时的参数:
-
Durability: 是否持久化
-
Auto delete:当交换机内没有队列绑定后是否自动删除。
-
Internal:如果是,这生产者不能将消息直接发送到该交换机,只接收其他exchange推送的消息
(exchange to exchange)
扩展参数:
- alternate-exchange:当交换机无法通过routingKey确定发送的队列时(找不到对应的队列),则会将消息发送到指定的其他交换机上(exchange to exchange)。
Queue 队列
RabbitMQ中的消息都只能存储在Queue中,生产者将消息投递给Exchange,Exchange通过RoutingKey推送给Queue,由Queue存放消息,消费者通过监听Queue获取Queue的消息。
多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
创建队列时的参数:
- Durability: 是否对消息进行持久化
- Auto delete:当没有消费者进行消费该队列时,该队列是否删除。
扩展参数:
参数名称 | 参数类型 | 作用 |
---|---|---|
x-expires | Number | 队列自身的空闲存活时间,当前的queue在指定的时间内,没有consumer消费,也就是未被访问,就会被删除。 |
x-max-length | Number | 队列存放最大的消息条数,当队列内消息超出该值时,会删除部分以前存入的消息,余出空间存新数据 |
x-max-length-bytes | Number | 队列存放最大字节数,当队列内消息超出该值时,会删除部分以前存入的消息,余出空间存新数据 |
x-message-ttl | Number | 消息存放在队列的时间,超过了该时间,消息会别销毁或发送到死信队列 |
x-dead-letter-exchange | String | 当消息过期时,将消息发送指定的交换机 |
x-dead-letter-routing-key | String | 当消息过期时,将消息发送指定的交换机时使用的RoutingKey |
x-max-priority | Number | 队列所支持的优先级别,列如设置为5,表示队列支持0到5六个优先级别,5最高,0最低,当然这需要生产者在发送消息时指定消息的优先级别,消息按照优先级别从高到低的顺序分发给消费者 |
安装
本次我通过Docker安装RabbitMQ
1 下载
docker pull rabbitmq:management
2 启动
docker run -d
--name rabbitMQ
-v rabbitMQ:var/lib/rabbitmq
-e RABBITMQ_DEFAULT_USER=guest
-e RABBITMQ_DEFAULT_PASS=guest
--restart=always
-p 15672:15672
-p 5672:5672
rabbitmq:management
#详解
-d 后台运行, -v 挂载容器内数据到宿主机,-e设置启动时的参数(这里设置rabbitMQ web端的用户名和密码),-p映射端口 --name 给容器起个名字,--restart=always 开机自启
SpringBoot整合RabbitMQ
1 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2.配置配置文件
#rabbitMQip地址
spring.rabbitmq.host=192.168.95.128
#rabbitMQ端口
spring.rabbitmq.port=5672
#rabbitMQ用户名
spring.rabbitmq.username=guest
#tabbitMQ密码
spring.rabbitmq.password=guest
3 添加注解
@EnableRabbit
@SpringBootApplication
public class Rabbitmq01Application {
public static void main(String[] args) {
SpringApplication.run(Rabbitmq01Application.class, args);
}
}
声明交换机,队列和绑定规则
注意:即使声明了,也不会立即创建,而是在发送消息时才会创建。
@Configuration
public class RabbitConfig {
//创建一个 direct 类型的交换机
@Bean
public DirectExchange directExchange(){
return new DirectExchange("direct-exchange",true,false);
}
//创建一个 fanout 类型的交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("fanout-exchange",true,false);
}
//创建一个 topic 类型的交换机
@Bean
public TopicExchange topicExchange(){
return new TopicExchange("topic-exchange",true,false);
}
//创建队列
@Bean
public Queue logInfoQueue(){
return new Queue("logInfoQueue",true,false,false);
}
@Bean
public Queue logErrorQueue(){
return new Queue("logErrorQueue",true,false,false);
}
@Bean
public Queue messageInfoQueue(){
return new Queue("messageInfoQueue",true,false,false);
}
@Bean
public Queue messageErrorQueue(){
return new Queue("messageErrorQueue",true,false,false);
}
//创建绑定规则
@Bean
public Binding binding1(){
return BindingBuilder.bind(logInfoQueue()).to(directExchange()).with("logInfo");
}
@Bean
public Binding binding2(){
return BindingBuilder.bind(logErrorQueue()).to(directExchange()).with("logError");
}
@Bean
public Binding binding3(){
return BindingBuilder.bind(messageInfoQueue()).to(fanoutExchange());
}
@Bean
public Binding binding4(){
return BindingBuilder.bind(messageErrorQueue()).to(fanoutExchange());
}
@Bean
public Binding binding5(){
return BindingBuilder.bind(logInfoQueue()).to(topicExchange()).with("log.*");
}
@Bean
public Binding binding6(){
return BindingBuilder.bind(logErrorQueue()).to(topicExchange()).with("*.error");
}
@Bean
public Binding binding7(){
return BindingBuilder.bind(messageInfoQueue()).to(topicExchange()).with("message.*");
}
@Bean
public Binding binding8(){
return BindingBuilder.bind(messageErrorQueue()).to(topicExchange()).with("*.error");
}
}
规则如下:
发送消息
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void contextLoads() {
LogInfo logInfo = new LogInfo("log1",100,new Date(),new BigDecimal(199.99));
rabbitTemplate.convertAndSend("direct-exchange","logInfo",logInfo);
System.out.println("消息发送成功");
}
@Test
void send2(){
LogInfo logInfo = new LogInfo("message1",200,new Date(),new BigDecimal(299.99));
rabbitTemplate.convertAndSend("fanout-exchange","",logInfo);
System.out.println("消息发送成功");
}
@Test
void send3(){
LogInfo logInfo = new LogInfo("log.error",100,new Date(),new BigDecimal(199.99));
rabbitTemplate.convertAndSend("topic-exchange","log.error",logInfo);
System.out.println("消息发送成功");
}
接收消息
方式一 : 通过RabbitTemplate每次获取一个message
@Test
public void get() throws Exception {
Message message = rabbitTemplate.receive("logInfoQueue");
MessageProperties messageProperties = message.getMessageProperties();
String clusterId = messageProperties.getClusterId();
System.out.println("message===>"+message);
byte[] body = message.getBody();
LogInfo logInfo = (LogInfo) new ObjectInputStream(new ByteArrayInputStream(body)).readObject();
System.out.println("loginfo===>"+logInfo.getPrice());
}
方式二: 通过@RabbitListener 监听队列
@Component
@RabbitListener(queues = "logInfoQueue")
public class LogInfoQueueListener {
@RabbitHandler
public void consumer(Message message, @Headers Map<String,Object> headers, Channel channel, LogInfo logInfo){
System.out.println("\n===============接收到消息===============");
System.out.println("logInfo--->"+logInfo);
System.out.println("message====>"+message);
System.out.println("headers====>"+headers);
}
}
保证消息投递可靠性
在消息的投递过程中,可能存在消息发送失败的情况,我们可以对这些发送失败的消息进行重新投递。
RabbitMQ为我们提供了confirmCallback
和 returnCallback
用来处理消息无法发送到对应交换机和消息发送到交换机但无法发送到队列的回调
开启 confirmCallback
confirmCallback 是当消息无法到达交换机时的回调。
1 配置 application.properites
spring.rabbitmq.publisher-confirm-type=correlated
2 自定义RabbitTemplate
@Bean
public RabbitTemplate rabbitTemplate(@Autowired ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
// correlatioData 是在发送消息时设置的(看下面示例)
// ack 消息是否发送成功
if(ack){
System.out.println("消息发送成功!");
System.out.println("消息内容==》"+correlationData.getId());
}else{
System.out.println("消息发送失败!");
System.out.println("消息内容==》"+correlationData.getId());
//这里可以进行消息重复投递,如果先将失败的消息放入数据库,然后开定时任务重新投递发送失败的消息。如果投递n次仍失败,修改状态,取消投递该消息
}
}
});
return rabbitTemplate;
}
//发送消息时设置correlationData
@Test
void contextLoads2() {
LogInfo logInfo = new LogInfo("log1",100,new Date(),new BigDecimal(199.99));
CorrelationData correlationData = new CorrelationData();
correlationData.setId(logInfo.getName());
rabbitTemplate.convertAndSend("direct-exchange22","logInfo",logInfo, correlationData);
System.out.println("消息发送成功");
}
开启ReturnCallback
ReturnCallback 是当交换机无法将消息推送给队列时的回调。
1 配置application.properites
spring.rabbitmq.publisher-returns=true
2 配置rabbitTemplate
rabbitTemplate.setMandatory(true); //设置Mandatory 重点,不设置就不开启回调
//设置在Returncallback
rabbitTemplate.setReturnsCallback(returnedMessage -> {
System.out.println("<===无法将消息发送到队列===>");
Message message = returnedMessage.getMessage();
String exchange = returnedMessage.getExchange();
String routingKey = returnedMessage.getRoutingKey();
});
避免消息重复投递或重复消费
在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id
,作为去重和幂等的依据(消息投递失败并重传),避免重复的消息进入队列;在消息消费时,要求消息体中必须要有一个bizId
(对于同一业务全局唯一,如支付ID、订单ID、帖子ID等)作为去重和幂等的依据,避免同一条消息被重复消费。
这个问题针对业务场景来答分以下几点:
1.比如,你拿到这个消息做数据库的insert操作。那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
2.再比如,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。
3.如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。
通过死信队列实现消息延迟投递
场景:用户创建的订单会保存30分钟,30分钟后仍未付款则自动取消订单。
实现原理:创建一个交换机和一个队列,设置队列的x-message-ttl
,x-dead-letter-exchange
和 x-dead-letter-routing-key
每当消息到达30分钟后消息投放到其他的交换机,然后交换机在将消息放入接受过期消息的队列(死信队列),我们只需要消费这个死信队列,然后判断订单是否付款,如果付款就不管,否则取消订单。
原生方式实现
1 创建交换机,队列和绑定规则
@Configuration
public class RabbitConfig {
@Bean
public DirectExchange deadLetterExchange(){
return new DirectExchange("dead-letter-exchange",true,false);
}
@Bean
public Queue deadLetterQueue(){
return new Queue("dead-letter-queue");
}
@Bean
public Binding binding1(){
return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with("a");
}
@Bean
public DirectExchange direct01(){
return new DirectExchange("direct-exchange",true,false);
}
@Bean
public Queue queue1(){
Map<String,Object> args = new HashMap();
//设置消息过期时间
args.put("x-message-ttl",1000*60); //message过期时间
args.put("x-dead-letter-exchange","dead-letter-exchange");//过期后将消息发送给的交换机
args.put("x-dead-letter-routing-key","a"); //发送时设置的routingkey
return new Queue("queue1",true,false,false,args);
}
@Bean
public Binding binding2(){
return BindingBuilder.bind(queue1()).to(direct01()).with("queue1");
}
}
通过RabbitLinstener 监听死信队列
@Component
public class DeadLetterLinstener {
@RabbitListener(queues = "dead-letter-queue")
public void consumer(Message message, @Headers Map<String,Object> headers, Channel channel) throws Exception{
System.out.println("消费消息====》"+message);
byte[] body = message.getBody();
Date o = (Date)new ObjectInputStream(new ByteArrayInputStream(body)).readObject();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("当前时间====>"+sdf.format(new Date(System.currentTimeMillis())));
System.out.println(String.format("message 时间===>" + sdf.format(o)));
}
}
发送消息:
@Test
public void test1(){
rabbitTemplate.convertAndSend("direct-exchange","queue1",new Date());
}
通过插件实现
RabbitMQ提供了很多查询,我们可以用rabbitmq_delayed_message_exchange
这个插件来实现延迟队列
插件下载地址:https://www.rabbitmq.com/community-plugins.html
安装
安装方式有两种:
1.使用Dockerfile 做成镜像,运行镜像即可,简单方便
2.启动rabbitmq容器,拿到容器Id,然后把插件复制进去,再启用插件
第一种方式(先下载好插件,放入Dockerfile文件所在目录):
1.Dockerfile内容
FROM rabbitmq:3.8.2-management
COPY ["rabbitmq_delayed_message_exchange-3.8.0.ez" , "/plugins/"]
RUN rabbitmq-plugins enable rabbitmq_delayed_message_exchange
2.构建:docker build -t rabbitmq:3.8.2-management .
3.运行:docker run -it -d --hostname my-rabbit --name rabbitmq -p 15672:15672 -p 5672:5672 -v /data/rabbitmq/data:/var/lib/rabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin123456 rabbitmq:3.8.2-management
第二种方式:
1.把插件上传到服务器目录
docker cp rabbitmq_delayed_message_exchange-3.8.0.ez rabbitMQ/plugins
2.进入容器:docker exec -it rabbitmq /bin/bash ,查看插件是否复制进去了
3.激活插件:cd /plugins ,rabbitmq-plugins enable rabbitmq_delayed_message_exchange
8.重启容器:docker restart rabbitmq
9.进入15672后台,可以看到rabbitMq客户端就会出现消息延迟队列的类型:delayedExchange 队列
使用:
1 创建队列,交换机和绑定规则
@Bean
public DirectExchange test1(){
Map args = new HashMap();
//必须设置此参数标注使用插件创建 x-delayed-message类型的交换机 并设置交换机类型为direct
args.put("x-delayed-type", "direct");
DirectExchange directExchange = new DirectExchange("test1", true, false,args);
directExchange.setDelayed(true);
return directExchange;
}
@Bean
public Queue queue1(){
return new Queue("queue1",true);
}
@Bean
public Binding binding5(){
return BindingBuilder.bind(queue1()).to(test1()).with("queue1");
}
2 发送消息
//设置过期时间
rabbitTemplate.convertAndSend("test1", "queue1", new Date(), message -> {
message.getMessageProperties().setDelay(1000);
return message;
});
3 通过RabbitLinstener消费
@RabbitListener(queues = "queue1")
public void consumer(Message message, @Headers Map<String,Object> headers, Channel channel) throws Exception{
System.out.println("消费消息====》"+message);
byte[] body = message.getBody();
Date o = (Date)new ObjectInputStream(new ByteArrayInputStream(body)).readObject();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("当前时间====>"+sdf.format(new Date(System.currentTimeMillis())));
System.out.println(String.format("message 时间===>" + sdf.format(o)));
}
附: