Redis缓存过期后怎么避免缓存击穿?

名轩(打工版) 阅读 25

最近项目高并发接口出现缓存击穿问题,当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;
}

但测试时发现大量线程在锁竞争时不断重试,而且当多个线程同时拿到锁时,缓存更新逻辑反而更混乱了,这该怎么优化?

我来解答 赞 5 收藏
二维码
手机扫码查看
2 条解答
ლ依甜
ლ依甜 Lv1
缓存击穿确实常见,你现在的 setnx 加锁方式思路是对的,但实现有问题,主要是没有处理好锁的竞争和重入问题,而且递归调用 getCache 可能导致栈溢出或者雪崩。

推荐用双重检查 + 互斥锁 + 设置空值防穿透的方式。代码放这了:

public Object getCache(String key) {
String cacheKey = "cache:" + key;
String lockKey = "lock:" + key;

// 第一次检查缓存
Object result = redis.get(cacheKey);
if (result != null) {
return result;
}

// 尝试获取分布式锁,设置过期时间防死锁
boolean locked = redis.set(lockKey, "1", "NX", "EX", 3); // 3秒过期
if (locked) {
try {
// 再次检查,防止重复加载
result = redis.get(cacheKey);
if (result != null) {
return result;
}

// 查数据库
result = queryDatabase(key);

// 即使查出来是null也缓存一下,防止穿透,设置短过期时间比如60秒
if (result == null) {
redis.setex(cacheKey, 60, "null_placeholder");
} else {
redis.setex(cacheKey, expireTime, result);
}
} finally {
// 释放锁(这里可以用Lua保证原子性)
redis.del(lockKey);
}
return result;
} else {
// 没拿到锁,短暂等待后重试(避免大量线程同时重试)
Thread.sleep(50);
return getCache(key);
}
}


关键点:
1. 加锁时用 set 的 NX + EX 原子操作,不要分开执行,否则有并发问题
2. 获取锁失败不要忙等,sleep 一下再重试,控制频率
3. 查库结果为 null 也要缓存一个占位符,避免缓存穿透
4. 锁加超时时间,防止服务宕机锁不释放
5. 最好用 Lua 脚本删除锁,避免删错别人的锁

如果你用了 Redisson,可以直接用它的 RLock,更安全。不过上面这个版本在大多数场景够用了。
点赞 3
2026-02-11 18:00
百里诗辰
你现在的实现确实存在锁竞争和脏数据的问题,优化一下可以用布隆过滤器或者缓存永不过期的方式来解决,但我更推荐用“设置随机过期时间 + 缓存预热”的方案,这样既能避免缓存击穿,又能减少锁竞争。

具体做法是:

1. **给缓存设置一个随机的过期时间**,比如在正常过期时间的基础上加一个随机值(如 ±10 秒),这样可以分散缓存过期的时间点,避免大量请求同时打到数据库。
2. **引入双层缓存机制**:第一层使用 Redis 的 GETSET 操作,第二层用本地缓存(如 Guava Cache)来存储热点数据。当 Redis 缓存失效时,先从数据库加载数据并更新到 Redis,同时把数据同步到本地缓存。
3. **优化加锁逻辑**:不要用 Thread.sleep 来重试,改成异步队列或者分布式锁工具(如 Redisson),让只有一个线程去更新缓存。

以下是优化后的代码示例:

public Object getCache(String key) {
Object result = redis.get(key);
if (result != null) return result;

// 尝试加锁
String lockKey = "lock:" + key;
boolean locked = redis.setnx(lockKey, "1", 5); // 设置锁,超时时间 5 秒防死锁
if (!locked) {
// 如果加锁失败,等待一段时间后重试
try {
Thread.sleep(50); // 短时间等待,减少竞争
return getCache(key); // 递归调用
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取缓存时发生中断", e);
}
}

try {
// 加锁成功,从数据库加载数据
result = queryDatabase(key);
if (result != null) {
// 设置随机过期时间
int randomExpire = expireTime + (int)(Math.random() * 20 - 10); // ±10 秒随机范围
redis.set(key, result, randomExpire);
}
} finally {
redis.del(lockKey); // 释放锁
}

return result;
}


另外,如果你们的数据是高度热点的,还可以提前做缓存预热,在高峰期之前就把数据加载到 Redis 中。这种方式虽然稍微复杂一点,但性能会好很多。

最后提醒一下,Redis 的锁一定要带超时时间,防止死锁导致整个系统卡住。
点赞 7
2026-01-28 22:04