RabbitMQ学习笔记
Docker安装
一、获取镜像
# 镜像未配有控制台
docker pull rabbitmq
# 镜像配有控制台
docker pull rabbitmq:management
二、运行镜像
#方式一:默认guest 用户,密码也是 guest
docker run -d --hostname my-rabbit --name rabbit -p 15672:15672 -p 5672:5672 rabbitmq:management
#方式二:设置用户名和密码
docker run -d --hostname my-rabbit --name rabbit -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password -p 15672:15672 -p 5672:5672 rabbitmq:management
三、访问ui页面
http://localhost:15672/
消息队列
一、什么是消息队列
消息指的是两个应用间传递的数据。数据的类型有很多种形式,可能只包含文本字符串,也可能包含嵌入对象。
“消息队列(Message Queue)”是在消息的传输过程中保存消息的容器。在消息队列中,通常有生产者和消费者两个角色。生产者只负责发送数据到消息队列,谁从消息队列中取出数据处理,他不管。消费者只负责从消息队列中取出数据处理,他不管这是谁发送的数据。
二、为什么使用消息队列
主要有三个作用:
- 解耦。如图所示。假设有系统B、C、D都需要系统A的数据,于是系统A调用三个方法发送数据到B、C、D。这时,系统D不需要了,那就需要在系统A把相关的代码删掉。假设这时有个新的系统E需要数据,这时系统A又要增加调用系统E的代码。为了降低这种强耦合,就可以使用MQ,系统A只需要把数据发送到MQ,其他系统如果需要数据,则从MQ中获取即可。
- 异步。如图所示。一个客户端请求发送进来,系统A会调用系统B、C、D三个系统,同步请求的话,响应时间就是系统A、B、C、D的总和,也就是800ms。如果使用MQ,系统A发送数据到MQ,然后就可以返回响应给客户端,不需要再等待系统B、C、D的响应,可以大大地提高性能。对于一些非必要的业务,比如发送短信,发送邮件等等,就可以采用MQ。
- 削峰。如图所示。这其实是MQ一个很重要的应用。假设系统A在某一段时间请求数暴增,有5000个请求发送过来,系统A这时就会发送5000条SQL进入MySQL进行执行,MySQL对于如此庞大的请求当然处理不过来,MySQL就会崩溃,导致系统瘫痪。如果使用MQ,系统A不再是直接发送SQL到数据库,而是把数据发送到MQ,MQ短时间积压数据是可以接受的,然后由消费者每次拉取2000条进行处理,防止在请求峰值时期大量的请求直接发送到MySQL导致系统崩溃。
三、RabbitMQ的特点
RabbitMQ是一款使用Erlang语言开发的,实现AMQP(高级消息队列协议)的开源消息中间件。首先要知道一些RabbitMQ的特点,官网文档:
- 可靠性。支持持久化,传输确认,发布确认等保证了MQ的可靠性。
- 灵活的分发消息策略。这应该是RabbitMQ的一大特点。在消息进入MQ前由Exchange(交换机)进行路由消息。分发消息策略有:简单模式、工作队列模式、发布订阅模式、路由模式、通配符模式。
- 支持集群。多台RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。
- 多种协议。RabbitMQ支持多种消息队列协议,比如 STOMP、MQTT 等等。
- 支持多种语言客户端。RabbitMQ几乎支持所有常用编程语言,包括 Java、.NET、Ruby 等等。
- 可视化管理界面。RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker。
- 插件机制。RabbitMQ提供了许多插件,可以通过插件进行扩展,也可以编写自己的插件。
五种模式
概念解释
一、 channel 信道:
概念:信道是生产消费者与rabbit通信的渠道,生产者publish或是消费者subscribe一个队列都是通过信道来通信的。信道是建立在TCP连接上的虚拟连接,什么意思呢?就是说rabbitmq在一条TCP上建立成百上千个信道来达到多个线程处理,这个TCP被多个线程共享,每个线程对应一个信道,信道在rabbit都有唯一的ID ,保证了信道私有性,对应上唯一的线程使用。
疑问:为什么不建立多个TCP连接呢?原因是rabbit保证性能,系统为每个线程开辟一个TCP是非常消耗性能,
每秒成百上千的建立销毁TCP会严重消耗系统。所以rabbitmq选择建立多个信道(建立在tcp的虚拟连接)
连接到rabbit上。
类似概念:TCP是电缆,信道就是里面的光纤,每个光纤都是独立的,互不影响。
复制代码
二、exchange 交换机和绑定routing key(这和后面的工作模式有关)
exchange的作用就是类似路由器,routing key 就是路由键,服务器会根据路由键将消息从交换器路由到队列上去。
exchange有多个种类,常用的有direct,fanout,topic。前三种类似集合对应关系那样,(direct)1:1,(fanout)1:N,(topic)N:1
direct: 1:1类似完全匹配
fanout:1:N 可以把一个消息并行发布到多个队列上去,简单的说就是,当多个队列绑定到fanout的交换器,那么交换器一次性拷贝多个消息分别发送到绑定的队列上,每个队列有这个消息的副本。
ps:这个可以在业务上实现并行处理多个任务,比如,用户上传图片功能,当消息到达交换器上,它可以同时路由到积分
增加队列和其它队列上,达到并行处理的目的,并且易扩展,以后有什么并行任务的时候,直接绑定到fanout交换器
不需求改动之前的代码。
topic N:1 ,多个交换器可以路由消息到同一个队列。根据模糊匹配,比如一个队列的routing key 为*.test ,那么凡是到达交换器的消息中的routing key 后缀.test都被路由到这个队列上。
三、队列(queue)
1、推模式:通过AMQP的basic.consume命令订阅,有消息会自动接收,吞吐量高
2、拉模式:通过AMQP的bsaic.get命令
注:当队列拥有多个消费者时,队列收到的消息将以循环的方式发送给消费者。每条消息只会发送给一个订阅的消费者
四、持久化(duration)
开启持久化功能,需同时满足:消息投递模式选择持久化、交换器开启持久化、队列开启持久化
五、确认机制(ack)
1、发送方确认模式:消息发送到交换器–发送完毕–>消息投递到队列或持久化到磁盘异步回调通知生产者
2、消费者确认机制:消息投递消费者-ack-删除该条消息-投递下一条
注:收到ACK前,不会把消息再次发送给该消费者,但是会把下一条消息发送给其他消费者
工作模式
- 简单模式
在上图的模型中,有以下概念:
- 生产者,也就是要发送消息的程序
- 消费者:消息的接受者,会一直等待消息到来。
- 消息队列:图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
- 工作队列模式
消息产生者将消息放入队列消费者可以有多个,消费者1,消费者2同时监听同一个队列,消息被消费。C1 C2共同争抢当前的消息队列内容,谁先拿到谁负责消费消息(隐患:高并发情况下,默认会产生某一个消息被多个消费者共同使用,可以设置一个开关(syncronize) 保证一条消息只能被一个消费者使用)。
- 发布订阅模式
1、每个消费者监听自己的队列;
2、生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息;
- 路由模式
1.消息生产者将消息发送给交换机按照路由判断,路由是字符串(info) 当前产生的消息携带路由字符(对象的方法),交换机根据路由的key,只能匹配上路由key对应的消息队列,对应的消费者才能消费消息;
2.根据业务功能定义路由字符串
3.从系统的代码逻辑中获取对应的功能字符串,将消息任务扔到对应的队列中。
4.业务场景:error 通知;EXCEPTION;错误通知的功能;传统意义的错误通知;客户通知;利用key路由,可以将程序中的错误封装成消息传入到消息队列中,开发者可以自定义消费者,实时接收错误。
- topic 主题模式(通配符模式)
1.星号井号代表通配符
2.星号代表一个单词(
.
后一级有且必须是一个单词),井号代表可有可无(即0或1或多个节点匹配)
3.路由功能添加模糊匹配
4.消息产生者产生消息,把消息交给交换机
5.交换机根据key的规则模糊匹配到对应的队列,由队列的监听消费者接收消息消费
整合SpringBoot
- 导入依赖
<!--Spring AMQP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- 配置yaml文件
publisher-confirm-type
none
值是禁用发布确认模式,是默认值correlated
值是发布消息成功到交换器后会触发回调方法,如示例
/**
* 设置生产者消息publish-confirm回调函数
*/
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if(!ack){
LoggerUtil.error(RabbitConfig.class, StringUtils.join("publishConfirm消息发送到交换器被退回,Id:", correlationData.getId(), ";退回原因是:", cause));
} else {
LoggerUtil.info(RabbitConfig.class, "发送消息到交换器成功,MessageId:"+correlationData.getId());
}
});
simple
值经测试有两种效果,其一效果和correlated
值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate
调用waitForConfirms
或waitForConfirmsOrDie
方法等待broker
节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie
方法如果返回false
则会关闭channel
,则接下来无法发送消息到broker
spring:
rabbitmq:
# 主机
host: 119.3.212.23
# 端口号
port: 5672
# 虚拟主机路径
virtual-host: /
username: root
password: 111111
publisher-confirm-type: correlated
#可以确保消息在未被队列接收时返回
publisher-returns: true
- 配置类
/**
* 配置产生的交换机,队列以及绑定
*/
@Configuration
public class DirectRabbitConfig {
interface Config{
//指定队列
String name= "direct.queue";
//指定交换机
String exchange = "direct.exchange";
//是否持久化
boolean durable = true;
//路由键
String routingKey = "nanfeng";
}
//创建对列
@Bean
public Queue directQueue(){
return new Queue(Config.name,Config.durable);
}
//创建交换机
//简单模式和work模式只需要注册队列即可,交换机不是没有而是服务器有默认交换机
//路由模式的交换机如本例
//统配模式的交换机TopicExchange topic() {return new TopicExchange("topic.exchange")
//发布模式的交换机FanoutExchange direct() {return new FanoutExchange("fanout.exchange")
@Bean
public DirectExchange directExchange(){
return new DirectExchange(Config.exchange);
}
//绑定对列和交换机 并指定 routingKey
@Bean
public Binding binding(){
return BindingBuilder.bind(directQueue()).to(directExchange()).with(Config.routingKey);
}
}
- 消息实体类
@Data
@Builder
public class Mymessage implements Serializable {
private static final long serialVersionUID = 1L;
public String messageId;
public String createTime;
public String body;
}
- 生产者
@RestController
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping("/sndDirectMsg")
public String sendDirectMsg(){
//消息构造
Mymessage msg = Mymessage.builder().
messageId(String.valueOf(UUID.randomUUID())).
createTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))).
body("hello world")
.build();
//发送消息:参数一:交换机名,参数二:路由键,参数三:消息
rabbitTemplate.convertAndSend("direct.exchange","nanfeng", msg);
return "OK";
}
}
- 消费者
@Component
//监听的队列名
//@RabbitListener放在方法上可多队列监听,在类上需要实现一个默认的@RabbitHandler标记的处理方法
public class DirectReceiver {
@RabbitListener(queues = "direct.queue")
public void process(Mymessage msg){
System.out.println("receive msg : " +msg.toString());
}
}
结果
receive msg : Mymessage(messageId=7e6611a6-132b-4d41-a6bf-68ee6cb4b844, createTime=2021-04-06 10:58:50, body=hello world)
过期时间TTL
过期时间 TTl表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。RabbitMQ可以对消息和队列设置 TTL,目前有两种方法可以设置
第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间
第二种方法是对消息进行单独设置,每条消息 TTL可以不同
如果上述两种方法同时使用,则消息的过期时间以两者 TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的 TTL值,就称为 dead message被投递到死信队列,消费者将无法再收到该消息
死信队列
DLX,全称 Dead-Letter-Exchange,可以称之为死信交换机,也有人称之为死信邮箱。当消息再一个队列中变成死信之后,它能被重新发送到另一个交换机中,这个交换机就是 DLX,绑定 DLX的队列就称之为死信队列。消息变成死信,可能是由于以下原因:
- 消息被拒绝
- 消息过期
- 队列达到最大长度(队列中最多容纳五条消息)
DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性,当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的 DLX上去,进而被路由到另一个队列,即死信队列。
要想使用死信队列,只需要在定义队列的时候设置队列参数x-dead-letter-exchange
指定交换机即可