RabbitMQ 高级特性
1.1 消息的可靠投递
在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。
- confirm 确认模式
📌
确认队列中的消息是否被消费者消费
- return 退回模式
💡
交换机中的消息是否到达队列里面
到 returnCallBack
不到 走returnCallBack
rabbitmq 整个消息投递的路径为:
producer—>rabbitmq-broker—>exchange—>queue—>consumer
生产:消息从 producer 到 exchange 则会返回一个 confirmCallback()。都会执行,返回false就失败
内部:消息从 exchange–>queue 投递失败则会返回一个 returnCallback()。
我们将利用这两个 callback 控制消息的可靠性投递
确认:
使用springboot项目整合生产者
1.配置文件:
spring:
rabbitmq:
host: 192.168.159.34
username: root
password: root
virtual-host: /root
port: 5672
publisher-confirms: true #设置开启确认模式
server:
port: 8081
#消息的可靠性传递
2.配置类
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConfirmConfig {
public static final String QUEUE_CONFIRM_NAME="test_confirm_queue";
public static final String EXCHAGE_CONFIRM_NAME="test_confirm_exchange";
//定义一个交换机,定义一个queue
@Bean("test_confirm_queue")
public Queue getQueue(){
return QueueBuilder.durable(QUEUE_CONFIRM_NAME).build();
}
/**
* 定义一个交换机
*/
@Bean("test_confirm_exchange")
public Exchange getExchange(){
return ExchangeBuilder.directExchange(EXCHAGE_CONFIRM_NAME).durable(true).build();
}
/**
* 绑定一下交换机和queue
*/
@Bean
public Binding bindQueueAndExchange(@Qualifier("test_confirm_queue")Queue queue,@Qualifier("test_confirm_exchange")Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("confirm").noargs();
}
}
3.测试类:
import org.junit.Test;
import org.junit.runner.RunWith;
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 org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import yyl.ProducerApplication;
import yyl.config.ConfirmConfig;
import yyl.config.RabbitMqConfig;
import javax.annotation.Resource;
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProducerApplication.class)
public class ProcedureTest {
@Resource
private RabbitTemplate rabbitTemplate;
//确认模式的测试
//1.开启确认模式 开启publisher-confirms="true"
//2. 定义回调 在rabbitTemplate中设置信息的确认
@Test
public void testConfirm(){
/**
* @param correlationData 相关配置信息
* @param ack exchange交换机 是否成功收到了消息。true 成功,false代表失败
* @param cause 失败原因
*/
//匿名的内部类
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
System.out.println("confirm方法执行了。。。。。。");
if(ack){
System.out.println("接收成功");
}else{
System.out.println("接收失败,失败的原因是:"+s);
}
}
});
//发送消息
rabbitTemplate.convertAndSend(ConfirmConfig.EXCHAGE_CONFIRM_NAME,"confirm","confirm message");
}
}
4.总结:
设置publisher-confirms="true" 开启 确认模式。
使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。
回退:
1.配置文件:
spring:
rabbitmq:
host: 192.168.159.34
username: root
password: root
virtual-host: /root
port: 5672
# publisher-confirms: true #设置开启确认模式
publisher-returns: true #设置开启回退模式
server:
port: 8081
#消息的可靠性传递
2.配置类
跟确认中的配置文件保持一致即可
3.测试类
/**
* return的测试
* 1.开启 回退模式
* 2.在rabbitTemplate中设置returnCallBack
* 3.设置exchange处理消息的模式
* 1. 如果消息没有路由到queue 则丢弃消息
* 2. 如果没有路由到queue 则将消息返回给发送方 returnCallBack
*/
@Test
public void testReturn(){
//设置交换机处理失败消息的模式
rabbitTemplate.setMandatory(true);
//匿名的内部类
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("return 执行了....");
// System.out.println(message);
// System.out.println(replyCode);
// System.out.println(replyText);
// System.out.println(exchange);
// System.out.println(routingKey);
}
});
//发送消息
rabbitTemplate.convertAndSend(ConfirmConfig.EXCHAGE_CONFIRM_NAME,"confirm","confirm message");
}
总结:
设置publisher-returns="true"开启 退回模式。
使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage。
事务机制
在RabbitMQ中也提供了事务机制,但是性能较差。
使用channel下列方法,完成事务控制:
txSelect() :用于将当前channel设置成transaction模式
txCommit():用于提交事务
txRollback():用于回滚事务
有兴趣自己操作
1.2 Consumer Ack
ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
broker发送消息给消费端的一种可靠性保证
有三种确认方式:
自动确认:acknowledge="none" 。不管处理成功与否,业务处理异常也不管
(当消费者意担接收到消息之后,消费者就会给broker一个回执,证明已经接收到消息 了,不管消息到底是否成功)
手动确认:acknowledge="manual" 。可以解决业务异常的情况
(收到消息之后不会立马确认收到消息,当业务处理没有问题的时候手动的调用代码的方 式来进行处理,如果业务失败了,就可以进行额外的操作)
根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,不作讲解)
其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。
自动确认:
消费端:
1.配置文件:
spring:
rabbitmq:
host: 192.168.159.34
username: root
password: root
virtual-host: /root
port: 5672
2.监听类:
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class AckConsumer implements MessageListener {
@RabbitListener(queues = "test_confirm_queue")
@Override
public void onMessage(Message message) {
System.out.println(new String(message.getBody()));
}
}
总结:
什么都不需要设置使用的就是自动确认模式
手动确认:
1.配置文件
spring:
rabbitmq:
host: 192.168.159.34
username: root
password: root
virtual-host: /root
port: 5672
listener:
simple:
acknowledge-mode: manual #设置手动确认
2.监听类
手动签收 使用的channel里的成功或者是失败的方法
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
* Consumer ACK机制:
* 1. 设置手动签收。acknowledge="manual"
* 2. 让监听器类实现ChannelAwareMessageListener接口
* 3. 如果消息成功处理,则调用channel的 basicAck()签收
* 4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
*/
@Component
public class AckConsumer implements ChannelAwareMessageListener {
@Override
@RabbitListener(queues = "test_confirm_queue")
public void onMessage(Message message, Channel channel) throws Exception {
Thread.sleep(2000);
//1.接收消息
System.out.println(new String(message.getBody()));
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//2.处理业务luoji
System.out.println("开始处理业务逻辑");
int i=3/0;
//3.成功就直接签收
/**
* basicAck(long deliveryTag, boolean multiple)
* 参数1 当前收到的消息的tag标签的内容
* 参数2 是否允许多条消息被同时签收
*/
//得到第一个参数
channel.basicAck(deliveryTag, true);
}catch(Exception e){
//发生异常的时候就拒接签收
/**
* basicNack(long deliveryTag, boolean multiple, boolean requeue)
* 参数3 是否重回队列 如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
*/
channel.basicNack(deliveryTag,true,true);
}
}
}
总结:
设置手动模式需要在配置文件里面添加 acknowlwdge-mode:manual
如果消费端没有异常就会走basicAck的方法,确认签收消息,如果有异常就会调用basicNack的方法拒接签收消息,让MQ重新发送消息
消息可靠性总结
持久化
- exchange要持久化
- queue要持久化
- message要持久化
(当broker重启的时候信息还在)
生产方确认Confirm
消费方确认Ack
Broker高可用
1.3 消费端限流
请求瞬间增多,每秒5000个请求
在配置 prefetch属性设置消费端一次拉取多少消息
消费端的确认模式一定为手动确认。acknowledge="manual"
需要配置一次拉取多少消息
1.配置文件:
spring:
rabbitmq:
host: 192.168.159.34
username: root
password: root
virtual-host: /root
port: 5672
listener:
simple:
acknowledge-mode: manual
prefetch: 1
监听类
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
* Consumer ACK机制:
*/
@Component
public class XianLiuConsumer implements ChannelAwareMessageListener {
@Override
@RabbitListener(queues = "test_confirm_queue")
public void onMessage(Message message, Channel channel) throws Exception {
//1.接收消息
System.out.println(new String(message.getBody()));
//没有手动的确认接收消息 所以就会显示有一条未被确认消息
}
}
如果没有设置限流,会直接全部拉取消息,但是并不消费
总结:
配置 prefetch属性设置消费端一次拉取多少消息
消费端的确认模式一定为手动确认。acknowledge="manual"
1.4 TTL
全称 Time To Live(存活时间/过期时间)。
当消息到达存活时间后,还没有被消费,会被自动清除。
RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。
可以在管理台新建队列、交换机,绑定
图形化操作:
添加队列
添加交换机
将交换机和对应的队列进行绑定
代码:
1.配置文件
spring:
rabbitmq:
host: 192.168.159.34 # ip
port: 5672
username: root
password: root
virtual-host: /root
2.配置类
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TtlRabbitMqConfig {
//1.交换机的名字
public static final String EXCHANGE_NAME="exchange_topic_ttl";
//2.队列名字
public static final String QUEUE_NAME_1="queue_topic_ttl";
//1.获取交换机
@Bean("exchange_name")
public Exchange getExcahnge(){
//return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).build();
}
//队列1
@Bean("queue_name")
public Queue getQueue(){
//设置队列的过期时间 5s
return QueueBuilder.durable(QUEUE_NAME_1).withArgument("x-message-ttl",30000).build();
}
//队列和交换机进行绑定
@Bean
public Binding bindExchangeAndQueue(@Qualifier("exchange_name") Exchange exchange,@Qualifier("queue_name") Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("ttl.*").noargs();
}
}
3.测试类
package proceduretest;
import org.springframework.test.context.ContextConfiguration;
import yyl.ProcedureApplication;
import yyl.config.RabbitMqConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import yyl.ttlconfig.TtlRabbitMqConfig;
import javax.annotation.Resource;
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes= ProcedureApplication.class)
public class ProcedureTest {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void testProcedure(){
rabbitTemplate.convertAndSend(TtlRabbitMqConfig.EXCHANGE_NAME,"ttl.hh","我就是测试一下设置的过期时间而已!!!");
}
}
可以设置队列的统一的过期时间也可以设置消息的单独的过期时间
package proceduretest;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.test.context.ContextConfiguration;
import yyl.ProcedureApplication;
import yyl.config.RabbitMqConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import yyl.ttlconfig.TtlRabbitMqConfig;
import javax.annotation.Resource;
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes= ProcedureApplication.class)
public class ProcedureTest {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void testProcedure(){
// 消息后处理对象,设置一些消息的参数信息
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//1.设置message的信息
message.getMessageProperties().setExpiration("5000");//消息的过期时间
//2.返回该消息
return message;
}
};
rabbitTemplate.convertAndSend(TtlRabbitMqConfig.EXCHANGE_NAME,"ttl.hh","我就是测试一下设置的过期时间而已!!!",messagePostProcessor);
}
}
总结:
设置队列过期时间使用参数:x-message-ttl,单位:ms(毫秒),会对整个队列消息统一过期。
设置消息过期时间使用参数:expiration。单位:ms(毫秒),当该消息在队列头部时(消费时),会单独判断这一消息是否过期。
如果两者都进行了设置,以时间短的为准。
消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉)
1.5 死信队列
死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机,因为其他MQ产品中没有交换机的概念),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。
比如消息队列的消息过期,如果绑定了死信交换器,那么该消息将发送给死信交换机
消息在什么情况下会成为死信?(面试会问)
1.队列消息长度到最大的限制
最大的长度设置为10当第11条消息进来的时候就会成为死信
2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false(不重新回到队列中)
设置消费者为手动签收的状态
3. 原队列存在消息过期设置,消息到达超时时间未被消费;
队列绑定交换机的方式是什么?
给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key
// 1. 交换机 :正常的交换机 死信交换机
// 2.队列 :正常的 死信
//3.绑定 正常ex - 正常的que
正常的que和死信交换机
死信ex-死信queue
代码:
配置文件
(略)不需要额外配置信息
配置类
package yyl.deadconfig;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DeadRabbitMqConfig {
//1.正常交换机的名字
public static final String NORMAL_EXCHANGE_NAME="normal_exchange";
//2.正常队列名字
public static final String NORMAL_QUEUE_NAME="normal_queue";
//1.死信交换机的名字
public static final String DEAD_EXCHANGE_NAME="dead_exchange";
//2.死信队列名字
public static final String DEAD_QUEUE_NAME="dead_queue";
//1.正常交换机
@Bean("normal_exchange")
public Exchange getNormalExcahnge(){
//return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
return ExchangeBuilder.topicExchange(NORMAL_EXCHANGE_NAME).build();
}
@Bean("dead_exchange")
public Exchange getDeadExcahnge(){
//return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
return ExchangeBuilder.topicExchange(DEAD_EXCHANGE_NAME).build();
}
//正常队列
@Bean("normal_queue")
public Queue getNormalQueue(){
//设置队列的过期时间 5s
Queue queue=QueueBuilder.durable(NORMAL_QUEUE_NAME).withArgument("x-message-ttl",30000)
//最大的长度 10 超过10 条就会成为死信消息
.withArgument("x-max-length",10)
//正常队列的死信消息 绑定对应的死信交换机
.withArgument("x-dead-letter-exchange",DEAD_EXCHANGE_NAME)
//发送信息时携带的routingkey
.withArgument("x-dead-letter-routing-key","dead.msg")
.build();
return queue;
//return QueueBuilder.durable(NORMAL_QUEUE_NAME).withArgument("x-message-ttl",30000).build();
}
//死信队列
@Bean("dead_queue")
public Queue getDeadQueue(){
//设置队列的过期时间 5s
return QueueBuilder.durable(DEAD_QUEUE_NAME).build();
}
//正常队列和交换机进行绑定
@Bean
public Binding bindNormalExchangeAndQueue(@Qualifier("normal_exchange") Exchange exchange,@Qualifier("normal_queue") Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("normal.*").noargs();
}
//死信队列和交换机
@Bean
public Binding bindDeadExchangeAndQueue(@Qualifier("dead_exchange") Exchange exchange,@Qualifier("dead_queue") Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("dead.*").noargs();
}
}
测试类:
/**
* 发送测试死信消息:
* 1. 过期时间
* 2. 长度限制
* 3. 消息拒收
*/
@Test
public void testDlx(){
//1. 测试过期时间,死信消息
//rabbitTemplate.convertAndSend(DeadRabbitMqConfig.NORMAL_EXCHANGE_NAME,"normal.msg","我是一条消息,我会死吗?");
//2. 测试长度限制后,消息死信
// for (int i = 0; i < 20; i++) {
// rabbitTemplate.convertAndSend(DeadRabbitMqConfig.NORMAL_EXCHANGE_NAME,"normal.msg","我是一条消息,我会死吗?");
// }
//3. 测试消息拒收
rabbitTemplate.convertAndSend(DeadRabbitMqConfig.NORMAL_EXCHANGE_NAME,"normal.msg","我是一条消息,我会死吗?");
}
拒收消息的消费者
package yyl.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
/**
* Consumer ACK机制:
* 1. 设置手动签收。acknowledge="manual"
* 2. 让监听器类实现ChannelAwareMessageListener接口
* 3. 如果消息成功处理,则调用channel的 basicAck()签收
* 4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
*/
@Component
public class AckConsumer implements ChannelAwareMessageListener {
@Override
@RabbitListener(queues = "normal_queue")
public void onMessage(Message message, Channel channel) throws Exception {
//Thread.sleep(2000);
//1.接收消息
System.out.println(new String(message.getBody()));
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//2.处理业务luoji
System.out.println("开始处理业务逻辑");
int i=3/0;
//3.成功就直接签收
/**
* basicAck(long deliveryTag, boolean multiple)
* 参数1 当前收到的消息的tag标签的内容
* 参数2 是否允许多条消息被同时签收
*/
//得到第一个参数
channel.basicAck(deliveryTag, true);
}catch(Exception e){
//发生异常的时候就拒接签收
/**
* basicNack(long deliveryTag, boolean multiple, boolean requeue)
* 参数3 是否重回队列 如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
*/
channel.basicNack(deliveryTag,true,false);
}
}
}
总结:
1. 死信交换机和死信队列和普通的没有区别
2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
3. 消息成为死信的三种情况:
1. 队列消息长度到达限制;
2. 消费者拒接消费消息,并且不重回队列;
3. 原队列存在消息过期设置,消息到达超时时间未被消费;
1.6 延迟队列
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
需求:
- 1. 下单后,30分钟未支付,取消订单,回滚库存。
- 2. 新用户注册成功7天后,发送短信问候。
实现方式:
1. 定时器
2. 死信队列
在RabbitMQ中并未提供延迟队列功能。但是可以使用:TTL+死信队列
组合实现延迟队列的效果。
代码
配置文件
略
配置类:
package yyl.delayconfig;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
延迟队列:
1. 定义正常交换机(normal_exchange)和队列(normal_queue) 订单
2. 定义死信交换机(dead_exchange)和队列(dead_queue)
3. 绑定,设置正常队列过期时间为30分钟
*/
@Configuration
public class DelayRabbitMqConfig {
//1.正常交换机的名字
public static final String NORMAL_EXCHANGE_NAME="normal_exchange";
//2.正常队列名字
public static final String NORMAL_QUEUE_NAME="normal_queue";
//1.死信交换机的名字
public static final String DEAD_EXCHANGE_NAME="dead_exchange";
//2.死信队列名字
public static final String DEAD_QUEUE_NAME="dead_queue";
//1.正常交换机
@Bean("normal_exchange")
public Exchange getNormalExcahnge(){
return ExchangeBuilder.topicExchange(NORMAL_EXCHANGE_NAME).durable(true).build();
}
@Bean("dead_exchange")
public Exchange getDeadExcahnge(){
return ExchangeBuilder.topicExchange(DEAD_EXCHANGE_NAME).durable(true).build();
}
//正常队列
@Bean("normal_queue")
public Queue getNormalQueue(){
//设置队列的过期时间 5s
Queue queue=QueueBuilder.durable(NORMAL_QUEUE_NAME)
.withArgument("x-message-ttl",10000)
//最大的长度 10 超过10 条就会成为死信消息
// .withArgument("x-max-length",10)
//正常队列的死信消息 绑定对应的死信交换机
.withArgument("x-dead-letter-exchange",DEAD_EXCHANGE_NAME)
//发送信息时携带的routingkey
.withArgument("x-dead-letter-routing-key","dlx.order.cancel")
.build();
return queue;
//return QueueBuilder.durable(NORMAL_QUEUE_NAME).withArgument("x-message-ttl",30000).build();
}
//死信队列
@Bean("dead_queue")
public Queue getDeadQueue(){
return QueueBuilder.durable(DEAD_QUEUE_NAME).build();
}
//正常队列和交换机进行绑定
@Bean
public Binding bindNormalExchangeAndQueue(@Qualifier("normal_exchange") Exchange exchange,@Qualifier("normal_queue") Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("order.*").noargs();
}
//死信队列和交换机
@Bean
public Binding bindDeadExchangeAndQueue(@Qualifier("dead_exchange") Exchange exchange,@Qualifier("dead_queue") Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("dlx.order.*").noargs();
}
}
测试类:(发布消息)
@Test
public void testDelay(){
//发送的是订单的消息
rabbitTemplate.convertAndSend(DelayRabbitMqConfig.NORMAL_EXCHANGE_NAME,"order.msg","订单信息:id=1,time="+new Date());
}
消费类:
package yyl.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
@Component
public class DelayListener implements ChannelAwareMessageListener {
@Override
@RabbitListener(queues = "dead_queue")
public void onMessage(Message message, Channel channel) throws Exception {
//Thread.sleep(2000);
//1.接收消息
System.out.println(new String(message.getBody()));
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//2.处理业务luoji
System.out.println("开始处理业务逻辑");
System.out.println("根据订单的id查询订单的状态");
System.out.println("判断订单是否已经支付成功");
System.out.println("如果取消订单就回滚库存");
//确认消息
channel.basicAck(deliveryTag, true);
}catch(Exception e){
//发生异常的时候就拒接签收
/**
* basicNack(long deliveryTag, boolean multiple, boolean requeue)
* 参数3 是否重回队列 如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
*/
channel.basicNack(deliveryTag,true,false);
}
}
}
总结:
1. 延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。
2. RabbitMQ没有提供延迟队列功能,但是可以使用 : TTL + DLX 来实现延迟队列效果。
注意:
消费者在监听队列的时候监听的是死信队列
1.7 rabbitmqctl管理和监控
# 查看队列
rabbitmqctl list_queues
# 查看exchanges
rabbitmqctl list_exchanges
# 查看用户
rabbitmqctl list_users
# 查看连接
rabbitmqctl list_connections
# 查看消费者信息
rabbitmqctl list_consumers
# 查看环境变量
rabbitmqctl environment
# 查看未被确认的队列
rabbitmqctl list_queues name messages_unacknowledged
# 查看单个队列的内存使用
rabbitmqctl list_queues name memory
# 查看准备就绪的队列
rabbitmqctl list_queues name messages_ready
2. RabbitMQ 应用问题
2.1. 消息可靠性保障
需求:100%确保消息发送成功
•消息补偿机制
2发送正常消息,3过会再发一条相同的消息
2发送的消息在Q1中被正常消费到写入DB,发送ack给Q2。回调检查服务监听到Q2的消息,将消息写入MDB
如果1成功2失败,因为3页发送了消息放入Q3。此时回调检查服务也监听到了Q3,要去比对MDB是否一致,如果一致则代表消费过。如果MDB中不存在,就代表2失败了,就走8让生产者重新发。
如果2个都发送失败了,有MDB的定时检查服务,比对业务数据库DB与消息数据库MDB,就能发现差异
2.2. 消息幂等性保障
幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果
数据库
面试题:
1.如何保证消息按顺序执行
出现顺序错乱的场景
(1)rabbitmq
①一个queue,有多个consumer去消费,这样就会造成顺序的错误,consumer从MQ里面读取数据是有序的,但是每个consumer的执行时间是不固定的,无法保证先读到消息的consumer一定先完成操作,这样就会出现消息并没有按照顺序执行,造成数据顺序错误。
②一个queue对应一个consumer,但是consumer里面进行了多线程消费,这样也会造成消息消费顺序错误。
保证消息的消费顺序
1)rabbitmq
①拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点;这样也会造成吞吐量下降,可以在消费者内部采用多线程的方式取消费。
②或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理
2.设计一个秒杀活动
秒杀活动的特点:
同时并发量大
秒杀时会有大量用户在同一时间进行抢购,瞬时并发访问量突增 10 倍,甚至 100 倍以上都有。
库存量少
一般秒杀活动商品量很少,这就导致了只有极少量用户能成功购买到。
业务简单
流程比较简单,一般都是下订单、扣库存、支付订单
技术难点
有业务的冲击
秒杀是营销活动中的一种,如果和其他营销活动应用部署在同一服务器上,肯定会对现有其他活动造成冲击,极端情况下可能导致整个电商系统服务宕机
直接下订单
下单页面是一个正常的 URL 地址,需要控制在秒杀开始前,不能下订单,只能浏览对应活动商品的信息。简单来说,需要 Disable 订单按钮
页面流量突增
秒杀活动开始前后,会有很多用户请求对应商品页面,会造成后台服务器的流量突增,同时对应的网络带宽增加,需要控制商品页面的流量不会对后台服务器、DB、Redis 等组件的造成过大的压力
架构设计思想
限流
由于活动库存量一般都是很少,对应的只有少部分用户才能秒杀成功。所以我们需要限制大部分用户流量,只准少量用户流量进入后端服务器
削峰
秒杀开始的那一瞬间,会有大量用户冲击进来,所以在开始时候会有一个瞬间流量峰值。如何把瞬间的流量峰值变得更平缓,是能否成功设计好秒杀系统的关键因素。实现流量削峰填谷,一般的采用缓存和 MQ 中间件来解决
异步
秒杀其实可以当做高并发系统来处理,在这个时候,可以考虑从业务上做兼容,将同步的业务,设计成异步处理的任务,提高网站的整体可用性
缓存
秒杀系统的瓶颈主要体现在下订单、扣减库存流程中。在这些流程中主要用到 OLTP 的数据库,类似 MySQL、SQLServer、Oracle。由于数据库底层采用 B+ 树的储存结构,对应我们随机写入与读取的效率,相对较低。如果我们把部分业务逻辑迁移到内存的缓存或者 Redis 中,会极大的提高并发效率
整体架构
秒杀整体流程图
秒杀服务层和后端
秒杀业务的核心是库存处理,用户购买成功后会进行减库存操作,并记录购买明细。当秒杀开始时,大量用户同时发起请求,这是一个并行操作,多条更新库存数量的SQL语句会同时竞争秒杀商品所处数据库表里的那行数据,导致库存的减少数量与购买明细的增加数量不一致,因此,我们使用RabbitMQ进行削峰限流并且将请求数据串行处理
削峰填谷加延迟加载
秒杀450件商品
设置每次最大拉取100个人,
@1.查询剩余的秒杀商品的个数
@2.用拉取数跟剩余的秒杀商品的个数进行比较如果小于就直接拉取100个,如果大于就将拉取的值改为剩余的商品的数量,拉取成功并保证是同一个人的下单之后并将其添加到订单表中,库存的数量减少。
@3.设置超时时间为30分钟,当30分钟之后还没有付款的取消订单,然后将对应的库存补上