开发项目中,我们可能存在这个场景。比如订单创建30分钟未支付自动超时取消,也可能存在调用第三方接口不是实时响应结果,需要间隔一些时间获取处理状态。往往解决这些场景的技术手段无外乎于两种,1 定时任务调度 2 延时队列。鉴于定时任务实时性不好控制,往往使用延时队列来实现处理。
JDK 自带延时队列,致命缺点一重启信息就丢失不能持久化。借助消息中间件rabbitmq也可以实现延时队列。
rabbimq实现延时队列有两种方式
第一种方式:死信队列: Time To Live(TTL)、Dead Letter Exchanges(DLX)
第二种方式:插件x-delay-message
这里不过多描述具体实现,只记录一下自己遇到的问题
死信队列方式,在仅对队列设置过期时间或对每个消息设置的延时时间相同的场景下,该实现是没有问题的。原因死信队列方式还是基于队列实现,当检测到第一个消息没有过期,便不会继续检查之后的消息,于是就算第二个消息时间到了,也不会消费消息会一直堆积。
那么插件方式就没有问题了吗?
rabbitTemplate.convertAndSend(DELAY_EXCHANGE,
API_ALERT_QUEUE, msg, message -> {
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
message.getMessageProperties().setDelay(next.intValue()); // 毫秒为单位,指定此消息的延时时长
return message;
})
插件方式是设置一个int类型的时间毫秒数,经测试当时间过长例如一个月后。计算出来的时间差long转int后变成负数,此时延时队列失效,立马发送消息。
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
里面有一句
For each message that crosses an “x-delayed-message” exchange, the plugin will try to determine if the message has to be expired by making sure the delay is within range, ie: Delay > 0, Delay =< ?ERL_MAX_T (In Erlang a timer can be set up to (2^32)-1 milliseconds in the future).
可以得到验证。
综上所述,只能借助于redis zset 数据结构实现一个简易版的,实时性差了一点
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import redis.clients.jedis.JedisCluster;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author One
* @version v1.0
* @description
* @date 2021/10/22 12:37
*/
@Data
@Slf4j
public class RedisDelayQueue {
private RedisDelayQueue() {
}
private String queueName;
private JedisCluster jedisCluster;
private ScheduledThreadPoolExecutor scheduled;
private IDelayMessageHandle delayMessageHandle;
private long period;
private TimeUnit unit;
public RedisDelayQueue(String queueName, JedisCluster jedisCluster,
IDelayMessageHandle delayMessageHandle,
long period,
TimeUnit unit) {
this.queueName = queueName;
this.jedisCluster = jedisCluster;
this.delayMessageHandle = delayMessageHandle;
this.period = period;
this.unit = unit;
scheduled = new ScheduledThreadPoolExecutor(1);
scheduled.scheduleAtFixedRate(() -> {
try {
handleData();
} catch (Exception e) {
log.error("执行redis延时队列出错", e);
}
}, 0, period, unit);
}
private String getDetailKey(String id) {
return this.queueName + ":detail:" + id;
}
public void add(DelayMessage msg) {
String uuid = UUID.randomUUID().toString().replace("-", "");
if (jedisCluster.zadd(this.queueName, System.currentTimeMillis() + msg.getDelay(), uuid) > 0) {
long time = (msg.getDelay()/1000)+unit.toSeconds(period)+10;
jedisCluster.set(getDetailKey(uuid), msg.getBody(), "NX", "EX", time);
}
}
private void handleData() {
log.debug("RedisDelayQueue:{}扫描任务开始", this.getQueueName());
String lockKey = this.queueName + ":lock";
if (!"OK".equalsIgnoreCase(jedisCluster.set(lockKey, String.valueOf(System.currentTimeMillis()), "NX", "EX", unit.toSeconds(period)))) {
log.debug("RedisDelayQueue:{},获取锁失败,跳过", this.queueName);
return;
}
try {
Set<String> taskIdSet = jedisCluster.zrangeByScore(this.queueName, 0, System.currentTimeMillis());
if (CollectionUtils.isEmpty(taskIdSet)) {
log.debug("RedisDelayQueue:{},暂无延时任务", this.queueName);
return;
}
for (String id : taskIdSet) {
String detailId = getDetailKey(id);
String body = jedisCluster.get(detailId);
if (!StringUtils.isBlank(body)) {
jedisCluster.del(detailId);
this.delayMessageHandle.execute(body);
}
jedisCluster.zrem(this.getQueueName(), id);
}
} finally {
jedisCluster.del(lockKey);
}
log.debug("RedisDelayQueue:{}扫描任务结束", this.getQueueName());
}
}
@FunctionalInterface
public interface IDelayMessageHandle {
void execute(String body);
}
@Data
public class DelayMessage {
/**
* 消息延迟/毫秒
*/
private long delay;
/**
* 消息体,对应业务内容
*/
private String body;
}