本地缓存不能替代Redis,但可缓解雪崩冲击;其适用场景是Redis超时或短暂不可用时兜底返回旧值,而非强一致或跨实例共享;必须配置maximumSize和expireAfterWrite,禁用refreshAfterWrite,redisTemplate超时需设短(如100ms),本地TTL须短于Redis(如30秒vs5分钟)。
本地缓存(如Caffeine、Guava Cache)对单进程有效,不解决集群一致性问题,也不感知Redis是否宕机。它真正起作用的场景是:Redis响应超时、网络抖动、或短暂不可用时,用本地已有的旧值兜住一部分请求,避免全部穿透到DB。一旦把它当成“强一致缓存”或“跨实例共享缓存”来用,就踩进坑了。
关键不在加依赖,而在控制调用顺序和失败边界。下面这个 get 方法是实际线上可用的最小闭环:
public String get(String key) {// 1. 先查本地,命中直接返回String local = caffeineCache.getIfPresent(key);if (local != null) return local;// 2. 查 Redis,必须设短超时(比如 100ms)try {String remote = redisTemplate.opsForValue().get(key);if (remote != null) {// 写回本地时注意:过期时间要比 Redis 短(如 Redis 5min → 本地 30s)caffeineCache.put(key, remote);}return remote;} catch (Exception e) {// 3. Redis异常时,再捞一次本地——可能刚被其他线程写入return caffeineCache.getIfPresent(key);}}
caffeineCache 必须配置 maximumSize 和 expireAfterWrite,否则内存泄漏风险极高refreshAfterWrite:它会异步加载,掩盖超时问题,且无法控制 fallback 行为redisTemplate 的 timeout 要显式设小(setConnectionTimeout/setSoTimeout),否则本地缓存形同虚设这是最容易被忽略的设计点。如果本地缓存比Redis活得久,就会出现“Redis已更新,本地还在返回脏数据”的情况,且无法通过常规手段感知和刷新。典型配置是:
expireAfterWrite(30, TimeUnit.SECONDS))每个应用实例都有自己的本地缓存副本,它们之间完全独立。这意味着:
invalidate 事件广播(如通过 Redis Pub/Sub 主动清空各实例本地 key),但这增加了复杂度和延迟真正的难点从来不是“怎么加一层缓存”,而是明确接受它带来的 stale 窗口,并把 fallback、超时、降级逻辑落到每一处 get 调用里。