1、异步处理
消息发送的时间取决于业务执行的最长的时间
2、应用解耦
原本是需要订单系统直接调用库存系统
只需要将请求发送给消息队列,其他的就不需要去处理了,节省了处理业务逻辑的时间
3、流量消峰
某一时刻如果请求特别的大,那就先把它放入消息队列,从而达到流量消峰的作用
大多应用中,可通过消息服务中间件来提升系统异步通信,扩展解耦能力
消息服务中两个重要概念:
消息代理(message broker) 和 目的地(destination)
当消息发送者发送消息后,将由消息代理接管,消息代理保证消息传递到指定目的地
消息队列主要有两种形式的目的地
队列(Queue):点对点消息通信(point - to - point)
主题(topic):发布(publish)/订阅(subscribe)消息通信
点对点式:
消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移出队列
消息只有唯一的发送者和接受者,单并不是说只能有一个接收者
发布订阅式:
发送者(发布者)发到消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息
JMS(Java Message Service) Java消息服务:
基于JVM消息代理的规范,ActiveMQ、HornetMQ是JMS的实现
AMQP(Advanced Message Queuing Protocol)
高级消息队列协议,也是一个消息代理的规范,兼容JMS
RabbitMQ是AMQP的实现
Spring 支持
spring - jms提供了对JMS的支持
spring - rabbit提供了对AMQP的支持
需要ConnectionFactory的实现来连接消息代理
提供 JmsTemplate、RabbitTemplate 来发送消息
@JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息
@EnableJms、@EnableRabbit开启支持
Spring Boot 自动配置
JmsAutoConfiguration
RabbitAutoConfiguration
市面上的MQ产品
ActiveMQ、RabbitMQ、RocketMQ,kafka
RabbitMQ是一由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现
Message
消息,消息是不具名的,它是由消息头和消息体组成,消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括 routing - key (路由键),priority(相对于其他消息的优先权),**delivery - mode(指出该消息可能需要持久性存储)**等
Publisher
消息的生产者,也是一个像交换器发布消息的客户端应用程序
Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列
Exchange有4种类型:direct(默认)、fanout、topic,和heades,不同类型的 Exchange 转发消息的策略有所区别
Queue
消息队列,用来保存消息直到发送给消费者,他是消息的容器,也是消息的重点,一个消息可以投入一个或多个队列,消息一直在队列里面,等待消费者连接到这个队列将其取走
Binding
绑定,用于消息队列和交换器之间的关联,一个绑定就是基于路由键将交换器和消息队列连接起来的规则,所有可以将交换器理解成一个由绑定构成的路由表
Connection
网路连接,比如一个TCP连接
Channel
信道,多路复用连接中的一个独立的双向数据流通道,信道是建立在真实的TCP连接的内的虚拟连接,AMQP 命令都是通过信息到发送出去的,不管是发布消息,订阅队列还是接收消息,这些动作都是通过队列完成,因为对应操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接
Consumer
消息的消费者,表示一个消息队列中取得消息的客户端应用程序
Virtual Host
虚拟主机,表示交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个Virtual host本质上就是一个 mini 版的RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。Virtual host 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ默认的vhost是/。
Broker
表示消息队列服务器实体
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
# 自动启动
docker update rabbitmq --restart=always
端口说明
4369, 25672 (Erlang发现&集群端口)
5672, 5671 (AMQP端口)
15672 (web管理后台端口)
61613, 61614 (STOMP协议端口)
1883, 8883 (MQTT协议端口)
1、 AMQP 中的消息路由
AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP中增加了 Exchange 和 Binding 的角色 生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送给那个队列
2、Exchange 类型
Exchange 分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、tanout、topic、headers header匹配AMQP消息的 header 而不是路由键,headers 交换器和 direct 交换器完全一致,但性能差能多,目前几乎用不到了,所以直接看另外三种类型
1、引入 Spring-boot-starter-amqp:RabbitAutoConfig就会自动生效
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
2、application.properties配置
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
#开启发送端确认
spring.rabbitmq.publisher-confirms=true
#开启发送端消息抵达队列确认
spring.rabbitmq.publisher-returns=true
# 只要抵达队列,以异步发送优先回调这个returnConfirm
spring.rabbitmq.template.mandatory=true
#手动ack消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual
3、注解使用
监听消息:使用@RabbitListener,必须有@EnableRabbit
@RabbitListener:类+方法上(监听哪些队列即可)
@RabbitHandler:标在方法上(重载区分不同的消息
4、测试RabbitMQ
AmqpAdmin:管理组件,利用AmqpAdmin创建Exchange,Queue,Binding
RabbitTemplate:消息发送处理组件
PS:测试这儿就不粘贴了,直接用配置类处理更有条理些
官方link
保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制
publisher confirmCallback 确认模式
publisher returnCallback 未投递到 queue 退回
consumer ack 机制
1、可靠抵达 - ConfirmCallback
在创建 connectionFactory 的时候设置 PublisherConfirms(true) 选项,开启 confirmcallback。
spring.rabbitmq.publisher-confirms=true
CorrelationData 用来表示当前消息唯一性
消息只要被 broker 接收到就会执行 confirmCallback,如果 cluster 模式,需要所有 broker 接收到才会调用 confirmCallback被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里,所以需要用到接下来的 returnCallback
2、可靠抵达 - ReturnCallback
spring.rabbitmq.publisher-retuns=true
spring.rabbitmq.template.mandatory=true
confirm 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有些模式业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到 return 退回模式
这样如果未能投递到目标 queue 里将调用 returnCallback,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据
3、可靠抵达 - Ack 消息确认机制
消费者获取到消息,成功处理,可以回复Ack给Broker
basic.ack 用于肯定确认:broker 将移除此消息
basic.nack 用于否定确认:可以指定 beoker 是否丢弃此消息,可以批量
basic.reject用于否定确认,同上,但不能批量
默认,消息被消费者收到,就会从broker的queue中移除
消费者收到消息,默认自动ack,但是如果无法确定此消息是否被处理完成,或者成功处理,我们可以开启手动ack模式
消息处理成功,ack(),接受下一条消息,此消息broker就会移除
消息处理失败,nack()/reject() 重新发送给其他人进行处理,或者容错处理后ack
消息一直没有调用ack/nack方法,brocker认为此消息正在被处理,不会投递给别人,此时客户端断开,消息不会被broker移除,会投递给别人
4、配置 RabbitTemplate设置回调
com/atguigu/gulimall/order/config/MyRabbitConfig.java
@Slf4j
@Configuration
public class MyRabbitConfig {
// @Autowired
RabbitTemplate rabbitTemplate;
@Primary
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
this.rabbitTemplate = rabbitTemplate;
rabbitTemplate.setMessageConverter(messageConverter());
initRabbitTemplate();
return rabbitTemplate;
}
/**
* 使用json序列化机制,进行消息转换
*
* @return
*/
@Bean
public MessageConverter messageConverter() {
Jackson2JsonMessageConverter jsonMessageConverter = new Jackson2JsonMessageConverter();
return jsonMessageConverter;
}
/**
* 1.设置确认回调: ConfirmCallback
* 先在配置文件中开启 publisher-confirms: true
*
* @PostConstruct: MyRabbitConfig对象创建完成以后 执行这个方法
*
* 2.消息抵达队列的确认回调
* 开启发送端消息抵达队列确认
* publisher-returns: true
* 只要抵达队列,以异步优先回调我们这个 returnconfirm
* template:
* mandatory: true
* 3.消费端确认(保证每一个消息被正确消费才可以broker删除消息)
* 1.默认是自动确认的 只要消息接收到 服务端就会移除这个消息
*
* 如何签收:
* 签收: channel.basicAck(deliveryTag, false);
* 拒签: channel.basicNack(deliveryTag, false,true);
* 配置文件中一定要加上这个配置
* listener:
* simple:
* acknowledge-mode: manual
*/
// @PostConstruct
public void initRabbitTemplate() {
/**
* 设置确认回调
* correlationData: 消息的唯一id
* ack: 消息是否成功收到
* cause:失败的原因
*/
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> log.info("\n收到消息: " + correlationData + "\tack: " + ack + "\tcause: " + cause));
/**
* 设置消息抵达队列回调:可以很明确的知道那些消息失败了
*
* message: 投递失败的消息详细信息
* replyCode: 回复的状态码
* replyText: 回复的文本内容
* exchange: 当时这个发送给那个交换机
* routerKey: 当时这个消息用那个路由键
*/
//这个失败才会回调
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routerKey) -> log.error("Fail Message [" + message + "]" + "\treplyCode: " + replyCode + "\treplyText:" + replyText + "\texchange:" + exchange + "\trouterKey:" + routerKey));
}
}
场景:
比如未付款的订单,超过一定时间后,系统自动取消订单并释放占有物品
解决:rabbitmq的消息TTL和死信Exchange结合
消息的TTL(Time To Live)
消息的TTL 就是消息的存活时间,
RabbitMQ可以对队列还有消息分别设置TTL
对队列设置就是没有消费者连着的保持时间,也可以对每一个消息单独的设置,超过了这个时间我们可以认为这个消息他死了,称之为死信
如果队列设置了,消息也设置了,那么会取小,所以一个消息如果被路由到不同的队列中,这个消息死亡时间有可能不一样的(不同队列设置),这里讲的是单个TTL 因为他是实现延时任务的关键,可以通过设置消息的 expiration 字段或者 x-message-ttl 来设置时间两者是一样的效果
Dead Letter Exchange(DLX)
一个消息在满足如下条件下,会进死信路由,记住这里是路由不是队列,一个路由可以对应很多队列,(什么是死信)
一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不被再次放在队列里,被其他消费者使用。(basic.reject/ basic.nack)
requeue= false上面的消息的TTL到了,消息过期了。
队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
Dead Letter Exchange其实就是一种普通的exchange, 和创建其他exchange没有两样。只是在某一个设置 Dead Letter Exchange的队列中有消息过期了自动触发消息的转发,发送到Dead Letter Exchange中去。
我们既可以控制消息在一段时间后变成死信, 又可以控制变成死信的消息被路由到某一个指定的交换机, 结合C者,其实就可以实现一个延时队列
流程图地址link
com/atguigu/gulimall/order/config/MyMQConfig.java
@Configuration
public class MyMQConfig {
// @RabbitListener(queues = "order.release.order.queue") //有监听才会创建该配置中的Binding, Queue, Exchange
// public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
//
// System.out.println("收到过期的订单信息,准备关闭订单" + entity.getOrderSn());
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
// }
/**
* 容器中的 Binding, Queue, Exchange 都会自动创建(RabbitMQ没有的情况
* @return
*/
@Bean
public Queue orderDelayQUeue() {
// public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, @Nullable Map arguments)
// 特殊参数(死信队列
Map<String,Object> map = new HashMap<>();
map.put("x-dead-letter-exchange", "order-event-exchange"); // 死信交换机
map.put("x-dead-letter-routing-key","order.release.order"); // 死信路由键
// 消息过期时间
map.put("x-message-ttl",60000);
Queue queue = new Queue("order.delay.queue", true, false, false,map);
return queue;
}
@Bean
public Queue orderReleaseOrderQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
@Bean
public Exchange orderEventExchange() {
// public TopicExchange(
// String name, 交换机的名字
// boolean durable, 是否持久
// boolean autoDelete, 是否自动删除
// Map arguments)
TopicExchange exchange = new TopicExchange("order-event-exchange", true, false);
return exchange;
}
@Bean
public Binding ordrCreateOrderBingding() {
// public Binding(String destination, 目的地
// DestinationType destinationType, 目的地类型
// String exchange,交换机 //Topic交换机支持绑定模糊的路由键
// String routingKey,//路由键
//@Nullable Map arguments
Binding binding = new Binding("order.delay.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.create.order", null);
return binding;
}
@Bean
public Binding orderReleaseOrderBinding() {
Binding binding = new Binding("order.release.order.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.release.order", null);
return binding;
}
/**
* 订单释放直接和库存释放绑定
* @return
*/
@Bean
public Binding orderReleaseOther() { //给库存释放队列
Binding binding = new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.release.other", null);
return binding;
}
@Bean
public Queue orderSeckillOrderQueue() {
Queue queue = new Queue("order.seckill.order.queue", true, false, false);
return queue;
}
@Bean
public Binding ordrSeckillOrderBingding() {
// public Binding(String destination, 目的地
// DestinationType destinationType, 目的地类型
// String exchange,交换机
// String routingKey,//路由键
//@Nullable Map arguments
Binding binding = new Binding("order.seckill.order.queue", Binding.DestinationType.QUEUE, "order-event-exchange", "order.seckill.order", null);
return binding;
}
}
订单创建成功会基于路由键借助交换机切换给order.delay.queue发消息
com/atguigu/gulimall/order/service/impl/OrderServiceImpl.java
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
这里就仅仅给出订单释放消息的监听
com/atguigu/gulimall/order/listener/OrderCloseListener.java
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息,准备关闭订单" + entity.getOrderSn());
try {
orderService.closeOrder(entity); //这个关闭订单方法里还会通知提前解锁库存 rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true); //放回消息队列
}
}
}
去RabbitMQ控制台看看效果,link 账号密码默认都是guest
1、消息丢失
消息发送出去,由于网络问题没有抵达服务器
做好容错方法(try-Catch) ,发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式
做好日志记录,每个消息状态是否都被服务器收到都应该记录
做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
消息抵达Broker, Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。
publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机
一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队
2、消息重复
消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
消息消费失败,由于重试机制,自动又将消息发送出去
成功消费,ack时宕机,消息由unack变为ready, Broker又重新发送
消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
使用防重表(redis/mysq|) ,发送消息每一 个都有业务的唯一 标识,处理过就不用处理
rabbitMQ的每一个消息都有redelivered字段, 可以获取是否是被重新投递过来的,而不是第一次投递过来的
3、消息积压
消费者积压
消费者消费能力不足积压
发送者发送流量太大
上线更多消费者,进行正常消费
上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理