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面试中的碎碎念