队列:
MQ的主要功能
下图第一个是正常调用流程,第二个是采用异步的方式,第三个则是使用mq的方式。
给消息中间件写消息的耗时是非常短的,数据库插入数据持久化可能需要50ms,写消息类似于操作redis一样,只会花费5ms。
订单系统下订单完成之后,会调用库存系统来减库存,假设库存系统经常升级的话,接口的参数
也可能发生变化,这时候订单系统也不得不跟着升级。
使用消息中间件,订单完成之后,我们将用户及所购买商品等等信息存入消息队列中,就无需关心库存系统了。
库存系统实时订阅
队列中的内容,只要有内容库存系统就会收到消息,最终库存系统来实现业务逻辑。
下单业务
如果不通过mq
的话,它需要等待支付业务、订单物流业务、第三方商家业务等
,
如果此时有任何一个业务出现问题,那么受牵连
的将会是所有业务,
而通过mq
发送关键信息
到其他业务,此时任何一个业务出现问题都不会关联
到其他业务。
例如秒杀业务,瞬间流量会非常大,瞬间百万个请求都要进来秒杀一个商品,就算我们前端服务器接收了所有请求,我们要执行业务代码,秒杀完要下订单等,整个流程会非常慢,后台一直阻塞,可能就会导致最终的资源耗尽,服务器宕机,
此时我们可以将大并发量的请求全部进来,先存储到消息队列中,直接响应,后台相关业务处理的服务订阅消息队列中的秒杀请求,然后根据服务器的能力进行消费和处理,则不会导致我们的服务器宕机。
只要是消息中间件一定有点对点式和发布订阅式。
消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移出队列
消息只有唯一的发送者和接受者,但并不是说只能有一个接收者,消息最终只会交给一个人,谁先抢到就是谁的。
如果后端全部是用java来实现的,我们可以首选jms,多语言的话就使用AMQP。
RabbitMQ 是一个由 Erlang 语言(面向并发的编程语言)
开发的 AMQP(高级消息队列协议)
的开源实现。
RabbitMQ 最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展 性、高可用性等方面表现不俗。
交换机中可以设置路由键,并且也可以绑定消息队列,
我们访问的时候只需要指定路由键,交换机就会根据路由键的规则,发送到绑定的消息队列中。
建立长连接
的好处,一旦消费者出现了宕机或其他问题,导致连接中断了,rabbitmq就会实时感知有消费者下线,消息没办法派发了,就会再次存储起来,不造成大面积的消息丢失等。
如果队列不能感知消费者的话,把消息发出去之后认为发送成功了就会删除该消息,就会造成消息丢失。
RabbitMQ Server
也叫broker server(表示消息队列服务器实体),它是一种传输服务。 他的角色就是维护一条从Producer到Consumer的路线,保证数据能够按照指定的方式进行传输。
Producer(Publisher)
消息生产者,也是一个向交换器发布消息的客户端应用程序。如图A、B、C,数据的发送方。消息生产者连接RabbitMQ服务器然后将消息投递到Exchange。
Consumer
消息消费者,表示一个从消息队列中取得消息的客户端应用程序。如图1、2、3,数据的接收方。消息消费者订阅队列, RabbitMQ将Queue中的消息发送到消息消费者。
Message
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
RoutingKey(路由键)
生产者在将消息发送给Exchange的时候,一般会指定一个routing key, 来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联 合使用才能最终生效。在Exchange Type与binding key固定的情况下(在正常使用时一 般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过 指定routing key来决定消息流向哪里。RabbitMQ为routing key设定的长度限制为255 bytes。
Exchange(交换机)
用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
生产者将消息发送到Exchange(交换器),由Exchange将消息路由到一个或多个Queue中(或者丢弃)。Exchange并不存储消息。RabbitMQ中的Exchange有 direct(默认)、fanout、topic、headers四种类型,同类型的Exchange转发消息的策略有所区别。
Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和Queue的绑定可以是多对多的关系。
Connection
网络连接,比如一个TCP连接。Producer和Consumer都是通过TCP连接到RabbitMQ Server 的。以后我们可以看到,程序的起始处就是建立这个TCP连接。(长连接,一直保持连接)
Channel(信道)
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
VirtualHost(虚拟主机)
权限控制的基本单位,一个VirtualHost里面有若干Exchange和MessageQueue,以及指定被哪些user使用。例如商品空间,订单空间,保证消息队列的安全性,互不影响。
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / ,虚拟主机是按路径来分的。
例如不同的语言,不同的服务都需要使用rabbitMq,然而配置需要隔离,就需要用到虚拟主机。
可靠性(Reliability) RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
消息确认(需要开启):MQ将消息发送给消费者,MQ并没有将消息删除,MQ把这条消息锁定,消息消费者用完这条消息之后,消费者会给MQ一个确认,当MQ收到返回之后,解除锁定并删除。
灵活的路由(Flexible Routing)
在消息进入队列之前,通过 Exchange(交换机) 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
消息集群(Clustering)
多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
高可用(Highly Available Queues)
队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
跟踪机制(Tracing)
如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。
插件机制(Plugin System)
RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。
// https://www.rabbitmq.com/networking.html
// 4369, 25672 (Erlang发现&集群端口)
// 5672, 5671 (AMQP端口)
// 15672 (web管理后台端口)
// 61613, 61614 (STOMP协议端口)
// 1883, 8883 (MQTT协议端口)
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 --restart=always rabbitmq:management
overview(概览)
direct和headers都是点对点的实现,fanout和topic都是发布订阅的实现,headers已被弃用。
fanout,广播模式(不处理路由键,简单的将队列绑定到交换机上)
无论交换机的路由键是什么,消息都会被该交换机发送到绑定的所有队列中。
topic,主题模式(实际上也是广播模式),(处理/区分路由键)
spring.rabbitmq.host=192.168.56.10
# 虚拟主机
spring.rabbitmq.virtual-host=/
# 生产端消息确认机制
# 老师的配置是这个,但我的版本比老师的高,这个源码中过时配置级别设置了Error,已经不能使用了
#spring.rabbitmq.publisher-confirms=true
# SIMPLE,
# CORRELATED,发布消息成功到交换器后会触发回调方法
# NONE; 禁用发布确认模式,是默认值
spring.rabbitmq.publisher-confirm-type=CORRELATED
# 开启发送端消息抵达队列的确认
spring.rabbitmq.publisher-returns=true
# 只要抵达队列以异步方式优先回调returnConfirm
spring.rabbitmq.template.mandatory=true
# 手动ack(acknowledge,确认收获)消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
// 如果远程仓库获取不到的话,换下面的仓库地址
<mirror>
<id>aliyunmavenid>
<mirrorOf>*mirrorOf>
<name>阿里云公共仓库name>
<url>https://maven.aliyun.com/repository/publicurl>
mirror>
package com.atlinxi.gulimall.order;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 使用rabbitmq
*
* 1. 引入amqp依赖,RabbitAutoConfiguration就会自动生效
* 2. 给容器中自动配置了
* RabbitTemplate,AmqpAdmin,CachingConnectionFactory,RabbitMessagingTemplate
*
* 所有属性都是 @ConfigurationProperties(
* prefix = "spring.rabbitmq"
* )
* public class RabbitProperties
*
* 3. 给配置文件中配置spring.rabbitmq.xxx
* 4. @EnableRabbit,开启功能
* 5. 监听消息,使用@RabbitListener,必须有@EnableRabbit
* @RabbitListener:类/方法上(监听哪些队列)
* @RabbitHandler:标在方法上(重载区分不同的消息)
* 不同的消息实际上就是不同的实体类
*/
@EnableRabbit
@SpringBootApplication
public class GulimallOrderApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallOrderApplication.class, args);
}
}
package com.atlinxi.gulimall.order.config;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
/**
* rabbitmq在发送消息时,如果消息是实体类的话,就会进行序列化,我们是看不懂的
*
* 指定消息转换器为Jackson,就会帮我们转换成json
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**
* 定制rabbitTemplate
*
* 1. rabbitmq服务器收到消息就回调
* 1. spring.rabbitmq.publisher-confirm-type=CORRELATED
* 2. 设置确认回调
*
* 2. 消息正确抵达队列进行回调
*
* spring.rabbitmq.publisher-returns=true
* spring.rabbitmq.template.mandatory=true
*
*
* 3. 消费端消息确认(保证每个消息被正确消费,此时才可以rabbitmq才可以删除这个消息)
* spring.rabbitmq.listener.simple.acknowledge-mode=manual 手动确认
* 1. 默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
* 问题:
* 我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了.发生消息丢失.
* 手动确认.(处理一个消息,确认一个消息,没处理的不能被删除)
* 只要我们没有明确告诉mq,消息被消费,没有ack,消息一直就是unack状态.
* 即使consumer宕机,消息不会丢失,会重新变为ready状态,下一次有新的
* consumer连接进来就发给它
*
*
* 2. 如何确认消息
* channel.basicAck(deliveryTag,false);确认消息,业务逻辑完成就应该确认
* channel.basicNack(deliveryTag,false,true);拒绝消息,业务逻辑失败就拒绝
*
*
*
* @PostConstruct 对象创建完之后再来调用这个方法
* 在这里就指的是MyRabbitConfig对象创建完成之后
*/
@PostConstruct
public void initRabbitTemplate(){
// 设置消息抵达rabbitmq服务器确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*
* 1. 只要消息抵达broker ack就为true
*
* @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id),消息发送成功为null
* @param ack 消息是否成功收到
* @param cause 失败的原因,否则为null
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm...correlationData:" + correlationData);
System.out.println("confirm ack:" + ack);
System.out.println("confirm cause:" + cause);
System.out.println();
}
});
// 设置消息抵达队列的确认回调
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
/**
*
* 只要消息没有投递给指定的队列,就触发这个失败回调,否则不会触发
*
* @param returned 投递失败的相关信息
*
* private final Message message; 投递失败的消息的详细信息
* private final int replyCode; 回复的状态码
* private final String replyText; 回复的文本内容
* private final String exchange; 交换机
* private final String routingKey; 路由键
*/
@Override
public void returnedMessage(ReturnedMessage returned) {
System.out.println("=========消息抵达队列的确认回调==========");
// ReturnedMessage [message=(Body:'"Hello World"' MessageProperties [headers={__TypeId__=java.lang.String},
// contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT,
// priority=0, deliveryTag=0]), replyCode=312, replyText=NO_ROUTE, exchange=hello-java-exchange, routingKey=hello22.java]
System.out.println(returned);
}
});
}
}
package com.atlinxi.gulimall.order;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.UUID;
@SpringBootTest
@Slf4j
class GulimallOrderApplicationTests {
@Autowired
AmqpAdmin amqpAdmin;
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 1. 如何创建exchange[hello-java-exchange],queue,binding
* 1) 使用AmqpAdmin进行创建
* 2. 如何收发消息
*/
@Test
void creatExchange() {
// amqpAdmin
// 创建交换机
// public DirectExchange(String name, boolean durable,
// boolean autoDelete, Map arguments)
DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
amqpAdmin.declareExchange(directExchange);
log.info("exchange创建成功{}","hello-java-exchange");
}
@Test
void creatQueue() {
//public Queue(String name, boolean durable, boolean exclusive,
// boolean autoDelete, @Nullable Map arguments)
// exclusive 排它,只允许一个connection连接,
Queue queue = new Queue("hello-java-queue",true,false,false);
DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
amqpAdmin.declareQueue(queue);
log.info("queue创建成功{}","hello-java-queue");
}
@Test
void creatBinding() {
/**
* public Binding(
* String destination, 目的地(队列)
* Binding.DestinationType destinationType,目的地类型
* String exchange, 交换机
* String routingKey, 路由键
* @Nullable Map arguments,自定义参数
* ) {
*
* 将exchange和destination进行绑定,使用routingKey作为路由键
*/
Binding binding = new Binding("hello-java-queue",Binding.DestinationType.QUEUE,
"hello-java-exchange","hello.java",null);
Queue queue = new Queue("hello-java-queue",true,false,false);
DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
amqpAdmin.declareBinding(binding);
log.info("binding创建成功{}","hello-java-binding");
}
@Test
void sendMessage() {
// 1. 发送消息
// 如果发送的消息是个对象,我们会使用序列化机制,将对象写出去。对象必须实现Serializable接口
// 发送的对象类型的消息,可以是一个json
// rabbitTemplate.convertAndSend("hello-java-exchange","hello.java","Hello World");
/**
* 测试消息抵达队列的确认回调
* CorrelationData,设置唯一id,消息失败我们好确认
*
* 在发送消息到rabbitmq之外,还可以保存到mysql中,每一个唯一id对应子消息,如果rabbitmq收到消息就在数据库中给个提示,
* 同时就会知道哪些消息没有收到,我们可以定时扫描一下数据库,把没有送达的消息再重新投递一次
*/
rabbitTemplate.convertAndSend("hello-java-exchange","hello22.java","Hello World",new CorrelationData(UUID.randomUUID().toString()));
log.info("消息发送完成{}","hello-java-exchange","Hello World");
}
}
package com.atlinxi.gulimall.order.service.impl;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.atlinxi.common.utils.PageUtils;
import com.atlinxi.common.utils.Query;
import com.atlinxi.gulimall.order.dao.OrderItemDao;
import com.atlinxi.gulimall.order.entity.OrderItemEntity;
import com.atlinxi.gulimall.order.service.OrderItemService;
@Service("orderItemService")
@RabbitListener(queues = {"hello-java-queue"})
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<OrderItemEntity> page = this.page(
new Query<OrderItemEntity>().getPage(params),
new QueryWrapper<OrderItemEntity>()
);
return new PageUtils(page);
}
/**
* queues,声明需要监听的所有队列
*
* 消息类型是 class org.springframework.amqp.core.Message
*
* 接收消息的参数可以写以下类型
* 1. Message message,原生消息详细信息。消息头和消息体,需要进行json转换
* 2. 第二个参数 T<发送的消息的类型>,就写java的实体类就行
* 发送的消息是哪个实体类,接收消息就写哪个实体类即可
* spring帮我们进行json的转换
* 3. Channel channel:当前传输数据的通道
*
*
*
* Queue:可以很多人都来监听。只要收到消息,队列就会删除消息,而且只能有一个收到此消息
*
* 场景:
* 1)订单服务启动多个,同一个消息只能有一个客户端收到
* 2)只有一个消息完全处理完(该监听函数业务逻辑全部运行完成),
* 才可以接收到下一个消息
*
*/
// @RabbitListener(queues = {"hello-java-queue"})
// @RabbitHandler
public void receiveMessage(/**Object message*/Message message, Channel channel) {
// public void receiveMessage(JavaEntity javaEntity){
// 消息的内容
byte[] body = message.getBody();
// 消息头属性信息
MessageProperties messageProperties = message.getMessageProperties();
// 当前channel内按顺序自增的
long deliveryTag = messageProperties.getDeliveryTag();
// 确认消息,非批量模式
try {
// 确认消息
channel.basicAck(deliveryTag,false);
// 两个都是拒收消息,上面的可以批量,下面的不能批量
// channel.basicNack();
// channel.basicReject();
/**
* param1,唯一标识
* param2,是否批量拒收,之前的消息都被拒收
* param3,消息是否重新入队
*/
channel.basicNack(deliveryTag,false,true);
}catch (IOException e){
// 网络中断
}
System.out.println("接收到消息,内容:" + message + "==>类型:" + message.getClass());
}
}
todo应该和下面那个结合着看,有时间再说吧
在分布式系统中,有非常多的微服务连接消息队列服务器,监听消息,但是可能会由于网络抖动原因,网络闪断,包括服务器的宕机,mq的宕机等各种问题,都可能导致消息的丢失。
以上三个步骤都有可能会失败,2可能是由于路由的错误,或者在将消息存入队列的时候有客户端对队列进行了删除操作等。
每一步骤都对应着不同的回调函数或机制。
下面的五种模式感觉和上面的交换机的模式可能是一个意思,有时间再说吧
直接模式:我们需要将消息发给唯一一个节点时使用这种模式,这是最简单的一种形式。一个生产者对应一个消费者
。
工作模式:在直接模式的基础上,一个生产者,多个消费者
。
分配有两种,一种是平均分配(默认),例如生产者生产消息1234,消费者1消费消息13,消费者2消费24,
另一种是按劳分配,见下面代码的注释
package com.itheima.rabbit.consumer.listener;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
//设置Rabbit的监听器,这里监听器监听的是itcast队列
@RabbitListener(queues = "itheima")
public class MyListener {
//消息处理注解,会获取消息,把消息内容注入到方法的参数中
@RabbitHandler
public void handlerMessage(String message) {
System.out.println("itheima消费者2消息是:" + message);
}
}
@Test
public void test() {
//第一个参数设置路由键,消息发送到对应名字的队列中
//第二个参数设置发送的消息内容
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("abc", "直接模式消息发送" + i);
}
//以下方式成为工作模式
//在直接模式基础上,增加队列监听的消费者,
// 默认情况下,消息消费是轮询的方式(平均分配的工作模式)
// 另一种方式,可以根据消费者消费消息的速度来分配消息数量,能力强的处理更多的消息,能力弱的处理更少的消息
//成为能者多劳模式,参考课后资料
}
绑定
,一个Exchange
可以绑定多个 Queue,一个Queue
可以同多个Exchange进行绑定(这个操作可以在rabbitmq界面操作)。交换机不能持久化消息,队列可以
工作模式是把消息平均分配给每个消费者,分列模式是把每一个消息发送给每一个消费者。
//1. 把队列和交换机进行绑定
//2. 生产者把消息发给交换机
//3. 交换机根据绑定的信息,把消息转发给指定的队列,交换机本身保存消息
@Test
public void test2() {
//第一个参数是交换机名字,消息发给哪个交换机
//第二个参数是路由键,本例不做使用,填写""空串
//第三个参数是消息
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("chuanzhi", "", "测试分列模式消息");
}
}
在分列模式的前提下,指定路由键(其实就是再增加一次筛选)
//路由模式
//Routing
//需要指定路由键,无论是路由模式,分列模式,主题模式进行切换,或者改变使用情况的时候,都不需要修改消费者
// 修改以上那些完全可以用rabbitmq的官方界面来完成
@Test
public void test3() {
//第一个参数是交换机名字,消息发给哪个交换机
//路由模式需要指定交换机的类型为direct,需要指定路由键绑定队列
//第二个参数是路由键
//第三个参数是消息
//for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("chuanzhi", "abc", "测试路由模式消息abc");
//}
}
路由键,*代表一个词,#代表多个词,以点为分隔,判断几个词
//主题模式
//Topics
//对路由键使用了*(代表不多不少一个词)或者#(代表零个一个或多个)
@Test
public void test5() {
//第一个参数是交换机名字,消息发给哪个交换机
//主题模式需要指定交换机的类型为topic,需要指定路由键绑定队列
//第二个参数是路由键
//第三个参数是消息
rabbitTemplate.convertAndSend("chuanzhi", "itcast", "主题模式消息123");
}
如果消息发送失败如何处理
重发 重发3次 6次
消息重发失败怎么处理
RabbitMQ 确认机制
默认
是自动应答, 手动应答 需要消费者提供应答,如果正确应答,消息消费成功 没有正确应答,消息失败
死信队列
就是把所有发送失败的消息都发送到一个队列中,这个队列就被称为死信队列
rabbitmq的管理界面在创建队列
的时候,可以添加参数为x-dead-letter-exchange
来绑定
死信交换机,并设置x-message-ttl
超时时间,如果消息在规定时间
内消息没有被成功消费
,则发送到死信交换机中,进而进入到死信队列
设置死信队列要根据具体的业务场景去应用,一般应用在当正常业务处理时出现异常
时,将消息拒绝则会进入到死信队列中,有助于统计异常数据并做后续处理
延迟队列
存储的是延迟消息,延迟消息指的是,当消息被发发布出去之后,并不立即投递给消费者,而是在指定时间之后投递。如:在订单系统中,订单有30秒的付款时间,在订单超时之后在投递给消费者处理超时订单。
rabbitMq没有直接支持延迟队列,可以通过死信队列实现
。
使用备用方案 重要的业务场景
设置备用方案
例如两个服务,a服务调用b服务,因为业务场景的原因,直接调用耦合性
太高,所以我们通过MQ
来处理,当mq的服务崩了,我们启用备用服务
,a服务直接调用b服务,这也会增加开发成本。
使用MySQL(数据库)存放消息信息
在发送消息的同时,存放消息的关键信息到mysql中,消息是否消费可以通过状态码来判断,当消息发送失败时,可以设置定时任务,来执行业务逻辑。
消息确认(生产者推送消息成功,消费者接收消息成功)。
package com.anhangxunxi.consumer.config;
import com.anhangxunxi.common.entity.RabbitMQPayment;
import com.anhangxunxi.common.util.ObjectUtil;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;
import org.springframework.context.annotation.Configuration;
/**
* dzh
* rabbitmq的配置类
* 1.交换机和队列及它俩的绑定
* 2. 生产者的消息确认机制
* 消息的回调,即消息确认(生产者推送消息成功,消费者接收消息成功(这个在ConsumerMonitor中))
* 如果出现找不到交换机或者队列的情况就会触发消息的回调,可以把结果打印在日志中
*/
@Configuration
@EnableRabbit
// dzh,开启注解扫描
@ComponentScan
public class RabbitMQConfig {
// dzh,这三个都是起名
public final static String PAYMENT_QUEUE_NAME = "payment_queue";
public final static String PAYMENT_EXCHANGE_NAME = "payment_exchange";
public final static String PAYMENT_BINDING_NAME = "payment_binding";
@Value("${info.rabbitmq.host}")
private String host;
@Value("${info.rabbitmq.port}")
private Integer port;
@Value("${info.rabbitmq.username}")
private String userName;
@Value("${info.rabbitmq.password}")
private String password;
/**
* 创建支付功能队列
* @return
*/
@Bean
public Queue paymentQueue() {
// dzh,durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// dzh,exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// dzh,autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除
//dzh,一般设置一下队列的持久化就好,其余两个就是默认false
return new Queue(PAYMENT_QUEUE_NAME);
}
/**
* 创建topic交换器
* @return
*/
@Bean
public TopicExchange paymentExchange() {
return new TopicExchange(PAYMENT_EXCHANGE_NAME);
}
/**
* dzh,绑定 将队列和交换机绑定, 并设置用于匹配键PAYMENT_BINDING_NAME
* @return
*/
@Bean
public Binding binding() {
return BindingBuilder.bind(paymentQueue()).to(paymentExchange()).with(PAYMENT_BINDING_NAME);
}
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port);
connectionFactory.setUsername(userName);
connectionFactory.setPassword(password);
// dzh,确认消息已发送到交换机
// 消息发送确认,到达exchange返回
connectionFactory.setPublisherConfirms(true);
// 启动消息失败返回,没有到达队列
// dzh,确认消息已发送到队列(Queue)
connectionFactory.setPublisherReturns(true);
return connectionFactory;
}
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
// 手动确认消息
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
/**
* 配置相关的消息确认回调函数
* @param connectionFactory
* @return
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
// 开启mandatory returncallback才会生效
rabbitTemplate.setMandatory(true);
// 消息没有正确到达队列时触发回调,如果正确到达队列不执行。
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
byte[] bytes = message.getBody();
RabbitMQPayment rabbitMQPayment = (RabbitMQPayment) ObjectUtil.getObjectFromBytes(bytes);
System.out.println("ackMQSender 发送消息被退回" + rabbitMQPayment.toString().toString());
});
// 只确认消息是否正确到达 Exchange 中
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
System.out.println("ackMQSender 消息发送失败!" + correlationData.getReturnedMessage().toString());
}
else {
byte[] bytes = correlationData.getReturnedMessage().getBody();
RabbitMQPayment rabbitMQPayment = (RabbitMQPayment) ObjectUtil.getObjectFromBytes(bytes);
System.out.println("ackMQSender 消息发送成功 " + rabbitMQPayment.toString());
}
});
return rabbitTemplate;
}
}
package com.aihangxunxi.consumer.monitor;
import com.aihangxunxi.common.entity.RabbitMQPayment;
import com.aihangxunxi.common.util.ObjectUtil;
import com.aihangxunxi.consumer.config.RabbitMQConfig;
import com.aihangxunxi.consumer.entity.PaymentMqErrorRecord;
import com.aihangxunxi.consumer.repository.PaymentMqErrorRecordMapper;
import com.aihangxunxi.consumer.service.PaymentedBusiness;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
import java.time.LocalDateTime;
/**
* dzh,消费者接收到消息的消息确认机制
*
* 和生产者的消息确认机制不同,因为消息接收本来就是在监听消息,符合条件的消息就会消费下来。
所以,消息接收的确认机制主要存在三种模式:
①自动确认, 这也是默认的消息确认情况。 AcknowledgeMode.NONE
RabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递。
所以这种情况如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。
一般这种情况我们都是使用try catch捕捉异常后,打印日志用于追踪数据,这样找出对应数据再做后续处理。
② 根据情况确认, 这个不做介绍
③ 手动确认 , 这个比较关键,也是我们配置接收消息确认机制时,多数选择的模式。
消费者收到消息后,手动调用basic.ack/basic.nack/basic.reject后,RabbitMQ收到这些消息后,才认为本次投递成功。
basic.ack用于肯定确认
basic.nack用于否定确认(注意:这是AMQP 0-9-1的RabbitMQ扩展)
basic.reject用于否定确认,但与basic.nack相比有一个限制:一次只能拒绝单条消息
消费者端以上的3个方法都表示消息已经被正确投递,但是basic.ack表示消息已经被正确处理。
而basic.nack,basic.reject表示没有被正确处理:
着重讲下reject,因为有时候一些场景是需要重新入列的。
channel.basicReject(deliveryTag, true); 拒绝消费当前消息,如果第二参数传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行。 下次不想再消费这条消息了。
使用拒绝后重新入列这个确认模式要谨慎,因为一般都是出现异常的时候,catch异常再拒绝入列,选择是否重入列。
但是如果使用不当会导致一些每次都被你重入列的消息一直消费-入列-消费-入列这样循环,会导致消息积压。
顺便也简单讲讲 nack,这个也是相当于设置不消费某条消息。
channel.basicNack(deliveryTag, false, true);
第一个参数依然是当前消息到的数据的唯一id;
第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认。
第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。
同样使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压。
*/
@Component
public class ConsumerMonitor {
@Autowired
private PaymentedBusiness paymentedBusiness;
@Resource
private PaymentMqErrorRecordMapper paymentMqErrorRecordMapper;
@RabbitListener(queues = RabbitMQConfig.PAYMENT_QUEUE_NAME)
public void consumePaymentMessage(byte[] msg, Channel channel, Message message) {
RabbitMQPayment rabbitMQPayment = null;
try {
// 将字节码转化为对象
rabbitMQPayment = (RabbitMQPayment) ObjectUtil.getObjectFromBytes(msg);
System.out.println("consume message:" + rabbitMQPayment.toString());
String type = rabbitMQPayment.getType();
if ("hotelReservation".equals(type)) {
paymentedBusiness.hotelReservationPayment(rabbitMQPayment);
}
else if ("eCardReservation".equals(type)) {
paymentedBusiness.cardBuyPayment(rabbitMQPayment);
}
else {
paymentedBusiness.otherPayment(rabbitMQPayment);
}
// 告诉服务器收到这条消息 已经被我消费了 可以在队列删掉 这样以后就不会再发了 否则消息服务器以为这条消息没处理掉 重启应用后还会在发
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
System.out.println("ACK_QUEUE_B 接受信息成功");
}
catch (Exception e) {
e.printStackTrace();
try {
if (rabbitMQPayment != null) {
PaymentMqErrorRecord paymentMqErrorRecord = new PaymentMqErrorRecord();
BeanUtils.copyProperties(rabbitMQPayment, paymentMqErrorRecord);
paymentMqErrorRecord.setConsumerType(rabbitMQPayment.getType());
paymentMqErrorRecord.setErrorMessage("消息接收后处理业务异常");
paymentMqErrorRecord.setCreateAt(LocalDateTime.now());
paymentMqErrorRecordMapper.insert(paymentMqErrorRecord);
// 消息重新回到队列
// channel.basicNack(message.getMessageProperties().getDeliveryTag(),
// false, true);
// 告诉服务器收到这条消息 已经被我消费了 可以在队列删掉 这样以后就不会再发了 否则消息服务器以为这条消息没处理掉 重启应用后还会在发
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
catch (IOException ex) {
ex.printStackTrace();
}
System.out.println("ACK_QUEUE_B 接受信息异常");
}
}
}
部分知识引用自:
https://blog.csdn.net/qq_35387940/article/details/100514134
李老师软音软语对她们说:“不然我有诺贝尔文学奖全集?”这一幕晞晞正好。诺贝尔也正好。扮演好一个期待女儿的爱的父亲角色。一个偶尔泄露出灵魂的教书匠,一个流浪到人生的中年还等不到理解的语文老师角色。一整面墙的原典标榜他的学问,一面课本标榜孤独,一面小说等于灵魂。没有一定要上过他的课。没有一定要谁家的女儿。
房思琪的初恋乐园
林奕含