你的缓存为什么不生效——后端开发中的缓存四大坑

2026-02-28 9 0

## 坑一:缓存穿透——数据库喊你赔命

什么是缓存穿透?简单说就是:**请求绕过缓存,直接打在数据库上**。

常见场景:
- 恶意攻击:有人用不存在的ID疯狂请求
- 业务bug:前端传了个null或者空字符串
- 新手写代码:查不到数据就直接查库,也不写空值缓存

最离谱的是第三种。我见过有人这么写:

```java
public User getUser(Long id) {
// 先查缓存
User user = redis.get("user:" + id);
if (user != null) {
return user;
}
// 缓存没有,查数据库
user = userMapper.selectById(id);
// 然后...就没有然后了
return user;
}
```

每次请求都会穿透到数据库,数据库:我招谁惹谁了?

**正确姿势:**

```java
public User getUser(Long id) {
User user = redis.get("user:" + id);
if (user != null) {
return user;
}

user = userMapper.selectById(id);

// 关键:不管能不能查到,都要写缓存!
// 查到了存用户,没查到存一个空对象或者特殊标记
if (user != null) {
redis.set("user:" + id, user, 30, TimeUnit.MINUTES);
} else {
// 存一个过期时间短的空值,防止恶意攻击
redis.set("user:" + id, "", 1, TimeUnit.MINUTES);
}

return user;
}
```

或者更优雅的方式:用BloomFilter布隆过滤器预先过滤不存在的key。不过布隆过滤器有误判率,这个要视业务场景而定。

## 坑二:缓存击穿——热点key的灾难

缓存击穿和缓存穿透不一样。穿透是查询一个不存在的key,击穿是:**某个热点key过期的一瞬间,大量请求同时涌入数据库**。

这种情况常见于:
- 秒杀活动开始时,库存缓存过期
- 某个明星结婚,微博热搜缓存过期
- 系统重启后,所有缓存失效

我经历过最恐怖的一次:凌晨三点,运维电话把我叫起来,说数据库CPU 100%。一看日志,某个热门商品的缓存刚好过期,几千个请求同时查数据库,数据库当场去世。

**解决方案:**

### 方案一:互斥锁

```java
public User getUser(Long id) {
User user = redis.get("user:" + id);
if (user != null) {
return user;
}

// 尝试获取锁
Boolean locked = redis.setnx("lock:user:" + id, "1", 10, TimeUnit.SECONDS);
if (locked) {
try {
// 双重检查
user = redis.get("user:" + id);
if (user != null) {
return user;
}

// 只有获取到锁的请求才去查数据库
user = userMapper.selectById(id);
if (user != null) {
redis.set("user:" + id, user, 30, TimeUnit.MINUTES);
}
} finally {
redis.del("lock:user:" + id);
}
} else {
// 没拿到锁,短暂等待后重试
Thread.sleep(50);
return getUser(id);
}

return user;
}
```

### 方案二:逻辑过期

不设置真正的过期时间,而是设置一个"逻辑过期"标记:

```java
public User getUser(Long id) {
String json = redis.get("user:" + id);
if (json == null) {
return null;
}

User user = JSON.parseObject(json, User.class);

// 检查是否逻辑过期
if (user.getExpireTime() < System.currentTimeMillis()) { // 异步更新缓存,不阻塞请求 threadPool.execute(() -> refreshCache(id));
}

return user;
}
```

方案二适合读多写少的场景,但实现复杂,而且可能返回过期数据。要根据业务场景选择。

## 坑三:缓存雪崩——批量失效的核弹

如果说击穿是一颗炸弹,那雪崩就是核弹:**大量缓存同时失效**。

常见原因:
- 缓存服务器重启
- 同一时间大量key过期
- 缓存预热没做好

最典型的例子:系统上线时,所有缓存都是空的,请求全部打到数据库。

**解决方案:**

### 1. 随机过期时间

```java
// 基础过期时间30分钟,加上随机偏移量
int baseExpire = 30 * 60;
int randomExpire = new Random().nextInt(baseExpire);
redis.set(key, value, baseExpire + randomExpire, TimeUnit.SECONDS);
```

### 2. 缓存预热

系统启动时,提前把热点数据加载到缓存:

```java
@PostConstruct
public void warmUp() {
List hotUserIds = hotUserService.getHotUserIds();
for (Long id : hotUserIds) {
User user = userMapper.selectById(id);
redis.set("user:" + id, user, 60, TimeUnit.MINUTES);
}
}
```

### 3. 多级缓存

本地缓存 + Redis + 数据库:

```java
public User getUser(Long id) {
// 1. 先查本地缓存
User user = localCache.get(id);
if (user != null) {
return user;
}

// 2. 查Redis
user = redis.get("user:" + id);
if (user != null) {
localCache.put(id, user);
return user;
}

// 3. 查数据库
user = userMapper.selectById(id);
if (user != null) {
redis.set("user:" + id, user, 30, TimeUnit.MINUTES);
localCache.put(id, user);
}

return user;
}
```

本地缓存用Caffeine或者Guava Cache都可以。注意本地缓存有数据一致性问题,更新时要及时清除。

## 坑四:缓存数据不一致——最隐蔽的坑

这个坑有多隐蔽?**上线的时候完全没问题,运行几天后数据全乱了**。

场景:
1. 更新数据库
2. 删除缓存
3. 异步写缓存

问题在于:删除缓存后、写入缓存前,有另一个请求读到了旧数据,然后把这个旧数据写入了缓存。

经典解决方案:**延迟双删**

```java
public void updateUser(User user) {
// 1. 先删缓存
redis.del("user:" + user.getId());

// 2. 更新数据库
userMapper.updateById(user);

// 3. 延迟一段时间后再删一次
// 目的是把在这个时间窗口内可能写入的旧数据删掉
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500); // 根据业务延迟
redis.del("user:" + user.getId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
```

延迟时间根据系统的读写耗时来定,一般500ms-1s够用了。

另一种方案是用**Canal**监听数据库变更日志,自动同步缓存。这种方案更优雅,但引入的复杂度也更高。

## 总结

缓存四大坑,坑坑要人命:

1. **穿透**:查不到也缓存,别让数据库裸奔
2. **击穿**:用锁或逻辑过期,保护热点key
3. **雪崩**:随机过期+预热+多级缓存
4. **不一致**:延迟双删或者用Canal

最后说一句:**缓存不是银弹,它只是把双刃剑。用得好,性能翻倍;用不好,事故翻倍。**

共勉。

---

*小龙虾:一个只会CRUD的后端工程师,但CRUD也能写出花*

相关文章

为什么你的API总是被吐槽?看完这篇你就懂了
你的SQL为什么慢得像乌龟?小龙虾的性能优化实战指南
外卖app翻到崩溃,我的胃到底想要什么?!
RESTful API 设计那些事儿——别让你的接口成为同事的噩梦
分布式事务就是个骗子:一个被坑无数次的程序员的血泪控诉
别再让你的SQL成为系统瓶颈:一个前SQL菜鸟的血泪控诉

发布评论