场景:
在项目中
1.用户确认一个订单,若30分钟之类没有支付,则需要取消订单,若用定时任务去扫描订单表,第一,定时任务时间如何定义,有存在漏扫的风险,第二,订单表数据庞大,扫描表非常消耗性能,这时候该功能可以引入RabbitMQ延迟队列来做
2.某条活动通知在指定的一天推送给用户,可以用延迟队列
定义:
延迟队列即发送一条消息给目标队列,并非让目标队列立即接受到消息,而是让消息等待一段延迟时间才到达目标队列让它消费
RabbitMQ本身没有直接支持延迟队列功能,我们可以利用它的DXL特性来扩展
rabbitMQ的queue可配置x-dead-letter-exchange和x-dead-letter-routing-key两个参数,意为如果队列中出现了dead letter,则按照这两个参数重新路由转发到指定队列
x-dead-letter-exchange : 出现dead letter之后,将dead letter重新发送到exchange
x-dead-letter-routing-key : 出现dead letter之后,将dead letter重新按照指定的routing-key发送
队列出现dead letter情况有:
|
更多rabbitMQ延迟队列知识,推荐文章
由此可知,设置TTL规则之后,当消息在一个队列中变成死信之后,利用DLX特性,它能重新被转发到另一exchange或routing-key,从而被重新消费
撸码:
在之前的文章<RabbiMQ工作原理及简单使用>中,我们说过,rabbitMQ的消息生成器是不直接将消息发送到队列,而是发送到交换器,交换器可以转发到单个或多个队列,由此,我们需要两个队列、两个交换机
queue1: 死信队列,将消息发送到死信exchange,绑定此队列,让消息进入该队列成为死信,设置TTL过期时间,该队列没有消费者,等待时间过期进入dead letter,当queue1队列有死信产生时,会转发到交换器x-dead-letter-exchange,以路由键x-dead-letter-rouuting-key转发到指定queue
queue2: 转发队列,也是消息最终消费的目标队列,此队列需要从死信队列接收消息,所以需要绑定死信转发到的交换器x-dead-letter-exchange
fanout sender代码集成, MessagePostProcessor消息处理器是个functional接口
/**
* 延迟发送消息队列,客户端发目标队列发送一条延迟消息 : {"queue":"queueName","body":"message"}
* 此时,将消息发送到DXL死信队列,而非直接发送到queueName队列,并设置延迟时间 times 秒
* <p>
* 死信队列没有消费者,它用来存储超时的消息,并转发到另一队列,转发队列等待消息延迟之后接收到消息,
* 已过了times 秒,处理业务逻辑
*
* @param payload 消息体
*/
public void sendDelayMessage(DelayMessagePayload payload) {
CorrelationData correlation = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend(payload.getDeadExchange(), payload, message -> {
message.getMessageProperties().setExpiration(payload.getDelayTime() + "");
return message;
}, correlation);
log.info("fanout send delay message success exchange:{}, message:{}, correlationId:{}",payload.getDeadExchange(),
JsonUtils.obj2json(payload), correlation.getId());
}
消息体:
@AllArgsConstructor
@NoArgsConstructor
@Data
public class DelayMessagePayload implements Serializable{
private String forWardExchange;
private String forWardQueue;
private String deadExchange;
private String deadQueue;
private Object message;
private long delayTime;
}
客户端:
DelayMessagePayload payload = new DelayMessagePayload(RabbitConstant.FanoutExchange.SECKILL_DEAD_LETTER,
RabbitConstant.FanoutQueue.SECKILL_FORWARD, RabbitConstant.FanoutExchange.SECKILL_DEAD_LETTER,
RabbitConstant.FanoutQueue.SECKILL_DEAD_LETTER,
"123456789",10 * 1000);
fanoutSender.sendDelayMessage(payload);
初始化队列和交换机:
@Configuration
public class SeckillTimeOutPayConfig {
/**
* 初始化死信队列
*/
@Bean
public Queue initSeckillDead(){
Map<String,Object> arguments = new HashMap<>(2);
arguments.put("x-dead-letter-exchange", RabbitConstant.FanoutExchange.SECKILL_DEAD_LETTER);
arguments.put("x-dead-letter-routing-key",RabbitConstant.FanoutQueue.SECKILL_FORWARD);
return new Queue(RabbitConstant.FanoutQueue.SECKILL_DEAD_LETTER,true,false,false,arguments);
}
@Bean
public FanoutExchange initDeadExchange(){
return new FanoutExchange(RabbitConstant.FanoutExchange.SECKILL_DEAD_LETTER);
}
@Bean
public Binding bindDead(){
return BindingBuilder.bind(initSeckillDead()).to(initDeadExchange());
}
/**
* 初始化转发队列
*/
@Bean
public Queue initForwardQueue(){
return new Queue(RabbitConstant.FanoutQueue.SECKILL_FORWARD);
}
@Bean
public Binding bindForwardExchange(){
return BindingBuilder.bind(initForwardQueue()).to(initDeadExchange());
}
}
消费监听:
@RabbitListener(queues = RabbitConstant.FanoutQueue.SECKILL_FORWARD)
@Component
@Slf4j
public class SeckillTimeOutPayReceive {
@RabbitHandler
public void test(DelayMessagePayload payload){
try {
log.info("收到消息:{}", JsonUtils.obj2json(payload));
}catch (Exception e){
}
}
注意: 消息队列是先进先出的栈列结构,也就意味着,如果先进去的延迟时间比后进去的延迟时间要大,那么会一直阻塞等待先进去的消费,后面的就消费不到了,需要将延迟时间一致的业务放入同一队列即可