定义
- 在缓存没命中的情况下,从数据库中查数据,到在缓存中更新,有一个时间窗口;
- 如果数据库的查询时间很长,并且在这个窗口期有大量的缓存 miss 的情况,就会在窗口期发生大量的数据库查询操作;
- 这样对数据库造成很大的压力;
解决方案的三个目标
- 减少重建缓存的次数;
- 数据尽可能一致;
- 减少潜在风险:死锁,线程池被大量 hang 住;
解决方案(一)互斥锁
- 第一个重建 key 的请求在缓存把 key 锁住,缓存重建完了再打开;
- 后续对该 key 重建的请求等待第一个重建请求完成,直接缓存命中;
优点
缺点
示例伪代码
String get(String key) {
String value = redis.get(key);
if(value == null) {
String mutexKey = "mutex:key:" + key;
if(redis.set(mutexKey, "1", "ex 180", "nx")) {
value = db.get(key);
redis.set(key, value);
redis.delete(mutexKey);
} else {
Thread.sleep(50);
get(key);
}
}
return value;
}
解决方案(二)永不过期
- 缓存层面:没有为 key 设置过期时间;
- 功能层面:为每个 key 添加逻辑过期时间,只是发现其超过逻辑过期时间后,会使用单独的线程去重构缓存;
优点
缺点
- 会存在数据不一致问题,即重建线程没重建完,另一个线程拿到了老数据;
示例伪代码
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
long logicTimeout = v.getLogicTimeout();
if(logicTimeout >= System.currentTimeMills()) {
String mutexKey = "mutex:key:" + key;
if(redis.set(mutexKey, "1", "ex 180", "nx")) {
threadPool.execute(() -> {
String dbValue = db.get(key);
redis.set(key, (dbValue, newLogicTimeout));
redis.delete(keyMutex);
});
}
}
return value;
}