需求背景

在开发场景中,如预约的签到,用户会有不同的预约签到时间,例如:7:00、7:30、8:00。

对于这种场景,使用定点轮询查表的方式,一方面时间颗粒度需要严格的控制,另一方面在数据库的记录一多就很浪费性能。所以可以考虑使用 MQ 的过期队列来实现功能。

核心思路

基于 RabbitMQ 的死信队列:

  1. 将消息的存活时间设置为”从当前时间到目标判断截止时间”的时间差
  2. 消息过期后自动转入死信队列
  3. 在项目中编写方法监听死信队列,触发业务逻辑

这样就实现了动态的过期时间设置,无需轮询数据库。

配置实现

1. 定义队列与死信队列

配置一个正常队列,设置一个极大的默认过期时间(如 24h),并绑定死信交换机:

1
2
3
4
5
6
7
8
9
@Bean
public Queue queueLog() {
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", TTL_MAX); // 消息最大存活时间 24h
args.put("x-dead-letter-exchange", EXCHANGE_DLX_LOG); // 死信交换机
args.put("x-dead-letter-routing-key", ROUTING_KEY_LOG); // 死信路由键

return QueueBuilder.durable(QUEUE_LOG).withArguments(args).build();
}

TTL_MAX 设置为 24h(注意单位是毫秒,24 * 60 * 60 * 1000)。

配置流程:

  1. 创建一个正常的交换机(Exchange)
  2. 创建一个正常的队列(Queue),绑定到正常交换机
  3. 定义一个死信交换机(DLX)和死信队列(DLQ)
  4. 将死信队列绑定到死信交换机
  5. 将正常队列通过参数绑定到死信交换机(第 2 步的 args

2. 发送消息时动态设置过期时间

计算当前时间到目标时间的差值,作为消息的过期时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Autowired
private AmqpTemplate rabbitTemplate;

public void sendLogMessage(String message) {
QueryWrapper<ReservationsDTO> qw = new QueryWrapper<>();
qw.eq("reservation_id", message);
ReservationsDTO dto = reservationsMapper.selectOne(qw);

// 计算当前时间到 startTime 的时间差
LocalDateTime startTime = dto.getStartTime();
Duration duration = Duration.between(LocalDateTime.now(), startTime);

if (duration.isNegative()) {
log.error("签到时间已过: {}", message);
duration = Duration.ZERO;
}

long expirationTimeMillis = duration.toMillis();

rabbitTemplate.convertAndSend(RabbitMQ.EXCHANGE, message, msg -> {
msg.getMessageProperties().setExpiration(String.valueOf(expirationTimeMillis));
return msg;
});
}

如果需要路由键:

1
2
3
4
rabbitTemplate.convertAndSend(RabbitMQ.EXCHANGE, "路由键", message, msg -> {
msg.getMessageProperties().setExpiration(String.valueOf(expirationTimeMillis));
return msg;
});

3. 消费死信队列

在消费者中监听对应的死信队列,正常编写业务逻辑即可。

注意事项

  • 设置的单个消息过期时间**不要超过队列的 x-message-ttl**(即 24h),所以在设计时要考虑极端情况的最大间隔
  • 时间差为负数时兜底处理为 Duration.ZERO,防止消息永远不过期
  • setExpiration 接收的是字符串类型的毫秒数

小结

  • 利用死信队列 + 动态 TTL,可以优雅地实现定时触发,避免轮询数据库
  • 核心是三个配置:正常队列绑定死信交换机、设置队列最大 TTL、发送时动态计算过期时间
  • 适用于预约签到、订单超时取消等场景

参考:CSDN - JAVA-MQ设置动态的过期队列