前言
这段时间项目中遇到了客户提出的新的功能,需要当用户参加志愿者活动的时候,在改活动开始的前5分钟和后10分钟推送一条微信消息到用户的微信上。其实这样的延时操作的场景还有很多,比如常见的电商系统里面的30分钟订单为支付就关闭这种功能。
然后我进行技术方案的调研,发现目前能满足该业务场景方案主要有以下几种:
- 轮询数据库表 ,构建消息之后存放到数据库中,然后开启一个每分钟执行定时任务扫描数据库表中,活动开始时间在当前时间的后5分钟和活动的结束时间在当前时间的后10分钟。这种方式实现简单,但是对数据库的压力较大(最次)
- 基于JDK中的DelayQueue,Java中自带了一个延时队列的功能,通过实现
Delayd
接口可以实现自定义的延时逻辑,非常简单。但是消息数据没有吃就会,当发生了故障宕机了之后,这些消息就不存在了。不过可以考虑将消息持久化到数据库中,同时使用延时队列,当消费成功之后删除数据库中的消息。如果应用宕机了,在重启的时候将满足时间要求的消息重新投放到延时队列中。(较次) - 基于Redis的Key过期通知,用户订阅消息之后,将消息存放到Redis中然后设置Key的过期时间,服务端开启一个线程监听Redis Key过期事件的回调。这种方式实现简单,利用Redis本身提供过的特性,但是Key过期的回调中并不能获取到该Key对应的Value,所以还需要在Redis中冗余一份Key+“特殊字符”->Value映射的一个键值对,这样对导致不必要的键值对存在。(较次)
- 基于RabbitMQ的延时队列,RabbitMQ通过一个普通队列和一个死信队列可以实现一个延时队列的功能,首先将消息设置一个过期时间投放到普通队列中,这个普通队列没有消费者,那么当消息过期后该消息将会被转移到死信队列中,开启一个消费者消费死信队列中的数据即可;默认RabbitMQ是不支持延时队列的,不过提供了延时队列的插件可以集成到RabbitMQ,集成的过程非常简单,百度一下,你就知道哦。(好)
下面我将会把上面的四种方式都实现一遍(除了第一种),然后各位体会一下不同实现的方式的优越点
各种实现
基于JDK的DelayQueue
public class DelayedMessage implements Delayed {
//微信的用户Id
private final String openId;
//活动名称
private final String activityName;
private final Long expireTime;
public DelayedMessage(String openId, String activityName, long expireTime) {
this.openId = openId;
this.activityName = activityName;
this.expireTime = expireTime;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime, TimeUnit.NANOSECONDS) - unit.convert(System.currentTimeMillis(), TimeUnit.NANOSECONDS);
}
@Override
public int compareTo(Delayed o) {
DelayedMessage message = (DelayedMessage) o;
return this.expireTime.compareTo(message.getExpireTime());
}
public String getOpenId() {
return openId;
}
public String getActivityName() {
return activityName;
}
public Long getExpireTime() {
return expireTime;
}
public static void main(String[] args) throws InterruptedException {
DelayQueue<DelayedMessage> delayQueue = new DelayQueue<>();
//投放消息线程
new Thread(() -> {
DelayedMessage message1 = new DelayedMessage("微信用户1", "打扫门前雪活动",
System.currentTimeMillis() + 1000 * 10); //10S后过期
delayQueue.put(message1);
System.out.println("投放消息:" + message1 + " 投放时间:" + LocalDateTime.now());
}).start();
//消费消息线程
new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
DelayedMessage delayedMessage = null;
try {
delayedMessage = delayQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("获取到消息:" + delayedMessage