我开发的KE Reliable Executor:基于 Redis/MySQL 的 Spring Boot 可靠任务执行框架
- 约 10 分钟读完 - 全文 3813 字我开发的KE Reliable Executor:基于 Redis/MySQL 的 Spring Boot 可靠任务执行框架
在微服务里,异步任务随处可见:支付后通知第三方、订单超时取消、批量对账、报表生成……这些任务有一个共同诉求——不能丢、不能重复执行、失败能重试、状态要能查。
传统做法是直接引入 Kafka、RocketMQ 这类重型消息中间件。但对于很多中小型业务来说,这未免太重:
- 需要额外维护 Broker、Topic、消费组;
- 团队要熟悉一套新的运维和监控体系;
- 为了“可靠”还要配置死信队列、事务消息等高级特性。
如果你的系统里已经有 Redis(或 MySQL),并且任务量级尚未达到 MQ 的吞吐上限,那么完全可以基于 Redis 搭一个轻量且可靠的异步任务执行框架。
我开发是这样一个框架:KE Reliable Executor。
开源地址:https://gitee.com/soulnew/reliableexecutor
一、为什么需要这个工具?
先回顾一下常见异步任务场景里的痛点:
| 痛点 | 说明 |
|---|---|
| 任务丢失 | 服务重启或执行线程异常退出后,未执行的任务找不到了 |
| 重复执行 | 多实例部署时,同一个任务被多个节点同时消费 |
| 失败即结束 | 业务异常或网络抖动后,没有自动重试机制 |
| 状态黑盒 | 无法查询任务当前是成功、失败、执行中还是已取消 |
| 接入成本高 | 引入 MQ 需要改代码、加依赖、搭集群 |
KE Reliable Executor 要解决的就是这些问题。它把“可靠任务执行”抽象成一个 Spring Boot Starter:
- 业务方只负责定义任务处理器和提交任务;
- 框架负责存储、调度、加锁、重试、状态流转、清理;
- 所有状态都可通过 API 查询,也可通过 Micrometer 指标监控。
适合什么场景?
- 已有 Redis/MySQL,不想额外维护 MQ;
- 任务量中等,对可靠性和重试有要求;
- 需要任务状态可查询、可人工介入;
- 团队希望以最小成本获得“类 MQ”的可靠执行能力。
不适合什么场景?
- 海量消息、高吞吐(Kafka/RocketMQ 更合适);
- 严格顺序消费(ZSET 按时间排序,不保证业务绝对顺序);
- 需要复杂路由、死信队列、事务消息等高级 MQ 特性。
二、与竞品的对比
2.1 与 Kafka / RocketMQ 对比
| 维度 | Kafka / RocketMQ | KE Reliable Executor |
|---|---|---|
| 基础设施 | 需要独立 Broker 集群 | 复用 Redis 或 MySQL |
| 运维成本 | 高(Topic、消费组、监控) | 低(Spring Boot Starter) |
| 接入成本 | 需要学习 SDK 与语义 | 注解 + 注入接口即可 |
| 吞吐量 | 极高 | 中等 |
| 状态查询 | 需自建审计表 | 内置状态查询 |
| 重试机制 | 需配置消费重试 | 内置可配置重试 |
| 适用阶段 | 大型业务、高并发 | 中小型业务、快速落地 |
结论:KE Reliable Executor 不是取代 MQ,而是在“已有 Redis/MySQL、任务量可控”的场景下,提供一个更轻量的选择。
2.2 与 Spring @Scheduled + 数据库轮询对比
很多团队最早会用数据库表 + @Scheduled 轮询来实现异步任务。这种做法的问题:
- 无分布式锁:多实例同时轮询会重复执行;
- 无状态机:任务状态全靠字段控制,容易混乱;
- 无统一重试策略:重试间隔、重试次数需要手写;
- 无优雅关闭:应用关闭时可能丢失正在执行的任务。
KE Reliable Executor 把这些都封装好了:
- Master 选举 + 任务级锁保证不重复;
- 明确的状态机(CREATED → RUNNING → PENDING/COMPLETED/FAILED);
- 可配置的重试间隔与次数;
- 优雅释放锁与线程池关闭。
2.3 与 XXL-JOB / PowerJob 对比
XXL-JOB 和 PowerJob 是优秀的分布式任务调度平台,但它们更适合定时任务和调度中心模式:
- 需要单独部署调度中心;
- 适合 Cron 表达式触发的批处理任务;
- 任务由业务主动提交的场景下,接入不够自然。
KE Reliable Executor 更像一个嵌入式任务队列:
- 无额外服务,随业务应用启动;
- 任务由业务代码主动提交;
- 强调“提交即可靠执行”,而非定时触发。
三、整体架构
3.1 模块解构图
graph TD
A[业务服务] -->|submitTask| B(ReliableExecutor API)
B --> C[TaskScheduler]
C --> D{Master 锁}
D -->|持有锁| E[扫描 ZSET 任务]
E --> F[任务级锁]
F -->|获取成功| G[提交线程池]
G --> H[TaskExecutor]
H --> I["任务处理器 @ReliableTaskHandler"]
I -->|成功| J[移入 FINISHED]
I -->|失败| K[更新为 PENDING 等待重试]
C --> L[TaskStorage]
L --> M[RedisTaskStorage]
L --> N[JdbcTaskStorage]
H --> O[ReliableTaskListener]
C --> P[ReliableExecutorMetrics]
3.2 两层锁模型
graph TB
subgraph 全局层
A[Master 锁] -->|仅一个实例| B[负责调度扫描]
end
subgraph 任务层
C["任务级锁 task-lock:{taskId}"] -->|仅一个线程| D[执行任务]
end
B --> C
- Master 锁:同一时刻只有一个实例负责扫描任务,避免多节点拉取同一批任务。
- 任务级锁:针对每个 CREATED 任务,提交执行前再抢一次锁,保证同一任务不会被并发执行。
3.3 队列设计
graph LR
A[提交任务] -->|add| B[Redis ZSET running-task-zset]
B -->|ZRANGE 按 score| C[调度器]
C -->|执行成功/失败/超时| D[Lua 原子转移]
D -->|RPUSH + ZREM| E[Redis List finished-task-list]
使用 ZSET 而非 List 的核心原因:
- 天然支持时间调度:score 编码创建时间或下次重试时间;
- 任务唯一:同一个 member 在队列中只有一份;
- 状态分区:CREATED 与 PENDING 使用不同 score 基数,一次范围查询即可区分。
Score 编码规则:
score = BASE + epochMillis * 100 + retryCount
CREATED 基数:1_000_000_000_000_000
PENDING 基数:2_000_000_000_000_0003.4 任务状态机
stateDiagram-v2
[*] --> CREATED: submitTask
CREATED --> RUNNING: 调度/立即执行
RUNNING --> COMPLETED: 执行成功
RUNNING --> PENDING: 执行失败,未超重试
PENDING --> RUNNING: 到达重试时间
RUNNING --> FAILED: 超过重试次数
RUNNING --> TIMEOUT: 超过最大时长
CREATED --> CANCELLED: 取消任务
PENDING --> CANCELLED: 取消任务
RUNNING --> ERROR: 无处理器/参数错误
四、核心设计要点
4.1 Lua 原子转移
任务进入终态时,必须保证“写入 finished list”和“从 ZSET 移除”是原子的,否则可能出现:
- 任务已判定成功但还在 ZSET 里,被下一次调度捞出来;
- 任务已从 ZSET 移除但 finished list 没写入,查询不到结果。
框架使用 Lua 脚本完成这一转移:
local score = redis.call('ZSCORE', KEYS[1], ARGV[1]);
redis.call('RPUSH', KEYS[2], ARGV[2]);
if score then
redis.call('ZREM', KEYS[1], ARGV[1]);
return 1;
else
return 0;
end逻辑:先查 ZSET,无论是否存在都把终态信息写入 finished list,如果存在再删除。整个脚本在 Redis 单线程执行,天然原子。
4.2 Double-Check
调度器拿到任务级锁后、提交线程池前,会再次回查 Redis:
- 任务是否仍在 running ZSET 中?
- 任务状态是否仍是 CREATED?
任一条件不满足,立即释放锁并跳过。这是经典的双重检查模式,把“检查-执行”串在锁保护的临界区内。
4.3 双存储后端
graph TD
A[TaskStorage 接口] --> B[RedisTaskStorage]
A --> C[JdbcTaskStorage]
B --> D[Redisson ZSET + List]
C --> E[reliable_executor_task 表]
F[配置 reliable-executor.task-storage] -->|redis/mysql| A
redis:默认,适合已有 Redis 的场景;mysql:适合不希望引入 Redis 或需要更强持久化的场景。
两种后端通过 Spring 条件装配切换,业务代码无感知。
五、快速开始
5.1 引入依赖
dependencies {
implementation 'com.ftmao:ke-reliable-executor:1.0.0-SNAPSHOT'
implementation 'org.redisson:redisson-spring-boot-starter:3.27.2'
}5.2 配置
spring:
application:
name: my-app
redis:
redisson:
config: |
singleServerConfig:
address: "redis://127.0.0.1:6379"
reliable-executor:
task-storage: redis
core-pool-size: 5
max-pool-size: 10
queue-capacity: 1005.3 定义处理器
@Component
public class OrderTaskHandler {
@ReliableTaskHandler(name = "process_order", paramType = OrderParam.class)
public void processOrder(OrderParam param) {
// 业务逻辑
}
@ReliableTaskHandler(name = "send_notify")
public void sendNotify() {
// 无参任务
}
}5.4 提交任务
@Service
@RequiredArgsConstructor
public class OrderService {
private final ReliableExecutor reliableExecutor;
public void submitOrder(OrderParam param) {
TaskSubmitRequest request = TaskSubmitRequest.builder()
.taskId(UUID.randomUUID().toString())
.taskName("process_order")
.taskParameter(param)
.retryLimit(3)
.retryInterval(2000)
.maxDurationMillis(60000)
.build();
reliableExecutor.submitTask(request);
}
}5.5 监听生命周期
@Component
public class MyTaskListener implements ReliableTaskListener {
@Override
public void onTaskCompleted(String taskId, String taskName) {
// 任务成功
}
@Override
public void onTaskFailed(String taskId, String taskName, String status) {
// 任务最终失败
}
}六、使用注意点与避坑指南
6.1 业务幂等仍然是最后一道防线
框架通过两层锁、double-check、Lua 原子转移把重复执行的概率降到极低,但任何分布式系统都无法 100% 保证幂等。业务 Handler 仍然应该保证幂等,尤其是涉及写数据库、调用外部接口的场景。
6.2 taskId 需要业务保证唯一
框架使用业务传入的 taskId 作为任务唯一标识。如果同一业务单据重复提交相同 taskId,第二次提交会生成新的 ZSET member(如果业务字段不同)或覆盖(如果完全相同)。建议用业务主键 + 操作类型组合生成 taskId,例如 order_12345_pay。
6.3 重试间隔与最大时长的权衡
reliable-executor:
retryInterval: 2000 # 失败后 2s 再试
maxDurationMillis: 60000 # 任务最长存活 60sretryInterval过小会导致失败任务快速反复执行,放大下游压力;maxDurationMillis是从任务创建时间算起,不是单次执行时间;- 建议失败任务采用指数退避(当前版本为固定间隔,后续可扩展)。
6.4 MySQL 分支需要自行引入 JPA
框架中 spring-boot-starter-data-jpa 是 compileOnly,使用 MySQL 存储时需要在业务应用里显式引入:
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'并执行建表脚本 db/migration/V1__create_reliable_executor_tables.sql。
6.5 Redis 集群环境注意 Key 前缀
框架默认使用 spring.application.name 作为 Redis Key 前缀。如果多个应用共用同一个 Redis 实例,务必设置不同的 spring.application.name,否则任务会互相串扰。
6.6 线程池队列满载时会跳过调度
当线程池队列达到 queueCapacity 时,调度器会跳过本次周期,避免无限制堆积。如果业务出现持续高峰,建议:
- 调大
maxPoolSize和queueCapacity; - 监控
reliable_executor_threadpool_queue_size指标; - 必要时配合业务限流。
6.7 不要忽略超时任务
maxDurationMillis 是从创建时间算起的总时长。如果一个任务在队列里排队太久,可能还没执行就触发 TIMEOUT。对于允许较长延迟的任务,要把这个值设大;对于实时性任务,要把重试间隔和排队容量一起考虑。
6.8 本地开发无 Redis 怎么办?
- 使用 MySQL 分支(H2 也可以跑测试);
- 或者本地启动一个 Redis Docker 容器;
- 框架的 Redis 集成测试会在无 Redis 时自动跳过,不影响 CI。
七、监控指标
框架内置 Micrometer 指标,可接入 Prometheus/Actuator:
| 指标名 | 含义 |
|---|---|
reliable_executor_is_master |
当前节点是否为 Master |
reliable_executor_tasks_scheduled_total |
累计调度任务数 |
reliable_executor_tasks_executed_total |
累计执行任务数 |
reliable_executor_tasks_completed_total |
累计成功任务数 |
reliable_executor_tasks_failed_total |
累计失败任务数 |
reliable_executor_tasks_timeout_total |
累计超时任务数 |
reliable_executor_tasks_retried_total |
累计重试次数 |
reliable_executor_threadpool_queue_size |
线程池队列大小 |
reliable_executor_task_execution_seconds |
任务执行耗时 |
八、开源与示例
- 开源地址:https://gitee.com/soulnew/reliableexecutor
- 示例模块:
example/子项目包含可运行的 Spring Boot 应用和 REST 接口示例,可直接启动体验任务提交与执行。
九、总结
KE Reliable Executor 是一个轻量、可靠、易接入的异步任务执行框架。它用 Redis ZSET + 两层锁 + Lua 原子转移,在“不引入重型 MQ”的前提下,提供了任务提交、调度、重试、状态查询、监控等完整能力;同时通过 MySQL 分支提供了另一种存储选择。
如果你的业务符合以下特征,不妨试试它:
- 已有 Redis/MySQL;
- 任务量中等,重视可靠性;
- 希望快速落地,不想为 MQ 付出额外运维成本;
- 需要任务状态可观测、可人工介入。
记住:框架层负责把重复消费的概率降到极低,业务层仍应保证 Handler 幂等,这才是工程上的稳妥做法。