MySQL热点:索引失效陷阱与缓存穿透修复方案
说白了,你写SQL的时候觉得“这不就是查个字段嘛”,结果一跑,慢得离谱——不是你代码写得烂,而是索引被你玩废了。
别信那些“建个索引就完事”的鬼话。你以为你加了个索引,它就听话地为你服务?那得看你怎么用。
一、索引失效的3种“隐藏杀招”
🚨 错误用法1:OR条件引发全表扫描
SELECT * FROM users WHERE id = 1 OR name = 'Tom';
你以为这是“精准查询”?错了,MySQL优化器一看这结构,直接给你干回表+全表扫描。
本质: OR 条件破坏了索引的有序性,让优化器放弃了走索引的念头。
💡 避坑指南:把OR拆成UNION,或者重构为复合索引。比如:
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE name = 'Tom';
🚨 错误用法2:函数调用导致索引失效
SELECT * FROM logs WHERE DATE(created_at) = '2026-02-05';
你加了索引 created_at,但函数一加,索引就失效了。因为函数会打断B+树的查找路径。
💡 避坑指南:改写为范围查询:
SELECT * FROM logs WHERE created_at >= '2026-02-05' AND created_at < '2026-02-06';
🚨 错误用法3:隐式类型转换
SELECT * FROM orders WHERE user_id = '12345'; -- user_id 是 INT 类型
哪怕你传的是字符串,MySQL也会进行隐式转换。一旦转换,索引就凉了。
💡 避坑指南:统一数据类型。别让字符串在数字字段里混进来。
二、缓存穿透的真实困境
你有没有遇到过这样一种情况:
用户访问一个根本不存在的页面,系统去查缓存,没命中;再去查数据库,也无数据。于是缓存一个空值,结果呢?每次请求都打穿数据库,成了“缓存穿透”。
🔍 缓存穿透的三大成因
| 原因 | 描述 |
|---|---|
| 空值缓存未设过期 | 缓存空值后不设置 TTL,导致永久存在 |
| 无互斥锁保护 | 多线程同时查一个不存在的 key,全部穿透 |
| 查询数据量过大 | 业务设计缺陷,无兜底策略 |
三、真实案例:某电商系统缓存穿透崩溃事件
某电商平台,用户频繁搜索一个不存在的商品 ID,导致数据库压力骤增。
❌ 原始做法
def get_product(product_id):
cache_key = f"product:{product_id}"
data = redis.get(cache_key)
if not data:
data = db.query("SELECT * FROM product WHERE id = %s", product_id)
if not data:
redis.set(cache_key, "", ex=300) # 设置空值缓存
else:
redis.set(cache_key, data)
return data
结果呢?大量无效请求打穿 DB,系统雪崩。
✅ 修复策略(三步走)
-
空值缓存 + 过期时间
redis.set(cache_key, "", ex=60) # 只缓存 60 秒 -
互斥锁防穿透
lock_key = f"lock:{product_id}" if redis.set(lock_key, "locked", nx=True, ex=10): # 获取锁 data = db.query(...) redis.set(cache_key, data, ex=3600) redis.delete(lock_key) -
布隆过滤器预判
- 提前将所有存在的商品 ID 加入布隆过滤器
- 请求前先判断是否可能真的存在,否则直接返回空
四、专业对比表:缓存穿透修复前后性能差异
| 方案 | 平均响应时间 | QPS | 数据库压力 |
|---|---|---|---|
| 无防护 | 1.8s | 300 | 高 |
| 空值缓存 + TTL | 0.2s | 1200 | 中 |
| 空值 + 锁 + 布隆 | 0.08s | 2800 | 低 |
五、深度问答(FAQ)
Q1:为什么索引不生效?我明明加了!
A:你可能用了 OR、函数、隐式转换、或者字段类型不匹配。索引不是万能的,用错了才废。
Q2:缓存穿透怎么查?是不是没做?
A:你得看日志。一般查不到的 key 会反复打穿数据库,这种请求量激增就是信号。
建议加监控: 统计未命中次数 / 异常请求比例。
Q3:布隆过滤器是不是太复杂了?能不能不用?
A:能不用就别用。但如果你的系统有大量无效请求,那它是救命稻草。
简单替代: 用 Redis 的 SET 结构记录所有存在 key,简单有效。
Q4:为什么我的索引明明建了,但 EXPLAIN 显示没有走?
A:因为 MySQL 优化器太聪明了,它会根据成本判断是否走索引。
查原因: 查 SHOW INDEX FROM table; 和 EXPLAIN 的执行计划,看看是否命中。
六、结语:别再靠运气扛数据库
你以为你写的是SQL,其实是“索引的战场”。
你以为你缓存了数据,其实你可能正在放血。
别再信那些“索引随便建就完事”的鬼话。
你得知道:索引是“快”的前提,不是“有”的标签。
缓存不是万能药,是“挡板”;
索引不是装饰品,是“武器”。
别让数据库成为你的短板。
别让你的系统,在“你以为很稳”的时候,突然崩了。