SpringBoot中使用rabbitMQ
SpringBoot为了使用RabbitMQ,提供了一个工具类RabbitTemplate,使用此工具类可以发送消息。
1、父工程中引入相关的依赖
<dependencies>
<!--rabbitMQ的依赖: 启动类加载。读取配置文件:
springboot自动装配原理: 引用starter启动依赖时,把对应的自动装配类加载进去,该自动装配类可以读取application配置文件中
内容。 DispatherServlet
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2、相应的生产者和消费者的配置(此处为yml文件格式)
#SpringBoot开启的端口号,为了防止与其他的冲突设置的
server:
port: 8888
#rabbit的配置
spring:
rabbitmq:
host: 192.168.31.168
#port: 5672,java默认rabbit端口号为5672,,可修改
3、生产者(producer)代码
//模拟下单业务
@RestController
public class HelloController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("hello")
public String hello(){ //业务层
System.out.println("下单成功");
//String exchange, String routingKey, Object message
Map<String,Object> map=new HashMap<>();
map.put("productId",1);
map.put("num",10);
map.put("price",12);
rabbitTemplate.convertAndSend("ban_exchange","", JSON.toJSONString(map)); //序列化过程
return "下单成功";
}
4、消费者(consumer)监听消息(运行后实时监听消息的产生)
@Component
public class MyRabbitListener {
//队列中存在消息则立即回调该方法,此注解值为需要监听的队列名
@RabbitListener(queues = {"ban_queue_fanout01"})
public void listener(String msg){//此处使用字符串接收,也可用Message类型接收消息
Map map = JSON.parseObject(msg, Map.class);//接收的json字符串转化成map类型(此消息发送时就是使用的map发送的,因此可以)
System.out.println(map);
}
}
Rabbit的高级特性
1、什么是RabbitMQ的高级特性?
rabbitMQ的高级特性是对rabbitMQ的深入理解及使用。
主要包含有:
1、消息的可靠性传递
2、Consumer ACK(即手动确认签收消息)
3、消费端限流
4、TTL(设置队列或消息的过期时间)
5、死信队列
6、延迟队列
7、消息的幂等性
2、springboot中使用代码创建队列和交换机及其绑定。
创建配置文件类RabbitConfig,编写代码:
@Configuration
public class RabbitConfig {
private final String exchange_name="myexchange";
private final String queue_name="myqueue";
//创建交换机对象
@Bean
public Exchange exchange(){
Exchange exchange= ExchangeBuilder.fanoutExchange(exchange_name).durable(true).build();
return exchange;
}
//创建队列
@Bean(value = "queue")
public Queue queue(){
Queue queue= QueueBuilder.durable(queue_name).withArgument("x-message-ttl",20000).build();
return queue;
}
//绑定交换机和队列
@Bean
public Binding binding(Queue queue,Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("").noargs();
}
}
3、消息的可靠性传递
3.1、什么是消息的可靠性传递?
在使用rabbitMQ的时候,作为消息的发送方希望杜绝任何消息的丢失或者投递失败场景。RabbitMQ为我们提供了两种方式用来控制消息的投递可靠性模式。
1、消息的投递步骤:
生产者-----》交换机-------》队列
2、为了确保消息的可靠性投递,提供了如下两种方式:
confirm 确认模式
return 退回模式
3.2、确认模式
(1)必须开启确认模式(手动确认)
spring:
rabbitmq:
host: 192.168.31.168
#开启rabbitMQ的生产方确认模式
publisher-confirm-type: correlated
(2)创建普通测试类,设置RabbitTemplate的确认回调函数(消费者端):
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 保证发送方到交换机的可靠性。
* 1.开启confirm模式,publisher-confirm-type: correlated
* 2.设置rabbitTemplate的确认回调函数。如果消息到达交换机则返回true,如果消息没有到达交换机则返回一个false
*/
@Test
public void test2(){
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
if(b==false){//消息没有到达交换机 根据业务需求。
System.out.println("继续发现消息");
//取消订单
}
}
});
rabbitTemplate.convertAndSend("ban_exchange","","hello confirm");
}
3.3、退回模式
(1)开启回退机制
server:
port: 8001
spring:
rabbitmq:
host: 192.168.31.168
#开启rabbitMQ的生产方确认模式
publisher-confirm-type: correlated
# 开启发布者退回模式
publisher-returns: true
(2)设置RabbitTemplate回调的函数
/**
* 退回模式:
* 1. 开启退回模式。
* 2. 设置RabbitTemplate的退回回调函数。
*/
@Test
public void test3(){
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
//只要交换机到队列失败时才会触发该方法。 可以继续发送也可以取消相应的业务功能。
System.out.println("消息从交换机到队列失败"+returnedMessage.getReplyText());
}
});
rabbitTemplate.convertAndSend("ban_exchange_direct","error2","hello confirm2");
}
3.4、如何保证消息的可靠性?
1、保证消息从产生者到交换机的可靠性;--》使用confim确认机制。
2、保证消息从交换机到队列的可靠性;--》使用return回退机制
3、消息在队列中的可靠性。---》设置消息和队列的持久化(存活时间)
4、保证消息从队列到消费者的可靠性。--》使用消费端的手动确认机制。
4. Consumer ACK(手动确认签收)
表示消费端收到消息后的确认方式。
其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息队列中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后。
调用channel.basicAck(),手动签收,如果出现异常。
则调用channel.basicNack()方法,让其自动重新发送消息。
(1)消费端(consumer)配置手动开启确认模式
spring:
rabbitmq:
host: 192.168.31.168
listener:
simple:
#表示手动确认
acknowledge-mode: manual
# 表示自动确认模式
# acknowledge-mode: none
(2)消费端接收消息后确认消息
@RabbitListener(queues = "ban_queue_direct01")
public void listener(Message message, Channel channel) throws Exception{
long deliveryTag = message.getMessageProperties().getDeliveryTag();
byte[] body = message.getBody();
String msg=new String(body);
System.out.println(msg);
try {
// int c = 10 / 0;//模拟异常
System.out.println("处理业务逻辑");
//消费端手动确认消息
//long deliveryTag, 表示的标识。
// boolean multiple:是否允许多确认
channel.basicAck(deliveryTag,true); //从队列中删除该消息。
}catch (Exception e){
//(long deliveryTag, boolean multiple, boolean requeue: 是否让队列再次发送该消息。
channel.basicNack(deliveryTag,true,true);
}
}
5. 消费端限流模式
(1)开启前提:
1. 必须为手动确认模式。
2. 必须配置限流的个数。
spring:
rabbitmq:
host: 192.168.213.188
listener:
simple:
# 表示自动确认模式
# acknowledge-mode: none
#表示手动确认
acknowledge-mode: manual
# 设置每次消费的个数。
prefetch: 100
(2)消费端接收消息。
@Component
public class MyListener {
@RabbitListener(queues = "ban_queue_direct01")
public void listener(Message message, Channel channel) throws Exception{
long deliveryTag = message.getMessageProperties().getDeliveryTag();
byte[] body = message.getBody();
String msg=new String(body);
System.out.println(msg);
try {
// int c = 10 / 0;
//System.out.println("处理业务逻辑");
//消费端手动确认消息
//long deliveryTag, 表示的标识。
// boolean multiple:是否允许多确认
channel.basicAck(deliveryTag,true); //从队列中删除该消息。
}catch (Exception e){
//(long deliveryTag, boolean multiple, boolean requeue: 是否让队列再次发送该消息。
channel.basicNack(deliveryTag,true,true);
}
}
}
6.TTL设置队列或消息存活时间
1.设置队列过期;
2.设置消息的过期;该消息必须在队列的头部时才会被移除。
设置队列或消息的过期时间:
//为队列设置过期时间 相当于该队列里面的消息都由过期时间
@Test
public void test01(){
rabbitTemplate.convertAndSend("myexchange","","hello xiaoxi");
}
//设置消息的过期时间 如果由设置了队列的过期时间 也设置了消息的过期时间 谁的过期时间短 以谁为准。
//该消息必须在头部才能从队列中移除。
@Test
public void test02(){
for(int i=0;i<10;i++) {
if(i==3){
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("20000");
return message;
}
};
rabbitTemplate.convertAndSend("myexchange", "", "hello xiaoxi"+i, messagePostProcessor);
}else {
rabbitTemplate.convertAndSend("myexchange", "", "hello xiaoxi"+i);
}
}
}
7.死信队列
死信队列,英文缩写:DLX 。Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。
(1)消息成为死信的三种情况:
1. 队列消息长度到达限制;
2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
3. 原队列存在消息过期设置,消息到达超时时间未被消费;
队列需要绑定死信交换机才会生效:
给队列设置参数(绑定死信交换机):
x-dead-letter-exchange 死信交换机
x-dead-letter-routing-key 死信交换机的路由key
(2)创建死信队列及普通队列并进行绑定:
@Configuration
public class RabbitConfig {
//创建死信队列
@Bean
public Queue dead_queue(){
return QueueBuilder.durable("dead_queue")
.build();
}
//创建死信交换机
@Bean
public Exchange dead_exchange(){
return ExchangeBuilder.directExchange("dead_exchange")
.build();
}
//绑定死信交换机和死信队列,并设置死信路由key
@Bean
public Binding dead_binding(){
return BindingBuilder.bind(dead_queue()).to(dead_exchange()).with("error").noargs();
}
//创建普通队列并绑定死信交换机
@Bean
public Queue queue(){
return QueueBuilder.durable("queue")
//设置消息的存活时间
.withArgument("x-message-ttl",30000)
//设置最大消息长度
.withArgument("x-max-length",10)
.withArgument("x-dead-letter-exchange","dead_exchange")
.withArgument("x-dead-letter-routing-key","error")
.build();
}
//创建普通消息交换机
@Bean
public Exchange exchange(){
return ExchangeBuilder.directExchange("exchange")
.durable(true)
.build();
}
//绑定普通队列及普通交换机并设置路由key
@Bean
public Binding binding(){
return BindingBuilder.bind(queue()).to(exchange()).with("error").noargs();
}
}
(3)创建消息产生者:
@SpringBootTest(classes = SiXingApp.class)
public class SiXingTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void kfjkfj(){
for (int i = 0; i < 15; i++) {
rabbitTemplate.convertAndSend("exchange","error","nfiwjfjw");
}
}
}
超过设置队列长度的消息会直接进入死信队列。
存在于普通队列中的消息超过存活时间未被消费后会进入死信队列。
8、延迟队列
8.1、为什么要使用延迟队列?一般的使用场景有哪些?
(1)为什么使用延迟队列:
例如订单下单之后30分钟后,如果用户没有付钱,则系统自动取消订单。
(2)使用场景:
这样类似的需求是我们经常会遇见的问题。最常用的方法是定期轮训数据库,设置状态。在数据量小的时候并没有什么大的问题,但是数据量一大轮训数据库的方式就会变得特别耗资源。当面对千万级、上亿级数据量时,本身写入的IO就比较高,导致长时间查询或者根本就查不出来。此时通过使用延迟队列来解决这种问题。
8.2、例如订单系统:
(1)给消息队列设置30分钟的存活时间但不绑定消费者(当30分钟过后,消息会进入死信队列)
(2)对死信队列中的消息进行判断是否支付。
如果未支付则代表支付超时,直接取消订单,并回滚库存。
如果支付了则代表已付款,则库存系统什么都不需要做,只需要商家确定订单并发货即可
模拟小案例:
(1)使用图形化界面设置队列存活时间
(2)创建消息的产生者
@Test
public void testDeadQueue() {
rabbitTemplate.convertAndSend("exchange", "info", "下单成功。");
}
(3)创建消费者:
@Component
public class MyListener {
@RabbitListener(queues = "dead_queue")
public void listener(Message message, Channel channel){
System.out.println("超时没有处理订单,取消订单");
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
} catch (IOException e) {
try {
channel.basicNack(message.getMessageProperties().getDeliveryTag(),true,true);
} catch (IOException ioException) {
}
}
}
}
9、消息的幂等性
9.1、什么是消息的幂等性?
无论执行多少次,得到的结果和第一次都是相同的。
即保证消息不被重复消费
例如:
(1)当我们下达一个订单后,消息发送到消费者需要将id=2的商品库存减10.
(2)消费者接受消息,并将库存减少了,后像库存系统发送确认消息(表示库存已减少,即此消息已被消费)
(3)因为网络原因或其他原因,确认的消息未及时回调到队列,此时队列认为消息未被消费,因此向消费者再次发送此条减库存的请求。造成库存多次减少(即同一条消息多次消费)。
解决方法:
//消费端
@Component
public class MyListener {
@Autowired
private RedisTemplate redisTemplate;
@RabbitListener(queues = "queue")
public void listener(Message msg, Channel channel) throws Exception{
long deliveryTag = message.getMessageProperties().getDeliveryTag();
//根据消息的唯一标识获取缓存,如果有则代表此消息以及被处理,如果没有则需要处理该消息
Object o = redisTemplate.opsForValue().get(deliveryTag);
if(o==null){
//业务代码
try {
System.out.println("完成业务功能");
//设置消息唯一标识作为key的缓存
redisTemplate.opsForValue().set(deliveryTag, "slz");
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), true);
}catch (Exception e){
channel.basicNack(msg.getMessageProperties().getDeliveryTag(),true,true);
}
}else{
channel.basicAck(msg.getMessageProperties().getDeliveryTag(),true);
}
}
}