目录
二、RabbitMQ-02
2.1 消息的可靠性投递(Procedure)
2.1.1 消息投递介绍
在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。
• confirm 确认模式
• return 退回模式
rabbitmq 整个消息投递的路径为:
producer —> rabbitmq-broker —> exchange —> queue —> consumer
生产:消息从 producer 到 exchange 则会返回一个 confirmCallback()。都会执行,返回false就失败
内部:消息从 exchange–>queue 投递失败则会返回一个 returnCallback()。
我们将利用这两个 callback 控制消息的可靠性投递
2.1.2 确认模式
消息从 producer 到 exchange 则会返回一个 confirmCallback()。都会执行,返回false就失败
1、配置文件(application.properties)
#主机的IP地址:默认为localhost
spring.rabbitmq.host=192.168.111.127
#虚拟主机的名字:默认为/
spring.rabbitmq.virtual-host=/lwl
#登录的用户名:默认为guest
spring.rabbitmq.username=lwl
#登录的密码:默认为guest
spring.rabbitmq.password=密码
#RabbitMQ的端口号:默认为5672
spring.rabbitmq.port=5672
#设置开启确认模式
spring.rabbitmq.publisher-confirms=true
2、配置类
package com.aaa.procedure.test;
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:声明为配置类,可以更早加载
*/
@Configuration
public class ProcedureConfigConfirm {
//声明队列的名字
public static final String ROUTING_QUEUE_NAME1 = "confirm_queue_name1";
public static final String ROUTING_QUEUE_NAME2 = "confirm_queue_name2";
//声明一个交换机的名字
public static final String ROUTING_EXCHANGE_NAME = "confirm_exchange_name";
/**
* 声明一个交换机并作为bean注入到spring容器中
* durable默认为durable
* autoDelete默认为true
* 交换机起名字,在绑定队列时使用
*/
@Bean(name = "ex")
public Exchange getExchange(){
// 交换机构建器.交换机类型(交换机名字).是否持久化.是否自动删除.构建
return ExchangeBuilder.directExchange(ROUTING_EXCHANGE_NAME).durable(false).autoDelete().build();
}
/**
* 声明一个队列并作为bean注入到spring容器中
* autoDelete默认为true
* 队列起名字,在绑定交换机时使用
*/
@Bean(name = "qu1")
public Queue getQueue1(){
// 队列构建器.不持久化(队列名字).自动删除.构建
return QueueBuilder.nonDurable(ROUTING_QUEUE_NAME1).autoDelete().build();
}
@Bean(name = "qu2")
public Queue getQueue2(){
// 队列构建器.不持久化(队列名字).自动删除.构建
return QueueBuilder.nonDurable(ROUTING_QUEUE_NAME2).autoDelete().build();
}
/**
* 绑定队列和交换机后作为bean注入到容器中
* @Qualifier注解:为了标明对应的交换机和队列
*/
@Bean
public Binding bingQu1AndEx(@Qualifier("ex") Exchange exchange,@Qualifier("qu1") Queue queue){
// 绑定构建器.队列名字.交换机名字.routingkey.参数
return BindingBuilder.bind(queue).to(exchange).with("error").noargs();
}
@Bean
public Binding bingQu2AndEx(@Qualifier("ex") Exchange exchange,@Qualifier("qu2") Queue queue){
// 绑定构建器.队列名字.交换机名字.routingkey.参数
return BindingBuilder.bind(queue).to(exchange).with("warnings").noargs();
}
}
3、测试类
package com.aaa.procedure;
import com.aaa.procedure.test.ProcedureConfigConfirm;
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.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
/**
* @SpringBootTest:声明为测试类
* 因为SpringBoot的版本较低,所以还需要添加这两个注解来确保项目没有问题
* @RunWith(SpringRunner.class)
* @ContextConfiguration(classes = {ProcedureRabbitTest.class})
*/
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProcedureRabbitTest.class)
public class TestPublish {
//将操作mq的对象引入进来,是通过jar包amqp进行封装的对象
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 在进行测试时,生产者队列中不能有两个相同的bean,否则项目将不会正常启动比如,@Bean(name="ex")不能存在两个
*/
@Test
public void testConfirm(){
//在rabbitTemplate中设置信息的确认
//这里的ConfirmCallback()接口,是一个函数式接口,接口中只有一个方法,可以转换成lambda表达式的形式
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* (CorrelationData correlationData, boolean b, String s)
* @param correlationData 消息相关数据
* @param b 是否接收成功
* @param s 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
if (b){
System.out.println("接收成功");
}else {
System.out.println("接收失败的原因是:"+s);
}
}
});
/**
* 使用convertAndSend发送消息
* (String exchange, String routingKey, Object object)
* exchange:交换机的名字
* routingKey:定向路由
* object:消息的内容
*/
rabbitTemplate.convertAndSend(ProcedureConfigConfirm.ROUTING_EXCHANGE_NAME,"warnings","确认模式");
}
}
控制台输出 “接收成功” 代表项目成功且消息传递到MQ了
总结
设置publisher-confirms="true" 开启 确认模式。
使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。
2.1.3 回退模式
消息从 exchange–>queue 投递失败则会返回一个 returnCallback()。
1、配置文件
#主机的IP地址:默认为localhost
spring.rabbitmq.host=192.168.111.127
#虚拟主机的名字:默认为/
spring.rabbitmq.virtual-host=/lwl
#登录的用户名:默认为guest
spring.rabbitmq.username=lwl
#登录的密码:默认为guest
spring.rabbitmq.password=密码
#RabbitMQ的端口号:默认为5672
spring.rabbitmq.port=5672
#设置回退模式
spring.rabbitmq.publisher-returns=true
2、配置类
package com.aaa.procedure.test;
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:声明为配置类,可以更早加载
*/
@Configuration
public class ProcedureConfigReturn {
//声明队列的名字
public static final String ROUTING_QUEUE_NAME1 = "return_queue_name1";
public static final String ROUTING_QUEUE_NAME2 = "return_queue_name2";
//声明一个交换机的名字
public static final String ROUTING_EXCHANGE_NAME = "return_exchange_name";
/**
* 声明一个交换机并作为bean注入到spring容器中
* durable默认为durable
* autoDelete默认为true
* 交换机起名字,在绑定队列时使用
*/
@Bean(name = "ex")
public Exchange getExchange(){
// 交换机构建器.交换机类型(交换机名字).是否持久化.是否自动删除.构建
return ExchangeBuilder.directExchange(ROUTING_EXCHANGE_NAME).durable(false).autoDelete().build();
}
/**
* 声明一个队列并作为bean注入到spring容器中
* autoDelete默认为true
* 队列起名字,在绑定交换机时使用
*/
@Bean(name = "qu1")
public Queue getQueue1(){
// 队列构建器.不持久化(队列名字).自动删除.构建
return QueueBuilder.nonDurable(ROUTING_QUEUE_NAME1).autoDelete().build();
}
@Bean(name = "qu2")
public Queue getQueue2(){
// 队列构建器.不持久化(队列名字).自动删除.构建
return QueueBuilder.nonDurable(ROUTING_QUEUE_NAME2).autoDelete().build();
}
/**
* 绑定队列和交换机后作为bean注入到容器中
* @Qualifier注解:为了标明对应的交换机和队列
*/
@Bean
public Binding bingQu1AndEx(@Qualifier("ex") Exchange exchange,@Qualifier("qu1") Queue queue){
// 绑定构建器.队列名字.交换机名字.routingkey.参数
return BindingBuilder.bind(queue).to(exchange).with("error").noargs();
}
@Bean
public Binding bingQu2AndEx(@Qualifier("ex") Exchange exchange,@Qualifier("qu2") Queue queue){
// 绑定构建器.队列名字.交换机名字.routingkey.参数
return BindingBuilder.bind(queue).to(exchange).with("warnings").noargs();
}
}
3、测试类
package com.aaa.procedure;
import com.aaa.procedure.test.ProcedureConfigReturn;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
/**
* @SpringBootTest:声明为测试类
* 因为SpringBoot的版本较低,所以还需要添加这两个注解来确保项目没有问题
* @RunWith(SpringRunner.class)
* @ContextConfiguration(classes = {ProcedureRabbitTest.class})
*/
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProcedureRabbitTest.class)
public class TestPublish {
//将操作mq的对象引入进来,是通过jar包amqp进行封装的对象
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 在进行测试时,生产者队列中不能有两个相同的bean,否则项目将不会正常启动比如,@Bean(name="ex")不能存在两个
*/
@Test
public void testReturn(){
//设置交换机处理消息失败的模式
rabbitTemplate.setMandatory(true);
//这里的ReturnCallback()接口,是一个函数式接口,接口中只有一个方法,可以转换成lambda表达式的形式
// 如果注释写在函数式接口里面,是不能转成lambda表达式的
/**
* 下面对应的参数
* (Message message, int replyCode, String replyText, String exchange, String routingKey)
*/
rabbitTemplate.setReturnCallback((message, i, s, s1, s2) -> System.out.println("routingKey:"+s2));
/**
* 使用convertAndSend发送消息
* (String exchange, String routingKey, Object object)
* exchange:交换机的名字
* routingKey:定向路由
* object:消息的内容
*/
rabbitTemplate.convertAndSend(ProcedureConfigReturn.ROUTING_EXCHANGE_NAME,"warnings","回退模式");
}
}
总结
设置publisher-returns="true"开启 退回模式。
使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage。
2.2 事务机制
在RabbitMQ中也提供了事务机制,但是性能较差。
使用channel下列方法,完成事务控制:
txSelect() :用于将当前channel设置成transaction模式
txCommit():用于提交事务
txRollback():用于回滚事务
2.3 确认方式(Consumer)
2.3.1 确认方式介绍
ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
broker发送消息给消费端的一种可靠性保证
有三种确认方式 1、自动确认:acknowledge="none" 。不管处理成功与否,业务处理异常也不管(当消费者意担接收到消息之后,消费者就会给broker一个回执,证明已经接收到消息了,不管消息到底是否成功) 2、 手动确认:acknowledge="manual" 。可以解决业务异常的情况(收到消息之后不会立马确认收到消息,当业务处理没有问题的时候手动的调用代码的方 式来进行处理,如果业务失败了,就可以进行额外的操作) 3、 根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,不作讲解) 其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。
2.3.2 自动确认
1、配置类
#主机的IP地址:默认为localhost
spring.rabbitmq.host=192.168.111.127
#虚拟主机的名字:默认为/
spring.rabbitmq.virtual-host=/lwl
#登录的用户名:默认为guest
spring.rabbitmq.username=lwl
#登录的密码:默认为guest
spring.rabbitmq.password=密码
#RabbitMQ的端口号:默认为5672
spring.rabbitmq.port=5672
2、监听类
/**
* @Component:将该类作为注解bean注入
* @RabbitListener:标注监听队列的名字
*/
@Component
public class ConsumerListener {
//监听队列里面的消息:可以同时监听很多个队列
@RabbitListener(queues = {"routing_queue_name1","routing_queue_name2"})
public void listenerQueues(Message message){
//取出队列里面的消息(bytes类型),并将其转换为String类型
String msg = new String(message.getBody());
System.out.println("监听到的消息为:"+msg);
}
}
2.3.3 手动确认
1、配置类
#主机的IP地址:默认为localhost
spring.rabbitmq.host=192.168.111.127
#虚拟主机的名字:默认为/
spring.rabbitmq.virtual-host=/lwl
#登录的用户名:默认为guest
spring.rabbitmq.username=lwl
#登录的密码:默认为guest
spring.rabbitmq.password=密码
#RabbitMQ的端口号:默认为5672
spring.rabbitmq.port=5672
#设置手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
2、监听类
package com.aaa.consumer.test;
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;
import java.io.IOException;
/**
* @Component:将该类作为注解bean注入
* @RabbitListener:标注监听队列的名字
* 1. 设置手动签收。acknowledge="manual"
* 2. 让监听器类实现ChannelAwareMessageListener接口
* 3. 如果消息成功处理,则调用channel的 basicAck()签收
* 4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
*/
@Component
public class ConsumerListenerAck implements ChannelAwareMessageListener {
@Override
@RabbitListener(queues = "return_queue_name2")
public void onMessage(Message message, Channel channel) throws Exception {
//模拟处理业务逻辑
Thread.sleep(2000);
//获取消息
String msg = new String(message.getBody());
System.out.println("msg = " + msg);
//获取标识信息的id
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
/** 确认消息
* basicAck(long deliveryTag, boolean multiple)
* deliveryTag:用来标识消息的id
* multiple:是否批量。true:将一次性 ACK 所有小于 deliveryTag 的消息
*/
int a = 2/0;
System.out.println("确认接收消息");
channel.basicAck(deliveryTag, false);
//这里捕获的异常需要是Exception,如果只捕获IOException,会报异常,然后关闭
} catch (Exception e) {
/** 不确认消息
* basicNack(long deliveryTag, boolean multiple, boolean requeue)
* deliveryTag:用来标识消息的id
* multiple:是否批量不签收
* requeue:是否重新回到队列,不确认的消息就会变为Unacked,
* 如果这里设置为true,那么消息会回到队列,重新在进行发送
* 如果这里设置为false,那么消息不会回到队列
*/
System.out.println("消息重新回到队列里面");
channel.basicNack(deliveryTag,true,true);
}
}
}
设置手动模式需要在配置文件里面添加 acknowlwdge-mode:manual
如果消费端没有异常就会走basicAck的方法,确认签收消息,如果有异常就会调用basicNack的方法拒接签收消息,让MQ重新发送消息
2.4 消费端限流
消费端限流是为了避免请求瞬间增多的情况 ①消费端的确认模式一定为手动确认。acknowledge="manual" ②在配置 prefetch属性设置消费端一次拉取多少消息,如果没有设置限流,会直接全部拉取消息,但是并不消费
2.4.1 代码实现
1、配置类
RabbitMQ的配置参考可靠性投递的配置类即可
#设置为手动确认模式
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#每次拉取的条数,每次拉取一条
#如果没有设置限流,会直接全部拉取消息,但是并不消费
spring.rabbitmq.listener.simple.prefetch=1
2、监听类
package com.aaa.consumer.test;
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:将该类作为注解bean注入
* @RabbitListener:标注监听队列的名字
*/
@Component
public class ConsumerListenerPrefetch implements ChannelAwareMessageListener {
@Override
@RabbitListener(queues = "return_queue_name2")
public void onMessage(Message message, Channel channel) throws Exception {
//模拟处理业务逻辑
Thread.sleep(6000);
//获取消息
String msg = new String(message.getBody());
System.out.println("msg = " + msg);
//获取标识信息的id
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
/** 确认消息
* basicAck(long deliveryTag, boolean multiple)
* deliveryTag:用来标识消息的id
* multiple:是否批量。true:将一次性 ACK 所有小于 deliveryTag 的消息
*/
int a = 2/0;
System.out.println("确认接收消息");
channel.basicAck(deliveryTag, false);
//这里捕获的异常需要是Exception,如果只捕获IOException,会报异常,然后关闭
} catch (Exception e) {
/** 不确认消息
* basicNack(long deliveryTag, boolean multiple, boolean requeue)
* deliveryTag:用来标识消息的id
* multiple:是否批量不签收
* requeue:是否重新回到队列,不确认的消息就会变为Unacked,
* 如果这里设置为true,那么消息会回到队列,重新在进行发送
* 如果这里设置为false,那么消息不会回到队列
*/
channel.basicNack(deliveryTag,true,false);
}
}
}
//没有手动的确认接收消息 所以就会显示有一条未被确认消息
//每次都会拉取一条信息进行消费,错误后不回到队列,会被丢弃
2.5 TTL(存活时间)
全称 Time To Live(存活时间/过期时间)。
当消息到达存活时间后,还没有被消费,会被自动清除。
RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。
2.5.1 可视化
使用可视化客户端创建队列时添加ttl参数
2.5.2 代码实现
1、配置类文件
RabbitMQ的配置参考可靠性投递的配置类
2、配置类
package com.aaa.procedure.test;
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:声明为配置类,可以更早加载
* 这里测试只使用一个队列进行测试
*/
@Configuration
public class ProcedureConfigTTL {
//声明队列的名字
public static final String ROUTING_QUEUE_NAME1 = "ttl_queue_name";
//声明一个交换机的名字
public static final String ROUTING_EXCHANGE_NAME = "ttl_exchange_name";
/**
* 声明一个交换机并作为bean注入到spring容器中
* durable默认为durable
* autoDelete默认为true
* 交换机起名字,在绑定队列时使用
*/
@Bean(name = "ex")
public Exchange getExchange(){
// 交换机构建器.交换机类型(交换机名字).是否持久化.是否自动删除.构建
return ExchangeBuilder.directExchange(ROUTING_EXCHANGE_NAME).durable(false).autoDelete().build();
}
/**
* 声明一个队列并作为bean注入到spring容器中
* 声明队列的时候添加参数
* withArgument("x-message-ttl",10000):此队列的生存周期为5秒
*/
@Bean(name = "qu1")
public Queue getQueue1(){
// 队列构建器.不持久化(队列名字).自动删除.构建
return QueueBuilder.nonDurable(ROUTING_QUEUE_NAME1).autoDelete().withArgument("x-message-ttl",10000).build();
}
/**
* 绑定队列和交换机后作为bean注入到容器中
* @Qualifier注解:为了标明对应的交换机和队列
*/
@Bean
public Binding bingQu1AndEx(@Qualifier("ex") Exchange exchange,@Qualifier("qu1") Queue queue){
// 绑定构建器.队列名字.交换机名字.routingkey.参数
return BindingBuilder.bind(queue).to(exchange).with("ttl").noargs();
}
}
3、测试类
package com.aaa.procedure;
import com.aaa.procedure.test.ProcedureConfigTTL;
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.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProcedureRabbitTest.class)
public class TestT {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void testTTL(){
//设置交换机处理消息失败的模式
rabbitTemplate.setMandatory(true);
//这里的ReturnCallback()接口,是一个函数式接口,接口中只有一个方法,可以转换成lambda表达式的形式
// 如果注释写在函数式接口里面,是不能转成lambda表达式的
/**
* 下面对应的参数
* (Message message, int replyCode, String replyText, String exchange, String routingKey)
*/
rabbitTemplate.setReturnCallback((message, i, s, s1, s2) -> System.out.println("routingKey:"+s2));
/**
* 使用convertAndSend发送消息
* (String exchange, String routingKey, Object object)
* exchange:交换机的名字
* routingKey:定向路由
* object:消息的内容
*/
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend(ProcedureConfigTTL.ROUTING_EXCHANGE_NAME,"ttl","测试队列生存周期");
}
}
}
会生成十条消息在队列中,在10秒钟生命周期到期时,十条消息都会直接丢掉
如果要测试消息的存活时间,可以使用消息后处理对象,只需要更改测试类代码即可
package com.aaa.procedure;
import com.aaa.procedure.test.ProcedureConfigTTL;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProcedureRabbitTest.class)
public class TestT {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void testTTL(){
//消息后处理对象,设置一些消息的参数信息
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//设置message的信息,设置了过期时间
message.getMessageProperties().setExpiration("7000");
return message;
}
};
//设置交换机处理消息失败的模式
rabbitTemplate.setMandatory(true);
//这里的ReturnCallback()接口,是一个函数式接口,接口中只有一个方法,可以转换成lambda表达式的形式
// 如果注释写在函数式接口里面,是不能转成lambda表达式
/**
* 下面对应的参数
* (Message message, int replyCode, String replyText, String exchange, String routingKey)
*/
rabbitTemplate.setReturnCallback((message, i, s, s1, s2) -> System.out.println("routingKey:" + s2));
/**
* 使用convertAndSend发送消息
* (String exchange, String routingKey, Object object)
* exchange:交换机的名字
* routingKey:定向路由
* object:消息的内容
*/
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend(ProcedureConfigTTL.ROUTING_EXCHANGE_NAME,"ttl","测试队列中消息的生存周期");
}
}
}
2.5.3 总结
上面队列设置的过期时间为10秒,而消息的过期时间设置的是7秒,所以消息会在7秒之后就过期。
当既设置队列的过期时间,也设置消息的过期时间时,哪个时间短就按照哪个时间来执行
2.6 死信队列
死信队列,英文缩写:DLX 。
Dead Letter Exchange(死信交换机,因为其他MQ产品中没有交换机的概念),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。
比如消息队列的消息过期,如果绑定了死信交换器,那么该消息将发送给死信交换机
队列绑定交换机的方式是什么?
给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key
1.交换机:正常交换机 死信交换机
2.队列:正常队列 死信队列
3.绑定 正常交换机 - 正常队列
死信交换机 - 正常队列
死信交换机 - 死信队列
2.6.1 消息在什么情况下会成为死信(面试)
1.队列消息长度到最大的限制 最大的长度设置为10当第11条消息进来的时候就会成为死信 2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false(不重新回到队列中) 设置消费者为手动签收的状态 3. 原队列存在消息过期设置,消息到达超时时间未被消费;
2.6.2 代码实现
1、配置类文件
不需要配置额外的信息
RabbitMQ的配置参考可靠性投递的配置类
2、配置类
package com.aaa.procedure.test;
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 ProcedureDLX {
//正常交换机的名字
private static final String NORMAL_EXCHANGE_NAME = "normal_exchange";
//死信交换机的名字
private static final String DEAD_EXCHANGE_NAME = "dead_exchange";
//正常队列的名字
private static final String NORMAL_QUEUE_NAME = "normal_queue";
//死信队列的名字
private static final String DEAD_QUEUE_NAME = "dead_queue";
//声明正常交换机
@Bean(name = "normalexchange")
public Exchange getNormalExchange(){
return ExchangeBuilder.directExchange(NORMAL_EXCHANGE_NAME).durable(false).build();
}
//声明死信交换机
@Bean(name = "deadexchange")
public Exchange getDeadExchange(){
return ExchangeBuilder.directExchange(DEAD_EXCHANGE_NAME).durable(false).build();
}
//声明正常队列:顺便绑定对应的死信队列
@Bean(name = "normalqueue")
public Queue getNormalQueue(){
return QueueBuilder.nonDurable(NORMAL_QUEUE_NAME)
//队列的过期时间设置为30秒
.withArgument("x-message-ttl",30000)
//最大的长度为6,超过6,后面的消息就会成为死信消息
.withArgument("x-max-length",6)
//绑定死信交换机的名字
.withArgument("x-dead-letter-exchange",DEAD_EXCHANGE_NAME)
//和死信交换机绑定的路由名称一致
.withArgument("x-dead-letter-routing-key","deadmsg")
.build();
}
//声明死信队列
@Bean(name = "deadqueue")
public Queue getDeadQueue(){
return QueueBuilder.nonDurable(DEAD_QUEUE_NAME).build();
}
//正常交换机和正常队列进行绑定
public Binding norExAndNorQu(@Qualifier("normalexchange")Exchange exchange,@Qualifier("normalqueue")Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("normalmsg").noargs();
}
//死信交换机和死信队列绑定
@Bean
public Binding deadExAndDeadQu(@Qualifier("deadexchange")Exchange exchange,@Qualifier("deadqueue")Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("deadmsg").noargs();
}
}
①消息长度过长和消息时间过期
package com.aaa.procedure;
import com.aaa.procedure.test.ProcedureDLX;
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.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProcedureRabbitTest.class)
public class TestDLX {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void testTTL(){
//设置交换机处理消息失败的模式
rabbitTemplate.setMandatory(true);
/**
* 下面对应的参数
* (Message message, int replyCode, String replyText, String exchange, String routingKey)
*/
rabbitTemplate.setReturnCallback((message, i, s, s1, s2) -> System.out.println("routingKey:" + s2));
/**
* 使用convertAndSend发送消息
* (String exchange, String routingKey, Object object)
* exchange:交换机的名字
* routingKey:定向路由
* object:消息的内容
*/
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend(ProcedureDLX.NORMAL_EXCHANGE_NAME,"normalmsg","测试死信队列的消息"+i);
}
}
}
发布了十条消息,消息队列的最大长度是6,所以队列中只会有6条消息,剩余的消息会变成死信消息
因为消息队列有先进先出的性质,所以只有最后六条消息留在消息队列中
②拒绝接收消息
package com.aaa.consumer.test;
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 ConsumerListenerDLX implements ChannelAwareMessageListener {
@Override
@RabbitListener(queues = "normal_queue")
public void onMessage(Message message, Channel channel) throws Exception {
//模拟处理业务逻辑
Thread.sleep(6000);
//获取消息
String msg = new String(message.getBody());
System.out.println("msg = " + msg);
//获取标识信息的id
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
/** 确认消息
* basicAck(long deliveryTag, boolean multiple)
* deliveryTag:用来标识消息的id
* multiple:是否批量。true:将一次性 ACK 所有小于 deliveryTag 的消息
*/
int a = 2/0;
System.out.println("确认接收消息");
channel.basicAck(deliveryTag, false);
//这里捕获的异常需要是Exception,如果只捕获IOException,会报异常,然后关闭
} catch (Exception e) {
/** 不确认消息
* basicNack(long deliveryTag, boolean multiple, boolean requeue)
* deliveryTag:用来标识消息的id
* multiple:是否批量不签收
* requeue:是否重新回到队列,不确认的消息就会变为Unacked,
* 如果这里设置为true,那么消息会回到队列,重新在进行发送
* 如果这里设置为false,那么消息不会回到队列
*/
channel.basicNack(deliveryTag,true,false);
}
}
}
2.6.3 总结
1. 死信交换机和死信队列和普通的没有区别 2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列 3. 消息成为死信的三种情况: ①. 队列消息长度到达限制; ②. 消费者拒接消费消息,并且不重回队列; ③. 原队列存在消息过期设置,消息到达超时时间未被消费;
2.7 延迟队列
延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
需求:
• 1. 下单后,30分钟未支付,取消订单,回滚库存。
• 2. 新用户注册成功7天后,发送短信问候。
实现:
1. 定时器
2. 死信队列
在RabbitMQ中并未提供延迟队列功能。但是可以使用:TTL+死信队列 组合实现延迟队列的效果。
2.7.1 代码实现
1、配置类文件
不需要配置额外的信息
RabbitMQ的配置参考可靠性投递的配置类
2、配置类
package com.aaa.procedure.test;
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 ProcedureDLX {
//正常交换机的名字
public static final String NORMAL_EXCHANGE_NAME = "normal_exchange";
//死信交换机的名字
public static final String DEAD_EXCHANGE_NAME = "dead_exchange";
//正常队列的名字
public static final String NORMAL_QUEUE_NAME = "normal_queue";
//死信队列的名字
public static final String DEAD_QUEUE_NAME = "dead_queue";
//声明正常交换机
@Bean(name = "normalexchange")
public Exchange getNormalExchange(){
return ExchangeBuilder.directExchange(NORMAL_EXCHANGE_NAME).durable(false).build();
}
//声明死信交换机
@Bean(name = "deadexchange")
public Exchange getDeadExchange(){
return ExchangeBuilder.directExchange(DEAD_EXCHANGE_NAME).durable(false).build();
}
//声明正常队列:顺便绑定对应的死信队列
@Bean(name = "normalqueue")
public Queue getNormalQueue(){
return QueueBuilder.nonDurable(NORMAL_QUEUE_NAME)
//队列的过期时间设置为30秒
.withArgument("x-message-ttl",180000)
//最大的长度为6,超过6,后面的消息就会成为死信消息
.withArgument("x-max-length",6)
//绑定死信交换机的名字
.withArgument("x-dead-letter-exchange",DEAD_EXCHANGE_NAME)
//和死信交换机绑定的路由名称一致
.withArgument("x-dead-letter-routing-key","deadmsg")
.build();
}
//声明死信队列
@Bean(name = "deadqueue")
public Queue getDeadQueue(){
return QueueBuilder.nonDurable(DEAD_QUEUE_NAME).build();
}
//正常交换机和正常队列进行绑定
@Bean
public Binding norExAndNorQu(@Qualifier("normalexchange")Exchange exchange,@Qualifier("normalqueue")Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("normalmsg").noargs();
}
//死信交换机和死信队列绑定
@Bean
public Binding deadExAndDeadQu(@Qualifier("deadexchange")Exchange exchange,@Qualifier("deadqueue")Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("deadmsg").noargs();
}
}
3、测试类
package com.aaa.procedure;
import com.aaa.procedure.test.ProcedureDLX;
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.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
@SpringBootTest
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ProcedureRabbitTest.class)
public class TestDLX {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void testTTL(){
//设置交换机处理消息失败的模式
rabbitTemplate.setMandatory(true);
/**
* 下面对应的参数
* (Message message, int replyCode, String replyText, String exchange, String routingKey)
*/
rabbitTemplate.setReturnCallback((message, i, s, s1, s2) -> System.out.println("routingKey:" + s2));
/**
* 使用convertAndSend发送消息
* (String exchange, String routingKey, Object object)
* exchange:交换机的名字
* routingKey:定向路由
* object:消息的内容
*/
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend(ProcedureDLX.NORMAL_EXCHANGE_NAME,"normalmsg","测试死信队列的消息"+i);
}
}
}
4、监听类
package com.aaa.consumer.test;
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 ConsumerListenerDLX implements ChannelAwareMessageListener {
@Override
@RabbitListener(queues = "dead_queue")
public void onMessage(Message message, Channel channel) throws Exception {
//获取消息
String msg = new String(message.getBody());
System.out.println("msg = " + msg);
//获取标识信息的id
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
/** 确认消息
* basicAck(long deliveryTag, boolean multiple)
* deliveryTag:用来标识消息的id
* multiple:是否批量。true:将一次性 ACK 所有小于 deliveryTag 的消息
*/
System.out.println("开始处理业务逻辑");
System.out.println("根据订单的id查询订单的状态");
System.out.println("判断订单是否已经支付成功");
System.out.println("如果取消订单就回滚库存");
channel.basicAck(deliveryTag, false);
//这里捕获的异常需要是Exception,如果只捕获IOException,会报异常,然后关闭
} catch (Exception e) {
/** 不确认消息
* basicNack(long deliveryTag, boolean multiple, boolean requeue)
* deliveryTag:用来标识消息的id
* multiple:是否批量不签收
* requeue:是否重新回到队列,不确认的消息就会变为Unacked,
* 如果这里设置为true,那么消息会回到队列,重新在进行发送
* 如果这里设置为false,那么消息不会回到队列
*/
channel.basicNack(deliveryTag,true,false);
}
}
}
2.7.2 总结
1. 延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。
2. RabbitMQ没有提供延迟队列功能,但是可以使用 : TTL + DLX 来实现延迟队列效果。
注:
消费者在监听队列的时候监听的是死信队列
2.8 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.9 RabbitMQ应用问题
2.9.1 消息可靠性保障
需求:100%确保消息发送成功
消息补偿机制: 2发送的消息在Q1中被正常消费到写入DB,发送ack给Q2。回调检查服务监听到Q2的消息,将消息写入MDB 如果1成功2失败,因为3页发送了消息放入Q3。此时回调检查服务也监听到了Q3,要去比对MDB是否一致,如果一致则代表消费过。如果MDB中不存在,就代表2失败了,就走8让生产者重新发。 如果2个都发送失败了,有MDB的定时检查服务,比对业务数据库DB与消息数据库MDB,就能发现差异
2.9.2 消息幂等性保障
幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果。
2.10 RabbitMQ面试题
2.10.1 保证消息按顺序执行
1、消息出现错乱的场景
①一个queue,有多个consumer去消费,这样就会造成顺序的错误,consumer从MQ里面读取数据是有序的,但是每个consumer的执行时间是不固定的,无法保证先读到消息的consumer一定先完成操作,这样就会出现消息并没有按照顺序执行,造成数据顺序错误。
②一个queue对应一个consumer,但是consumer里面进行了多线程消费,这样也会造成消息消费顺序错误。
2、保证消息消费顺序
①拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点;这样也会造成吞吐量下降,可以在消费者内部采用多线程的方式取消费。
②或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理
2.10.2 设置秒杀活动
秒杀活动特点:
1、并发量大
秒杀时会有大量用户在同一时间进行抢购,瞬时并发访问量突增 10 倍,甚至 100 倍以上都有。
2、库存量少
一般秒杀活动商品量很少,这就导致了只有极少量用户能成功购买到。
3、业务简单
流程比较简单,一般都是下订单、扣库存、支付订单
4、技术难点
4.1 有业务的冲击 秒杀是营销活动中的一种,如果和其他营销活动应用部署在同一服务器上,肯定会对现有其他活动造成冲击,极端情况下可能导致整个电商系统服务宕机 4.2 直接下订单 下单页面是一个正常的 URL 地址,需要控制在秒杀开始前,不能下订单,只能浏览对应活动商品的信息。简单来说,需要 Disable 订单按钮 4.3 页面流量突增 秒杀活动开始前后,会有很多用户请求对应商品页面,会造成后台服务器的流量突增,同时对应的网络带宽增加,需要控制商品页面的流量不会对后台服务器、DB、Redis 等组件的造成过大的压力
5、架构设计思想
5.1 限流 由于活动库存量一般都是很少,对应的只有少部分用户才能秒杀成功。所以我们需要限制大部分用户流量,只准少量用户流量进入后端服务器 5.2 削峰 秒杀开始的那一瞬间,会有大量用户冲击进来,所以在开始时候会有一个瞬间流量峰值。如何把瞬间的流量峰值变得更平缓,是能否成功设计好秒杀系统的关键因素。实现流量削峰填谷,一般的采用缓存和 MQ 中间件来解决 5.3 异步 秒杀其实可以当做高并发系统来处理,在这个时候,可以考虑从业务上做兼容,将同步的业务,设计成异步处理的任务,提高网站的整体可用性 5.4 缓存 秒杀系统的瓶颈主要体现在下订单、扣减库存流程中。在这些流程中主要用到 OLTP 的数据库,类似 MySQL、SQLServer、Oracle。由于数据库底层采用 B+ 树的储存结构,对应我们随机写入与读取的效率,相对较低。如果我们把部分业务逻辑迁移到内存的缓存或者 Redis 中,会极大的提高并发效率
6、整体架构
7、秒杀整体流程图
8、秒杀服务层和后端
秒杀业务的核心是库存处理,用户购买成功后会进行减库存操作,并记录购买明细。当秒杀开始时,大量用户同时发起请求,这是一个并行操作,多条更新库存数量的SQL语句会同时竞争秒杀商品所处数据库表里的那行数据,导致库存的减少数量与购买明细的增加数量不一致,因此,我们使用RabbitMQ进行削峰限流并且将请求数据串行处理 削峰填谷加延迟加载 场景: 秒杀450件商品 设置每次最大拉取100个人, @1.查询剩余的秒杀商品的个数 @2.用拉取数跟剩余的秒杀商品的个数进行比较如果小于就直接拉取100个,如果大于就将拉取的值改为剩余的商品的数量,拉取成功并保证是同一个人的下单之后并将其添加到订单表中,库存的数量减少。 @3.设置超时时间为30分钟,当30分钟之后还没有付款的取消订单,然后将对应的库存补上