别再想着单纯扩容数据库连接池就能解决缓存穿透了——那只是掩耳盗铃。真正的防线必须布在Redis层:空值缓存(比如 SETEX key 30 "")、布隆过滤器(得全量初始化并做好误判兜底)、接口参数校验,至少得选两样落地,否则扩多少池子都是白搭。

很多人以为“连接池不够就加连接数”,但缓存穿透这件事,扩容只会让问题迁移、资源内耗更严重。 穿透请求一旦形成风暴,哪怕连接池从50扩容到500,只要攻击者持续构造无效的user_id或product_sku,连接照样会被占满、超时堆积,甚至触发连接泄漏或事务死锁。真正有效的办法,是在Redis层就让这些请求“止步”——拦截得越早,代价越低。
空值缓存必须设TTL,且不能用永久存储
空值缓存是防御穿透最直接的手段,但很多人栽在TTL设置上:
- 不设过期时间(比如
SET key "")→ 空值永久驻留,Redis内存慢慢上涨,迟早OOM - 过期时间太长(比如24小时)→ 无效key长期占用内存,而且无法响应业务变更(比如某类ID规则已废弃)
- 过期时间太短(比如1秒)→ 拦不住并发冲击,多个线程几乎同时查到空,重复打库照样扛不住
推荐的做法是:对大多数业务场景,SETEX key 30 ""(30秒)是一个折中的平衡点。它足够挡住突发扫描,又不会长期污染缓存。实战中:PHP里用$redis->setex($key, 30, '');Java RedisTemplate用opsForValue().set(key, "", 30, TimeUnit.SECONDS)。
布隆过滤器不是“加了就完事”,得管住初始化和误判兜底
布隆过滤器能前置拦截99%的无效key,但实际落地常忽略两个要点:
- 初始化阶段没全量加载合法key → 过滤器形同虚设。比如用户表有800万记录,只导入了前10万,那么漏掉的key全部穿透
- 遇到误判(
bf.exists('product_bf', '123456789')返回True,但DB里实际没有该商品)→ 后续流程没做空值缓存,导致同一个key下次仍然穿透
务必要补上兜底:即使布隆说“可能存在”,查库为空后,仍要走一遍空值缓存逻辑。Python示例中,bf_client.exists()返回False直接拒绝;返回True后,DB查询为空 → 立即redis.setex('user:123456789', 30, '')。这环一断,前面的努力就全白费了。
接口层校验比缓存层拦截更早、更省资源
很多穿透源于参数本身非法,比如user_id=abc、order_no=(空)、sku=999999999999999999999(超出位数)。这类请求根本不该进Redis:
- PHP中用
filter_var($userId, FILTER_VALIDATE_INT)或正则/^d{1,10}$/提前拦截 - Spring Boot用
@Pattern(regexp = "^d{1,10}$")注解校验入参 - 网关层(比如Nginx、Kong)配置
if ($args ~* "user_id=[^0-9]") { return 400; }
这一层拦截成功,连Redis connect都省了,CPU和网络开销直接归零。别等请求跑到缓存再处理,那是把压力转嫁给了更脆弱的环节。
数据库连接池耗尽时,光看“连接数满”会误判根本原因
当监控显示HikariCP - ActiveConnections=50/50或MySQLThreads_connected持续高位,第一反应不该是调大池子,而是查三件事:
- 查慢日志:是否存在
SELECT * FROM user WHERE id = ?绑定参数为null或极长字符串,导致全表扫描 - 查应用线程堆栈:是否有大量线程卡在
JDBCStatement.execute(),说明SQL本身执行慢,而不是连接不够 - 查Redis
info stats:若rejected_connections在涨,说明穿透已严重到Redis自身都在拒绝新连接,此时扩DB连接池毫无意义
穿透引发的连接池耗尽,本质是“无效请求 + 无防护逻辑”双击打穿。修复必须回到请求入口——校验、布隆、空值,三者至少落地两项。连接池只是最后一道缓冲,不是保险丝,别总盯着它不放。
