Redis 缓存击穿介绍
缓存击穿(Cache Breakdown)是指在缓存中某个数据存在,但是由于某些原因(如缓存过期、缓存失效),导致大量请求直接访问数据库。这种情况通常发生在缓存的某个数据点在高并发场景下,数据失效或被删除时,多个请求同时击中缓存失效的区域,导致数据库承受大量的请求压力。
缓存击穿的原因
- 缓存过期:缓存中的某些数据由于设置了过期时间而过期了。
- 缓存删除:缓存中的某些数据被手动删除或清除。
- 高并发:在高并发请求的情况下,多个请求同时访问缓存失效的数据,导致请求并发打到数据库上。
缓存击穿的影响
- 数据库负担过重:多个请求直接访问数据库,数据库负载瞬间增加,可能会引发性能瓶颈,甚至导致数据库宕机。
- 系统响应延迟:高并发的数据库请求会导致响应延迟,影响系统整体性能。
- 资源浪费:无效的并发请求占用了计算资源和网络带宽。
如何解决缓存击穿
- 互斥锁:通过加锁(互斥锁)确保同一时间只有一个请求可以去访问数据库,其他请求等待缓存更新后再访问缓存。
- 使用双重检查:结合双重检查锁定模式,避免多个线程同时查询数据库。
- 设置合适的缓存过期时间:避免过多的缓存失效,合理设置缓存的过期时间,避免单一数据点的缓存失效对系统产生压力。
- 预热缓存:在缓存失效后,预先加载一部分热点数据到缓存,避免数据库被突发请求压垮。
1. 使用互斥锁防止缓存击穿
通过使用 Redis 的分布式锁(如 SETNX、Redisson)机制,保证在缓存失效时,只有一个请求能够去查询数据库并更新缓存,其他请求等待缓存更新完成后再从缓存获取数据。
代码实现:
使用 Redisson 库来实现分布式锁。
- 安装依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.1</version>
</dependency>
代码实现:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.api.RBucket;
import org.redisson.config.Config;
import org.redisson.Redisson;
public class CacheBreakdownExample {
private static RedissonClient redisson;
public static void main(String[] args) {
// 配置 Redisson 客户端连接 Redis
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
redisson = Redisson.create(config);
// 模拟并发访问数据库的操作
String key = "user:1001"; // 假设查询用户 ID 为 1001
String value = getUserFromCache(key);
if (value == null) {
// 缓存失效,查询数据库
value = getUserFromDatabase(key);
// 通过加锁确保只有一个线程可以更新缓存
RLock lock = redisson.getLock(key + ":lock");
lock.lock();
try {
// 如果缓存依然未命中,则更新缓存
if (getUserFromCache(key) == null) {
setUserToCache(key, value);
}
} finally {
lock.unlock();
}
}
System.out.println("Fetched value: " + value);
redisson.shutdown();
}
// 从缓存获取用户数据
public static String getUserFromCache(String key) {
RBucket<String> bucket = redisson.getBucket(key);
return bucket.get();
}
// 设置用户数据到缓存
public static void setUserToCache(String key, String value) {
RBucket<String> bucket = redisson.getBucket(key);
bucket.set(value);
}
// 模拟查询数据库
public static String getUserFromDatabase(String key) {
// 假设数据库查询结果
return "User data for " + key;
}
}
- 加锁机制:使用 Redis 锁(
RLock)确保同一时刻只有一个线程去访问数据库并更新缓存,避免缓存失效后高并发访问数据库的情况。 - 缓存更新:当缓存失效时,通过
lock.lock()获取锁,确保只有一个线程能够执行数据库查询和缓存更新,其他请求会等待锁释放后访问缓存。 - Redis 操作:使用 Redisson 的
RBucket存储和获取数据。
通过这种方式,只有一个请求可以查询数据库并更新缓存,其他请求在缓存更新之前等待,避免了缓存击穿。
2. 双重检查锁定模式
双重检查锁定模式是另一种常见的防止缓存击穿的方式,确保同一时间只有一个线程能够查询数据库并更新缓存,减少锁的粒度。
代码实现:
import redis.clients.jedis.Jedis;
public class CacheBreakdownDoubleCheck {
private static Jedis jedis = new Jedis("localhost", 6379);
public static void main(String[] args) {
String key = "user:1002"; // 假设查询用户 ID 为 1002
// 第一重检查:从缓存获取数据
String value = getFromCache(key);
if (value == null) {
// 如果缓存为空,进入第二重检查
synchronized (CacheBreakdownDoubleCheck.class) {
value = getFromCache(key);
if (value == null) {
// 如果缓存依然为空,则查询数据库并更新缓存
value = getFromDatabase(key);
setToCache(key, value);
}
}
}
System.out.println("Fetched value: " + value);
}
// 从缓存获取数据
public static String getFromCache(String key) {
return jedis.get(key);
}
// 设置数据到缓存
public static void setToCache(String key, String value) {
jedis.setex(key, 3600, value); // 设置缓存过期时间为1小时
}
// 模拟查询数据库
public static String getFromDatabase(String key) {
// 假设数据库查询返回数据
return "User data for " + key;
}
}
- 双重检查:第一次检查缓存,如果缓存中没有数据,则进入同步块。
- 同步块:进入同步块后,再次检查缓存,防止多个线程同时查询数据库。
- 缓存更新:如果缓存仍然为空,则查询数据库并更新缓存。
这种方法通过减少加锁的时间窗口,提升了系统的性能。
Redis的碎碎念 文章被收录于专栏
Redis面试中的碎碎念