场景
在日常购物平台中,如果某个订单超过30分钟还未支付成功,系统就会将该订单取消,并进行库存回滚,本实例就使用RabbitMQ提供的两大特性:延迟队列和死信队列 来模拟这一应用场景,但实际上RabbitMQ并没有提供真正意义上的延迟队列,这句话什么意思呢?简单来讲就是RabbitMQ任意队列都没有方法直接指定消息被消费的时间(一般消息到达队列时,就会立马被路由到与之绑定的消费者,而不是说该消息到队列之后,等待30分钟再路由到消费者,目前RabbitMQ是不支持这种功能的),但是我们可以通过设置消息的一个过期时间,来达到模拟效果。在这以前,需要先了解一下死信消息的概念
死信消息
顾名思义,死信消息就表示该条消息已经死掉了,无法继续被当前队列路由到消费者消费了,这时RabbitMQ就会将该条消息发送到死信队列里面,消息成为死信有三种情况:
1、队列消息长度到达限制;
2、消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
3、原队列存在消息过期设置,消息到达超时时间未被消费;
直接来看代码演示:
定义死信队列
@Configuration
public class DeadConfig {
/**
* 参数明细
* 1、name 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
@Bean
public Queue deadQueue() {
return new Queue("deadQueue", false);
}
/**
* 这种声明一个直连交换机
* 参数明细:
* 1、name,交换机名称
* 2、durable 是否持久化,如果持久化,mq重启后交换机还在
* 3、autoDelete 自动删除,交换机不再使用时是否自动删除此交换机
*/
@Bean
public DirectExchange deadExchange() {
return new DirectExchange("deadExchange", false, false);
}
/**
* 将死信队列与交换机进行绑定,路由键为: dead_route_key
*/
@Bean
public Binding bindDeadQueue() {
return BindingBuilder.bind(deadQueue()).to(deadExchange()).with("dead_route_key");
}
}
从上述代码可以看出,死信队列本质其实就是一个普通的队列,只不过将其定义成死信队列,专门来接收那些死信消息
定义延迟队列
@Configuration
public class WorkConfig {
@Bean
public Queue workQueue() {
Map<String, Object> map = new HashMap<>();
/**
* 绑定死信交换机
* <p><p/>
* x-dead-letter-exchange , value = '死信交换机的名字'
* x-dead-letter-routing-key value='路由key'
*/
map.put("x-dead-letter-exchange", "deadExchange");
map.put("x-dead-letter-routing-key", "dead_route_key");
Queue workQueue = new Queue("workQueue", false, false, false, map);
return workQueue;
}
@Bean
public DirectExchange workExchange() {
return new DirectExchange("workExchange", false, false);
}
@Bean
public Binding bindWorkQueue() {
return BindingBuilder.bind(workQueue()).to(workExchange()).with("work_route_key");
}
}
通过上述代码可以看出,跟死信队列定义相差不大,需要注意的是:这里需要为该延迟队列绑定一个死信队列,当该延迟队列中消息成为死信时,会将该条死信消息重新转发到死信队列。
发送消息
用户下单以后,就往工作队列发送一条订单消息,并设置该条订单消息过期时间为30分钟
@RestController
public class Publish {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendWorkMessage")
public String sendWorkMessage() {
//设置消息属性的处理函数
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
//设置消息的过期时间,为了方便测试,这里设置为 5秒
message.getMessageProperties().setExpiration("5000");
return message;
}
};
//用户下单消息
Map<String, Object> map = new HashMap<>();
map.put("用户tom","刚刚下单了一部手机");
map.put("createTime",String.format("yyyy-MM-dd HH:mm:ss",new Date()));
//往延迟队列发送一条订单消息
rabbitTemplate.convertAndSend("workExchange", "work_route_key", map, messagePostProcessor);
return "ok";
}
}
监听死信队列
public class DeadConsumer {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel();
//监听死信队列,如果死信队列中有消息,会立马
channel.basicConsume("deadQueue",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("监听到死信队列消息:" + new String(body,"utf8"));
//这里接收到订单消息后,就可以根据订单信息去查询订单系统
//如果用户未支付,再进行后续相应的业务逻辑
}
});
}
}
至此,这里就模拟了订单30分钟如果还未支付,则可以进行相应业务需求处理
总结
以上业务流程大致为:先定义两个队列,一个作为死信队列,一个作为延迟队列,并将死信队列绑定到延迟队列上面,然后向延迟队列中发送一条订单消息,并设置消息的过期时间,最后为死信队列绑定一个消费者,当死信队列接收到消息时,说明该条订单消息已经到30分钟了,然后就可以去查询订单系统来判断用户是否下单成功。但是这里需要特别注意的是:延迟队列不能绑定消费者!延迟队列不能绑定消费者!延迟队列不能绑定消费者! 重要的事说三遍!!!,因为一旦为延迟队列绑定了消费者,那么消息到达延迟队列以后,会立马被消费掉,那么该条消息就无法成为死信消息了,也就意味着死信队列会永远接收不到消息,正确的做法是发送一条消息到延迟队列,然后让该条消息静静地等待过期时间,变成一条死信,RabbitMQ检测到死信以后,会将该条死信消息转发到绑定该延迟队列上的死信队列