目的
一些消息或任务我们可能不希望立即执行, 希望在一定时间后或者某个时间点的前一定时间执行, 即在某个时间点执行某个任务
实现方式
java API自带的DelayQueue
DelayQueue 是 Java 的一个内置类,属于 java.util.concurrent 包,专门用于处理延迟消息。它是一个支持延时获取元素的无界阻塞队列,队列中的元素必须实现 Delayed 接口,并重写 getDelay(TimeUnit) 和 compareTo(Delayed) 方法
实现原理
DelayQueue 存储实现了 Delayed 接口的元素,只有在元素的延迟时间到达时才能从队列中移除。
每个元素的延迟时间由 getDelay(TimeUnit unit) 方法返回,队列会根据此时间自动管理元素的顺序。
创建 DelayQueue 和实现 Delayed 接口:
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class DelayedMessage implements Delayed {
private final String message;
private final long delayTime; // 延迟时间
private final long createTime; // 创建时间
public DelayedMessage(String message, long delayTime) {
this.message = message;
this.delayTime = delayTime;
this.createTime = System.currentTimeMillis();
}
@Override
public long getDelay(TimeUnit unit) {
long diff = createTime + delayTime - System.currentTimeMillis();
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS));
}
public String getMessage() {
return message;
}
}
使用 DelayQueue 存储和处理消息:
DelayQueue<DelayedMessage> delayQueue = new DelayQueue<>();
// 添加延迟消息
delayQueue.offer(new DelayedMessage("Hello, delayed message!", 5000)); // 5秒后可处理
// 处理延迟消息
new Thread(() -> {
try {
while (true) {
DelayedMessage msg = delayQueue.take(); // 阻塞直到有消息可处理
System.out.println("Processing: " + msg.getMessage());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
优点
简单易用,内置支持,无需依赖外部库。
在单机环境下处理延迟任务,性能较好。
缺点
不适合分布式系统,DelayQueue单线程的。
redis的zset类型或过期监听
实现原理:
利用ZSET的score属性,将消息存入 ZSET 中,score代表消息的过期时间戳, 实现一个延迟队列。通过轮询检查 ZSET,找出过期的消息并进行处理
Redis 通过发布/订阅机制支持过期事件。通过配置 Redis,可以订阅过期事件,放入消息时设置过期时间,通过监听拿到每个键值过期的事件,实现任务到指定时间触发
ZSET优点
支持高并发 Redis 是内存数据库,延迟队列的性能高
ZSET缺点
需要轮询、内存导致消息数量限制
监听优点
无需轮询、实现简单、轻量级
过期缺点
消息不可查询、过期事件异步导致可能不精准、多次通知不支持(单key)
kafka
实现原理
kafka本身并不支持原生的延迟消息,可以通过设置一个延迟topic实现放置延迟消息,在消费消息时,可以根据消息的延迟时间来判断是否要立即处理消息还是将消息再次发送到延迟topic队列中等待下次处理
优点
Kafka的优点在于其高并发、高吞吐量和可扩展性强,同时支持分片,多个消费者并行消费、消费者组等机制
缺单
没有原生的延迟队列功能,需要使用topic和消费者组来实现,实现延迟队列需要额外的开发工作、消费者需要主动拉取数据,可能会导致延迟
rabbitMQ中间件
实现原理(插件式)
使用延迟队列插件x-delayed-message,消息被发送到一个交换机,设置消息的延迟时间xdelay,
消息在延迟时间内不会被路由到目标队列,而是在延迟时间到达后自动转移
优缺点
RabbitMQ的延迟队列是通过RabbitMQ的插件实现的,易于部署和使用
消息持久化和分布式、支持消息重试和消息顺序处理,可靠性较高、支持任意时间点的延迟
缺点
延迟队列性能依赖于rabbitMQ,不适用于高吞吐量
具体实现
配置实现
// 绑定 将队列和交换机绑定, 并设置用于匹配键:
@Bean
Binding bindingMonitorDirect() {
return BindingBuilder.bind(monitorQueue()).to(monitorExchange())
.with(DELAY_ROUTING_KEY_XDELAY_MONITOR).noargs();
}
@Bean
public Queue monitorQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("TestDirectQueue",true,true,false);
// 一般设置一下队列的持久化就好,其余两个就是默认false
return new Queue(IMMEDIATE_QUEUE_XDELAY_MONITOR, true);
}
// Direct交换机
@Bean
CustomExchange monitorExchange() {
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_XDELAY_MONITOR, "x-delayed-message", true, false, args);
}
发送接收实现
public void sendMonitorInfo(FwMQSendInfo msg, int delayTime) {
this.rabbitTemplate.convertAndSend(XdelayConfig.DELAYED_EXCHANGE_XDELAY_MONITOR,
XdelayConfig.DELAY_ROUTING_KEY_XDELAY_MONITOR, msg, message -> {
message.getMessageProperties().setDelay(delayTime);
log.info("sendMonitorDelay.delayTime=={}:延迟后时间为=={},msg信息=={}", delayTime,
DatePattern.NORM_DATETIME_FORMAT.format(
DateUtil.offsetMillisecond(new Date(), delayTime)), msg);
return message;
});
}
@RabbitListener(queues = XdelayConfig.IMMEDIATE_QUEUE_XDELAY_MONITOR)
public void getMonitorFreshWaterInfo(FwMQSendInfo msg) {
log.info("收到监控的到期消息时间:" + DatePattern.NORM_DATETIME_FORMAT.format(new Date()) + " Delay sent.");
log.info("收到监控的到期消息时间:" + msg);
fwMessageDealService.handleMonitorMessageReceived(msg);
}
阿里资源受限,延迟最多24H,并发量10000。
中继延迟方案
private static Long setMqRemindTime(Long timeDifferenceMs, FwMQSendInfo sendInfo) {
if (FwConstants.MAX_MQ_XDELAYE.compareTo(timeDifferenceMs) <= 0) {
//时间间隔大于等于最大MQ间隔,拆分间隔,保存剩余时间
Long remainingTime = timeDifferenceMs - FwConstants.MQ_XDELAY;
sendInfo.setRemainingTime(remainingTime);
return FwConstants.MQ_XDELAY;
}
sendInfo.setRemainingTime(-1L);
return timeDifferenceMs;
}
----------------------------------------------------------------
log.info("接收到一次监控MQ消息:{}", msg);
Long remainingTime = msg.getRemainingTime();
if (remainingTime != null && remainingTime > 0) {
//剩余时间大于0,说明还未到期,继续MQ延迟等待
log.info("监控中继MQ,继续延迟,sendInfo={}", msg);
long mqDelay = setMqRemindTime(remainingTime, msg);
xdelaySender.sendMonitorInfo(msg, (int) mqDelay);
return;
}