一个案例教你学会用Redis实现延时队列

766 阅读2分钟

基本介绍

我们经常使用的美团点外卖,如果我点外卖下了订单,但是没有进行支付,如果超过半小时还没有支付的话。系统就会把我们的订单进行取消了。这其实是延时队列的使用场景。

我们最近项目中也遇到类似的需求场景:客户下完订单之后,商务人员如果超过五分钟还没审核订单的话,就让系统自动审核通过。

针对我们的这个需求场景,我们使用redis实现延时队列来完成。

步骤

在下订单成功后,将订单信息添加到rediszset数据结构中,取订单信息作为zset里面的value,取当前系统时间毫秒数加上五分钟作为value对应的score

public int createOrder(WareHouseApply apply) {
    //新增订单记录
    insertOrder(apply); 
    /**
     * 提交到redis延时队列
     */
    delayedQueue.pushDelayed(new OrderDelayed(apply.getId(),1,2));
    return 0;
}


@Component
public class DelayedQueue {
    Logger log=LoggerFactory.getLogger(DelayedQueue.class);

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 添加到redis的zset数据结构中,
     * 取订单信息作为`zset`里面的`value`,取当前系统时间毫秒数加上五分钟作为`value`对应的`score`
     */
    public void pushDelayed(OrderDelayed orderDelayed){
        boolean res = redisTemplate.opsForZSet().add(AxisUtil.DELAYED_KEY,
                orderDelayed.toString(),
                System.currentTimeMillis() + AxisUtil.DELAYED);
        log.info("pushDelayed"+orderDelayed+" result:"+res);
    }
}


public class AxisUtil {

    public static final String DELAYED_KEY="led_order_delay_queue";//订单延时队列key

    public static final long DELAYED=5*60*1000;//订单延时时间,五分钟

    public static final String SPILT=",";//延时订单分隔符

    /**
     * 先用zrangebyscore 
     */
    public static final String script="local expiredValues = redis.call('zrangebyscore',KEYS[1] , 0, ARGV[1], 'limit', 0, 100);" +
            "if #expiredValues > 0 then "+
                "redis.call('zrem', KEYS[1], unpack(expiredValues));"+
                "return table.concat(expiredValues,""+SPILT+"");"+
            "end;"+
            "return nil;";//延时消费队列脚本
            
}

接着起一个定时任务,配合lua脚本扫描redis中的延时队列数据进行处理,代码如下:

@Component
public class AccessTokenScheduler {
  
    private static final Logger  logger = LoggerFactory.getLogger(AccessTokenScheduler.class);

    StringRedisTemplate redisTemplate;

    //每10秒钟拉一次延时提交的订单
    @Scheduled(fixedRate = 10000,initialDelay = 5000)
    public void dealDelayed() {
        logger.info("*********************deal delayed order******************************");
        try {
            String execute = redisTemplate.execute(RedisScript.of(AxisUtil.script,String.class),
                    new StringRedisSerializer(),
                    new StringRedisSerializer(),
                    Arrays.asList(AxisUtil.DELAYED_KEY),
                    System.currentTimeMillis()+"");
            if(!StringUtils.isEmpty(execute)){
                try {
                    List<OrderDelayed> orderDelayeds=new ArrayList<>();
                    String[] orders=execute.split(AxisUtil.SPILT);
                    for(String order:orders){
                        String[] strs=order.replace("\"","").split(":");
                        orderDelayeds.add(new OrderDelayed(strs[0],
                                Integer.valueOf(strs[1]),
                                "null".equals(strs[2])?null:Integer.valueOf(strs[2])));
                    }
                    //将订单状态自动变成已审核通过
                    dealDelayedOrder(orderDelayeds);
                }catch (Exception e){
                    logger.info(e.getMessage());
                }
            }
        }catch (Exception e){
            logger.info(e.getMessage());
        }
    }
}

lua脚本如下:

public static final String SPILT=",";//延时订单分隔符
public static final String script="local expiredValues = redis.call('zrangebyscore',KEYS[1] , 0, ARGV[1], 'limit', 0, 100);" +
        "if #expiredValues > 0 then "+
            "redis.call('zrem', KEYS[1], unpack(expiredValues));"+
            "return table.concat(expiredValues,""+SPILT+"");"+
        "end;"+
        "return nil;";//延时消费队列脚本
  • 首先通过ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]命令,从zset中查找对应的keyscore值大于等于min并且小于等于maxvalue,接着用limit进行分页取出满足条件的数据。

  • 接着将取出的value值赋值给expiredValues数组,然后判断数组里面如果有元素的话,再接着用zrem key, value1, value2...命令将对应的valueredis中删除(unpack是将数组expiredValues转为一个一个的元素)

  • 执行完删除之后,最终使用table.concat(expiredValues,""+SPILT+"")value使用连接符,连接成字符串。