跳到主要内容

分布式缓存设计

问题

如何设计一个高可用的分布式缓存系统?

答案

多级缓存架构

缓存读写策略

策略一致性适用场景
Cache Aside先查缓存,miss 查 DB 回填先更新 DB,再删缓存最终一致通用方案(推荐)
Read/Write Through缓存代理读写 DB缓存代理写 DB强一致缓存中间件支持
Write Behind同上异步批量写 DB弱一致写多读少
Cache Aside 模式
public User getUser(long userId) {
String key = "user:" + userId;
// 1. 查缓存
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) return user;

// 2. 查数据库
user = userMapper.findById(userId);
if (user != null) {
// 3. 回填缓存(设置过期时间 + 随机偏移防雪崩)
int ttl = 3600 + new Random().nextInt(600);
redisTemplate.opsForValue().set(key, user, ttl, TimeUnit.SECONDS);
}
return user;
}

public void updateUser(User user) {
// 1. 先更新数据库
userMapper.update(user);
// 2. 再删除缓存
redisTemplate.delete("user:" + user.getId());
}

缓存三大问题

问题现象解决方案
穿透查不存在的数据,每次打 DB缓存空值 / 布隆过滤器
击穿热点 key 过期,大量请求打 DB互斥锁重建 / 永不过期
雪崩大批 key 同时过期过期时间加随机偏移 / 多级缓存
互斥锁防击穿
public User getUserWithLock(long userId) {
String key = "user:" + userId;
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) return user;

String lockKey = "lock:" + key;
try {
// 只有一个线程能获取锁去查 DB
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
user = userMapper.findById(userId);
redisTemplate.opsForValue().set(key, user, 3600, TimeUnit.SECONDS);
} else {
Thread.sleep(50);
return getUserWithLock(userId); // 重试
}
} finally {
redisTemplate.delete(lockKey);
}
return user;
}

本地缓存 + Redis 二级缓存

Caffeine 本地缓存 + Redis
@Component
public class TwoLevelCache {
private final Cache<String, Object> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();

@Autowired
private RedisTemplate<String, Object> redisTemplate;

public Object get(String key) {
// L1: 本地缓存
Object value = localCache.getIfPresent(key);
if (value != null) return value;

// L2: Redis
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value); // 回填本地缓存
}
return value;
}
}

常见面试问题

Q1: 为什么是删除缓存而不是更新缓存?

答案

  • 更新缓存:并发写时可能导致脏数据(A 先更新 DB 但 B 后更新缓存覆盖了 A 的最新值)
  • 删除缓存:下次读取时自然重建,保证最终一致

详见 缓存与数据库一致性

Q2: 先删缓存还是先更新 DB?

答案

先更新 DB,再删缓存。先删缓存会导致:删缓存后、更新 DB 前有请求读到旧数据并回填缓存,导致缓存和 DB 不一致。

Q3: 多级缓存的一致性怎么保证?

答案

  • Redis 缓存更新时通过 MQ/Redis Pub-Sub 通知所有实例清除本地缓存
  • 本地缓存设置较短的 TTL(如 5 分钟)作为兜底

相关链接