基本知识:
重要组件
- 生产者: 生产消息并选择发送给,某个exchange(交换机),并指定路由键,路由键决定了投递给哪个队列
- 交换机: 首先交换机本身通过路由键绑定多个队列,接收到消息后根据路由选择将消息发送到哪个队列
- 队列: 队列接收到消息
- 消费者: 消费者监听队列,获得消息
消息路由
- direct :生产者发送消息时的路由键 和交换机投递消息给队列时的路由键完全一致才会进行投递,多个队列只会消费一条信息
- fanout:生产者发送的消息,会投递给交换机绑定的所有队列,每个队列都会获得一份信息
- topic:和direct基本一致,区别在于路由键不再要求完全一致,类似正则匹配,"#“匹配0个和多个单词,”*"匹配一个单词
- headers:极少使用
安装
docker安装
1.docker pull rabbitmq:3.8-management
2.docker run -p 5672:5672 -p 15672:15672 --name rabbitmq -d rabbitmq:3.8-management
登录 ip:5672,输入guest,guest即可
springboot简单整合
添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
配置文件
如果不在本地安装的话,这里配置一下ip地址和端口号
spring:
rabbitmq:
host: new ip
创建并绑定交换机,队列
创建交换机
@Autowired
AmqpAdmin amqpAdmin;
@Test
void createExchange() {
/**
name : 交换机的名字
durable: 是否持久化
autoDelete: 当交换机上绑定的队列都消失,自动删除,如果是初次创建的话不会触发自动删除机制
**/
Exchange exchange = new TopicExchange("hello-topic-exchange", true, false);
amqpAdmin.declareExchange(exchange);
}
创建队列
@Autowired
AmqpAdmin amqpAdmin;
@Test
void createQueue() {
/**
name : 队列的名字
durable :是否持久化
exclusive : 1.只对首次声明它的连接(Connection)可见 2.会在其连接断开的时候自动删除
autoDelete :没有消费者监听队列后,自动删除
*/
Queue queue = new Queue("hello-topic-queue", true, false, false);
amqpAdmin.declareQueue(queue);
}
绑定交换机和队列
@Autowired
AmqpAdmin amqpAdmin;
@Test
void createBinding() {
/**
* destination 想绑定的队列或者交换机的名字
* destinationType 绑定类型(交换机可以和队列/也可以和交换机绑定)
* exchange 想绑定上的某交换机的名字
* routingKey 路由键
*/
Binding binding = new Binding("hello-topic-queue",
Binding.DestinationType.QUEUE,
"hello-topic-exchange",
"hello.java",null);
amqpAdmin.declareBinding(binding);
}
也可以通过另一种方法绑定:
@Test
void createAll(){
TopicExchange exchange = new TopicExchange("hello-topic-exchange", true, false);
amqpAdmin.declareExchange(exchange);
Queue queue = new Queue("hello-topic-queue", true, false, false);
amqpAdmin.declareQueue(queue);
Binding binding = BindingBuilder.bind(queue).to(exchange).with("hello.java");
amqpAdmin.declareBinding(binding);
}
发送接收消息
使用json方式传递对象
实体类
这里重要的是无参构造器和序列化
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Book implements Serializable {
private String name;
private String author;
private Double price;
}
MessageConverter
@Configuration
public class MyRabbitConfig{
@Bean
public MessageConverter MessageConverter() {
return new Jackson2JsonMessageConverter();
}
}
发送消息
@RestController
@RequestMapping("mq")
@Slf4j
public class SendController {
@Autowired
private RabbitTemplate template;
@GetMapping
public String send() {
log.info("开始给mq发送消息");
Book book = Book.builder().name("java编程思想").price(88.5d).author("埃克尔").build();
template.convertAndSend("hello-topic-exchange","hello.java",book);
log.info("给mq发送消息结束");
return "success";
}
}
接收消息
@RabbitListener(queues = "hello-topic-queue")
public void receiver(Book book, Message message, Channel channel) throws IOException {
System.out.println(book.toString());
}
@RabbitHandler
消息确认
生产者===>交换机确认
配置文件
spring.rabbitmq.publisher-confirm-type=correlated
回调函数
template.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
if (ack) {
log.info("correlationData:{} 投递成功", correlationData);
} else {
log.info("correlationData:{} ,投递失败,,reason:{}", correlationData, s);
}
}
});
交换机===>队列确认
配置文件
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true
回调函数
注意此回调函数只有在交换机投递队列失败的情况下才会调用
template.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.info("消息由交换机转发至队列的时候失败\n 返回码:{},回复文本:{}\n路由键:{},交换机:{}\n信息:{}",
returnedMessage.getReplyCode(), returnedMessage.getReplyText(),
returnedMessage.getRoutingKey(), returnedMessage.getExchange(),
returnedMessage.getMessage());
}
});
队列 ===>消费者确认
配置文件
spring.rabbitmq.listener.simple.acknowledge-mode=manual
回调函数
注意此回调函数只有在交换机投递队列失败的情况下才会调用
@RabbitListener(queues = "hello-topic-queue")
public void receiver(Book book, Message message, Channel channel) throws IOException {
System.out.println(book.toString());
log.info("消费得到数据");
//第二个参数代表批量操作,deliveryTag是rabbitmq自动打上的一个自增的标签,当选择可以批量操作的时候,意味着本消息,以及本消息之前的编号的消息都能接收
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
//basicNack 表示拒绝数据,第二个参数代表 批量操作 第三个参数代表拒绝的数据是否重新入队
//basicReject :和basicNack功能一样,只是不能批量操作
// channel.basicNack(deliveryTag,false,false);
// channel.basicReject(deliveryTag,true);
}
Only one ConfirmCallback is supported by each RabbitTemplate
此错误的原因很清楚,单个rabbitTemplate只允许一个ConfirmCallback对象进行处理,所以问题很好解决:
ConfirmCallBackService.java
@Slf4j
@Component
public class ConfirmCallBackService implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
if (ack) {
log.info("correlationData:{} 投递成功", correlationData);
} else {
log.info("correlationData:{} ,投递失败,,reason:{}", correlationData, s);
}
}
}
ReturnCallBackService.java
@Slf4j
@Component
public class ReturnCallBackService implements RabbitTemplate.ReturnsCallback {
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.info("消息由交换机转发至队列的时候失败\n 返回码:{},回复文本:{}\n路由键:{},交换机:{}\n信息:{}",
returnedMessage.getReplyCode(), returnedMessage.getReplyText(),
returnedMessage.getRoutingKey(), returnedMessage.getExchange(),
returnedMessage.getMessage());
}
}
Controller
@Autowired
ConfirmCallBackService confirmCallBackService;
@Autowired
ReturnCallBackService returnCallBackService;
@GetMapping("/queue")
public String sendTo(){
Book book = Book.builder().name("java编程思想").price(88.5d).author("埃克尔").build();
template.convertAndSend("hello-topic-exchange", "hello.java", book, new CorrelationData(UUID.randomUUID().toString()));
template.setConfirmCallback(confirmCallBackService);
template.setReturnsCallback(returnCallBackService);
return "success";
}
延迟队列
原理
首先我们了解几个原理:
队列中的消息怎么成为死信:
- 原队列消息长度到达限制
- 原队列存在消息过期设置,消息到达超时时间未被消费
- 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false
消息成为死信后如何投递到死信队列中:
- 给原队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key
之后我们就可以通过这些原理来构建延迟队列了:
信息投递==>延迟交换机===>死信队列===>延时===>死信交换机=>延时队列==>消费者监听
代码
@Test
void createDelay() {
String delayExchangeName="delay-exchange";
String delayQueueName="delay-queue";
String deadExchangeName="dead-exchange";
String deadQueueName="dead-queue";
String queueToExchangeKey="delay.queue.to.dead.exchange";
String productToExchangeKey="product.to.exchange";
// 创建延时交换机
DirectExchange delayExchange = new DirectExchange(delayExchangeName, true, false);
amqpAdmin.declareExchange(delayExchange);
// 创建死信队列
Map<String, Object> args = new <String, Object>HashMap(16);
//设置消息过期之后的死信交换机(真正消费的交换机)
args.put("x-dead-letter-exchange", deadExchangeName);
//设置消息过期之后死信队列的路由(真正消费的路由)
args.put("x-dead-letter-routing-key",queueToExchangeKey);
//设定消息的TTL,单位为ms,在这里指的是60s
args.put("x-message-ttl", 60000);
Queue deadQueue = new Queue(deadQueueName, true, false, false,args);
amqpAdmin.declareQueue(deadQueue);
//创建死信交换机
DirectExchange deadExchange = new DirectExchange(deadExchangeName, true, false);
amqpAdmin.declareExchange(deadExchange);
//创建延时队列
Queue delayQueue = new Queue(delayQueueName, true, false, false);
amqpAdmin.declareQueue(delayQueue);
//将延时交换机和死信队列绑定
Binding binding = BindingBuilder.bind(deadQueue).to(delayExchange).with(productToExchangeKey);
amqpAdmin.declareBinding(binding);
//将死信交换机和延时队列绑定
Binding deadBinding = BindingBuilder.bind(delayQueue).to(deadExchange).with(queueToExchangeKey);
amqpAdmin.declareBinding(deadBinding);
}
之后我们按照之前的布置配置生产者和消费者,就可以实现延时队列这一功能了
信息TTL
上述代码中,我们使用了队列的TTL,当超过60s消息自动过期,之后投递给死信交换机,在生产者投递信息的时候我们还可以设定消息的TTL,当消息TTL达到设定之后,可以提前进入死信交换机
template.convertAndSend("delay-exchange", "product.to.exchange", book, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//这里设定为30s
message.getMessageProperties().setExpiration("30000");
return message;
}
});
经过测验,30s之后此消息就会被消费