Skip to content

Spring Boot 工程启动以后,我希望将数据库中已有的固定内容,打入到 Redis 缓存中,请问如何处理? ​

  • 标签:Spring Boot、MySQL、Redis

🏛 业务架构师 ​

这是典型的**缓存预热(Cache Warming)**需求,我的标准方案分层实施:

1. 启动钩子 + 异步加载:

java
@Component
public class DictCacheWarmer implements ApplicationRunner {

    private final DictService dictService;
    private final StringRedisTemplate redis;

    @Override
    public void run(ApplicationRunner... args) {
        // 放在 ApplicationRunner:容器就绪后执行;重活可再异步
        List<Dict> all = dictService.loadAll();
        Map<String, String> pipelined = all.stream()
                .collect(toMap(d -> "dict:" + d.getCode(), Dict::toJson));
        redis.executePipelined(conn -> {              // pipeline 批量写入
            pipelined.forEach((k, v) -> conn.set(k.getBytes(), v.getBytes()));
            return null;
        });
    }
}

关键点:ApplicationRunner 时机(依赖就绪)、pipeline 批量写(几万条逐条 SET 会是分钟级,pipeline 秒级)、失败策略(重试 + 告警,不允许静默半残)。

2. 就绪探针联动:预热完成前不放流量(readiness 未通过),避免冷缓存实例接请求打穿 DB。

3. 运行期保鲜(更重要的架构问题):

  • 变更失效:字典在管理端修改时同步更新/删除缓存(事件或 Canal 监听 binlog)——预热只解决冷启动,一致性靠失效机制;
  • 过期兜底:设 TTL 防止永久脏数据;
  • 穿透保护:加载失败的 key 布隆过滤器/空值缓存兜底。

业务判断补充:数据量小(千级以内)时用 Caffeine 本地缓存 + 启动加载更简单,Redis 都可以不上——先量化数据量和 QPS 再选工具,避免为预热引入不必要的基础设施。

🏗 系统架构师 ​

标准做法:Runner/ReadyEvent 里分批读库,管道写入 Redis:

java
@Component
public class DictCacheWarmer implements ApplicationRunner {
    public void run(ApplicationArguments args) {
        List<Dict> all = dictMapper.loadAll();      // 大表分页游标读
        redisTemplate.executePipelined((RedisCallback<Object>) conn -> {
            all.forEach(d -> conn.stringCommands().set(
                (KEY_PREFIX + d.getCode()).getBytes(),
                serialize(d), Expiration.minutes(120)));
            return null;
        });
    }
}

架构决策点(比代码重要):

  1. 时点:放 Ready 事件之后、readiness 探针打开之前——预热未完不接流量,避免冷启动 miss 风暴;数据量大(千万级)要评估是否阻塞扩容,必要时分批限速后台预热;
  2. 容量与结构:10 万条乘 200B 约 20MB,单 key 整存(String 加 JSON)优于碎片 hash;TTL 加随机抖动防集体过期雪崩;
  3. 容错:Redis 不可用时预热失败不能阻断启动(降级懒加载加告警)——预热是加速手段,不是强依赖;
  4. 持续新鲜度:启动预热只管冷启动;变更走「更新库、删缓存、定时全量校准」三段式,冷热互补。

💻 全栈开发专家 ​

ApplicationRunner(或 ApplicationReadyEvent)里查库 → 写 Redis,加防重与容量保护:

java
@Component
@RequiredArgsConstructor
public class DictCacheWarmer implements ApplicationRunner {
    private final DictMapper dictMapper;
    private final StringRedisTemplate redis;

    @Override
    public void run(ApplicationArguments args) {
        List<Dict> all = dictMapper.selectList(null);
        // pipeline 批量写,万级数据逐条 set 要几十秒
        redis.executePipelined((RedisCallback<Void>) conn -> {
            for (Dict d : all) {
                byte[] key = ("dict:" + d.getCode()).getBytes();
                conn.stringCommands().set(key, d.getLabel().getBytes(),
                        Expiration.from(Duration.ofHours(24)), SetOption.UPSERT);
            }
            return null;
        });
        log.info("dict warmed, size={}", all.size());
    }
}

要点:

  1. 防新旧混杂:先删旧 key 再写,或用版本前缀(dict:v2:)写完原子切别名
  2. 大 value 拆 Hash:一个 dict:type:{code} 的 Hash 存整类字典,比万条 String key 省内存省时间
  3. TTL 兜底:预热数据设过期,防止源头更新后缓存永久陈旧,再配定时任务双保险
  4. 多实例:Runner 每个副本都跑,Redis 写天然幂等;重加载加 Redisson 锁防压
  5. 别拖慢启动:K8s readiness 有超时,大预热放 ReadyEvent 异步执行,期间降级读库
  6. 记录加载数量与耗时,加载 0 条立刻告警——防数据源空表把缓存清空