本文目录导读:

缓存穿透是指查询一个根本不存在的数据,缓存层(如 Redis)和存储层(如数据库)都没有,导致每次请求都穿透到数据库,失去缓存保护。
面试或写代码时,实现的思路通常是“模拟”穿透,而不是“实现”穿透——那是系统设计要避免的。
既然你问“怎么实现”,可能是想写一个小 Demo 来演示这种场景,然后验证布隆过滤器或空值缓存等解决方案。
下面给出一个 Java 案例,演示缓存穿透的发生与解决。
模拟缓存穿透(出现)
场景:
查用户信息,缓存用 Map 模拟(或 Redis),数据库用 List 模拟。
查一个不存在的 ID(-1),每次都会查库。
import java.util.HashMap;
import java.util.Map;
public class CachePenetrationDemo {
// 模拟缓存
private Map<String, String> cache = new HashMap<>();
// 模拟数据库
private Map<String, String> db = new HashMap<>();
public CachePenetrationDemo() {
db.put("1", "张三");
db.put("2", "李四");
}
// 没有穿透防护
public String getUserNoProtect(String id) {
// 1. 查缓存
String name = cache.get(id);
if (name != null) {
System.out.println("缓存命中: " + name);
return name;
}
// 2. 查数据库
System.out.println("缓存未命中,查询数据库...");
name = db.get(id);
if (name != null) {
cache.put(id, name); // 放入缓存
}
return name;
}
public static void main(String[] args) {
CachePenetrationDemo demo = new CachePenetrationDemo();
// 多次查询不存在的 id
System.out.println("===== 缓存穿透演示 =====");
System.out.println(demo.getUserNoProtect("-1")); // 查库
System.out.println(demo.getUserNoProtect("-1")); // 仍然查库!因为没有缓存
System.out.println(demo.getUserNoProtect("-1")); // 每次都是库
}
}
输出结果:
缓存未命中,查询数据库...
null
缓存未命中,查询数据库...
null
缓存未命中,查询数据库...
null
每次请求都落到数据库,这就是 缓存穿透。
解决方案一:缓存空对象(Null Cache)
思路:查询不存在的数据时,缓存一个空值(或特殊标记),设置较短的过期时间。
// 防止穿透:缓存空对象
public String getUserWithNullCache(String id) {
// 1. 查缓存
String name = cache.get(id);
if (name != null) {
System.out.println("缓存命中: " + name);
return "null".equals(name) ? null : name;
}
// 2. 查数据库
System.out.println("缓存未命中,查询数据库...");
name = db.get(id);
if (name != null) {
cache.put(id, name); // 缓存正常值
} else {
cache.put(id, "null"); // 缓存空标记(过期时间短)
}
return name;
}
测试:
System.out.println("===== 空值缓存保护 =====");
System.out.println(demo.getUserWithNullCache("-1")); // 查库,缓存 null
System.out.println(demo.getUserWithNullCache("-1")); // 缓存命中,返回 null
输出(第二次不再查库):
缓存未命中,查询数据库...
null
缓存命中: null ← 直接返回,不查库
解决方案二:布隆过滤器(Bloom Filter)
在查询前先判断是否存在,不存在则直接返回,存在才查缓存/库。
import java.util.BitSet;
// 简单的布隆过滤器实现(仅用于演示)
class BloomFilter {
private BitSet bits = new BitSet(1 << 24);
private int[] seeds = {3, 5, 7, 11, 13, 17};
private int hash(String key, int seed) {
int h = 0;
for (char c : key.toCharArray()) {
h = h * seed + c;
}
// 取模到 bits 大小
return Math.abs(h % bits.size());
}
public void add(String key) {
for (int seed : seeds) {
bits.set(hash(key, seed));
}
}
public boolean mightContain(String key) {
for (int seed : seeds) {
if (!bits.get(hash(key, seed))) {
return false; // 肯定不存在
}
}
return true; // 可能存在
}
}
// 使用布隆过滤器保护
BloomFilter bloom = new BloomFilter();
// 初始化:把所有合法 ID 加入布隆过滤器
{
bloom.add("1");
bloom.add("2");
}
public String getUserWithBloom(String id) {
// 1. 布隆过滤器快速判断
if (!bloom.mightContain(id)) {
System.out.println("布隆过滤器判定不存在,直接返回");
return null;
}
// 2. 查缓存
String name = cache.get(id);
if (name != null) {
System.out.println("缓存命中: " + name);
return "null".equals(name) ? null : name;
}
// 3. 查库
System.out.println("查数据库...");
name = db.get(id);
if (name != null) {
cache.put(id, name);
} else {
cache.put(id, "null");
}
return name;
}
测试:
System.out.println("===== 布隆过滤器保护 =====");
System.out.println(demo.getUserWithBloom("-1")); // 直接返回,不查库也不查缓存
System.out.println(demo.getUserWithBloom("1")); // 正常查缓存
关键点总结
| 方案 | 原理 | 缺点 | 适合场景 |
|---|---|---|---|
| 缓存空对象 | 不存在的数据也缓存,设短过期时间 | 占用缓存空间,不一致窗口 | 穿透量不大 |
| 布隆过滤器 | 提前判断 key 是否存在 | 有误判率(会把存在判为不存在) | 大量穿透攻击 |
如果你想要的是 纯“实现穿透”代码(即故意制造穿透),直接看第一个 getUserNoProtect 方法即可——它没有任何防护,每次不存在就查库,这就是缓存穿透的表现。
在真实项目中,你通常要做的是:认出它,然后加上空值缓存或布隆过滤器。