Redis 缓存雪崩介绍
缓存雪崩(Cache Avalanche)是指在高并发的情况下,缓存的某一部分或者大量缓存数据同时过期或被清除,导致大量请求直接访问数据库,从而使数据库压力瞬间增加,造成系统性能严重下降,甚至引发数据库宕机。
缓存雪崩的原因
- 缓存同时过期:当大量的缓存数据设置了相同的过期时间,可能会导致这些缓存数据在同一时刻同时失效或过期。这时,大量请求会同时去访问数据库,造成数据库的负载急剧增加。
- 缓存清除:由于缓存清除策略(如 LRU 淘汰策略),大量缓存数据被同时清除,导致请求直接访问数据库。
- 集群中断:Redis 集群出现故障或者 Redis 节点宕机,导致缓存数据不可用,从而所有请求都需要访问数据库。
缓存雪崩的影响
- 数据库压力剧增:大量请求直接访问数据库,可能会造成数据库负载过高,响应变慢,甚至数据库宕机。
- 系统性能下降:数据库压力增大,系统的整体响应时间和吞吐量下降。
- 服务不可用:数据库处理不过来请求时,可能导致服务崩溃或响应异常,影响用户体验。
如何解决缓存雪崩
- 缓存过期时间随机化:通过随机化缓存数据的过期时间,避免大量缓存同时过期,减少缓存雪崩的风险。
- 使用互斥锁:在缓存失效时,使用分布式锁确保只有一个请求会去查询数据库并更新缓存,避免大量请求同时访问数据库。
- 缓存预热:提前将一些常用或热点数据加载到缓存中,避免在高并发情况下数据库被压垮。
- 高可用缓存方案:使用 Redis 集群和 Sentinel 实现高可用,防止 Redis 单点故障导致缓存不可用。
1. 缓存过期时间随机化
为了防止大量缓存数据在同一时间过期,可以通过对缓存的过期时间进行随机化,使得不同的缓存数据有不同的过期时间,从而避免在某一时刻大量缓存数据同时失效。
代码实现:
import redis.clients.jedis.Jedis; import java.util.Random; public class CacheAvalancheExample { private static Jedis jedis = new Jedis("localhost", 6379); public static void main(String[] args) { String key = "user:1003"; // 假设查询用户 ID 为 1003 String value = getFromCache(key); if (value == null) { // 如果缓存为空,查询数据库 value = getFromDatabase(key); // 随机化过期时间,避免大量缓存同时过期 Random random = new Random(); int randomExpireTime = 3600 + random.nextInt(3600); // 随机过期时间为1-2小时之间 setToCache(key, value, randomExpireTime); } System.out.println("Fetched value: " + value); } // 从缓存获取数据 public static String getFromCache(String key) { return jedis.get(key); } // 设置数据到缓存 public static void setToCache(String key, String value, int expireTime) { jedis.setex(key, expireTime, value); // 设置随机过期时间 } // 模拟查询数据库 public static String getFromDatabase(String key) { // 假设数据库查询返回数据 return "User data for " + key; } }
- 随机化过期时间:通过
random.nextInt(3600)
给每个缓存数据添加一个随机的过期时间,避免同一时刻大量缓存同时过期。 - 设置缓存:使用
setex
设置数据到 Redis 中,并设置随机的过期时间。
通过这种方法,缓存的过期时间不再是固定的,避免了大量数据在同一时刻过期,减轻了数据库的压力。
2. 使用分布式锁避免缓存雪崩
为了确保当缓存失效时,只有一个请求能够查询数据库并更新缓存,可以使用 Redis 的分布式锁来加锁,防止其他请求同时访问数据库。
代码实现:
import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.redisson.Redisson; public class CacheAvalancheWithLockExample { 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:1004"; // 假设查询用户 ID 为 1004 String value = getUserFromCache(key); if (value == null) { // 缓存失效,查询数据库 RLock lock = redisson.getLock(key + ":lock"); lock.lock(); try { // 如果缓存依然未命中,则查询数据库并更新缓存 value = getUserFromCache(key); if (value == null) { value = getUserFromDatabase(key); setUserToCache(key, value); } } finally { lock.unlock(); } } System.out.println("Fetched value: " + value); redisson.shutdown(); } // 从缓存获取用户数据 public static String getUserFromCache(String key) { return jedis.get(key); } // 设置用户数据到缓存 public static void setUserToCache(String key, String value) { jedis.setex(key, 3600, value); // 设置缓存过期时间为1小时 } // 模拟查询数据库 public static String getUserFromDatabase(String key) { // 假设数据库查询返回数据 return "User data for " + key; } }
- 加锁:当缓存失效时,通过 Redis 分布式锁(
RLock
)确保只有一个线程会查询数据库并更新缓存,避免多个线程同时访问数据库。 - 查询数据库:获取锁后,查询数据库并更新缓存。如果缓存仍然为空,则执行数据库查询。
- 锁释放:在数据库查询和缓存更新完成后,释放锁。
这种方法通过分布式锁,避免了高并发情况下数据库的压力,从而有效防止缓存雪崩。
3. 高可用缓存方案
为了防止 Redis 单点故障导致缓存不可用,可以采用 Redis 集群模式或 Redis Sentinel 模式来提供高可用性,确保即使某个 Redis 节点出现故障,系统也能够正常运行。
可以通过配置 Redis Sentinel 或 Redis Cluster 来实现高可用,具体实现过程需要配置多个 Redis 节点,确保在主节点出现故障时,自动进行故障转移。
Redis的碎碎念 文章被收录于专栏
Redis面试中的碎碎念