Redis缓存过期后怎么避免缓存击穿?
最近项目高并发接口出现缓存击穿问题,当Redis缓存过期后,大量请求直接打到数据库。我尝试用加锁方式让一个线程更新缓存,但发现锁竞争导致接口响应变慢,而且偶尔还是会有脏数据穿透,有没有更好的解决方案?
我目前的实现是这样的:
public Object getCache(String key) {
Object result = redis.get(key);
if (result != null) return result;
// 尝试加锁
if (redis.setnx("lock:" + key, "1")) {
try {
result = queryDatabase(key);
redis.set(key, result, expireTime);
} finally {
redis.del("lock:" + key);
}
} else {
// 其他线程等待
Thread.sleep(100);
return getCache(key);
}
return result;
}
但测试时发现大量线程在锁竞争时不断重试,而且当多个线程同时拿到锁时,缓存更新逻辑反而更混乱了,这该怎么优化?
推荐用双重检查 + 互斥锁 + 设置空值防穿透的方式。代码放这了:
关键点:
1. 加锁时用 set 的 NX + EX 原子操作,不要分开执行,否则有并发问题
2. 获取锁失败不要忙等,sleep 一下再重试,控制频率
3. 查库结果为 null 也要缓存一个占位符,避免缓存穿透
4. 锁加超时时间,防止服务宕机锁不释放
5. 最好用 Lua 脚本删除锁,避免删错别人的锁
如果你用了 Redisson,可以直接用它的 RLock,更安全。不过上面这个版本在大多数场景够用了。
具体做法是:
1. **给缓存设置一个随机的过期时间**,比如在正常过期时间的基础上加一个随机值(如 ±10 秒),这样可以分散缓存过期的时间点,避免大量请求同时打到数据库。
2. **引入双层缓存机制**:第一层使用 Redis 的
GET和SET操作,第二层用本地缓存(如 Guava Cache)来存储热点数据。当 Redis 缓存失效时,先从数据库加载数据并更新到 Redis,同时把数据同步到本地缓存。3. **优化加锁逻辑**:不要用
Thread.sleep来重试,改成异步队列或者分布式锁工具(如 Redisson),让只有一个线程去更新缓存。以下是优化后的代码示例:
另外,如果你们的数据是高度热点的,还可以提前做缓存预热,在高峰期之前就把数据加载到 Redis 中。这种方式虽然稍微复杂一点,但性能会好很多。
最后提醒一下,Redis 的锁一定要带超时时间,防止死锁导致整个系统卡住。