概述
项目中常常需要延时触发一些操作,比如订单30min没有付款就取消订单、有些金融机构的支付接口退款没有退款通知,需要主动调用查询接口等等。这些场景有如下特点:
- 吞吐量要求不大
- 一般的消费逻辑都是查询更新或更改一条数据,对于消息重复发送/消费并不严格
- 消息消费一次后可能还需要重新加入队列再次消费(延时退款查询状态仍为退款中,需要再次查询)
基于以上几点,可以尝试用Redis的zset数据结构来实现一个延时队列(Redis Based SImple Delat queue)
接口设计
- BaseDelayQueue是一个抽象类,继承自Thread类,重写了run()方法,实现了消费流程;还定义了一个抽象方法job(),同于对外提供消费逻辑的实现,返回值为布尔值,表示消息是否要删除,为false则消息的score会被置为当前时间戳从而放到队尾。其还包含两个属性DeleyQueueConfig和ExceptionStrategy,分别用于消费参数配置和消费异常相关的配置。
- BaseQueueConfig是一个抽象类,两个属性分别指定了redis的key和一个template对象用于消费/异常处理相关的操作
- DeleyQueueConfig队列配置,继承自BaseQueueConfig。主要参数有消息延时时长delay和线程最大休眠时长maxIdle。
- ExceptionStrategy是异常配置策略,定义了一个属性openExceptionList指定是否开启异常集合。
- QueueFactory是一个工厂类,其聚合了客户端定义的所有的队列实例,实现简单的管理。
消费流程
其中线程休眠时长delay指定队列的延时消费时间,maxIdle参数指定线程最大休眠时间,ExceptionStrategy对象的openExceptionList属性指定是否开启异常集合。当队列中没有消息或者消息未到消费时间,则线程会进入休眠,休眠时长计算规则如下:
- 队列中无消息,则休眠时长为maxIdle
- 若消息没到消费时间,时间差为diff(<delay),需要休眠:如果diff>maxIdle,则休眠时长为maxIdle;反之休眠时间为diff;
可以通过减小maxIdle的值来提高队列消费的时效性,但这样在空队列的情况下可能导致频繁访问redis,影响性能。具体根据需要设置合适的值。
DEMO
starter写好后,就可以引入项目中进行使用了
- 创建一个延时队列类RefundUpdateQueue,继承BaseDelayQueue抽象类;由于BaseDelayQueue中没有定义无参构造,所以先要为RefundUpdateQueue类增减一个带有DelayQueueConfig类型参数的有参构造;
- 实现job方法完成消费逻辑,返回Boolean值指定消息消费完是否删除
- 将RefundUpdateQueue对象加载到spring容器中
public class RefundUpdateQueue extends BaseDelayQueue {
private RefundOrderService refundOrderService;
public RefundUpdateQueue (DelayQueueConfig config, RefundOrderService refundOrderService){
super(config);
this.refundOrderService = refundOrderService;
}
@Override
protected Boolean job(String value) {
BankRefundOrderStatus status = refundOrderService.validateRefund(value,null,null).getStatus();
return status != BankRefundOrderStatus.REFUNDING;
}
}
@Configuration
public class DelayQueueRegistry{
@Bean
public RefundUpdateQueue refundUpdateQueue(StringRedisTemplate stringRedisTemplate, RefundOrderService refundOrderService){
DelayQueueConfig config = new DelayQueueConfig(RedisComponent.REFUND_UPDATE_QUEUE,stringRedisTemplate,60*1000,1000*60*5,
new ExceptionStrategy(RedisComponent.REFUND_UPDATE_EX_MAP,stringRedisTemplate));
return new RefundUpdateQueue(config,refundOrderService);
}
}
这里可以看到job方法是校验退款是否已经完成(成功/失败),如果退款状态为退款中,返回false下次继续校验;注意这里的QueueFactory已经在starter定义好了,只要定义了BaseDelayQueue类型的bean,其会自动被加入到QueueFactory中,线程被启动。
不足
- 只支持string类型的值,作为队列的值
- 在空队列时会进性线程休眠,这样可能会存在一定的消费时间误差;如果一味的减小maxIdle的值,虽然消费时间的误差可能会减小,返回造成频繁的访问redis,消耗过多资源;具体的参数根据需要来定
- 目前还未解决分布式情况下,多消费者并发消费同一队列的问题;初步想法时,各消费者对队列进行争抢上锁,抢到拿到队列一定时间段的消费权,到时间各消费者重新争抢上锁。
针对以上几点,使用场景大致可以限定在:支付、退款相关的延时查询、订单的延时取消等查询更新场景。