乐闻世界logo
搜索文章和话题

Redis 缓存穿透、缓存击穿、缓存雪崩有什么区别?如何解决?

2月19日 19:36

Redis 缓存策略是使用 Redis 作为缓存时的核心问题,需要解决缓存穿透、缓存击穿、缓存雪崩等问题,同时需要设计合理的缓存更新策略。

1. 缓存穿透

问题描述: 缓存穿透是指查询一个不存在的数据,由于缓存中没有这个数据,请求会直接打到数据库。如果大量这样的请求,会对数据库造成巨大压力。

解决方案

方案一:缓存空对象

java
public User getUserById(Long id) { User user = redis.get("user:" + id); if (user != null) { return user.equals("NULL") ? null : user; } user = db.queryUserById(id); if (user == null) { redis.set("user:" + id, "NULL", 300); // 缓存空对象,5分钟过期 } else { redis.set("user:" + id, user, 3600); } return user; }

方案二:布隆过滤器

java
// 使用布隆过滤器判断 key 是否存在 if (!bloomFilter.mightContain("user:" + id)) { return null; // 直接返回,不查询数据库 } User user = redis.get("user:" + id); if (user != null) { return user; } user = db.queryUserById(id); if (user != null) { redis.set("user:" + id, user, 3600); } return user;

2. 缓存击穿

问题描述: 缓存击穿是指某个热点 key 在缓存过期的一瞬间,大量请求同时查询这个 key,导致所有请求都打到数据库。

解决方案

方案一:互斥锁

java
public User getUserById(Long id) { User user = redis.get("user:" + id); if (user != null) { return user; } // 获取分布式锁 String lockKey = "lock:user:" + id; try { if (redis.setnx(lockKey, "1", 10)) { // 10秒过期 user = db.queryUserById(id); redis.set("user:" + id, user, 3600); } else { // 等待并重试 Thread.sleep(100); return getUserById(id); } } finally { redis.del(lockKey); } return user; }

方案二:逻辑过期

java
public User getUserById(Long id) { String value = redis.get("user:" + id); if (value != null) { JSONObject json = JSON.parseObject(value); if (json.getBoolean("expired")) { // 异步更新缓存 asyncUpdateCache(id); } return json.getObject("data", User.class); } // 缓存不存在,直接查询数据库 User user = db.queryUserById(id); JSONObject json = new JSONObject(); json.put("data", user); json.put("expired", false); redis.set("user:" + id, json.toJSONString(), 3600); return user; }

3. 缓存雪崩

问题描述: 缓存雪崩是指大量的 key 在同一时间过期,或者 Redis 宕机,导致大量请求直接打到数据库。

解决方案

方案一:设置随机过期时间

java
// 设置过期时间时加上随机值 int expire = 3600 + new Random().nextInt(600); // 3600-4200秒 redis.set("user:" + id, user, expire);

方案二:缓存预热

java
// 系统启动时预热缓存 @PostConstruct public void init() { List<User> users = db.queryAllUsers(); for (User user : users) { redis.set("user:" + user.getId(), user, 3600); } }

方案三:使用 Redis 高可用

  • 使用 Redis Sentinel 或 Redis Cluster
  • 避免单点故障

4. 缓存更新策略

策略一:Cache Aside Pattern(旁路缓存模式)

java
// 读操作 public User getUserById(Long id) { User user = redis.get("user:" + id); if (user != null) { return user; } user = db.queryUserById(id); redis.set("user:" + id, user, 3600); return user; } // 写操作 public void updateUser(User user) { db.updateUser(user); redis.del("user:" + user.getId()); // 删除缓存,而不是更新缓存 }

策略二:Write Through Pattern(写穿透模式)

java
public void updateUser(User user) { db.updateUser(user); redis.set("user:" + user.getId(), user, 3600); // 同时更新缓存和数据库 }

策略三:Write Behind Pattern(写回模式)

java
public void updateUser(User user) { redis.set("user:" + user.getId(), user, 3600); // 先更新缓存 // 异步写入数据库 asyncWriteToDB(user); }

5. 缓存一致性

问题描述: 缓存和数据库的数据不一致,导致读取到脏数据。

解决方案

方案一:延时双删

java
public void updateUser(User user) { db.updateUser(user); redis.del("user:" + user.getId()); // 第一次删除 try { Thread.sleep(500); // 延时 } catch (InterruptedException e) { e.printStackTrace(); } redis.del("user:" + user.getId()); // 第二次删除 }

方案二:订阅 Binlog

java
// 订阅数据库的 Binlog,当数据库变更时,自动更新缓存 @CanalEventListener public class CacheUpdateListener { @ListenPoint(destination = "example", schema = "test", table = "user") public void onEvent(CanalEntry.Entry entry) { // 解析 Binlog,更新缓存 User user = parseUserFromBinlog(entry); redis.set("user:" + user.getId(), user, 3600); } }

6. 缓存预热

问题描述: 系统启动时缓存为空,大量请求直接打到数据库。

解决方案

方案一:定时任务预热

java
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点预热 public void warmUpCache() { List<User> users = db.queryHotUsers(); for (User user : users) { redis.set("user:" + user.getId(), user, 3600); } }

方案二:异步加载

java
public User getUserById(Long id) { User user = redis.get("user:" + id); if (user != null) { return user; } // 异步加载缓存 asyncLoadCache(id); // 返回默认值或从数据库查询 return db.queryUserById(id); }

7. 缓存降级

问题描述: 当 Redis 故障时,系统如何保证可用性。

解决方案

方案一:直接返回默认值

java
public User getUserById(Long id) { try { User user = redis.get("user:" + id); if (user != null) { return user; } } catch (Exception e) { log.error("Redis error", e); } // Redis 故障,直接查询数据库 return db.queryUserById(id); }

方案二:使用本地缓存

java
public User getUserById(Long id) { try { User user = redis.get("user:" + id); if (user != null) { return user; } } catch (Exception e) { log.error("Redis error", e); // 使用本地缓存 return localCache.get("user:" + id); } User user = db.queryUserById(id); redis.set("user:" + id, user, 3600); return user; }

总结

Redis 缓存策略需要综合考虑缓存穿透、缓存击穿、缓存雪崩等问题,同时需要设计合理的缓存更新策略和缓存一致性方案。在实际应用中,需要根据具体的业务场景,选择合适的缓存策略。同时,需要持续监控缓存的命中率和性能,及时调整缓存策略。

标签:Redis