延时队列作用
场景:订单超时未支付,取消订单,恢复库存
可以将创建的订单加入redis延时队列,开一个单独的线程轮循处理过期订单
注意,如果订单已经支付,需要在延时队列中删除该订单记录
延时队列实现思路
延时队列可以通过 Redis 的 zset (有序列表)来实现。我们将消息序列化成一个字符串作为 zset 的 value,
这个消息的到期处理时间作为 score,然后用多个线程轮询zset 获取到期的任务进行处理 。
多个线程是为了保障可用性,万一挂了一个线程还有其他线程可以继续处理。
因为有多个线程,所以需要考虑并发争抢任务,确保任务不会被多次执行。
延时队列实现代码
import java.lang.reflect.Type ;
import java.util.Set ;
import java.util.UUID ;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference ;
import redis.clients.jedis.Jedis;
public class RedisDelayingQueue<T> {
static class Taskitem<T> {
public String id;
public T msg;
}
private Jedis jedis;
private String queueKey;
private Type TaskType = new TypeReference<Taskitem<T>>(){}.getType(); //自定义序列化转换对象
public RedisDelayingQueue(Jedis jedis, String queueKey) {
this.jedis = jedis;
this.queueKey = queueKey;
}
//延时任务的时间单位为秒
public void delay(T msg, Integer delayTime){
Taskitem<T> task = new Taskitem<T>();
task.id = UUID.randomUUID().toString();
task.msg = msg;
String s = JSON.toJSONString(task);
jedis.zadd(queueKey, System.currentTimeMillis() + delayTime * 1000, s);
}
public void loop(){
while (true || !Thread.interrupted()){
Set<String> values = jedis.zrangeByScore(queueKey, 0, System.currentTimeMillis(), 0, 1);
if (values.isEmpty()){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
break;
}
continue;
}
String s = values.iterator().next();
if (jedis.zrem(queueKey, s) > 0){
Taskitem<T> taskitem = JSON.parseObject(s, TaskType);
handleMsg(taskitem.msg);
}
}
}
public void handleMsg(T msg){
System.out.println(msg);
}
public static void main(String[] args) {
//连接redis
Jedis jedis = new Jedis("localhost");
jedis.auth("123456");
RedisDelayingQueue<String> queue = new RedisDelayingQueue<>(jedis, "delayQueue");
//生产者
Thread product = new Thread(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
queue.delay("redis" + i, 5);
}
}
};
//消费者
Thread consume = new Thread(){
@Override
public void run() {
queue.loop();
}
};
product.start();
consume.start();
try {
product.join();
Thread.sleep(6000);
consume.interrupt(); //关闭消费者线程
consume.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
延时队列优化
在上面的算法中,同一个任务可能会被多个进程取到之后再使用 zrem 进行争抢,那些没抢到的进程都白取了一次任务,这是浪费。
可以考虑使用 lua scripting 来优化一下这个逻辑,将 zrangebyscore 和 zrem 一同挪到服务器端进行原子化操作,
这样多个进程之间争抢任务时就不会出现这种浪费了。