前言

经典的 Spring 定时任务知识点!


📋 核心原因:Spring 的 @Scheduled 注解!

这个方法没有显式调用,但它会自动执行!因为加了 @Scheduled 注解!


第一步:Spring Boot 扫描并解析

当 Spring 项目启动时,会自动扫描@Scheduled 注解的方法:

@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
public void refreshLocalCache() {
    // 这个方法会被 Spring 定时任务调度器自动调用!
}

第二步:Spring 内部的调度机制

Spring Boot 内部有一个任务调度器(TaskScheduler),会自动执行这些定时任务:

项目启动
  ↓
Spring Boot 自动配置(@EnableScheduling)
  ↓
扫描所有 @Scheduled 注解的方法
  ↓
注册到任务调度器(TaskScheduler)
  ↓
启动后,按规则自动执行!

📋 完整的触发流程

时间点 操作 说明
启动时 @PostConstruct 执行 initLocalCache 缓存初始化,打印第一次日志
60秒后 调度器自动调用 refreshLocalCache 第一次定时任务执行
再60秒后 调度器再次调用 第二次执行
再60秒后 无限循环…

🔍 代码佐证:为什么没有显式调用?

MpAccountServiceImpl 里,refreshLocalCache 方法:

// ✅ 只加了注解,没有其他方法调用它!
@Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
public void refreshLocalCache() {
    // ...
}

💡 Spring Boot 如何启用定时任务?

通常在启动类里,或者配置里,加了 @EnableScheduling 注解:

@EnableScheduling  // ← 这个注解启用定时任务
@SpringBootApplication
public class YudaoServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(YudaoServerApplication.class, args);
    }
}

📊 总结对比

调用方式 例子 说明
显式调用 service.method() 你主动在代码里写调用语句
注解驱动调用 @Scheduled Spring 容器自动调度,你不用管!

🎯 你看到的日志来源

2026-04-25 18:27:45  → refreshLocalCache 执行
2026-04-25 18:28:4560秒后,又自动执行!
2026-04-25 18:29:45  → 再60...
2026-04-25 18:30:45...

这些日志都来自Spring 定时任务调度器,自动调用 refreshLocalCache → 然后 refreshLocalCache 调用 initLocalCache → 打印日志!


Spring 官方的定时任务机制,不需要显式调用!😎

📋 这个类是用来清理 Redis Stream 消息的!

这个定时任务,每小时执行一次,清理 Redis Stream 里的旧消息!


🎯 核心功能:清理 Redis Stream 消息

Redis Stream 是什么?

Redis Stream 是 Redis 5.0 新的数据结构,用来实现消息队列(MQ),这个项目用它做异步消息!


📊 定时任务的配置(第40行)

@Scheduled(cron = "0 0 * * * ?")
public void cleanup() {
    // cron = "0 0 * * * ?" → 每小时执行一次!
    // 0秒 0分 *时 → 每小时的整点执行
}

💡 完整的执行逻辑

第一步:加分布式锁(第42-51行)

RLock lock = redissonClient.getLock(LOCK_KEY);
// 尝试加锁
if (lock.tryLock()) {
    // 加锁成功 → 执行清理
    try {
        execute();
    } finally {
        lock.unlock();
    }
}

为什么加锁?

  • 如果集群部署多节点!
  • 防止多个节点同时清理!
  • Redisson 分布式锁保证只有一个节点执行清理!

第二步:执行清理(第58-71行)

private void execute() {
    StreamOperations<String, Object, Object> ops = redisTemplate.opsForStream();
    
    listeners.forEach(listener -> {
        try {
            // 使用 XTRIM 命令!
            Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, true);
            if (trimCount > 0) {
                log.info("[清理了 {} 条消息", trimCount);
            }
        } catch (Exception ex) {
            log.error("[清理异常]", ex);
        }
    });
}

📊 工作原理图解

┌─────────────────────────────────────────────────────────┐
│   Redis Stream (消息队列)                          │
├─────────────────────────────────────────────────────────┤
│   消息1 (老的,很久以前)                           │
│   消息2 (老的)                                       │
│   ...                                                 │
│   消息9999 (最近的)                                  │
│   消息10000 (最近的)                                 │
└─────────────────────────────────────────────────────────┘
                        ↓ 每小时自动运行一次定时任务
                  使用 XTRIM streamKey MAXLEN ~ 10000
                        ↓
┌─────────────────────────────────────────────────────────┐
│   只保留最近10000条消息!                             │
│   删除超过的老消息删掉,释放内存!                      │
└─────────────────────────────────────────────────────────┘

💡 为什么需要这个定时任务?

看注释(第18行):

/**
 * 记一次 redis stream 数据类型内存不释放问题
 */

问题: Redis Stream 消息消费后不会自动删除,越积越多,内存会炸!
解决: 定时清理,只保留最近10000条!


📋 两个定时任务对比

定时任务类 频率 功能
MpAccountServiceImpl.refreshLocalCache() 每分钟 刷新公众号账号缓存
RedisStreamMessageCleanupJob.cleanup() 每小时 清理 Redis Stream 旧消息

🎯 总结

| 实现方式 | 注解 | 用途 |
|---------|------|
| 定时任务1 | @Scheduled(initialDelay=60, fixedRate=60) | 每分钟刷新缓存 |
| 定时任务2 | @Scheduled(cron="0 0 * * * ?") | 每小时清理Redis |

这都是 Spring 标准的定时任务,@Scheduled 注解驱动,Spring 容器自动调度执行!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐