核心是区分读请求类型:80%可缓存(如用户订单)优先用缓存,实时行情才考虑从库;需评估缓存穿透/雪崩、主从延迟、双写不一致等风险,并针对性实施空值缓存、随机TTL、强制主库读、异步删缓存等策略。

主库扛不住读流量时,先别急着加从库
读写分离不是万能解药,盲目加从库反而可能让延迟更不可控。核心问题是:你当前的读请求里有多少是「可缓存」的?有多少是「强一致性要求」的?如果 80% 的读请求查的是用户个人订单状态,那缓存比从库更直接有效;如果是实时行情数据,才值得考虑从库分流。
实操建议:
- 用
SHOW PROCESSLIST和慢查询日志快速定位 Top 5 耗时最高的SELECT语句,优先对它们做缓存评估 - 检查这些查询是否含动态参数(如
user_id、order_no),带高基数参数的查询不适合走共享缓存(比如 Redis 全局 key),要考虑本地缓存或分片策略 - 确认应用层是否已开启数据库连接池的读写分离路由(如 MyBatis Plus 的
@DS("slave")),否则加了从库也根本不会被用到
Redis 缓存穿透和雪崩不是理论问题,是上线后第一周就爆的坑
缓存穿透本质是无效 key 频繁打到 DB,比如恶意刷 /api/user?id=-1 或大量不存在的手机号;雪崩则是缓存集体过期,所有请求瞬间压向数据库。这两个问题在高并发读场景下几乎必然出现,区别只在于时间早晚。
实操建议:
- 对查询结果为
null的请求,也写入缓存(如SET user:999999 "" EX 60),值为空字符串,过期时间设短(60–120 秒),避免反复穿透 - 不要给所有缓存设统一过期时间,用随机偏移:比如基础 TTL 是 3600 秒,实际设为
3600 + random(0, 600) - 关键接口加本地缓存兜底(如 Caffeine),即使 Redis 挂了,也能抗住几秒的缓存失效窗口,避免 DB 直接被打满
从库延迟导致读到旧数据,不是配置问题,是业务逻辑没对齐
MySQL 主从延迟常见于大事务、DDL、或者从库 IO 能力弱于主库。但真正要命的不是延迟本身,而是业务代码默认「写完立刻读从库」——比如用户刚提交订单,前端马上跳转到订单列表页,列表却查不到刚下的单。
实操建议:
- 对「写后立即读」的场景,强制走主库:比如在事务内更新后,用
DataSourceRouter.setForceMaster()(或对应框架的强制主库 API)确保后续 SELECT 不路由到从库 - 避免在从库上执行
SELECT ... FOR UPDATE,这会触发从库复制中断或死锁,且无法保证锁有效性 - 监控
Seconds_Behind_Master,但别只看平均值;用pt-heartbeat工具测真实延迟,它比 SHOW SLAVE STATUS 更准
缓存与数据库双写不一致,靠「先删缓存再更新 DB」根本靠不住
这个经典方案在并发写时大概率出错:线程 A 删缓存 → 线程 B 查缓存未命中 → 线程 B 读 DB 旧值 → 线程 A 更新 DB 新值 → 线程 B 把旧值写回缓存。结果就是缓存里永远存着脏数据。
实操建议:
- 改用「更新 DB 后,发消息异步删缓存」:更新成功后往 MQ(如 Kafka)发一条
cache_invalidate:user:123消息,由独立消费者执行删除,避免业务线程耦合 - 给缓存加版本号或时间戳字段(如
cache_version),DB 更新时同步更新该字段,读缓存时校验版本,不一致则拒绝返回并触发刷新 - 对一致性要求极高的数据(如账户余额),干脆不缓存,或者只缓存「展示用摘要」,详情页直连主库
最麻烦的从来不是加机器或配缓存,而是得想清楚:哪条数据允许有 1 秒延迟?哪次读失败可以降级成空状态?这些判断没法交给架构图,得翻着业务代码一条条标出来。










