你的结账接口P95延迟400毫秒,数据库查询占了七成。团队加上读副本,把SELECT全部切过去,P95骤降到90毫秒。庆祝还没结束,工单爆了——用户刚改的收货地址,确认页显示的还是旧的。更糟的是,有人被扣了两次款,因为"订单已存在"的校验读到了过期数据,漏掉了重复提交。

架构很简单:主库写,复制延迟约200毫秒;从库扛100%读流量。问题恰恰在于,从库按设计运行,但业务没按设计运行。

打开网易新闻 查看精彩图片

四个选项摆上台面,都是生产环境跑过的真方案。

打开网易新闻 查看精彩图片

方案A:写后读一致性。用户写入后的一小段时间内,他的读请求强制走主库。窗口期可以按用户ID或会话ID来路由,技术上靠应用层缓存最近写入的时间戳,或者让数据库连接池标记"粘性会话"。

方案B:同步复制。主库等从库确认后才返回ACK。延迟从200毫秒变成至少200毫秒,P95的90毫秒优势直接归零。这等于把读副本的优化彻底作废。

方案C:监控延迟+重试。检测到复制延迟超过阈值时,读请求降级到主库。阈值设多少?100毫秒能拦住大部分脏读,但用户体感仍是"偶尔抽风"。更麻烦的是延迟抖动时的雪崩风险——阈值一破,流量洪峰砸向主库。

方案D:关键读走主库。从库只服务非关键查询,比如报表、推荐位。订单校验、支付幂等、地址确认这些涉及一致性的读,全部回主库。问题是"关键"的边界在哪?每新增一个功能都要人工判定,技术债越积越厚。

回到故障现场:地址更新后刷新页面、支付前的重复订单检查、扣款前的幂等校验——这三类场景的共同点是,用户刚刚写完,立刻就要读。他们的大脑里,"保存"和"看到结果"是原子操作。但数据库的复制不是。

打开网易新闻 查看精彩图片

方案B和C的问题在于,它们试图用系统层面的妥协,解决应用层面的语义断层。方案D则是把决策成本转嫁给未来的开发者,每次需求评审都要争论"这算不算关键读"。

方案A的聪明之处,在于精准匹配了用户的心理模型:我自己的写操作,我必须立刻看到。它不追求全局强一致,只保证"我写的我能读到"。路由规则可以做得很轻——Redis里存个用户ID到过期时间的映射,或者更简单地,写入后5秒内该会话的所有读走主库。200毫秒的复制延迟,5秒的窗口绰绰有余。

代价是主库读流量会涨一点,但仅限于刚写完的那批用户,绝对值可控。相比B的性能自杀、C的复杂度和D的维护噩梦,这是唯一守住90毫秒优化成果的同时,解决脏读问题的路径。

那个"资深工程师陷阱"是方案B。它看起来最彻底、最正确,符合教科书里的"强一致性"审美。但生产环境不是教科书,200毫秒的同步等待会让你的P95回到原点,甚至更高。优化方案变成性能灾难,这是过度设计的经典死法。

真正难的不是选A,而是在团队庆祝90毫秒的时候,有人提前问一句:用户的下一页刷新,读的是哪一边?