基本介绍
我们经常使用的美团点外卖,如果我点外卖下了订单,但是没有进行支付,如果超过半小时还没有支付的话。系统就会把我们的订单进行取消了。这其实是延时队列的使用场景。
我们最近项目中也遇到类似的需求场景:客户下完订单之后,商务人员如果超过五分钟还没审核订单的话,就让系统自动审核通过。
针对我们的这个需求场景,我们使用redis实现延时队列来完成。
步骤
在下订单成功后,将订单信息添加到redis的zset数据结构中,取订单信息作为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中查找对应的key的score值大于等于min并且小于等于max的value,接着用limit进行分页取出满足条件的数据。 -
接着将取出的
value值赋值给expiredValues数组,然后判断数组里面如果有元素的话,再接着用zrem key, value1, value2...命令将对应的value从redis中删除(unpack是将数组expiredValues转为一个一个的元素) -
执行完删除之后,最终使用
table.concat(expiredValues,""+SPILT+"")将value使用连接符,连接成字符串。