什么是缓存击穿
缓存击穿是指在高并发场景下,一个缓存中不存在但是数据库中存在的数据被大量请求同时查询,导致请求直接穿透缓存,直接请求到数据库,从而导致数据库压力过大,系统性能下降。
造成缓存击穿的原因通常是因为某个热点数据过期或者被删除,而在此之后的一段时间内,大量请求同时访问该数据,导致缓存未命中,请求都落到了数据库上,引发缓存击穿问题。
下面有一幅图展示缓存击穿的场景:
解决方案
1. 使用互斥锁
在缓存更新的过程中使用互斥锁,确保只有一个线程去更新缓存,其他线程等待,防止大量线程同时访问查询数据库。
- 优点:没有额外的内存消耗;保证一致性;实现简单
- 缺点:线程需要等待,性能受到影响;可能存在死锁的风险
下面是使用互斥锁来应对缓存击穿的时序图(借鉴黑马程序员):
接着,给大家一个示例代码,演示了如何使用互斥锁来解决缓存击穿问题:
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
public class CacheUtil {
private static RedissonClient redissonClient;
// 初始化RedissonClient
static {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
redissonClient = Redisson.create(config);
}
public static Object get(String key) {
Object value = redissonClient.getBucket(key).get();
if (value == null) {
// 缓存未命中,使用互斥锁防止缓存击穿
RLock lock = redissonClient.getLock(key + "_lock");
lock.lock();
try {
// 再次尝试从缓存获取数据
value = redissonClient.getBucket(key).get();
if (value == null) {
// 从数据库查询数据
value = fetchDataFromDatabase();
// 将数据放入缓存,设置一个较短的过期时间,防止缓存穿透保护
redissonClient.getBucket(key).set(value, 10, TimeUnit.SECONDS);
}
} finally {
lock.unlock();
}
}
return value;
}
private static Object fetchDataFromDatabase() {
// 从数据库中查询数据的代码
// 省略具体实现
}
}
在这个示例代码中,我们使用了Redisson的RLock来实现互斥锁,防止缓存击穿。当发现缓存未命中时,先尝试获取互斥锁,如果成功获取锁,则查询数据库并更新缓存,否则等待其他线程更新缓存。这样可以确保只有一个线程去更新缓存,避免缓存击穿问题。同时,我们还设置了一个较短的过期时间,防止缓存穿透保护。请注意根据实际情况配置Redis的连接信息和缓存过期时间。
2. 逻辑过期
方案分析:我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。
我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。
这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。
- 优点:线程无需等待,性能较好
- 缺点:不保证一致性;有额外的内存消耗;实现复杂
其他解决方案
- 延迟缓存更新:在热点数据过期之后,不立即去更新缓存,而是等待一段时间后再更新,这样可以让大量请求在缓存更新之前落在缓存上,减轻数据库压力。
- 缓存穿透保护:对于查询结果为空的请求,也将其缓存起来,并设置一个较短的过期时间,防止恶意请求不断访问数据库。
- 降级策略:如果发现缓存击穿问题已经发生,可以通过降级策略来应对,例如直接返回默认值或者空数据,保证系统的稳定性。