Redis缓存击穿是指一个热点key(高并发访问的key)在缓存中失效的瞬间,导致大量请求直接落到数据库上,从而给数据库服务器带来巨大压力的情况。
原因分析
- 热点key突然过期(可能是缓存策略设置的到期时间到了)。
- 大量并发请求同时查询这个key。
- 由于缓存失效,所有请求都直接打到了数据库。
解决方案
- 设置热点数据永不过期:对于一些热点key,可以设置其永不过期,而是通过后台线程异步更新缓存内容。
public String getDataWithPermanentKey(String key) {
String data = jedis.get(key);
if (data == null) {
// 同步获取锁,单线程加载数据到缓存
synchronized (key.intern()) {
// 双重检测,防止多个线程进入同步块
data = jedis.get(key);
if (data == null) {
data = loadDataFromDb(key);
// 设置数据到redis,但不设置过期时间,使其永不过期
jedis.set(key, data);
}
}
}
return data;
}
private String loadDataFromDb(String key) {
// 数据库查询逻辑
return "data";
}
- 使用互斥锁:当缓存失效时,不是所有请求都去加载数据,而是用一个互斥锁(或者其它排他锁机制)保证只有一个请求去查询数据库然后更新缓存。
public String getDataWithMutex(String key) {
String data = jedis.get(key);
if (data == null) {
String lockKey = "lock:" + key;
// 尝试获取锁
String lock = jedis.set(lockKey, "1", "NX", "EX", 5);
if ("OK".equals(lock)) {
try {
// 加锁成功,查询数据库
data = loadDataFromDb(key);
jedis.setex(key, 3600, data); // 假设设置3600秒过期时间
} finally {
// 无论如何最后都释放锁
jedis.del(lockKey);
}
} else {
// 加锁失败,小睡一会儿后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getDataWithMutex(key); // 重试
}
}
return data;
}
- 设置过期标志更新:对于可能成为热点的数据,可以在缓存中设置两个值,一个是数据的过期时间
expire_at
,一个是数据值。当读取时,先检查expire_at
,如果即将过期,则启动一个后台线程进行更新。
public String getDataWithExpireFlag(String key) {
// 假设是一个Map对象,包含数据和过期时间戳
Map<String, Object> dataMap = (Map<String, Object>) jedis.get(key);
if (dataMap != null) {
long expireAt = (Long) dataMap.get("expire_at");
if (System.currentTimeMillis() > expireAt - 30000) { // 例如提前30秒进行续期
// 异步更新缓存
Thread updateThread = new Thread(() -> {
loadDataFromDb(key); // 加载数据并更新缓存
});
updateThread.start();
}
return (String) dataMap.get("data");
} else {
// 缓存中没有数据,同步查询数据库并设置缓存
String data = loadDataFromDb(key);
dataMap = new HashMap<>();
dataMap.put("data", data);
dataMap.put("expire_at", System.currentTimeMillis() + 3600000); // 假设设置1小时过期时间
jedis.set(key, dataMap);
return data;
}
}
注意事项
- 锁的实现:上面的互斥锁示例简化了锁的实现,使用
set
命令的NX
(Not eXists)和EX
(Expire)选项来实现。在分布式环境下,应该使用基于Redis的RedLock
算法或其他可靠的分布式锁实现。 - 锁的粒度:锁的粒度要尽可能小,比如针对每个key加锁,以减少锁的竞争。
- 重试的策略:加锁失败后的重试应该有合理的策略,如随机的延迟时间。
- 异常处理:要确保即使在数据加载过程中发生异常,锁也能被正确释放。
- 线程安全:如果使用异步线程更新缓存,确保操作是线程安全的。
- 批量预热:对于知道会成为热点的key,在缓存到期之前,可以通过定时任务或其他机制提前重新加载缓存。
以上示例仅为说明如何避免Redis缓存击穿问题,并未考虑所有生产环境的复杂性。在实际生产环境中,应该使用更完善的异常处理和线程安全措施。