1. 缓存问题概述
Redis 作为高性能缓存中间件,能够有效提高数据访问速度,但在实际使用中,可能会遇到 缓存击穿、缓存穿透、缓存雪崩 三大问题。这些问题可能会导致 缓存失效、数据库压力骤增、系统崩溃,因此必须采取合理的应对策略。
2. 缓存击穿、穿透、雪崩的区别
| 问题 |
定义 |
导致的后果 |
典型场景 |
| 缓存穿透(Cache Penetration) |
查询 缓存和数据库中都不存在的数据,导致每次请求都要访问数据库。 |
数据库压力骤增,影响系统性能。 |
攻击者恶意请求不存在的 key,例如查询 id=-1。 |
| 缓存击穿(Cache Breakdown) |
某个热点数据在缓存过期的瞬间,大量请求涌入数据库。 |
数据库瞬时负载过高,可能导致系统崩溃。 |
促销活动商品 ID 缓存过期时,流量暴增。 |
| 缓存雪崩(Cache Avalanche) |
大量缓存同时过期,导致大量请求打到数据库。 |
数据库承受不了瞬时高并发,可能宕机。 |
设定相同过期时间的大量缓存同时失效。 |
3. 具体案例分析与解决方案
3.1 缓存穿透
案例
1 2 3 4 5 6 7 8 9 10
| java 复制编辑 // 伪代码示例:查询用户信息 String key = "user:1001"; String user = redis.get(key); if (user == null) { // Redis 中没有数据 user = database.query("SELECT * FROM users WHERE id = 1001"); // 查询数据库 redis.setex(key, 3600, user); // 写入缓存 } return user;
|
问题:如果用户 id=9999 不存在,Redis 没有缓存,每次查询都会打到数据库,造成高并发压力。
解决方案
| 方案 |
具体措施 |
优缺点 |
| 布隆过滤器 |
使用 布隆过滤器(Bloom Filter) 维护一个所有合法 key 的集合,拦截非法请求。 |
低内存占用,误判率较低,但不能删除数据。 |
| 缓存空值 |
若查询数据库后发现数据不存在,将 null 存入 Redis,并设置短 TTL(如 60s)。 |
有效防止短时间内的重复查询,但可能会缓存无用数据。 |
| 接口层拦截 |
在应用层限制 ID 规则,如 ID 需大于 0,防止非法访问。 |
适用于特定业务规则。 |
布隆过滤器示例(Java 实现)
1 2 3 4 5 6 7
| java 复制编辑 BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 100000); bloomFilter.put("user:1001"); // 添加合法 key if (!bloomFilter.mightContain("user:9999")) { return null; // 直接拦截 }
|
3.2 缓存击穿
案例
1 2 3 4 5 6 7 8 9
| java 复制编辑 // 高并发访问某个热点 key String key = "hot-item:123"; String item = redis.get(key); if (item == null) { // 缓存过期 item = database.query("SELECT * FROM items WHERE id = 123"); // 直接访问数据库 redis.setex(key, 3600, item); // 重新缓存 }
|
问题:当 “hot-item:123” 这个热门 key 过期的瞬间,大量请求直接冲向数据库,造成高并发压力。
解决方案
| 方案 |
具体措施 |
优缺点 |
| 互斥锁(Mutex) |
缓存过期时,只允许一个线程查询数据库,其他线程等待。 |
避免数据库短时间高并发,但有一定等待时间。 |
| 设置热点数据永不过期 |
设置较长 TTL,并使用异步更新机制,减少过期瞬间的冲击。 |
适用于超热点数据,但可能导致数据不一致。 |
| 提前更新缓存 |
主动刷新缓存,在即将过期前预加载数据,确保缓存持续有效。 |
适用于可预测的缓存更新场景。 |
互斥锁示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| java 复制编辑 String key = "hot-item:123"; String item = redis.get(key); if (item == null) { if (redis.setnx("lock:hot-item:123", "1")) { // 获取锁 redis.expire("lock:hot-item:123", 30); // 设置锁过期时间 item = database.query("SELECT * FROM items WHERE id = 123"); redis.setex(key, 3600, item); redis.del("lock:hot-item:123"); // 释放锁 } else { Thread.sleep(100); // 休眠后重试 } }
|
3.3 缓存雪崩
案例
如果我们在 00:00 统一设置大量缓存的 TTL=3600s,那么 01:00 这些缓存会同时过期,导致数据库压力激增。
解决方案
| 方案 |
具体措施 |
优缺点 |
| 随机过期时间 |
给每个 key 设置不同的过期时间(如 3600 ± 600 秒),避免集中过期。 |
实现简单,避免缓存同时失效。 |
| 分批加载 |
使用双层缓存(L1+L2) ,当 Redis 失效时,先访问本地缓存(如 Guava Cache)。 |
适用于允许短时间一致性的业务。 |
| 自动重建缓存 |
使用后台线程异步刷新缓存,防止大规模过期后查询数据库。 |
适用于数据较稳定的场景。 |
随机过期时间示例
1 2 3 4
| java 复制编辑 int ttl = 3600 + new Random().nextInt(600); // 设置 3600 ~ 4200 秒随机过期 redis.setex("user:1001", ttl, user);
|
本地缓存 + Redis
1 2 3 4 5 6 7 8 9 10
| java 复制编辑 LoadingCache<String, String> localCache = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, String>() { public String load(String key) throws Exception { return redis.get(key); } });
|
4. 结论
| 问题 |
核心原因 |
最佳解决方案 |
| 缓存穿透 |
访问缓存和数据库都不存在的 key |
布隆过滤器 + 缓存空值 |
| 缓存击穿 |
热点 key 过期瞬间,大量请求打到数据库 |
互斥锁 + 提前更新缓存 |
| 缓存雪崩 |
大量 key 同时过期,数据库负载骤增 |
随机 TTL + 分批加载 |
🔹 企业级应用中,通常结合多种方案,例如 布隆过滤器 + 互斥锁 + 随机过期时间,来优化 Redis 缓存架构,确保高并发下的稳定性。