缓存穿透是分布式系统中常见的性能隐患,当客户端频繁查询根本不存在的数据时,缓存层因查不到而放行请求,压力直接传导到数据库。如果恶意流量或业务bug持续发起此类查询,数据库连接池会被迅速耗尽。要根治这一问题,业界通常采用布隆过滤器提前拦截,或者对空结果进行缓存。这两种思路在原理和适用场景上有明显区别,理解它们的底层机制才能正确落地。

布隆过滤器的底层原理与实现
布隆过滤器本质上是一个超长的位数组以及多个相互独立的哈希函数。当一个元素加入集合时,通过k个哈希函数计算出k个位置,将位数组中对应下标置为1。查询时同样计算这些位置,只要有一个位置为0,就说明该元素一定不在集合中;若全部为1,则元素可能在集合中。这种概率型数据结构不用保存原数据,因此空间效率极高,特别适合用来判断海量key是否合法。
在Java中可以利用Guava或Redisson实现布隆过滤器。下面示例展示基于Guava的本地过滤器用法,实际生产常将其放入Redis以保证多实例共享。需要注意,布隆过滤器无法删除元素,因为某个位可能由多个key共用,直接置0会影响其他key的判定。
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomDemo {
// 预计插入100万数据,误判率0.01
private static BloomFilter<String> filter = BloomFilter.create(
Funnels.stringFunnel(java.nio.charset.StandardCharsets.UTF_8),
1000000,
0.01);
public static void main(String[] args) {
filter.put("user_1");
filter.put("user_2");
// 返回false表示一定不存在
System.out.println(filter.mightContain("user_999"));
}
}
布隆过滤器的主要优势是内存占用小、查询速度快,缺点则是存在误判率且不支持删除。当业务需要下线某些key时,往往只能重建整个过滤器。此外,如果攻击者构造出恰好误判的key,仍会穿透到数据库,因此通常要配合其他手段使用。
空值缓存的设计与注意事项
空值缓存的思路非常直观:当数据库也查不到数据时,在缓存中写入一个特殊的空对象或null标记,并设置一个较短的过期时间。后续相同请求命中缓存中的空值,直接返回,不再访问数据库。该方案实现成本极低,不需要引入额外组件,适合大部分中小型系统。
使用空值缓存时要重点解决两个问题。一是空值过期时间不宜过长,否则真实数据插入后用户仍长时间看到空结果;二是要防止恶意刷大量不同不存在的key,导致缓存被空值占满。可以结合布隆过滤器先过滤非法key,或者对空值采用更短的TTL与最大数量限制。
import redis
r = redis.Redis(host='127.0.0.1', port=6379, db=0)
def get_user(uid):
key = "user:" + str(uid)
val = r.get(key)
if val is not None:
# 空值标记为字符串NULL
return None if val == b"NULL" else val
# 数据库查询
data = db_query(uid)
if data is None:
# 缓存空值60秒
r.setex(key, 60, "NULL")
return None
r.setex(key, 3600, data)
return data
空值缓存的劣势在于,如果穿透key的随机性极强,缓存层会存储大量无用占位符,浪费内存并可能驱逐热点数据。因此它与布隆过滤器并非互斥,而是互补关系:前者拦住格式非法或明显无关的请求,后者兜住偶发的查无结果。
组合方案与高并发下的优化策略
在真实高并发场景中,单靠一种手段难以覆盖所有情况。推荐架构是在网关或缓存前增加布隆过滤器,将系统允许的有效key范围固化;当请求通过过滤器后,若缓存未命中再查数据库,并将空结果按短TTL缓存。这样即便过滤器误判,空值缓存也能阻断重复查询对数据库的冲击。
此外还需考虑缓存击穿与雪崩的连带影响。可以使用互斥锁或逻辑过期来重建缓存,避免大量线程同时回源。对于空值缓存的写入,建议增加随机过期偏移,防止同一时刻大批空值一起失效。下表对比两种核心方案的关键指标:
| 方案 | 内存占用 | 误判率 | 删除支持 | 实现复杂度 |
|---|---|---|---|---|
| 布隆过滤器 | 极低 | 可配置 | 不支持 | 中 |
| 空值缓存 | 随空key增长 | 无 | 支持 | 低 |
落地时还应做好监控,统计穿透请求比例与数据库负载。若发现过滤器误判率升高,可适当增大位数组或调整哈希函数数量。若空值缓存命中率过高,说明可能存在大量非法查询,需要从业务入口或风控层面限制。通过多层防护,缓存穿透问题才能被稳定解决。