Redis 缓存击穿是指在缓存系统中,大量请求(高并发访问)同时访问一个不存在于缓存中(一般是因为缓存过期或者数据未被加载到缓存)但在数据库中存在的热点数据,从而导致这些请求直接穿透缓存层,涌向数据库,可能造成数据库压力过大甚至崩溃的情况。以下是关于它的详细介绍:
- 产生原因:一般是由于缓存中的某个热点数据过期,而此时有大量并发请求过来访问该数据。这些请求在发现缓存中没有对应数据后,都会去数据库中查询,从而形成对数据库的集中式访问压力。例如电商网站中某个热门商品的信息缓存过期,而此时大量用户同时访问该商品详情页,就可能引发缓存击穿问题。
- 解决方案
- 逻辑过期:不设置TTL,之前所说导致缓存击穿的原因就是该key的TTL到期了,所以我们在这就不设置TTL了,而是使用一个字段,例如:expire表示过期时间(逻辑上的)。当我们想让它 “ 过期 ” 的时候,我们可以直接手动将其删除(热点key,即只是在一段时间内,其被访问的频次很高。这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。
- 加互斥锁(Mutex Lock):在缓存数据过期后,当第一个请求来访问时,先获取一个互斥锁,然后去数据库加载数据并更新到缓存中,其他请求则等待该锁释放后再从缓存中获取数据。通过这种方式,可以确保在同一时刻只有一个请求去数据库查询数据,避免大量请求同时穿透到数据库。例如在 Java 中可以使用
ReentrantLock
来实现互斥锁。以下是一个简单的示例代码:
import java.util.concurrent.locks.ReentrantLock;
public class CacheBreakthroughHandler {
private static final ReentrantLock lock = new ReentrantLock();
private static final RedisCache redisCache = new RedisCache(); // 假设这是一个操作Redis缓存的类
public static Object getData(String key) {
Object data = redisCache.get(key);
if (data == null) {
try {
lock.lock();
// 再次检查缓存,防止在获取锁的过程中其他线程已经更新了缓存
data = redisCache.get(key);
if (data == null) {
// 从数据库获取数据
data = getDataFromDatabase(key);
if (data != null) {
// 将数据放入缓存
redisCache.set(key, data);
}
}
} finally {
lock.unlock();
}
}
return data;
}
private static Object getDataFromDatabase(String key) {
// 这里是从数据库获取数据的逻辑,根据实际情况实现
return null;
}
}
这两种解决方案对比:
- 使用分布式锁(如 Redisson):在分布式系统环境下,可以使用分布式锁来替代互斥锁。分布式锁能够在多个节点之间实现互斥访问,确保同一时刻只有一个节点去数据库加载数据。例如使用 Redisson 框架来实现分布式锁,它基于 Redis 实现了各种分布式锁的功能,以下是一个简单的示例代码:
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class CacheBreakthroughHandler {
private static final RedissonClient redissonClient;
private static final RedisCache redisCache = new RedisCache(); // 假设这是一个操作Redis缓存的类
static {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
redissonClient = Redisson.create(config);
}
public static Object getData(String key) {
Object data = redisCache.get(key);
if (data == null) {
RLock lock = redissonClient.getLock("lock:" + key);
try {
lock.lock();
// 再次检查缓存,防止在获取锁的过程中其他线程已经更新了缓存
data = redisCache.get(key);
if (data == null) {
// 从数据库获取数据
data = getDataFromDatabase(key);
if (data != null) {
// 将数据放入缓存
redisCache.set(key, data);
}
}
} finally {
lock.unlock();
}
}
return data;
}
private static Object getDataFromDatabase(String key) {
// 这里是从数据库获取数据的逻辑,根据实际情况实现
return null;
}
}
- 缓存预热:在系统启动或者数据初始化阶段,提前将一些热点数据加载到缓存中,避免在系统运行过程中因为缓存未命中而导致缓存击穿。可以通过定时任务或者在系统启动时执行的初始化方法来实现缓存预热。例如,在电商系统启动时,将热门商品的信息提前加载到缓存中。
- 二级缓存:采用二级缓存架构,例如在应用服务器内存中设置一级缓存,在 Redis 中设置二级缓存。当请求访问数据时,先从一级缓存中查找,如果未命中再从二级缓存中查找,若二级缓存也未命中,则去数据库查询并将数据依次放入二级缓存和一级缓存。这样可以在一定程度上减轻 Redis 缓存的压力,降低缓存击穿的风险。