搜索
当前位置:首页 > 娱乐 > 工程师讲解

工程师讲解

发表于 2026-10-17 01:26:54 来源:烟消火灭网
```html 工程师讲解

工程师讲解

一个周五深夜的调试笔记 · 关于缓存穿透那点事

上周三凌晨两点,我被电话叫醒。师讲线上订单查询接口挂了,工程整个页面白屏。师讲值班同事说日志里全是工程同一个错误:NullPointerException,位置在缓存层。师讲我揉着眼睛打开电脑,工程第一反应是师讲——又来了,经典的工程缓存穿透。

你问什么是师讲缓存穿透?简单说,就是工程有人拿一个数据库里压根不存在的 ID 疯狂查。比如 userId = -1 或者 orderId = 99999999999。师讲每次请求都绕过缓存,工程直接砸到数据库。师讲数据库扛不住,工程连接池耗尽,整个服务跟着雪崩。这个坑我踩过三次,每次都是不同的业务场景,但根因一模一样。

那晚我干了什么?先紧急扩容数据库连接数,把服务临时拉起来。然后翻代码,发现查询逻辑是这样的:

// 伪代码,别直接抄
public Order getOrder(Long orderId) {
    Order order = cache.get(orderId);
    if (order == null) {
        order = db.query(orderId);
        cache.set(orderId, order); // 注意这里
    }
    return order;
}

问题出在 cache.set 那行。如果 db.query 返回 null,也就是订单不存在,代码照样往缓存里塞了一个 null。但很多缓存客户端对 null 的处理很微妙——有的直接不存,有的存了但过期时间极短。更糟的是,如果用的是 Redis 并且序列化方式不对,null 可能变成字符串 "null",下次查出来还得反序列化,照样穿透。

我的观察:大部分团队在写第一版缓存逻辑时,都会漏掉“空值缓存”这个分支。不是能力问题,是思维惯性——我们总假设查了就有,没有就是异常。但线上环境里,“没有”才是常态。

怎么修?我见过三种方案。第一种,缓存空对象,但设一个很短的过期时间,比如 30 秒。这样同一个不存在的 ID 在 30 秒内只穿透一次。第二种,用布隆过滤器,把所有合法 ID 预加载进去,查询前先过滤。第三种,加互斥锁,只让一个线程去查库,其他线程等着。这三种我都用过,各有各的坑。

空对象缓存最简单,但要注意内存占用。如果攻击者用随机 ID 刷你,缓存里会堆满垃圾键。所以过期时间不能太长,还得加个上限或者定期清理。布隆过滤器听起来优雅,但实现成本高——你得保证数据同步,新增 ID 要实时加进去,删除 ID 就麻烦了,标准布隆过滤器不支持删除。互斥锁能扛住热点 key 失效,但分布式锁本身又引入新的复杂度,锁超时、锁误删,全是故事。

那天晚上我选了空对象缓存,过期时间设 60 秒,同时加了一个本地 Caffeine 做二级缓存,挡住大部分重复请求。改完上线,数据库 QPS 从 8000 掉到 200 左右。凌晨四点,我关掉电脑,心里清楚这只是止血——真正的解法得从 API 网关层做参数校验,把明显非法的 ID 直接拒掉。

后来复盘,有个细节挺有意思。攻击者用的 ID 全是负数,而我们的订单 ID 从 100000 开始自增。如果当初在 Controller 里加一行 if (orderId <= 0) return null;,这事根本不会发生。但没人加,因为大家觉得“前端会校验”。前端当然会校验,可攻击者不用前端。


写这些不是要教你怎么做缓存。我想说的是,很多线上故障的根因,不在复杂的架构图里,而在某一行你觉得“不可能出问题”的代码上。缓存穿透如此,空指针如此,线程池配置也如此。工程师的价值,有时候就是半夜爬起来,把那行代码改对。

现在我的习惯是:每次写缓存查询,先问自己一句——如果这个 key 永远不存在,我的系统会怎样?想清楚再动手。这个习惯帮我省了至少三次紧急扩容。

```
随机为您推荐
友情链接
版权声明:本站资源均来自互联网,如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

Copyright © 2016 Powered by 工程师讲解,烟消火灭网   sitemap

回顶部