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;
});
}
}架构决策点(比代码重要):
- 时点:放 Ready 事件之后、readiness 探针打开之前——预热未完不接流量,避免冷启动 miss 风暴;数据量大(千万级)要评估是否阻塞扩容,必要时分批限速后台预热;
- 容量与结构:10 万条乘 200B 约 20MB,单 key 整存(String 加 JSON)优于碎片 hash;TTL 加随机抖动防集体过期雪崩;
- 容错:Redis 不可用时预热失败不能阻断启动(降级懒加载加告警)——预热是加速手段,不是强依赖;
- 持续新鲜度:启动预热只管冷启动;变更走「更新库、删缓存、定时全量校准」三段式,冷热互补。
💻 全栈开发专家
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());
}
}要点:
- 防新旧混杂:先删旧 key 再写,或用版本前缀(
dict:v2:)写完原子切别名 - 大 value 拆 Hash:一个
dict:type:{code}的 Hash 存整类字典,比万条 String key 省内存省时间 - TTL 兜底:预热数据设过期,防止源头更新后缓存永久陈旧,再配定时任务双保险
- 多实例:Runner 每个副本都跑,Redis 写天然幂等;重加载加 Redisson 锁防压
- 别拖慢启动:K8s readiness 有超时,大预热放 ReadyEvent 异步执行,期间降级读库
- 记录加载数量与耗时,加载 0 条立刻告警——防数据源空表把缓存清空