Spring Boot+Redis Zset:三步构建高可靠延迟队列系统

系统设计

架构图

复制代码
+----------------+       +-----------------+       +----------------+
|                |       |                 |       |                |
|  生产者        |------>| Redis ZSet      |------>| 定时任务消费者  |
|  (添加延迟任务) |       | (延迟队列存储)  |       | (扫描并处理任务)|
|                |       |                 |       |                |
+----------------+       +-----------------+       +----------------+
         ↑                                              |
         |                                              ↓
         |                                   +---------------------+
         +-----------------------------------|     任务处理器       |
                                             | (执行具体业务逻辑)   |
                                             +---------------------+

核心流程

  1. 生产者将任务添加到Redis ZSet中,score为任务执行时间戳

  2. 定时任务定期扫描ZSet,找出score小于当前时间的任务

  3. 消费者线程池处理到期的任务

  4. 任务处理完成后从ZSet中移除

实现步骤

步骤一:添加依赖(pom.xml)

XML 复制代码
<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Redis集成 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- 定时任务 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>
    
    <!-- JSON处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
    
    <!-- Lombok简化代码 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

步骤二:配置Redis(application.yml)

Lua 复制代码
spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 2
        max-wait: 10000ms

# 自定义延迟队列配置
delay:
  queue:
    key: "delay_queue"  # Redis ZSet键名
    batch-size: 10      # 每次处理任务数量
    interval: 5000      # 定时任务执行间隔(ms)
    thread-pool-size: 5 # 消费者线程池大小

步骤三:创建任务模型(DelayTask.java)

java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DelayTask {
    /**
     * 任务类型枚举
     */
    public enum TaskType {
        ORDER_TIMEOUT,   // 订单超时处理
        EMAIL_REMINDER,  // 邮件提醒
        TASK_EXECUTION   // 定时任务执行
    }
    
    private TaskType type;    // 任务类型
    private String taskId;    // 任务唯一ID
    private String content;   // 任务内容
    private long createTime;  // 任务创建时间
    private long executeTime; // 任务执行时间
    
    // 重写toString方法用于序列化
    @Override
    public String toString() {
        return "DelayTask{" +
                "type=" + type +
                ", taskId='" + taskId + '\'' +
                ", content='" + content + '\'' +
                ", createTime=" + createTime +
                ", executeTime=" + executeTime +
                '}';
    }
}

步骤四:创建Redis配置类(RedisConfig.java)

java 复制代码
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;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // 使用Jackson序列化
        GenericJackson2JsonRedisSerializer jacksonSerializer = 
                new GenericJackson2JsonRedisSerializer();
        
        // Key序列化
        template.setKeySerializer(new StringRedisSerializer());
        // Value序列化
        template.setValueSerializer(jacksonSerializer);
        // Hash Key序列化
        template.setHashKeySerializer(new StringRedisSerializer());
        // Hash Value序列化
        template.setHashValueSerializer(jacksonSerializer);
        
        template.afterPropertiesSet();
        return template;
    }
}

步骤五:创建线程池配置类(ThreadPoolConfig.java)

java 复制代码
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
public class ThreadPoolConfig {

    @Value("${delay.queue.thread-pool-size:5}")
    private int threadPoolSize;

    @Bean("taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(threadPoolSize);
        // 最大线程数
        executor.setMaxPoolSize(threadPoolSize * 2);
        // 队列大小
        executor.setQueueCapacity(100);
        // 线程名前缀
        executor.setThreadNamePrefix("delay-task-");
        // 拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}

步骤六:创建延迟队列服务(DelayQueueService.java)

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Set;
import java.util.concurrent.Executor;

@Slf4j
@Service
public class DelayQueueService {

    @Value("${delay.queue.key}")
    private String delayQueueKey;

    @Value("${delay.queue.batch-size}")
    private int batchSize;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private Executor taskExecutor;

    /**
     * 添加延迟任务
     * @param task 任务对象
     * @param delaySeconds 延迟秒数
     */
    public void addTask(DelayTask task, long delaySeconds) {
        long executeTime = System.currentTimeMillis() + (delaySeconds * 1000);
        task.setExecuteTime(executeTime);
        
        // 添加到Redis ZSet
        redisTemplate.opsForZSet().add(delayQueueKey, task, executeTime);
        log.info("添加延迟任务成功, 任务ID: {}, 执行时间: {}", task.getTaskId(), executeTime);
    }

    /**
     * 定时扫描任务(每5秒执行一次)
     */
    @Scheduled(fixedRateString = "${delay.queue.interval}")
    public void scanExpiredTasks() {
        long now = System.currentTimeMillis();
        log.debug("开始扫描延迟队列, 当前时间: {}", now);
        
        // 获取当前时间之前的所有任务
        Set<ZSetOperations.TypedTuple<Object>> tasks = redisTemplate.opsForZSet()
                .rangeByScoreWithScores(delayQueueKey, 0, now, 0, batchSize);
        
        if (tasks == null || tasks.isEmpty()) {
            log.debug("未找到待处理任务");
            return;
        }
        
        log.info("发现 {} 个待处理任务", tasks.size());
        for (ZSetOperations.TypedTuple<Object> tuple : tasks) {
            Object taskObj = tuple.getValue();
            if (taskObj instanceof DelayTask) {
                DelayTask task = (DelayTask) taskObj;
                // 使用线程池异步处理任务
                taskExecutor.execute(() -> processTask(task));
            }
        }
    }

    /**
     * 处理任务
     * @param task 延迟任务
     */
    @Async
    public void processTask(DelayTask task) {
        try {
            log.info("开始处理任务: {}", task.getTaskId());
            
            // 根据任务类型执行不同逻辑
            switch (task.getType()) {
                case ORDER_TIMEOUT:
                    handleOrderTimeout(task);
                    break;
                case EMAIL_REMINDER:
                    sendReminderEmail(task);
                    break;
                case TASK_EXECUTION:
                    executeScheduledTask(task);
                    break;
                default:
                    log.warn("未知任务类型: {}", task.getType());
            }
            
            // 处理完成后从队列中移除
            redisTemplate.opsForZSet().remove(delayQueueKey, task);
            log.info("任务处理完成并移除: {}", task.getTaskId());
        } catch (Exception e) {
            log.error("任务处理失败: {}", task.getTaskId(), e);
            handleProcessingError(task);
        }
    }

    // 示例:订单超时处理
    private void handleOrderTimeout(DelayTask task) {
        log.info("处理订单超时任务: {}", task.getContent());
        // 实际业务逻辑:取消订单、释放库存等
        // 模拟处理时间
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ignored) {}
    }

    // 示例:发送提醒邮件
    private void sendReminderEmail(DelayTask task) {
        log.info("发送提醒邮件: {}", task.getContent());
        // 实际业务逻辑:调用邮件服务发送邮件
    }

    // 示例:执行定时任务
    private void executeScheduledTask(DelayTask task) {
        log.info("执行定时任务: {}", task.getContent());
        // 实际业务逻辑:执行定时任务
    }

    // 错误处理
    private void handleProcessingError(DelayTask task) {
        log.error("任务处理失败,加入死信队列: {}", task.getTaskId());
        // 可以将失败任务移到死信队列
        redisTemplate.opsForList().rightPush("delay:dead-letter", task);
    }
}

步骤七:创建测试Controller(DelayQueueController.java)

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/delay")
public class DelayQueueController {

    @Autowired
    private DelayQueueService delayQueueService;

    /**
     * 添加延迟任务
     * @param type 任务类型 (1-订单超时, 2-邮件提醒, 3-定时任务)
     * @param seconds 延迟秒数
     * @param content 任务内容
     */
    @PostMapping("/add")
    public String addDelayTask(
            @RequestParam("type") int type,
            @RequestParam("seconds") long seconds,
            @RequestParam("content") String content) {
        
        // 创建任务ID
        String taskId = "TASK-" + System.currentTimeMillis();
        
        // 转换任务类型
        DelayTask.TaskType taskType;
        switch (type) {
            case 1: taskType = DelayTask.TaskType.ORDER_TIMEOUT; break;
            case 2: taskType = DelayTask.TaskType.EMAIL_REMINDER; break;
            case 3: taskType = DelayTask.TaskType.TASK_EXECUTION; break;
            default: throw new IllegalArgumentException("无效的任务类型");
        }
        
        // 创建任务
        DelayTask task = new DelayTask(
                taskType, 
                taskId, 
                content, 
                System.currentTimeMillis(), 
                0
        );
        
        // 添加任务
        delayQueueService.addTask(task, seconds);
        return "任务添加成功! ID: " + taskId;
    }

    /**
     * 查看队列状态
     */
    @GetMapping("/status")
    public String queueStatus() {
        long size = delayQueueService.getQueueSize();
        return "当前延迟队列任务数量: " + size;
    }
}

启动类(DelayQueueApplication.java)

java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling // 启用定时任务
@EnableAsync     // 启用异步方法
public class DelayQueueApplication {
    public static void main(String[] args) {
        SpringApplication.run(DelayQueueApplication.class, args);
    }
}

方案优势与注意事项

优势

  1. 高性能:利用Redis内存操作和ZSet有序特性

  2. 低延迟:定时任务扫描保证任务及时处理

  3. 高可靠:任务处理失败可进入死信队列

  4. 可扩展:线程池支持并行处理多个任务

  5. 灵活配置:支持批量处理大小、扫描间隔等参数配置

注意事项

  1. 任务幂等性:确保任务可重复处理而不产生副作用

  2. 任务超时处理:长时间任务需考虑超时机制

  3. Redis持久化:根据业务需求配置RDB或AOF

  4. 分布式环境:多实例部署时需考虑任务竞争问题

  5. 监控告警:添加队列积压监控和任务失败告警

扩展建议

  1. 添加管理界面

    • 查看队列中的任务

    • 手动重试失败任务

    • 统计任务处理成功率

  2. 分布式锁优化

    java 复制代码
    // 在scanExpiredTasks方法中
    String lockKey = "delay_queue_lock";
    Boolean lockAcquired = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "locked", 30, TimeUnit.SECONDS);
    
    if (lockAcquired != null && lockAcquired) {
        try {
            // 执行扫描任务逻辑
        } finally {
            redisTemplate.delete(lockKey);
        }
    }
  3. 任务优先级支持

    复制代码
    // 在添加任务时,可将优先级加入score计算
    double score = executeTime + (priority * 0.001);
  4. 延迟时间精确控制

    • 使用Redisson的DelayedQueue组件

    • 或使用Redis的Keyspace通知功能

这个实现方案提供了一个完整、可扩展的延迟队列系统,适用于订单超时处理、定时提醒、延迟任务执行等多种业务场景。

相关推荐
止语Lab25 分钟前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活25 分钟前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困30 分钟前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn30 分钟前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
dkbnull33 分钟前
Spring Boot请求处理组件对比详解
java·spring boot
用户2986985301437 分钟前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
不能只会打代码37 分钟前
Day 011 — Spring 全家桶深度拆解
spring boot·mybatis·spring aop·spring mvc·spring ioc
用户772831049084039 分钟前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金1 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
X-⃢_⃢-X1 小时前
十、Redis之布隆过滤器
数据库·redis·缓存