延时取消订单
背景
简化需求图:
场景举例:
- 比如下订单后,迟迟不付款,是不是应该取消订单?(购物软件都有的)
- 我的需求:比如我发消息至消息队列后(消息队列为了异步、解耦),改变了服务状态(服务启动中),然后我等待别的服务返回服务启动结果,更新服务状态(启动成功/启动失败)。
假设传输过程中,消息在网络传输中消失了(网络、物理等故障),服务状态便会长时间得不到更新。我们是不是应该修改状态比如启动失败反馈给客户呢?
方案选型
注:以下都由订单超时取消举例
数据库轮询
用一个线程去扫数据库,判断失效时间与更新时间之间的差距,判断是不是过期。
缺点:
- 假如订单表有几千万条数据,每次循环扫描,服务器资源损耗大
- 数据库轮询时间的选取,导致超时取消时间不精确。(最坏延迟时间就是 轮询周期)
JDK延迟队列
class OrderTimeout<T> implements Delayed {
private long delayTime;
private long expiryTime;
private T data;
OrderTimeout(long delayTime, T data) {
this.delayTime = delayTime;
this.data = data;
this.expiryTime = delayTime + System.currentTimeMillis();
}
public void show() {
System.out.println(data);
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.expiryTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
}
public class DemoApplication {
public static void main(String[] args) throws InterruptedException {
DelayQueue queue = new DelayQueue();
queue.add(new OrderTimeout<String>(50000, "订单1"));
while (queue.size() > 0) {
OrderTimeout orderTimeout = (OrderTimeout) queue.take();
orderTimeout.show();
}
}
}
优点:触发延迟低
缺点:
- 服务器重启任务消失
- 任务过多,容易OOM
时间轮算法
和时钟一样,每一秒转动一格,当指针转到当前个子,执行格子里面任务。
那么试想,假如要实现延迟1分20秒该怎么设计呢?多级分层时间轮。
延迟1分20秒,首先在分钟时间轮转动一格,会自动发送到秒针时间轮,然后在秒针时间轮转动20格。
redis
通过expire 过期时间来检测,redis key过期时间与服务器删除时间不一致,而过期事件会按照服务器删除时间触发。
请勿过度依赖redis过期监听:https://developer.aliyun.com/article/760276
消息队列
例如RabbitMQ,RocketMQ,内部实现了延时队列,原理很简单,先将消息发送到延时队列,轮询等延时队列元素到期,便放入正确的队列。
实现
自定义延时工具类:
@Slf4j
@Component
public class RedisDelayQueueUtil {
@Autowired
private RedissonClient redissonClient;
/**
* 添加延迟队列
*
* @param value:队列值
* @param delay:延迟时间
* @param timeUnit:时间单位
* @param queueCode:队列键
* @param <T>
*/
public <T> boolean addDelayQueue(T value, long delay, TimeUnit timeUnit, String queueCode) {
if (StringUtils.isBlank(queueCode) || value==null) {
return false;
}
try {
// redission的阻塞队列
RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode);
// redission的延时队列
RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
// 延时队列添加数据
delayedQueue.offer(value, delay, timeUnit);
//delayedQueue.destroy();
log.info("添加延时队列成功,队列键:{},队列值:{},延迟时间:{}", queueCode, value, timeUnit.toSeconds(delay) + "秒");
} catch (Exception e) {
log.error("添加延时队列失败: {}", e.getMessage());
throw new RuntimeException("(添加延时队列失败)");
}
return true;
}
/**
* 获取延迟队列
*
* @param queueCode
* @param <T>
*/
public <T> T getDelayQueue(String queueCode) throws InterruptedException {
if (StringUtils.isBlank(queueCode)) {
return null;
}
RBlockingDeque<Map> blockingDeque = redissonClient.getBlockingDeque(queueCode);
// 将队列中放入的第一个元素取出
T value = (T) blockingDeque.poll();
return value;
}
/**
* 删除指定队列中的消息
* @param o 指定删除的消息对象队列值(同队列需保证唯一性)
* @param queueCode 指定队列键
*/
public boolean removeDelayedQueue(Object value,String queueCode) {
if (StringUtils.isBlank(queueCode) || value==null) {
return false;
}
RBlockingDeque<Object> blockingDeque = redissonClient.getBlockingDeque(queueCode);
RDelayedQueue<Object> delayedQueue = redissonClient.getDelayedQueue(blockingDeque);
boolean flag = delayedQueue.remove(o);
//delayedQueue.destroy();
if(flag){
log.info("删除延时队列成功, 删除信息:{},延迟时间:{}", o,queueCode);
}
return flag;
}
}
- 延时队列枚举类
@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum RedisDelayQueueEnum {
ORDER_PAYMENT_TIMEOUT("ORDER_PAYMENT_TIMEOUT","支付超时,自动取消订单", "orderPaymentTimeout");
/**
* 延迟队列 Redis Key
*/
private String code;
/**
* 描述
*/
private String name;
/**
* 延迟队列具体业务实现的 Bean
* 可通过 Spring 的上下文获取
*/
private String beanId;
}
- 延时队列执行器
/**
* @ClassName: RedisDelayQueueHandle
* @Desc: 延迟队列执行器
* @Author: txm
* @Date: 2022/10/19 21:27
**/
public interface RedisDelayQueueHandle<T> {
void execute(T t);
}
- 延时队列执行器的具体接口
/**
* @ClassName: OrderPaymentTimeout
* @Desc: 订单支付超时处理
* @Author: txm
* @Date: 2022/10/19 21:28
**/
@Component
@Slf4j
public class OrderPaymentTimeout implements RedisDelayQueueHandle<Map> {
@Override
public void execute(Map map) {
log.info("订单支付超时延迟消息:{}", map);
// TODO 订单支付超时,自动取消订单处理业务...
}
}
- 监听延时队列
/**
* @ClassName: RedisDelayQueueRunner
* @Desc: 启动延迟队列监测
**/
@Slf4j
@Component
public class RedisDelayQueueRunner implements CommandLineRunner {
@Autowired
private RedisDelayQueueUtil redisDelayQueueUtil;
@Autowired
private ApplicationContext context;
@Autowired
private ThreadPoolTaskExecutor ptask;
ThreadPoolExecutor executorService = new ThreadPoolExecutor(3, 5, 30, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(1000),new ThreadFactoryBuilder().setNameFormat("order-delay-%d").build());
@Override
public void run(String... args) throws Exception {
ptask.execute(() -> {
while (true){
try {
RedisDelayQueueEnum[] queueEnums = RedisDelayQueueEnum.values();
for (RedisDelayQueueEnum queueEnum : queueEnums) {
Object value = redisDelayQueueUtil.getDelayQueue(queueEnum.getCode());
if (value != null) {
RedisDelayQueueHandle<Object> redisDelayQueueHandle = (RedisDelayQueueHandle<Object>)context.getBean(queueEnum.getBeanId());
executorService.execute(() -> {redisDelayQueueHandle.execute(value);});
}
}
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
log.error("Redission延迟队列监测异常中断):{}", e.getMessage());
}
}
});
log.info("Redission延迟队列监测启动成功");
}
}
总结
- 技术选型按实际需求来选择,有时候过期时间不那么精确也是可以的。
- 要求精确还是选择MQ的方案吧。
优质博客推荐
有赞团队:https://tech.youzan.com/queuing_delay/
Redission 实现延时队列:https://developer.aliyun.com/article/1131971
redission api介绍:https://blog.csdn.net/A_art_xiang/article/details/125525864
面试指南:https://topjavaer.cn/system-design/2-order-timeout-auto-cancel.html#思路
本文来自博客园,作者:帅气的涛啊,转载请注明原文链接:https://www.cnblogs.com/handsometaoa/p/17172861.html