SpringBoot amqp MQ RabbitMQ book
参考文章
Springboot 整合RabbitMq ,用心看完这一篇就够了==>https://blog.csdn.net/qq_35387940/article/details/100514134
MQ应用场景
应用场景
-
注册完成时, 发送邮件和短信通知使用MQ.
-
支付完成时, 回调通知使用MQ
-
下单时,订单系统使用MQ存入, 库存系统使用MQ取出
-
流量削峰/抢单时, 前100用 户请求存入MQ, 超过100个直接返回"error", 秒杀业务从MQ中取那100个.
RabbitMQ各组件的关系
我的processon图片地址:https://www.processon.com/diagraming/5ce6114fe4b022becb418ee7
virtual host虚拟主机
每个virtual host本质上都是一个RabbitMQ Server,拥有它自己的queue,exchagne,和bings rule等等。这保证了你可以在多个不同的application中使用RabbitMQ。
Exchange交换器
exchange交换器用于将生产者生产的数据根据绑定规则转发到相应的队列中, 一共有4大类型,direct,fanout,topic,headers(不用)
Exchange direct类型
消息中的路由键(routing key)如果和Binding中的binding key 一致, 交换器就将消息发到对应的队列中。路由键与队列名完全一致才匹配。
Exchange Fanout类型
不处理路由键,一次性直接转发到所有绑定的队列上。就像广播一样,转发消息最快。
Exchange Topic类型
通过匹配模式来处理路由键,用于发布订阅模式。
"#"匹配多个词, "*"匹配一个词, 特别注意, 这里的一个词的概念不是一个英文单词, 而是以"."分隔后的词, 比如
词语1.词语2.词语3 , 具体样例为 abc1,def2,ghi3 那么abc1是一个词, def2是一个词, ghi3是一个词.
binding 绑定器
绑定各种转发规则
exchange-binding-queue关系图如下
我的processon地址: https://www.processon.com/diagraming/5ce79afde4b07b4302212db8
再次强调: exchange.topic中的绑定规则为 "#"匹配多个词, "*"匹配一个词, 特别注意, 这里的一个词的概念不是一个英文单词, 而是以"."分隔后的词, 比如
词语1.词语2.词语3 , 具体样例为 abc1,def2,ghi3 那么abc1是一个词, def2是一个词, ghi3是一个词.
queue 队列
队列queue就是等待生产者生产的数据写入, 及消费者把数据取出的一个数据队列.
docker安装rabbit
web端管理界面
5672是rabbitmq服务端口, 15672是rabbitmq 网页管理端口, 使用默认帐号/密码 guest/guest 访问 http://IP:15672/ 即可.
添加exchanger
添加Queues
添加绑定binding
注意: 下图经过PS,将三图合一
获取之后删除
Queues 界面 Get Message(s) 选择 ack mode 响应模式 , 测试使用一般选第1种, 正常使用一般选第2种.
-
Nack message requere true: 直译: 不响应消息 , 再从队列中取数,可得 ( 即获取数据后, 再次获取仍可以得到相同值)
-
Ack message requeue false : 直译: 响应消息, 再从队列中取数, 不可得 (即获取数据后, 再次获取将得到空值)
RabbitMQ基本原理
-
自动配置
-
1、RabbitAutoConfiguration
-
2、有自动配置了连接工厂ConnectionFactory;
-
3、RabbitProperties 封装了 RabbitMQ的配置
-
4、 RabbitTemplate :给RabbitMQ发送和接受消息;
-
5、 AmqpAdmin : RabbitMQ系统管理功能组件; AmqpAdmin:创建和删除 Queue,Exchange,Binding
-
6、@EnableRabbit + @RabbitListener 监听消息队列的内容
SpringBoot中使用RabbitMQ
pom.xml导入RabbitMQ依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
application.properties配置
spring.rabbitmq.addresses=centos
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 以下默认
# spring.rabbitmq.port=5672
# spring.rabbitmq.virtual-host=/
RabbitMQ自动装配类RabbitAutoConfiguration
@Configuration
@ConditionalOnClass({RabbitTemplate.class, Channel.class})
@EnableConfigurationProperties({RabbitProperties.class})
@Import({RabbitAnnotationDrivenConfiguration.class})
public class RabbitAutoConfiguration {
......
@Configuration
@Import({RabbitAutoConfiguration.RabbitConnectionFactoryCreator.class})
protected static class RabbitTemplateConfiguration {
private final ObjectProvider<MessageConverter> messageConverter;
private final RabbitProperties properties;
//RabbitProperties封装了RabbitMQ的配置
public RabbitTemplateConfiguration(ObjectProvider<MessageConverter> messageConverter, RabbitProperties properties) {
this.messageConverter = messageConverter;
this.properties = properties;
}
//自动装配RabbitTemplate ,用于给RabbitMQ发送和接收消息
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnMissingBean({RabbitTemplate.class})
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
MessageConverter messageConverter = (MessageConverter)this.messageConverter.getIfUnique();
//如果需要以json字符串的形式存入,则要自定义一个MessageConverter Bean 来替换默认bean
if (messageConverter != null) {
rabbitTemplate.setMessageConverter(messageConverter);
}
rabbitTemplate.setMandatory(this.determineMandatoryFlag());
Template templateProperties = this.properties.getTemplate();
Retry retryProperties = templateProperties.getRetry();
if (retryProperties.isEnabled()) {
rabbitTemplate.setRetryTemplate(this.createRetryTemplate(retryProperties));
}
if (templateProperties.getReceiveTimeout() != null) {
rabbitTemplate.setReceiveTimeout(templateProperties.getReceiveTimeout());
}
if (templateProperties.getReplyTimeout() != null) {
rabbitTemplate.setReplyTimeout(templateProperties.getReplyTimeout());
}
return rabbitTemplate;
}
......
//- AmqpAdmin : RabbitMQ系统管理功能组件; 用于创建和删除 Queue,Exchange,Binding
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnProperty(
prefix = "spring.rabbitmq",
name = {"dynamic"},
matchIfMissing = true
)
@ConditionalOnMissingBean({AmqpAdmin.class})
public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) {
return new RabbitAdmin(connectionFactory);
}
......
}
RabbitTemplate 和 AmqpAdmin 都是自动装配的Bean,可以直接@AutoWired使用它们
测试用例
package com.rabbitmq;
import com.rabbitmq.bean.Student;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@RunWith(SpringRunner.class)
@SpringBootTest
public class Springboot03RabbitmqApplicationTests {
Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
RabbitTemplate rabbitTemplate;
@Autowired
AmqpAdmin amqpAdmin;
@Test//声明交换器 , 并未使用
public void declareExchange() {
amqpAdmin.declareExchange(new DirectExchange("amqpadmin.exchange"));
logger.info("declareExchange");
}
@Test//声明队列 , 并未使用
public void declareQueue() {
amqpAdmin.declareQueue(new Queue("amqpadmin.queue", true));
logger.info("declareQueue");
}
@Test//声明绑定规则 , 并未使用
public void declareBinding() {
amqpAdmin.declareBinding(new Binding("amqpadmin.queue", Binding.DestinationType.QUEUE, "amqpadmin.exchange", "amqp.student", null));
logger.info("declareBinding");
}
@Test
public void contextLoads(){
//rabbitTemplate.send()方法,在使用Message时,需要自己构造一个,用于定义消息体内容和消息头
//rabbitTemplate.send(exchage,routeKey,message);
//rabbitTemplate.convertAndSend()方法是send()方法的简化版, 把object默认当成消息体,只需要传入要发送的对象,自动序列化发送给rabbitmq;内部还是用了Message对象, 只是把Message的Header默认了,只对body部分做操作转换
//rabbitTemplate.convertAndSend(exchage,routeKey,object);
}
/**
* 生产者
* 默认被序列化发送,因为rabbitTemplate默认使用的messageConverter是序列化后发送的,所以存入rabbitMQ中查看是乱的,
* 如果需要以json字符串的形式存入,则要自定义一个MessageConverter Bean 来替换默认bean , 本样例参见MyRabbitConfig.java
* direct单播(点对点)
*/
@Test
public void direct() {
Student stu = new Student(4, "bobo", 18, true, new Date(), "fenfen");
//对象被默认序列化以后发送出去
rabbitTemplate.convertAndSend("exchange.direct", "student.bobo", stu);
}
/**
* 生产者
* fanout广播模式
*/
@Test
public void fanout() {
Student stu = new Student(2, "bobo", 18, true, new Date(), "fenfen");
//对象被默认序列化以后发送出去
rabbitTemplate.convertAndSend("exchange.fanout", null, stu);//fanout广播模式,不需要routingKey, 就算指定了也会被无视
}
/**
* 生产者
* topic 主题模式
*/
@Test
public void topic() {
Student stu = new Student(3, "bobo", 18, true, new Date(), "fenfen");
//对象被默认序列化以后发送出去,由于student.bobo符合student.#绑定规则,该规则默认分发给student,student.bobo,student.sisi这三个队列
rabbitTemplate.convertAndSend("exchange.topic", "student.bobo", stu);
}
/**
* 消费者
* 默认是Ack message requeue false模式(响应消息,再从队列取数时将无法取到)
*/
@Test
public void receive() {
Object o = rabbitTemplate.receiveAndConvert("student.bobo");
logger.info(o.getClass()+"");
logger.info(o.toString());
}
/**
*
*/
public void listener(){
//参见具体MyRabbitListener
}
}
在使用Spring RabbitMQ做消息监听时,如果监听程序处理异常了,且未对异常进行捕获,会一直重复接收消息,然后一直抛异常。
RabbitMQ消息监听程序异常时,消费者会向rabbitmq server发送Basic.Reject,表示消息拒绝接受,由于Spring默认requeue-rejected配置为true,消息会重新入队,然后rabbitmq server重新投递,造成了程序一直异常的情况。
所以说了这么多,我们通过rabbitmq监听消息的时候,程序一定要添加try…catch语句!!!当然你也可以根据实际情况,选择设置requeue-rejected为false来丢弃消息。
本小段摘自: RabbitMQ消息监听异常问题探究==>https://blog.csdn.net/u014513883/article/details/77907898
如何转成json存储呢
源码分析总结:
1.MessageConverter
可以把java
对象转换成Message
对象,也可以把Message
对象转换成java
对象
2.MessageListenerAdapter
内部通过MessageConverter
把Message
转换成java对象,然后找到相应的处理方法,参数为转换成的java对象。
3.SimpleMessageConverter
处理逻辑: 如果content_type
是以text开头,则把消息转换成String
类型 如果content_type的
值是application/x-java-serialized-object
则把消息序列化为java对象,否则,把消息转换成字节数组。
所以我们需要自定义一个MessageConverter Bean来替换SimpleMessageConverter
package com.rabbitmq.config;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyRabbitConfig {
//替换默认转换器,把对象转成json字符串,存取rabbitMQ
@Bean
public MessageConverter messageConverter(){
System.err.println("MyRabbitConfig created");
return new Jackson2JsonMessageConverter();
}
}
正常消费者消费的时候需要监听模式
在Bean上添加@RabbitListener如下
package com.rabbitmq.service;
import com.rabbitmq.bean.Student;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
@Service
public class RabbitService {
Logger logger = LoggerFactory.getLogger(getClass());
/**
* 直接获取body并把它转成Student类型, queues参数可以指定多个队列,适合读取符合Student规范的json数据,所以也可以写成 Object 手动强转.
*/
@RabbitListener(queues = {"student","student.bobo"})
public void receiveStudent(Student stu) {
logger.info("receiveStudent:" + stu.toString());
}
/**
* 直接获取body并把它转成Student类型, queues参数可以指定多个队列
*/
@RabbitListener(queues = {"student","student.sisi"})
public void receiveMessage(Message message) {
logger.info(message.getMessageProperties().toString());//获取header
logger.info(message.getBody().toString());//获取body, 返回的是字节数组,适合读取非字符串文件
}
}
RabbitMQ:@RabbitListener 与 @RabbitHandler 及 消息序列化==>https://www.jianshu.com/p/911d987b5f11
延时队列
SpringBoot整合RabbitMQ实现延时队列==>https://www.cnblogs.com/jockming/p/13180669.html
rabbitmq多消费者重复
一、 为什么会出现消息重复?
消息重复的原因有两个:1.生产时消息重复,2.消费时消息重复。
1.1 生产时消息重复
由于生产者发送消息给MQ,在MQ确认的时候出现了网络波动,生产者没有收到确认,实际上MQ已经接收到了消息。这时候生产者就会重新发送一遍这条消息。
生产者中如果消息未被确认,或确认失败,我们可以使用定时任务+(redis/db)来进行消息重试
1.2消费时消息重复
消费者消费成功后,再给MQ确认的时候出现了网络波动,MQ没有接收到确认,为了保证消息被消费,MQ就会继续给消费者投递之前的消息。这时候消费者就接收到了两条一样的消息。
由于重复消息是由于网络原因造成的,因此不可避免重复消息。但是我们需要保证消息的幂等性。
二 、如何保证消息幂等性
让每个消息携带一个全局的唯一ID,即可保证消息的幂等性,具体消费过程为:
消费者获取到消息后先根据id去查询redis/db是否存在该消息
如果不存在,则正常消费,消费完毕后写入redis/db
如果存在,则证明消息被消费过,直接丢弃
本小节参考: https://www.csdn.net/tags/MtTakg2sMTk4NDktYmxvZwO0O0OO0O0O.html
另外一种说法: 可以使用rabbitmq的channel.basicQos限制信道上消费者所能保持的最大未确认消息的数
使用异步后的烦恼
烦恼一: 数据丢失的风险
解决方式:先写日志或数据库,后放入异步队列.
烦恼二:对其他系统的压力变大
解决方式:使用一定的限流和熔断,对其他系统进行保护。
烦恼三:数据保存后异步任务未执行
解决方式:使用异步任务补偿的方式,定期从数据库中获取数据,放到队列中进行执行,执行后更新数据状态位。
烦恼四:怎样队列长设置和消费者数量
解决方式:使用实际的压力测试来获得队列长度。或者使用排队论的数学公式得到初步的值,然后进行实际压测。
最后介绍一下项目中的经验:
1.量力而行:根据业务特点进行技术选型,业务量小尽量避免使用异步。有所为,有所不为
2.数据说话:异步时一定要进行必要的压力测试
3.先找出系统的关键点:优化单体系统内的性能,再通过整体系统分解来全局优化
4.根据团队和项目的特点选择框架。
一个可供参考的Java高并发异步应用案例==>http://www.uml.org.cn/zjjs/2016060310.asp
备份配置(导入导出配置)
导出: Overview | Import / export definitions | Download broker definitions 下载json格式配置文件, 然后将该配置文件
导入: 在Import | Definitions file | 选择文件 | 选择json格式配置文件 | 点击 Upload broker definitions
我的项目git地址
https://gitee.com/KingBoBo/springboot-03-rabbitmq
待了解
rabbitmq的备份与还原
【RabbitMQ】一文带你搞定RabbitMQ延迟队列==>https://www.cnblogs.com/mfrank/p/11260355.html
SpringBoot高级篇Redis之ZSet数据结构使用姿势==>https://blog.csdn.net/qq_17312239/article/details/104020031
遇见异常
o.s.a.rabbit.connection.CachingConnectionFactory - Attempting to connect to: [localhost:5672]
明明有配置文件, 却提示连接localhost:5672失败, 摆明了配置文件无效, 仔细核实配置即可
参考
RabbitMQ三种Exchange模式(fanout,direct,topic)的性能比较(转)==>https://www.cnblogs.com/shenyixin/p/9084249.html
rabbitMQ实现推迟队列==>https://www.cnblogs.com/zhshlimi/p/10913586.html
rabbitmq重试机制==>https://blog.csdn.net/xixingzhe2/article/details/84345054