【微服务】SpringBoot 整合Redis实现延时任务处理使用详解

目录

一、前言

二、延迟任务的高频使用场景

三、延迟任务常用解决方案

[3.1 Quartz](#3.1 Quartz)

[3.2 DelayQueue](#3.2 DelayQueue)

[3.2.1 Timer + TimerTask](#3.2.1 Timer + TimerTask)

[3.2.2 ScheduledExecutorService](#3.2.2 ScheduledExecutorService)

[3.3 Redis sorted set](#3.3 Redis sorted set)

[3.4 RabbitMQ](#3.4 RabbitMQ)

四、Redis实现延时队列操作实战

[4.1 Redis Sorted Set 概述](#4.1 Redis Sorted Set 概述)

[4.1.1 Redis Sorted Set 介绍](#4.1.1 Redis Sorted Set 介绍)

[4.1.2 Redis Sorted Set 主要特点](#4.1.2 Redis Sorted Set 主要特点)

[4.1.3 Redis Sorted Set 应用场景](#4.1.3 Redis Sorted Set 应用场景)

[4.1.4 核心实现思路](#4.1.4 核心实现思路)

[4.2 Jedis 实现延迟队列](#4.2 Jedis 实现延迟队列)

[4.2.1 前置准备](#4.2.1 前置准备)

[4.2.2 代码操作演示](#4.2.2 代码操作演示)

[4.3 SpringBoot 整合Redis实现延迟队列](#4.3 SpringBoot 整合Redis实现延迟队列)

[4.3.1 添加依赖](#4.3.1 添加依赖)

[4.3.2 添加配置文件](#4.3.2 添加配置文件)

[4.3.3 自定义redis序列化类](#4.3.3 自定义redis序列化类)

[4.3.4 定义抽象任务](#4.3.4 定义抽象任务)

[4.3.5 定义任务通知类](#4.3.5 定义任务通知类)

[4.3.6 定义任务管理器](#4.3.6 定义任务管理器)

[4.3.7 添加测试接口](#4.3.7 添加测试接口)

[4.3.8 效果测试](#4.3.8 效果测试)

[4.4 使用Redisson 实现延时任务队列](#4.4 使用Redisson 实现延时任务队列)

[4.4.1 导入依赖](#4.4.1 导入依赖)

[4.4.2 添加配置类](#4.4.2 添加配置类)

[4.4.3 添加自定义消息监听类](#4.4.3 添加自定义消息监听类)

[4.4.4 自定义任务阻塞队列](#4.4.4 自定义任务阻塞队列)

[4.4.5 添加测试接口](#4.4.5 添加测试接口)

五、写在文末


一、前言

在分布式微服务开发中,延迟(延时)任务的场景或需求可以说很多,比如大家熟悉的下单之后允许延后15分钟完成支付,或者某些因为业务的原因需要延迟执行的场景等,同时延迟任务的存在,也让系统或业务有了更多的可扩展空间,本文将通过案例操作演示下如何基于Redis实现延时任务的解决方案。

二、延迟任务的高频使用场景

下面是几种在日常开发中高频接触到的延迟任务场景:

  • 订单超时自动处理

    • 在电商领域,延迟队列对于处理订单超时问题至关重要。一旦用户下单,订单信息便进入延迟队列,并预设超时时长。若用户在此时间内未完成支付,订单信息将由消费者从队列中提取,并执行如取消订单、库存释放等后续操作,高效且自动化。
  • 消息超时重发

    • 当消息发送失败,为了保障消息尽可能送达到用户,可以使用延迟重试,即在第一次发送失败之后,让这个动作延迟一定时间之后再次发送。
  • 优惠券到期温馨提醒

    • 借助延迟队列,可实现优惠券到期前温馨提醒。将临近过期的优惠券信息入队,并设定精确延迟时间。时间一到,系统自动提醒用户优惠券的到期日,引导他们及时享用优惠,提升用户体验。
  • 异步通知与定时提醒

    • 延迟队列还能用于实现异步通知和定时提醒功能。用户完成操作后,系统将相关通知信息加入队列,并设定发送延时,确保在最佳时机向用户推送通知,既不打扰用户,又能保持信息的时效性。
  • 定时提醒

    • 定时(未来的某个时间)发送邮件提醒或其他消息通知等。

这些场景都需要任务在一段时间后自动执行,因此我们需要一种灵活、高效的解决方案来处理这类限时任务。

三、延迟任务常用解决方案

3.1 Quartz

Quartz 是一个定时任务框架,可通过其提供的API灵活实现定时任务的增删改查等功能,其功能强大之处正是在于用户可根据自身的需求对要执行的各种任务参数进行定制,因此也可以方便的实现延迟任务的配置和使用。

3.2 DelayQueue

JDK中自带了一些技术组件可以用于处理简单的任务延时场景。下面简单介绍几种。

3.2.1 Timer + TimerTask

JDK中提供了一组实现延迟队列的API,比如:

  • Timer + TimerTask

    • java.util包下,Timer类可以用来安排一个任务在指定的时间执行;

    • TimerTask是表示要执行的任务

java 复制代码
import java.util.Timer;
import java.util.TimerTask;

public class TimerExample {
    public static void main(String[] args) {
        Timer timer = new Timer();
        
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                System.out.println("执行延时任务: " + System.currentTimeMillis());
            }
        };

        // 安排任务在延迟1000毫秒后执行
        timer.schedule(task, 1000);
        
        // 也可以周期性执行任务
        // timer.scheduleAtFixedRate(task, 1000, 2000); // 每2秒执行一次
    }
}

工作原理:

  • 创建一个Timer实例。

  • 创建一个继承自TimerTask的匿名类,重写run()方法。

  • 调用schedule()方法安排任务延迟执行。

3.2.2 ScheduledExecutorService

相较于Timer,ScheduledExecutorService提供了更强大的功能,可以更加灵活地管理线程和任务。它可以在一个线程池中执行多个定时任务。参考下面的示例:

java 复制代码
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledExecutorServiceExample {
    public static void main(String[] args) {
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);

        // 安排任务在2秒后执行
        executorService.schedule(() -> {
            System.out.println("执行延时任务: " + System.currentTimeMillis());
        }, 2, TimeUnit.SECONDS);

        // 周期性执行任务
        // executorService.scheduleAtFixedRate(() -> {
        //     System.out.println("周期性任务: " + System.currentTimeMillis());
        // }, 1, 2, TimeUnit.SECONDS); // 每2秒执行一次
    }
}

3.3 Redis sorted set

Redis Sorted Set(有序集合)是一种数据结构,它将元素存储在一个有序的集合中,每个元素都有一个唯一的分数(score)与之关联。

具体做法是将任务的执行时间作为分数(score),任务的内容作为值(value),将任务按照执行时间排序存储在有序集合中。然后周期性地检查有序集合中的任务,根据当前时间和任务的执行时间来决定是否执行任务。

3.4 RabbitMQ

RabbitMQ 提供了死信队列可用于实现延迟任务的执行,具体来说,死信队列是那些主任务进行消息投递的时候投递失败了,但是这些消息不希望丢弃,还需要后续进行处理时,借助死信队列,就可以将此类消息投递到死信队列,让此类消息延时处理。

四、Redis实现延时队列操作实战

4.1 Redis Sorted Set 概述

4.1.1 Redis Sorted Set 介绍

Redis 的 Sorted Set(有序集合)是一种非常有用的数据结构,它不仅存储了唯一的成员(member),还为每个成员关联了一个分数(score),从而使得成员能够根据分数进行排序。这使得 Sorted Set 在实现排行榜、优先级队列等应用场景中特别有用。

4.1.2 Redis Sorted Set 主要特点

Redis Sorted Set 主要特点如下:

  1. 唯一性:Sorted Set 中的成员是唯一的,不能重复。

  2. 有序性:每个成员都有一个与之关联的分数(score),Redis 会根据这个分数对成员进行排序。分数可以相同,当分数相同时,成员按照字典序排列。

  3. 高效操作:支持高效的插入、删除和查找操作,时间复杂度为 O(log(N)),其中 N 是集合中的元素数量。

  4. 范围查询:支持基于分数或排名的范围查询,如获取某个分数区间内的所有成员,或者获取指定排名范围内的成员。

  5. 元素和分数:每个元素都有一个唯一的分数与之关联,分数可以是整数或浮点数。

  6. 插入和删除 :元素可以通过 ZADD 命令插入到集合中,通过 ZREM 命令删除元素。

  7. 分数更新 :可以通过 ZINCRBY 命令更新元素的分数。

Sorted Set 的底层实现使用了跳跃表(Skip List)数据结构,跳跃表是一种高效的有序数据结构,它可以在 O(log n) 的时间复杂度内进行插入、删除和查找操作。

4.1.3 Redis Sorted Set 应用场景

Sorted Set这种数据结构具备下面一些使用场景:

  • 排行榜:利用 Sorted Set 可以轻松实现用户积分排名等功能,实时更新和查询用户的排名情况。

  • 优先级队列:通过分数来表示任务的优先级,高分任务先被执行。

  • 缓存系统:例如,可以使用 Sorted Set 来管理缓存项的有效期,方便地找到最早到期的缓存项进行清理。

  • 社交网络:跟踪用户的关注列表、好友关系等,按照活跃度或其他指标排序展示。

4.1.4 核心实现思路

使用Redis Sorted Set 实现延迟任务时,可以将任务的执行时间作为 Sorted Set 中的 score,然后按时间顺序处理任务,确保在指定时间执行。

当需要添加新的延迟任务时,只需将任务的执行时间和内容添加到有序集合中即可。当然,你可能需要一个后台进程或定时任务来不断地检查有序集合,以执行到期的任务。

接下来使用Redis 的Sorted Set 完成几种方式来实现延时队列。

4.2 Jedis 实现延迟队列

4.2.1 前置准备

在maven的pom文件中引入如下依赖

java 复制代码
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.4</version>
    <relativePath/>
</parent>

<properties>
    <docker.image.prefix>dcloud</docker.image.prefix>
</properties>

<dependencies>

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.7.0</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>

</dependencies>

4.2.2 代码操作演示

在下面的代码中,我们通过在for循环为 delay_queue 这个Sorted Set类型的数据结构(队列)添加了几个元素作为生产任务的数据,中间会相隔一定的时间,只需要在程序运行之后,观察是否延时执行即可

java 复制代码
import redis.clients.jedis.Jedis;

import java.util.Set;

public class JedisScoreTest {

    private static final String QUEUE_KEY = "delay_queue";

    public static void main(String[] args) throws Exception{
        Jedis jedis = new Jedis("localhost", 6379);
        // 添加元素到延时队列,其中timestamp是希望处理该元素的时间戳
//        long timestamp = System.currentTimeMillis() + 5000; // 10秒后处理
//        String item = "item1";
//        jedis.zadd(QUEUE_KEY, timestamp, item);
        for(int i=0;i<5;i++){
            String item = "item-"+i;
            long timestamp = System.currentTimeMillis() + (1+i) * 2000;
            jedis.zadd(QUEUE_KEY, timestamp, item);
            Thread.sleep(2000);
        }
        // 模拟处理延时队列中的任务
        pollItems(jedis);
        jedis.close();
    }

    private static void pollItems(Jedis jedis) {
        while (true) {
            long currentTimestamp = System.currentTimeMillis();
            // 获取当前时间之前的所有元素
            Set<String> items = jedis.zrangeByScore(QUEUE_KEY, 0, currentTimestamp, 0, 1);
            if (items.isEmpty()) {
                // 没有可处理的元素,休眠一会儿继续轮询
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            } else {
                // 处理找到的元素
                for (String item : items) {
                    // 处理逻辑...
                    System.out.println("Processing item: " + item);
                    // 移除已处理的元素
                    jedis.zrem(QUEUE_KEY, item);
                }
            }
        }
    }

}

运行上面的代码,通过控制台效果可以看到模拟了延时任务的执行效果

4.3 SpringBoot 整合Redis实现延迟队列

4.3.1 添加依赖

首先,确保你的Spring Boot项目中已经配置好了Redis依赖。在pom.xml文件中添加如下依赖:

java 复制代码
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

4.3.2 添加配置文件

在配置文件中添加下面的信息

java 复制代码
server:
  port: 8081

spring:
  data:
    redis:
      host: localhost
      port: 6379

4.3.3 自定义redis序列化类

为了避免后续使用时出现对象序列化问题,建议在这里添加一个序列化的类

java 复制代码
package com.congge.web;

import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis配置
 */
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisConfig{

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        template.setValueSerializer(stringRedisSerializer);

        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // Hash的key也采用GenericJackson2JsonRedisSerializer的序列化方式
        template.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

4.3.4 定义抽象任务

定义操作任务的常用接口方法,方便上传API调用,参考下面的代码:

java 复制代码
package com.congge.demo;
public interface RedisDelayTask {
    /**
     *  任务ID
     * @return
     */
    String getId();

    /**
     *  队列中的值
     * @return
     */
    String getValue();

    /**
     *  延迟时间(单位:s)
     * @return
     */
    long getDelayTime();

    /**
     *  任务执行
     */
    void execute();
}
java 复制代码
package com.congge.demo;


public abstract class AbstractRedisDelayTask implements RedisDelayTask {

    protected String id;
    protected String value;
    private long delayTime;

    public AbstractRedisDelayTask(String id, String value, long delayTime) {
        this.id = id;
        this.value = value;
        this.delayTime = delayTime;
    }

    @Override
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public long getDelayTime() {
        return delayTime;
    }

    public void setDelayTime(long delayTime) {
        this.delayTime = delayTime;
    }

}

4.3.5 定义任务通知类

参考下面的代码

java 复制代码
package com.congge.demo;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class NoticeTask extends AbstractRedisDelayTask {

    public NoticeTask(String id, String value, long delayTime) {
        super(id, value, delayTime);
    }

    @Override
    public void execute() {
        log.info("task execute, {}", this);
    }
}

4.3.6 定义任务管理器

具体处理任务相关的逻辑

java 复制代码
package com.congge.demo;

import jakarta.annotation.Resource;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Component
public class RedisDelayQueueManager implements InitializingBean {

    @Resource
    private RedisTemplate redisTemplate;

    /**
     *  任务列表
     */
    private Map<String, RedisDelayTask> tasks = new ConcurrentHashMap<>();

    /**
     *  添加延迟任务到队列
     * @param task
     */
    public void addTask(RedisDelayTask task) {
        long delayedTime = System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(task.getDelayTime(), TimeUnit.SECONDS);
        boolean addSuccess = redisTemplate.opsForZSet().add(task.getId(), task.getValue(), delayedTime);
        if (addSuccess) {
            tasks.put(task.getId(), task);
        }
    }

    /**
     * 检查并执行任务
     */
    private void checkAndExecuteTask() {
        while (true) {
            Set<String> taskIds = tasks.keySet();
            for (String taskId : taskIds) {
                // score就是任务要执行的时间点,如果<=当前时间,说明任务该执行了
                Set<ZSetOperations.TypedTuple<String>> tuples = redisTemplate.opsForZSet().rangeByScoreWithScores(taskId, 0, System.currentTimeMillis());
                if (!CollectionUtils.isEmpty(tuples)) {
                    for (ZSetOperations.TypedTuple<String> tuple : tuples) {
                        // 获取(移除)一个任务并执行
                        RedisDelayTask task = tasks.remove(taskId);
                        System.out.println( "taskId: " + task.getId());
                        System.out.println("taskValue: " + task.getValue());
                        if (task != null) {
                            task.execute();
                            // 任务执行之后从队列中删除
                            redisTemplate.opsForZSet().remove(taskId, tuple.getValue());
                        }
                    }
                }
            }
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // 另开一个线程执行任务
        new Thread(() -> {
            checkAndExecuteTask();
        }, "redis-delay-task").start();
    }
}

4.3.7 添加测试接口

为了方便后面的测试在这里添加一个测试用的接口

java 复制代码
package com.congge.web;

import com.congge.demo.NoticeTask;
import com.congge.demo.RedisDelayQueueManager;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DelayTaskController {

    @Resource
    private RedisDelayQueueManager redisDelayQueueManager;

    //localhost:8081/add/task
    @GetMapping("/add/task")
    public void addTask()  {
        NoticeTask task = new NoticeTask("task1", "value1", 3);
        redisDelayQueueManager.addTask(task);

        NoticeTask task2 = new NoticeTask("task2", "value2", 8);
        redisDelayQueueManager.addTask(task2);
    }

}

4.3.8 效果测试

运行上面的代码,请求一下接口,通过控制台输出效果可以看到,添加的任务已经延时执行了

4.4 使用Redisson 实现延时任务队列

4.4.1 导入依赖

导入Redisson 依赖包

java 复制代码
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.33.0</version>
        </dependency>

4.4.2 添加配置类

自定义一个RedissonClient 的类

java 复制代码
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "spring.data.redis")
public class RedissonConfig {

    private static final String REDIS_URL  = "redis://localhost:6379";
    private static final Integer TIMEOUT = 3000;

    @Bean
    RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress(REDIS_URL)
                .setTimeout(TIMEOUT);
        return Redisson.create(config);
    }

}

4.4.3 添加自定义消息监听类

该类用于处理队列中的任务消息

java 复制代码
package com.congge.config;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;

@Component
public class MessageListener {

    @Resource
    private RedissonConfigQueue redissonConfigQueue;

    @PostConstruct
    public void init() {
        new Thread(() -> {
            while (true) {
                try {
                    //阻塞等待队列内有可消费延时消息,避免无意义的循环占用CPU
                    String string = redissonConfigQueue.blockingQueue().take().toString();
                    System.out.println(string);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }

}

4.4.4 自定义任务阻塞队列

该类用于接收任务并加入到阻塞队列

java 复制代码
package com.congge.config;

import lombok.RequiredArgsConstructor;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class RedissonConfigQueue {

    private final RedissonClient redissonClient;

    private final static String queueName = "orderQueue";

    /**
     * 创建一个阻塞队列
     */
    public RBlockingQueue<Object> blockingQueue() {
        return redissonClient.getBlockingQueue(queueName);
    }

    /**
     * 延时队列
     * 阻塞队列放到延迟队列里面,消息放在延迟队列里面,时间到后 消息投递到阻塞队列,消费者监听阻塞队列
     */
    public RDelayedQueue<Object> getDelayedQueue(RBlockingQueue<Object> blockQueue) {
        return redissonClient.getDelayedQueue(blockQueue);
    }

}

4.4.5 添加测试接口

添加一个测试接口方便看效果

java 复制代码
@Resource
private RedissonConfigQueue redissonConfigQueue;

//localhost:8081/add/task/v2
@GetMapping("/add/task/v2")
public String addTaskV2()  {
    redissonConfigQueue.getDelayedQueue(redissonConfigQueue.blockingQueue())
            .offer("我是消息", 6, TimeUnit.SECONDS);//消息内容,延时时间,时间单位(秒)
    return UUID.randomUUID().toString();
}

五、写在文末

本文通过实际操作案例演示了在微服务中如何实现任务的延时处理解决方案,可以为技术选型提供参考,希望对看到的同学有用,本篇到此结束,感谢观看。