1. 前言
在平常的开发中经常会碰到定时任务处理的场景,比如说用户进行商品购买,下单之后超过半个小时还未进行支付则自动取消该笔订单,订单支付状态由待支付变更为已支付;用户收到商品后,可以对商品评价,限制三天或是七天内没有做出评价则将评价入口关闭,订单状态变更为已关闭。这类指定时间之后进行的业务逻辑处理都可以纳入定时任务处理的范畴中。
关于定时任务处理的实现方案有很多种,基于springboot搭建的项目架构,大部分都是使用@Schedual,原因是配置简单、cron表达式支持丰富、灵活的定时时间。就拿订单超时取消功能来讲,定期查询数据库中待支付订单,超过订单创建指定时间的进行批量更新操作变更为已取消。当然万事都是利弊,这种方式缺点也很明显,需要频繁对数据库进行查询、更新操操作,无疑会浪费数据库资源,毕竟会做多余无效的查询。那有没有其他实现方案,不通过数据库轮询的方式处理。答案是肯定有,定时任务的解决方案有多种,这里结合项目实战介绍一下基于redission延迟队列的处理方案。
2.实现过程
2.1添加依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.10.5</version> </dependency>
2.2.redis配置
spring: redis: database: 2 host: xxx.xxx.xxx.xxx password: ***** port: 6379
2.3 Redission配置类
@Slf4j @Configuration public class RedissionConfig { private final String REDISSON_PREFIX = "redis://"; private final RedisProperties redisProperties; public RedissionConfig(RedisProperties redisProperties) { this.redisProperties = redisProperties; } @Bean public RedissonClient redissonClient() { Config config = new Config(); String url = REDISSON_PREFIX + redisProperties.getHost() + ":" + redisProperties.getPort(); config.useSingleServer() .setAddress(url) .setPassword(redisProperties.getPassword()) .setDatabase(redisProperties.getDatabase()) .setPingConnectionInterval(2000); config.setLockWatchdogTimeout(10000L); try { return Redisson.create(config); } catch (Exception e) { log.error("RedissonClient init redis url:{}, Exception:{}", url, e); return null; } } }
2.4 自定义延时队列工具类
@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) || Objects.isNull(value)) { 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(@NonNull 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 o,String queueCode) { if (StringUtils.isBlank(queueCode) || Objects.isNull(o)) { 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; } }
2.5 延时队列枚举
可以将定时处理的场景都加入这里面,这里只演示订单未支付自动取消。
@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; }
2.6 延时队列执行器
/** * @ClassName: RedisDelayQueueHandle * @Desc: 延迟队列执行器 * @Author: txm * @Date: 2022/10/19 21:27 **/ public interface RedisDelayQueueHandle<T> { void execute(T t); }
2.7 订单超时执行器具体逻辑
/** * @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 订单支付超时,自动取消订单处理业务... } }
2. 8 延迟队列监测
项目启动之后就会执行
/** * @ClassName: RedisDelayQueueRunner * @Desc: 启动延迟队列监测 * @Author: txm * @Date: 2022/10/19 21:29 **/ @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延迟队列监测启动成功"); } }
2.9 支付业务实现
这里模拟下单之后未支付订单自动取消;另外下单并支付成功的需要从回调处理中将延时队列中的信息清除。 支付控制类:
@RequestMapping("/pay") @Validated @RestController @Api( tags = {"支付模块"}) public class PayController { @Autowired private PayServiceImpl payService; @ApiOperation("下单处理") @PostMapping("/placeOrder") public ResultVo placeOrder() throws IOException { payService.placeOrder(); return ResultVoUtil.success(); } @ApiOperation("支付回调处理") @PostMapping("/notifyOrder") public ResultVo notifyOrder() throws IOException { payService.notifyOrder(); return ResultVoUtil.success(); } }
支付实现类:
@Slf4j @Service public class PayServiceImpl implements PayService { private RedisDelayQueueUtil redisDelayQueueUtil = SpringUtils.getBean(RedisDelayQueueUtil.class); /** * @Author: txm * @Description: 下单处理 * @Param: [] * @return: void * @Date: 2022/10/19 21:36 **/ @Override public void placeOrder() { System.out.println("模拟下单处理完成"); Map<String, String> map1 = new HashMap<>(); map1.put("orderId", "100"); map1.put("remark", "支付超时,自动取消订单"); // 添加订单支付超时,自动取消订单延迟队列。延迟40秒钟取消订单 redisDelayQueueUtil.addDelayQueue(map1, 40, TimeUnit.SECONDS, RedisDelayQueueEnum.ORDER_PAYMENT_TIMEOUT.getCode()); } /** * @Author: txm * @Description: 订单回调处理 * @Param: [] * @return: void * @Date: 2022/10/19 22:05 **/ @Override public void notifyOrder() { System.out.println("模拟订单回调成功.............."); Map<String, String> map1 = new HashMap<>(); map1.put("orderId", "100"); map1.put("remark", "订单支付超时,自动取消订单"); // 删除延时队列中的消息信息 redisDelayQueueUtil.removeDelayedQueue(map1,RedisDelayQueueEnum.ORDER_PAYMENT_TIMEOUT.getCode()); } }
下单成功之后,会添加到延时队列
40秒之后订单取消
模拟回调处理之后删除延时队列消息
以上基于redission延时队列处理定时任务的实战记录。看到这里如果感觉还不错欢迎点赞!大家平常都是使用的什么哪种实现方案,欢迎评论区留言交流!