因为…所以… — 从业务增长到分布式存储的完整因果推导
当一张表只有几百行时,SELECT * FROM orders WHERE user_id = 123 瞬间返回。但当表增长到1000万行,MySQL必须从第一行扫到最后一行(全表扫描),查询从10ms变成10秒。
索引就像字典的部首检字表——不认识字时,先查部首,再翻到对应页码,而不用从第一页翻到最后一页。
1. 写入变慢:每次INSERT/UPDATE/DELETE都要同步更新B+树,索引越多,写入越慢
2. 索引失效:用了函数、隐式类型转换、OR条件、LIKE '%abc'等,索引直接不生效
3. 数据量继续增长:索引只是让"找"变快,但单表的存储上限并没有解决
大多数业务是读多写少(电商:浏览商品100次才买1次)。一台MySQL既要处理读请求又要处理写请求,并发到5000 QPS就顶不住了。
写入只走主库,读取分散到从库。主库写入后通过binlog同步给从库。
主库写入后,binlog异步同步到从库,这个同步需要时间(通常1-100ms)。在这段时间内,读从库会读到旧数据。
即使解决了主从延迟(强制走主库读),DB的读压力依然很大。如果所有热点数据都从DB读,DB还是扛不住。
把热点数据放内存(Redis),读取时先查Redis,没有再查DB,然后回写Redis。内存读取速度是磁盘的100倍+。
缓存穿透:查询一个根本不存在的数据,Redis没有→DB也没有→每次都穿透到DB
缓存击穿:一个热点key突然过期,大量请求瞬间打到DB
缓存雪崩:大量key同时过期,或者Redis宕机,所有请求瞬间打到DB
黑客发送SELECT * FROM user WHERE id = -1,Redis没有→DB也没有→返回null。黑客用脚本发10万个这样的请求,DB被打满。
方案1:布隆过滤器——在Redis前面加一层布隆过滤器,所有可能存在的数据hash位都标记为1。查询时先过布隆过滤器,如果bit位有0,说明数据一定不存在。
方案2:缓存空值——即使DB查不到,也往Redis写一个空值(带短过期时间),避免下次穿透。
iPhone首发、双11秒杀——这些商品页面访问量巨大。如果缓存key在高峰期过期,瞬间几千个请求同时打到DB。
方案1:互斥锁——只让一个线程查DB并重建缓存,其他线程等待。
方案2:逻辑过期——不设TTL,在value里存一个过期时间字段。发现逻辑过期时,异步线程更新缓存,当前请求先返回旧数据。
双11零点,很多商品缓存同时过期(都是1小时前预热设的),瞬间所有请求打到DB → DB也挂了 → 整个系统雪崩。
先更新DB还是先更新缓存?不管哪种顺序,并发时都可能不一致。
方案1:延迟双删——删缓存 → 更新DB → 延迟一段时间 → 再删一次缓存
方案2:Canal监听binlog——不侵入业务代码,Canal伪装成MySQL从库,监听binlog变更,自动同步到Redis
| 方案 | 一致性 | 复杂度 | 适用场景 |
|---|---|---|---|
| 先更新DB再删缓存 | 最终一致 | 低 | 大多数场景 |
| 延迟双删 | 较强一致 | 中 | 一致性要求高 |
| Canal监听binlog | 最终一致 | 高 | 大型系统,解耦需求 |
| 读写串行化 | 强一致 | 极高 | 金融场景(极少数) |
即使有索引、读写分离、缓存,当一张表到10亿行时:
用户下单涉及扣减库存和创建订单,但库存和订单可能在不同的库中——传统事务@Transactional只能管一个库,跨库就管不了了。
→ 这引出了分布式事务的需求(详见分布式因果链页面)
面试题不是孤立的,它们都在这条因果链上。理解了因果关系,面试时可以从根因出发推导,而不是死记硬背。
| 面试题 | 在因果链的位置 | 因为…所以… |
|---|---|---|
| MySQL索引原理 | 第①环 | 因为全表扫描太慢 → 所以B+树索引,3-4层即可定位百万行 |
| 索引失效场景 | 第①环 | 因为用了函数/类型转换/左模糊 → 优化器无法用索引 → 所以要避免 |
| 主从复制原理 | 第②环 | 因为单机扛不住读写压力 → 所以主从复制,通过binlog异步同步 |
| 主从延迟怎么处理 | 第②环 | 因为异步复制有延迟 → 所以写后读走主库/引入缓存 |
| Redis为什么快 | 第③环 | 因为DB磁盘IO慢 → 所以用内存缓存,0.1ms vs 10ms |
| 缓存穿透/击穿/雪崩 | ④⑤⑥环 | 因为缓存引入了"不存在数据"和"过期"的问题 → 所以需要布隆/互斥锁/随机过期 |
| 缓存与DB一致性 | 第⑦环 | 因为缓存和DB是两个系统 → 所以存在不一致窗口 → 延迟双删/Canal |
| 分库分表策略 | 第⑧环 | 因为单表数据量太大 → 所以水平拆分 → 但引入了跨库事务问题 |
| 分布式事务 | 第⑧环延伸 | 因为分库后本地事务管不了跨库 → 所以需要Seata/TCC/Saga |