一、基于 Redis 键空间通知(适合精确延时任务)
原理:利用 Redis 的键过期事件(EXPIRE
)触发任务执行,通过监听 __keyevent@*__:expired
通道捕获事件。
步骤:
启用 Redis 键空间通知(redis.conf 或运行时配置):
CONFIG SET notify-keyspace-events Ex
Spring Boot 监听器实现:
@Component public class KeyExpiredListener extends KeyExpirationEventMessageListener { public KeyExpiredListener(RedisMessageListenerContainer listenerContainer) { super(listenerContainer); } @Override public void onMessage(Message message, byte[] pattern) { String expiredKey = new String(message.getBody()); if (expiredKey.startsWith("task:")) { // 过滤业务键 System.out.println("执行任务: " + expiredKey); // 例如:task:123 过期时执行订单超时逻辑 } } }
注册监听器:
@Configuration public class RedisConfig { @Bean RedisMessageListenerContainer container(RedisConnectionFactory factory, KeyExpiredListener listener) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(factory); container.addMessageListener(listener, new PatternTopic("__keyevent@*__:expired")); return container; } }
调度任务(设置带过期时间的键):
@Service public class TaskScheduler { @Autowired private StringRedisTemplate redisTemplate; public void scheduleTask(String taskId, long delaySeconds) { redisTemplate.opsForValue().set("task:" + taskId, "data", delaySeconds, TimeUnit.SECONDS); // 键在 delaySeconds 秒后过期 } }
注意:需在 Redis 配置中开启 notify-keyspace-events Ex
。
二、基于 Redis 有序集合轮询(适合批量定时任务)
原理:将任务执行时间作为 ZSET
的 score
,通过定时任务查询到期的任务并执行。
步骤:
添加任务到 ZSET:
public void addTask(String taskId, Instant executeTime) { stringRedisTemplate.opsForZSet().add("scheduled_tasks", taskId, executeTime.getEpochSecond()); }
定时扫描并执行任务(每分钟轮询):
@Scheduled(cron = "0 * * * * *") // 每分钟执行 public void pollTasks() { long now = Instant.now().getEpochSecond(); Set<String> tasks = stringRedisTemplate.opsForZSet() .rangeByScore("scheduled_tasks", 0, now); // 获取所有到期任务 for (String task : tasks) { System.out.println("执行任务: " + task); // 执行后移除任务 stringRedisTemplate.opsForZSet().remove("scheduled_tasks", task); } }
优点:避免键空间通知的丢失风险,适合任务量大的场景。
三、基于 Redis 分布式锁(防集群任务重复执行)
原理:在分布式环境中,通过 Redis 锁确保同一时间只有一个实例执行定时任务。
代码示例:
@Component
public class DistributedTask {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_KEY = "TASK_LOCK:ORDER_CLEAN";
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
public void dailyTask() {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(LOCK_KEY, "locked", Duration.ofMinutes(10));
if (Boolean.TRUE.equals(locked)) {
try {
cleanExpiredOrders(); // 执行核心任务
} finally {
redisTemplate.delete(LOCK_KEY); // 释放锁(可选)
}
}
}
}
关键点:
- 使用
setIfAbsent
原子操作获取锁,避免并发冲突。 - 锁自动过期防止死锁(如任务执行超时)。
版本要求:
Spring Data Redis ≥ 2.3.0:该版本引入了 setIfAbsent(key, value, duration) 方法,支持原子性设置键值+过期时间(对应 Redis 的 SET key value NX EX seconds 命令)
四、方案对比
方案 | 适用场景 | 注意事项 |
---|---|---|
键空间通知 | 精确延时任务(如30分钟后关单) | 需配置 Redis,事件可能丢失 |
有序集合轮询 | 批量任务、高可靠性场景 | 需自行处理任务分页和重试 |
分布式锁 | 集群环境防重复执行(如日报生成) | 锁超时时间需大于任务执行时间 |
*补充:
- 关键业务(如支付超时)建议结合 数据库日志+重试机制 补偿;
- 高频任务优先选 ZSET 轮询,避免键空间通知的性能瓶颈;
- 分布式锁的锁键需包含业务标识(如
LOCK_KEY:业务名
)。