# redis-queue-executor **Repository Path**: decentant/redis-queue-executor ## Basic Information - **Project Name**: redis-queue-executor - **Description**: 基于 Redis 的轻量级分布式任务队列,支持三种消费模式(轮询 / 广播唤醒 / Stream 消费组),内置任务保活、废弃任务自动重入队、队列背压、节点监控与优雅停机能力。适用于中小规模、无需引入专业消息中间件的分布式任务调度场景。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-27 - **Last Updated**: 2026-09-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 分布式队列执行器(queue-execute) 基于 Redis 的轻量级分布式任务队列,支持三种消费模式(轮询 / 广播唤醒 / Stream 消费组),内置任务保活、废弃任务自动重入队、队列背压、节点监控与优雅停机能力。适用于中小规模、无需引入专业消息中间件的分布式任务调度场景。 ## 设计初衷与定位 **为什么不用 RabbitMQ / Kafka?** 本项目面向**轻量级项目**而设计。RabbitMQ、Kafka 等专业消息中间件功能完善,但运行成本较高:通常需要 2GB+ 内存以及独立的部署、监控、备份、升级体系,在便宜的云服务器(1~2 核 / 1~2GB 内存)上难以承载——对轻量项目来说,"再维护一个中间件"本身就是最大的成本。 本项目的核心思路是:**复用项目中已有的 Redis(通常用于缓存 / Session),在不引入任何新组件、不增加运维成本的前提下,提供一套够用的分布式任务队列能力**。 这不是"退而求其次",而是轻量场景下的务实选择:对资源受限、团队规模小、追求稳定运行的项目来说,少一个需要专门伺候的中间件,就少一类运维事故。 **一句话定位**:面向轻量级项目的、尽力而为(best-effort)的分布式任务队列。 ## 适用场景与边界 **适合使用的场景**: - 任务量在每秒几十到几千的量级 - 允许极低概率的消息丢失,或业务上具备补偿手段(重新触发、定时对账) - 秒级延迟可接受(异步任务、削峰、解耦场景) - 任务可重试、可幂等设计 **不适合的场景**(请改用专业消息中间件): - 丢一条消息就是事故且无法补偿(资金、订单核心链路) - 每秒万级以上的吞吐量 - 需要延迟队列、严格顺序、事务消息等高级特性 **可靠性语义**: | 模式 | 投递语义 | |------|---------| | Loop / Channel | 尽力投递(best-effort):存在极小的消息丢失窗口(如节点取出消息后、写入监控 Key 前崩溃),由保活监控兜底重入队 | | Stream | 接近"至少一次"(at-least-once):消费组 + ACK 确认 + Pending 恢复,推荐生产使用 | **消费语义(重要)**:三种模式全部为**点对点(争抢式)**——一条任务只会被**一个**节点取出并执行,多个节点自动分摊负载。Channel 模式名称中的"广播"指的是**唤醒信号**的广播(Pub/Sub 通知所有节点"有活了"),**不是**任务数据的广播;如果需要"每条消息所有节点都处理"的广播消费,本项目不适用(可基于 Stream 多消费组自行扩展)。 ## 模块结构 | 模块 | 说明 | |------|------| | `queue-execute-core` | 核心实现:执行器、作业基类、任务下发工具、监控与工具类 | | `queue-execute-starter` | Spring Boot 自动配置:读取 `queue-execute` 配置并创建、启动执行器 | | `queue-execute-demo` | 使用示例:三种模式的完整演示(含 Web 接口下发任务) | ## 快速开始 ### 1. 引入依赖 ```xml com.itdct.quexor queue-execute-starter 1.5.0-SNAPSHOT ``` ### 2. 添加配置 ```yaml queue-execute: enable: true queues: # 模块名(会被转换为大写作为队列标识) loop-queue: # 0=轮询 1=广播唤醒 2=Stream消费组 getJobListenMode: 0 # 可选:手动指定节点IP,不配置则自动探测 nodeIp: 192.168.1.102 stream-queue: getJobListenMode: 2 getJobConfig: # Stream消费组名 streamGroup: default ``` ### 3. 定义作业 继承 `BaseQueueJob`,实现核心回调方法: ```java public class MyJob extends BaseQueueJob { @Override public void onInit() { /* execute和onAbort前都会调用,用于参数初始化 */ } @Override public boolean execute() { // 核心业务逻辑,返回true表示成功 return true; } @Override public void onSuccess() { /* 执行成功回调 */ } @Override public void onAbort() { /* 任务被判定废弃时的回调 */ } @Override public void onFail() { /* 执行失败回调 */ } @Override public void onFinish() { /* 无论成功/失败/出错都会执行 */ } @Override public void onError(Exception e) { /* 执行抛异常时回调 */ } } ``` 作业实例由执行端通过反射创建(无参构造),因此作业类必须提供无参构造函数。 ### 4. 下发任务 ```java QueueJobConfig jobConfig = new QueueJobConfig(taskId, "", MyJob.class.getName(), "业务数据JSON"); // 按消费模式选择对应的下发方法 AssignJob.addLoopQueueJob(redisTemplate, "LOOP-QUEUE", jobConfig); AssignJob.addChannelQueueJob(redisTemplate, "CHANNEL-QUEUE", jobConfig); AssignJob.addStreamQueueJob(redisTemplate, "STREAM-QUEUE", "default", jobConfig); ``` ## 三种消费模式 | 模式 | 常量 | 实现机制 | 可靠性 | 适用场景 | |------|------|---------|--------|---------| | 轮询 | `LOOP_GET(0)` | Redis List + 动态间隔轮询 | 中,依赖保活监控兜底 | 简单场景、任务量稳定 | | 广播唤醒 | `CHANNEL_CAST(1)` | Pub/Sub 唤醒 + List 取数据 | 中,信号可能丢失(超时兜底) | 低延迟要求的突发任务 | | Stream | `STREAM(2)` | Redis Stream 消费组 + Pending 扫描 | 高,ACK 确认 + pending 恢复 | 要求消息不丢的任务 | 三种模式通用机制: - **保活刷新**:任务执行期间由独立线程定期刷新 `REFRESH_KEY`,监控线程据此判定任务是否"废弃"(节点崩溃、线程卡死等) - **废弃重入队**:废弃任务触发 `onAbort` 回调后重新进入队列,`abortTimes` 递增 - **失败归还**:无法实例化等原因无法处理的任务会归还队列(`returnTimes` 递增),超过 `maxReturnTimes` 后进入死信队列 - **任务结果记录**:任务完成时记录结果(成功/失败/出错)与异常摘要(默认保留 7 天,最近 1000 条可查),支持任务追踪 - **死信队列**:超过最大归还次数的任务进入死信队列(默认不自动清理),可通过 `DeadJobManager` 查询、重试、删除 - **幂等扩展点**:重写 `BaseQueueJob.isAlreadyProcessed()` 返回 `true` 可跳过已处理过的任务 ## 演示台(queue-execute-demo) demo 模块内置可视化演示台(端口为 application.yaml 配置的 4000~5000 随机端口): - **队列演示** `http://localhost:{port}/queue-ui`: - 实时展示三种队列(Loop/Channel/Stream)的状态、等待队列长度、活跃线程与执行统计 - 选择任务类型(小/中/大)与数量后一键发送,自动追踪消费进度(进度条 + 实时速率曲线) - 全部完成后自动计算吞吐量(个/秒)与总耗时 - **任务追踪** `http://localhost:{port}/task-ui`: - 查询最近执行完成的任务:结果(成功/失败/出错)、异常摘要、执行节点、端到端耗时、重试次数 - **死信管理** `http://localhost:{port}/dead-ui`: - 查看进入死信队列的任务(超过最大归还次数被作废),支持手动重试(重新入队)与删除 - **节点信息** `http://localhost:{port}/node-ui`(根路径 `http://localhost:{port}/` 默认即打开此页): - 按模块分组展示 Redis 中所有活跃节点(节点停止上报后约 5 秒自动从列表消失) - 展示每个节点的状态、活跃线程、等待队列、执行统计、CPU/堆内存、最后刷新时间等运行指标 ```bash # 启动演示,需要可用的 Redis 连接(见 application.yaml) # 1. 首次运行(或修改了 core/starter 后)先安装依赖模块到本地仓库 mvn install -DskipTests # 2. 进入 demo 目录启动应用 cd queue-execute-demo mvn spring-boot:run ``` ## 典型用法:日志批量入库 把应用日志异步、批量地写入数据库,是轻量项目最典型的使用场景之一。demo 模块内置了完整可运行的示例(`com.itdct.quexe.demo.log` + `LogPersistJob`),链路如下: ``` 应用日志 (Logback Appender 采集) ↓ 内存有界缓冲(默认 5 万条,满则丢弃) ↓ 每 200 条 或 每 5 秒(双条件) 攒批器组装“日志批次任务” → AssignJob 下发(LOG-QUEUE 模块,带背压保护) ↓ LogPersistJob:解包批次 → JDBC 批量插入 ↓ 失败:快速重试 → 重新入队(复用归还/死信链路)→ 超过重试上限进死信队列 ``` **核心设计**: - 一条“任务”承载一批日志(默认 200 条),把队列开销摊薄到 1/200;不要一条日志一个任务 - 内存缓冲有界,满了直接丢弃(日志允许少量丢失),不拖垮应用内存 - 数据库不可用时:批次级重试(`BaseQueueJob.retry()` 重新入队),超过 `maxReturnTimes` 进死信队列,可在死信管理页查看与手动重试 - 队列背压(`maxCapacity`)防止 Redis 内存膨胀;入队失败直接丢弃批次,不阻塞业务线程 - 采集端排除采集链路自身的 logger,避免“失败日志再入库”的自我循环 **体验方式**(demo 内置 H2 内存库,无需外部数据库): 1. 启动 demo 后正常产生日志(框架自身日志即会被采集) 2. 打开任务追踪页 `/task-ui` 选择 LOG-QUEUE:每批日志对应一个任务 3. 打开 H2 控制台 `http://localhost:4050/h2-console`(JDBC URL 填 `jdbc:h2:mem:queuedb`,用户名 `sa`,密码留空)执行 `SELECT COUNT(*) FROM app_log;` 验证入库 **关键配置**: | 配置 | 默认值 | 说明 | |------|--------|------| | `demo.log-batch.batch-size` | 200 | 批次大小(条) | | `demo.log-batch.flush-interval-millis` | 5000 | 攒批最长等待(毫秒),低流量兜底发送 | | `demo.log-batch.buffer-capacity` | 50000 | 内存缓冲容量(条),超出丢弃 | | `queue-execute.queues.log-queue.maxCapacity` | 50000 | 队列背压阈值,防止 Redis 膨胀 | **注意**: - 队列作业由框架反射创建,不能直接使用 `@Autowired`;demo 通过静态桥 `SpringBeanHolder` 获取 `JdbcTemplate`(正式项目可采用同样方式) - 生产使用时为日志表设置清理/归档策略(如按月分区表、定期删除历史数据),避免表无限增长 ## 配置参考 ### 队列级配置(QueueExeConfig) | 配置项 | 默认值 | 说明 | |--------|--------|------| | `enable` | true | 是否启用该队列 | | `autoRun` | true | 是否随 Spring 容器自动启动 | | `getJobListenMode` | 0 | 消费模式:0/1/2 | | `coreThreadCount` | CPU-1 | 核心执行线程数(最小为1) | | `maxThreadCount` | CPU*1.5 | 最大执行线程数(最小为1) | | `queueExecutorQueueSize` | 20 | 执行线程池阻塞队列长度 | | `threadKeepAliveTime` | 60 | 空闲线程存活时间(秒) | | `refreshIntervalTime` | 5000 | 任务保活刷新间隔(毫秒) | | `jobAbortTime` | 40000 | 判定任务废弃的超时时间(毫秒) | | `initWaitTime` | 1000 | 启动前等待时间(毫秒) | | `maxCapacity` | 10000000 | 队列最大长度(背压阈值,<=0 表示不限制) | | `throwErrorIfOverLimit` | true | 超出最大长度时抛异常;false 则循环等待 | | `overLimitSleepTime` | 1000 | 超出最大长度后的等待时间(毫秒) | | `canJobExpire` | false | 是否启用任务过期 | | `totalJobExpireTime` | 600 | 整体任务过期时间(秒) | | `maxReturnTimes` | 50 | 任务最大归还次数 | | `notFoundClassReturnQueue` | true | 无法实例化时是否归还队列 | | `threadPoolShutdownWaitTime` | 60 | 停机时线程池等待时间(秒) | | `jobClassPackagePrefixes` | 空 | 作业类包名白名单,为空不校验(建议配置以防御反序列化攻击) | | `recordJobResult` | true | 是否记录任务执行结果(用于任务追踪查询) | | `jobResultExpireTime` | 604800 | 任务执行结果保留时间(秒,默认 7 天) | | `jobResultIndexMaxSize` | 1000 | 最近任务索引保留的最大条数(超出后裁剪最旧索引) | ### 子配置 - `getJobConfig`(获取任务): - `getJobErrorWaitTime` 获取出错等待(2000ms) - `getJobSleepIntervalTime` 广播模式空转等待(1000ms) - `maxNotGetMessageWaitTimes` 广播模式进入休眠前空轮次数(5) - `channelAwaitTimeout` 广播模式最大休眠时间(30000ms,防永久阻塞) - `streamGroup` Stream 消费组名(default) - `streamPendingIntervalTime` pending 扫描间隔(2000ms) - `streamPendingTimeout` pending 超时重入队时间(30000ms) - `monitorJobConfig`(任务监控):监控间隔(6000ms)、误差偏移(2000ms)、monitorKey TTL(30s) - `nodeMonitorConfig`(节点上报):`nodeMonitor` 开关、上报间隔(2000ms)、信息过期时间(5000ms) - `logConfig`(日志):完成日志、开始日志、性能日志开关 - `broadcastConfig`(广播):`broadcastIntervalTime` 发送端节流间隔(1000ms,按模块生效)、`signalIntervalTime` 消费端唤醒防抖(2000ms) ## 运行机制 ### 启动与优雅停机 - 执行器由自动配置在**容器就绪阶段**(`SmartLifecycle.start`)统一启动;单个执行器启动异常不影响其他执行器 - 应用未引入 Redis 或未提供 `RedisConnectionFactory` 时,自动配置整体跳过,不影响应用启动;`enable=false` 或未配置 `queues` 时会跳过并输出日志 - 停机流程:置停止标志 → 停止外部监听容器(广播/Stream)→ 等待获取任务/监控/节点信息等线程退出 → 关闭线程池 → 清理节点信息 - 停机后 `shutdownFlag` 保持为 `true`,重新启动必须调用 `execute()`(内部会重置标志),请勿绕过 ### 手动创建执行器 不依赖自动配置时,需要手动注册配置并显式启动: ```java @Bean(destroyMethod = "shutdown") public StreamQueueExecute streamExecute(RedisTemplate redisTemplate) { QueueExeConfig config = new QueueExeConfig(); config.setModuleName("stream-test"); config.setGetJobListenMode(QueueListenMode.STREAM); // 注册配置:使任务下发时能应用背压与广播节流配置 AssignJob.registerQueueConfig(config.getModuleName(), config); StreamQueueExecute executor = new StreamQueueExecute(config, redisTemplate); // 手动创建的执行器不会自动启动,需要显式调用 executor.autoStartIfNeeded(); return executor; } ``` ### 队列容量与背压 - 任务下发前会检查队列长度,超过 `maxCapacity` 时: - `throwErrorIfOverLimit=true`(默认):抛出 `QueueExecuteException`,由调用方决定如何处理 - `throwErrorIfOverLimit=false`:循环等待(每轮 `overLimitSleepTime` 毫秒)直到队列低于阈值 - 手动创建执行器或跨应用下发任务时,需要先调用 `AssignJob.registerQueueConfig()` 注册配置,否则不启用容量检查 ## 生产使用建议 在保持轻量的前提下,按以下顺序加固即可(成本低、收益高): 1. **优先使用 Stream 模式**:Redis Stream 是官方为消息队列场景设计的结构(消费组 + ACK + Pending),把 Stream 模式用扎实,可靠性已接近专业 MQ 的投递语义;Loop / Channel 适合对可靠性要求更低的场景 2. **开启 Redis AOF 持久化**(`appendonly yes`):Redis 进程崩溃重启后队列数据不丢失,这是性价比最高的一道保险,每秒刷盘即可 3. **业务侧做幂等 + 兜底对账**:重写 `BaseQueueJob.isAlreadyProcessed()` 实现幂等;增加定时任务扫描"应完成未完成"的数据做补偿——比追求 MQ 层面的绝对可靠便宜得多 4. **统一团队认知**:明确本项目是"尽力投递、允许补偿"语义,不要假设它等价于专业消息中间件 **何时考虑迁移到专业中间件**: - Redis 的队列操作成为瓶颈(延迟升高、CPU 打满) - 业务可靠性要求升级为"绝不能丢"且无法业务补偿 - 出现因丢失消息导致的真实业务损失 ## 注意事项 1. **作业类白名单**:生产环境建议配置 `jobClassPackagePrefixes`,防止反序列化攻击 2. **节点ID**:同一队列的节点ID不能重复,未配置时自动生成 `node-{序号}`;如需固定请确保唯一 3. **广播模式可靠性**:Pub/Sub 信号可能丢失,消费端有超时兜底(`channelAwaitTimeout`),对延迟敏感且要求必达的场景请使用 Stream 模式 4. **Stream 模式内存**:长时间运行建议按需对 Stream 执行 `XTRIM` 控制内存增长 5. **JDK 兼容**:Lombok 需 1.18.30+ 以兼容高版本 JDK 编译 6. **配置校验**:`getJobListenMode` 只接受 0/1/2,非法值会导致应用启动失败(避免静默降级为轮询模式)