1. Cache Aside Pattern(旁路缓存模式)
核心思想:应用代码直接管理缓存与数据的同步,分为读写两个流程:
- 读取数据:
- 先查本地缓存(如 Guava Cache)。
- 若本地未命中,则查 Redis。
- 若 Redis 也未命中,则从数据库加载数据,并回填到本地缓存和 Redis。
- 写入数据:
- 直接更新数据库。
- 删除本地缓存和 Redis 中的相关数据(避免旧数据残留)。
// 伪代码示例
public Data getData(String key) {// 1. 查本地缓存Data data = localCache.getIfPresent(key);if (data != null) return data;// 2. 查 Redisdata = redis.get(key);if (data != null) {// 回填本地缓存localCache.put(key, data);return data;}// 3. 从数据库加载data = db.load(key);if (data != null) {localCache.put(key, data);redis.set(key, data);}return data;
}public void updateData(String key, Data newData) {// 1. 更新数据库db.update(newData);// 2. 删除缓存(本地 + Redis)localCache.invalidate(key);redis.delete(key);
}
优点:实现简单,适用于读多写少的场景。
缺点:存在短暂不一致窗口(如写入后需等待缓存过期)。
2. 发布订阅模式(Pub/Sub)
适用场景:分布式系统中,多个应用实例需要同步缓存状态。
方案:
- 当数据更新时,发送消息到消息队列(如 Redis 的 Pub/Sub 或 Kafka)。
- 所有订阅该主题的应用实例监听到消息后,主动删除本地缓存和 Redis 中的旧数据。
// 发布消息示例(更新数据时)
public void updateData(String key, Data newData) {db.update(newData);// 删除 Redis 缓存redis.delete(key);// 发布失效事件redis.publish("cache-invalidation", key);
}// 订阅消息示例(各实例启动时订阅)
redis.subscribe("cache-invalidation", (channel, message) -> {localCache.invalidate(message); // 删除本地缓存
});
优点:解耦缓存失效逻辑,适合分布式系统。
缺点:消息可能丢失或延迟,需处理幂等性。
3. 过期时间策略
核心思想:为缓存设置合理的 TTL(Time-To-Live),依赖自动过期减少不一致时间窗口。
适用场景:对实时一致性要求不高,允许最终一致性的场景。
优化点:
- 通过随机 TTL 避免缓存雪崩。
- 结合主动失效(如更新时删除)缩短过期时间。
// 本地缓存设置 TTL
Cache<String, Data> cache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES) // 本地缓存 5 分钟过期.build();// Redis 设置 TTL
redis.setex(key, 300, data); // Redis 缓存 5 分钟过期
4. 双删策略(Double Delete)
适用场景:高并发写操作场景,减少缓存脏数据。
流程:
- 更新数据库前,先删除缓存。
- 更新数据库后,延迟一段时间再次删除缓存(防止并发读写导致的脏数据)。
public void updateData(String key, Data newData) {// 第一步:删除缓存localCache.invalidate(key);redis.delete(key);// 更新数据库db.update(newData);// 第二步:延迟删除(如通过异步线程)scheduleDelete(key, 1000); // 1 秒后再次删除
}
5. 读写锁(Read-Write Lock)
核心思想:通过锁机制保证读写操作的原子性。
方案:
- 写操作时加锁,阻止其他读写操作。
- 读操作时加读锁,允许多个并发读取。
// 使用 Redis 分布式锁(示例)
public void updateDataWithLock(String key, Data newData) {String lockKey = "lock:" + key;boolean locked = redis.setnx(lockKey, "locked", 10, TimeUnit.SECONDS);if (locked) {try {// 更新数据库db.update(newData);// 删除缓存localCache.invalidate(key);redis.delete(key);} finally {redis.del(lockKey);}} else {// 获取锁失败,重试或返回错误}
}
6. 本地缓存与 Redis 的协同设计
- 分层缓存:本地缓存作为一级缓存,Redis 作为二级缓存。
- 优先从本地缓存读取,未命中则查询 Redis。
- 更新时同步删除两级缓存。
- 热点数据管理:对热点数据设置更短的 TTL 或主动推送更新。
关键注意事项
- 最终一致性:多数场景下无需强一致,允许短暂延迟。
- 缓存穿透:对空值或无效 Key 也进行缓存(如设置短 TTL)。
- 缓存雪崩:设置随机 TTL,避免大量缓存同时失效。
- 监控与告警:通过统计信息(如
cache.stats()
)监控命中率、延迟等指标。
总结方案选择
场景 | 推荐方案 |
---|---|
单机应用,低并发 | Cache Aside + TTL |
分布式系统,多实例 | Pub/Sub + Cache Aside |
高并发写操作 | 双删策略 + 分布式锁 |
允许最终一致性 | TTL 过期 + 异步更新 |
通过结合业务需求,灵活采用上述策略,可以有效降低本地缓存与 Redis 的不一致风险。