正文

这两种方法都有一个缺点:都是基于单体应用的内存的方式运行延时任务的,一旦出现单点故障,可能出现延时任务数据的丢失。所以此篇文章给大家介绍实现延时任务的第三种方式,结合redis zset实现延时任务,可以解决单点故障的问题。给出实现原理、完整实现代码,以及这种实现方式的优缺点。

一、实现原理

首先来介绍一下实现原理,我们需要使用redis zset来实现延时任务的需求,所以我们需要知道zset的应用特性。zset作为redis的有序集合数据结构存在,排序的依据就是score。

redis zset 命令行取数据 redis zset range_缓存

所以我们可以利用zset score这个排序的这个特性,来实现延时任务

  • 在用户下单的时候,同时生成延时任务放入redis,key是可以自定义的,比如:delaytask:order
  • value的值分成两个部分,一个部分是score用于排序,一个部分是member,member的值我们设置为订单对象(如:订单编号),因为后续延时任务时效达成的时候,我们需要有一些必要的订单信息(如:订单编号),才能完成订单自动取消关闭的动作。
  • 「延时任务实现的重点来了,score我们设置为:订单生成时间 + 延时时长」。这样redis会对zset按照score延时时间进行排序。
  • 开启redis扫描任务,获取"当前时间 > score"的延时任务并执行。即:当前时间 > 订单生成时间 + 延时时长的时候 ,执行延时任务。

二、准备工作

使用 redis zset 这个方案来完成延时任务的需求,首先肯定是需要redis,这一点毫无疑问。redis的搭建网上有很多的文章,我这里就不赘述了。

其次,笔者长期的java类应用系统开发都是使用SpringBoot来完成,所以也是习惯使用SpringBoot的redis集成方案。首先通过maven坐标引入spring-boot-starter-data-redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

其次需要在Spring Boot的application.yml配置文件中,配置redis数据库的链接信息。我这里配置的是redis的单例,如果大家的生产环境是哨兵模式、或者是集群模式的redis,这里的配置方式需要进行微调。其实这部分内容在我的个人博客里面都曾经系统的介绍过,感兴趣的朋友可以关注我的个人博客。

spring:
  redis:
    database: 0 # Redis 数据库索引(默认为 0)
    host: 192.168.161.3 # Redis 服务器地址
    port: 6379 # Redis 服务器连接端口
    password: 123456 # Redis 服务器连接密码(默认为空)
    timeout:  5000  # 连接超时,单位ms
    lettuce:
      pool:
        max-active: 8 # 连接池最大连接数(使用负值表示没有限制) 默认 8
        max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
        max-idle: 8 # 连接池中的最大空闲连接 默认 8
        min-idle: 0 # 连接池中的最小空闲连接 默认 0

三、代码实现

下面的这个类就是延时任务的核心实现了,一共包含三个核心方法,我们来一一说明一下:

  • produce方法,用于生成订单-order为订单信息,可以是订单流水号,用于延时任务达到时效后关闭订单
  • afterPropertiesSet方法是InitializingBean接口的方法,之所以实现这个接口,是因为我们需要在应用启动的时候开启redis扫描任务。即:当OrderDelayService bean初始化的时候,开启redis扫描任务循环获取延时任务数据。
  • consuming函数,用于从redis获取延时任务数据,消费延时任务,执行超时订单关闭等操作。为了避免阻塞for循环,影响后面延时任务的执行,所以这个consuming函数一定要做成异步的,参考Spring Boot异步任务及Async注解的使用方法。我之前写过一个SpringBoot的**「可观测、易配置」**的异步任务线程池开源项目,源代码地址:https://gitee.com/hanxt/zimug-monitor-threadpool 。我的这个zimug-monitor-threadpool开源项目,可以做到对线程池使用情况的监控,我自己平时用的效果还不错,向大家推荐一下!
@Component
public class OrderDelayService  implements InitializingBean {
  //redis zset key
  public static final String ORDER_DELAY_TASK_KEY = "delaytask:order";

  @Resource
  private StringRedisTemplate stringRedisTemplate;

  //生成订单-order为订单信息,可以是订单流水号,用于延时任务达到时效后关闭订单
  public void produce(String orderSerialNo){
    stringRedisTemplate.opsForZSet().add(
            ORDER_DELAY_TASK_KEY,     // redis key
            orderSerialNo,    // zset member
            //30分钟延时
            System.currentTimeMillis() + (30 \* 60 \* 1000)    //zset score
    );
  }

  //延时任务,也是异步任务,延时任务达到时效之后关闭订单,并将延时任务从redis zset删除
  @Async("test")
  public void consuming(){
       
      Set<ZSetOperations.TypedTuple<String>> orderSerialNos = stringRedisTemplate.opsForZSet().rangeByScoreWithScores(
              ORDER_DELAY_TASK_KEY,
              0,  //延时任务score最小值
              System.currentTimeMillis() //延时任务score最大值(当前时间)
      );
      if (!CollectionUtils.isEmpty(orderSerialNos)) {
        for (ZSetOperations.TypedTuple<String> orderSerialNo : orderSerialNos) {