应使用 redisson 的 rdelayedqueue 或 rscheduledexecutorservice 而非 @scheduled + 分布式锁,因前者由 redis 统一调度、避免重复触发,后者仅防并发执行不防重复调度;需用 redisson-spring-boot-starter ≥3.20.0 版本并正确配置线程参数。

直接用 Redisson 的 RQueue 或 RDelayedQueue,别自己基于 List/ZSet 手撸——封装成熟、线程安全、自动重连、支持集群,且不依赖额外中间件。
为什么不能只靠 Spring @Scheduled + Redis 分布式锁?
因为 @Scheduled 本质是每个节点独立触发:哪怕加了锁,也只防「同时执行」,不防「重复调度」。比如两个节点都算出「3:00 执行」,抢到锁的那个执行了,另一个等锁释放后可能又在 3:00:01 再次触发——尤其在时钟漂移或 JVM GC 暂停时更明显。@Scheduled 从不感知其他节点的调度计划,它只管自己 clock。
真正要解决的是「谁来决定什么时候该发任务」,而不是「谁来执行」。Redisson 把这个决策权交给 Redis,所有节点读同一个有序集合(ZSET),天然对齐时间点。
必须用 redisson-spring-boot-starter 3.20.0+ 版本
低版本(如 3.16.x)有硬伤:RScheduledExecutorService 在节点宕机时不会迁移待执行任务;CronSchedule.of() 对 */5 这类表达式解析不全,导致「每 5 分钟」变成「只在 :00 执行」。
-
redisson-spring-boot-starter必须 ≥ 3.20.0(Spring Boot 3.x 推荐 3.24.x) - YAML 中必须显式启用 executor 模块:
redisson.config下写threads: 16和nettyThreads: 32,设为 0 会导致schedule()静默失败 - 不要混用
spring-boot-starter-data-redis和原生 Redisson 客户端——starter 已包含完整集成,多引反而引发RedisConnection冲突
延迟队列必须用 RDelayedQueue,不是 RQueue + 定时轮询
RDelayedQueue 底层用 Redis 的 ZSET 存时间戳作为 score,消费者由 Redisson 自动监听并投递,无轮询开销。而手写轮询(比如每秒 ZRANGEBYSCORE)会浪费连接、增加 Redis 压力,且无法保证精确触发。
示例代码片段:
public void addDelayTask(String payload, long delaySeconds) {
RDelayedQueue<string> delayed = redissonClient.getDelayedQueue("order_timeout");
// 注意:offer 第二个参数是 delay,不是绝对时间戳
delayed.offer(payload, delaySeconds, TimeUnit.SECONDS);
}</string>
- 任务对象必须可序列化(不能传 Lambda 或匿名内部类)
- delay 单位是「从现在起延迟多久」,不是 cron 表达式
- 若需 cron 调度(如「每天 9 点」),必须用
RScheduledExecutorService.schedule(),而非RDelayedQueue
RScheduledExecutorService 提交任务时容易忽略的三件事
它和本地 ScheduledExecutorService 行为差异极大,踩坑点集中在这三处:
- Runnable 必须实现
Serializable,且所有闭包变量也要可序列化——JVM 本地类加载器无法跨节点还原 Lambda - 不能依赖
System.currentTimeMillis()做逻辑判断,不同节点系统时间可能差几百毫秒;应以 Redis 服务端时间(redissonClient.getServerTime())为准 - 异常不会自动重试:默认失败即丢弃,需手动 wrap 成
Callable并捕获异常后调用reschedule()
真正麻烦的从来不是「怎么放任务进去」,而是「任务失败后状态怎么回滚、重试边界在哪、超时怎么判定」——这些得结合业务幂等性和 Redis 中存储的元数据一起设计,不是靠框架自动解决的。











