
说到MySQL读写分离很多人都把它当成数据库扩展的第一站。它解决的问题很直白读多写少的业务里单库的连接数和IO很快会被查询压垮把一部分读流量分到只读实例上让主库专心处理写请求。这个方向听起来简单真正落地时从主从复制原理到数据源路由再到延迟监控每一步都有值得讲清楚的细节。这篇文章适合正在折腾数据库架构的开发者也适合打算从单库迈向主从架构的技术团队参考我会把原理拆开聊再把实操中的坑一个个摆出来。1. 读写分离的整体设计思路与核心价值1.1 为什么业务到了一定阶段就躲不开读写分离大多数业务的数据访问特征呈现明显的“读多写少”商品浏览、订单查询、报表统计、日志分析几乎都是读请求真正产生写入的只有用户下单、状态更新这类操作。我见过不少系统读写比例轻松到9:1甚至更高。单库架构下这些读请求和写请求挤在同一个实例里共享同一份连接池和磁盘IO互相争抢资源。单库的瓶颈通常不是CPU先到而是连接数。每个应用实例默认配几十个连接应用扩容到十几个节点之后主库的连接数很快被打满。数据库本身不是无状态服务每个连接都要分配线程和内存连接数上来后上下文切换成本急剧上升。紧接着是IO大量读请求重复扫描某些热点表缓存命中率下降磁盘读放大最终拖慢写事务的提交速度。读写分离的核心思路是把读压力从主库上卸载下来。主库保留全部写能力和一部分核心读能力只读实例通过主从复制拿到同一份数据承担大部分查询。这样主库的资源被“解放”出来处理写事务整体吞吐量能获得接近线性的扩展至少能阻挡住第一波连接数和IO危机。1.2 读写分离不是万金油适用边界先想清楚不少团队一听到读写分离就往上套但这是一套有代价的架构。先说复制延迟从库数据是异步追平的主库刚提交的事务从库可能几十毫秒甚至几秒后才可见某些审核、支付回调类场景对数据一致性要求极高一旦读到旧数据会触发严重业务问题。如果你的系统里大量操作都是“写完立刻读回来校验”那要先做好路由设计否则不如不拆。另一个容易忽视的点读写分离只能解决读扩展问题解决不了写扩展。写入仍然集中在主库上热点写、锁竞争、大事务这些单库问题依然存在甚至因为从库多了同步压力而变得更明显。某些极端场景比如短连接风暴、单点热点行更新读写分离甚至会让排查更复杂。判断是否适合读写分离可以参考三个条件读请求占比明显高于写业务对数据最终一致性的容忍度较高应用层有能力做数据源路由。如果不满足先把慢查询优化、缓存预热、连接池调优做扎实可能收益更大。架构上的每一步都要付出运维成本能被缓存解决的读压力就不值得引入一套新链路。2. 核心原理剖析主从复制与路由分发2.1 主从复制链路拆解要理解读写分离必须先吃透主从复制。主库把每一次数据变更记录到binlog二进制日志中相当于写了一份“流水账本”。从库启动后IO线程连接主库请求从某个位点开始读取binlog拿到后写入本地relay log。紧接着SQL线程读取relay log并逐条回放在从库上重放这些变更。整个复制链路里主库的dump线程负责推送binlog从库的IO线程负责拉取SQL线程负责应用。三个环节任何一个卡住延迟都会肉眼可见地上升。binlog的格式直接影响复制效果我强烈推荐使用row格式。statement格式记录的是SQL语句本身虽然日志量小但在涉及函数、UUID、自增等不确定操作时主从回放结果可能不一致。row格式记录的是变更前后的行数据虽然日志量更大但语义精确不容易出幺蛾子。默认的异步复制有天然短板主库提交事务时并不确认从库是否收到。主库一旦宕机未传送的binlog会永久丢失。所以生产环境里至少要把半同步复制考虑进来。半同步复制的原理是主库在提交事务时等待至少一个从库确认收到binlog再返回客户端成功。这样就保证了至少有一个从库持有最新日志主从切换时丢数据的风险大幅降低。复制还强烈推荐使用GTID模式。GTID是全局事务标识符每个事务在全局范围内都有一个唯一编号。相比传统基于binlog文件名和位点的定位方式GTID让从库自动感知主库的事务进度追主和切换都方便得多。新环境直接开启GTID不要再用老旧的位点复制逻辑省下的运维心智非常可观。2.2 读写分离的分发策略复制链路只解决了数据同步问题真正让读请求分流的是上层路由逻辑。最基础的分流规则是按SQL类型判断SELECT、SHOW这类只读操作走从库INSERT、UPDATE、DELETE走主库。但业务远比这个复杂单凭语句前缀判断远远不够。先看最典型的坑事务内读写。一个事务里如果先更新了某行数据再查询同一行这个查询必须走主库否则极可能读到旧值。这就是“先写后读”一致性问题。因此凡是处于事务内的SQL无论是不是SELECT都应该强制走主库。实现上很多方案是“开启事务时绑定主库数据源”直到事务提交或回滚后再释放。还有一类查询虽然不涉及事务但对延迟极度敏感例如刚提交后的结果回显。这类查询在路由规则上可以打标记强制走主库而不必等待从库追平。反过来报表、历史分析这类可以接受短暂延迟的查询就放心交给从库。分发规则的核心是根据业务容忍度把流量分层必须实时的走主能接受延迟的走从。最后是可用性兜底。从库可能宕机也可能在变更维护中被摘除。路由层必须设计降级策略从库不可用时是把读流量临时切回主库还是直接返回错误。我的经验是对高可用要求高的核心链路选择切回主库对非核心报表链路宁可短暂失败也不能把主库打爆。这个策略需要在架构设计阶段就和业务方对齐而不是等故障了再争论。核心点总结如下开关、配置类读写必须走主库保证全局一致事务内所有语句强制主库避免回话内的过期读可容忍秒级延迟的查询走从库分摊压力从库故障核心链路探活切主非核心限流降级3. 实操过程与核心环节实现3.1 基础环境准备与参数推荐搭建一套读写分离环境首先要准备好实例和网络。主从实例建议独立部署至少保证专有网络内互通不要共用同一个数据目录或磁盘避免IO干扰。实例版本尽量保持一致跨版本复制可能出现数据类型兼容问题升级时也建议先升级从库再升级主库。配置层面先把基础参数对齐。主库必须开启binlog设置server-id开启GTID。从库的server-id和主库不能相同。binlog格式设置为row。自动清理binlog的窗口期建议保留至少24小时方便追主和故障恢复时找到合适的位点。我习惯的最小参数组合如下server-id 100主从分别设为不同值log-bin mysql-binbinlog_format rowgtid_mode onenforce_gtid_consistency onrelay_log relay-binbinlog_expire_logs_seconds 86400半同步复制需要安装插件主库和从库各自启用相关插件。启用后注意观察性能损耗一般情况下对写入吞吐的影响在可接受范围内但如果写并发极高建议先在测试环境压一遍再上生产。3.2 主从复制搭建步骤复制账号的创建很简单在主库建立一个专用账号授予复制权限不要用root。权限最小化原则这个账号除了复制链路没有任何其他权限。然后锁表记录当前binlog位点再导出数据。数据量小时直接逻辑备份数据量大时用物理备份前提是保证备份起点和记录的位点一致。从库导入数据后执行change master操作。如果开了GTID不需要手动指定位点从库会自动从主库获取GTID集合继续同步。我建议用GTID方式手写位点方式在老版本太容易出错尤其是库表迁移后位点错位追主追到一半报主键冲突的场面碰过一次就不想再碰了。启动从库复制后第一时间检查两个核心状态IO线程和SQL线程是否都在运行。重点看Seconds_Behind_Master字段这个值刚开始可能是0也可能一时显示NULL先观察一会儿数据追平后应该稳定在0附近。如果这个值持续上涨说明从库正在被大量同步任务拖住后面我会专门讲延迟问题。3.3 应用层读写分离的落地实现应用层做读写分离核心是数据源路由。主库一个数据源从库一个数据源在同一个事务上下文内动态切换。最朴素的做法是扩展DataSource接口维护一个ThreadLocal变量保存当前线程应该使用哪个数据源。框架层面可以在AOP切面里根据方法名或注解判断走主还是走从。注意注解的优先级要高于方法名判断因为有些查询虽然前缀是select但业务上需要强制走主库。我给出一个可参考的伪代码思路定义读写类型枚举MASTER、SLAVE、AUTO切面拦截ReadRoute注解设置当前线程的数据源路由标记事务管理器启动时强制将路由标记设为主库数据源代理在获取连接时检查标记决定使用主库还是从库连接请求结束时清理ThreadLocal避免线程池复用导致路由串线其中最关键的一步是事务绑定。如果你用的Spring事务管理事务开启时会获取一个数据库连接并绑定到线程上整个事务期间都复用这个连接。因此事务一旦在从库连接上开启后续所有操作都留在从库哪怕标记已经切到主库也没用。所以规则必须前置判断当前是否处于活动事务如果是直接使用主库。MyBatis场景下也可以借助拦截器识别SQL语句前缀做自动路由。但拦截器能看到的信息有限拿不到事务状态所以最稳妥的方式还是控制数据源选择的入口统一走代理层方法和事务注解都留好扩展位。3.4 中间件方案对比与选型思考应用层路由的好处是轻量、可控坏处是每个语言栈都要实现一遍且业务代码侵入性偏强。如果团队里有多个语言同时访问数据库维护成本会翻倍。这时候可以考虑独立的数据库中间件独立代理统一接收SQL请求解析后分发给主库或从库对后端应用完全透明。中间件的优势是集中管理和多语言友好缺点也很明显引入了一层额外网络跳点链路的复杂度和故障面都变大了。代理节点需要做高可用自身要处理连接管理和SQL解析性能损耗必须压测验证。另外强一致路由、事务识别这些能力不同中间件实现深度参差不齐选型时要特别注意。从实用角度讲小型团队、单一语言栈优先用应用层路由简单直接。中大型团队、多语言并存、已有独立DBA团队再考虑代理层方案。两种方案不是互斥的很多团队先做应用层路由过渡流量和复杂度上来后再迁到代理层同时保留应用层的主库强制路由能力。4. 常见问题与排查技巧实录4.1 主从延迟现象、原因与对策主从延迟是读写分离最常遇到的麻烦。Seconds_Behind_Master长期大于0甚至不断上涨通常有几个原因。一是从库上有慢查询SQL线程在回放relay log的同时从库自身还要响应业务查询如果这些查询有性能问题回放会被挤占。二是大事务比如一次性更新几百万行binlog生成量大从库要连续回放很久这个期间主库的新事务只能排队。处理大事务的思路是把大事务拆分成小批量不要在一个UPDATE里处理全量数据按主键范围分批提交。还要检查从库是否存在未走索引的查询这是最容易被忽视的隐藏杀手。从库的角色虽然主要是只读但它的查询计划同样要满足索引优化否则同步回放和业务查询互相拖累延迟数据很难看。延迟特别严重的另一个原因是老版本MySQL的并行复制能力有限。新版本引入了基于写集合的并行复制从库SQL线程可以并行回放不同事务但前提是主库的binlog设置了binlog_transaction_dependency_tracking同时表结构不能有外键约束。开启并行复制后延迟通常会有肉眼可见的改善。排查延迟时不要只盯Seconds_Behind_Master。这个值是从库SQL线程的时间差估算出来的如果IO线程断开了它可能显示为NULL造成“没延迟”的假象。要同时检查Slave_IO_Running和Slave_SQL_Running是否正常再看Exec_Master_Log_Pos是否在持续前进。如果位点长时间不动说明回放真正卡住了。4.2 数据不一致与复制中断复制中断最常见的报错是SQL线程报错比如主键冲突、表不存在、字段长度不足。这类错误大多源于从库被手工修改过或者表结构变更没有沿着主库走一遍。比如某天DBA直接在从库上改了某张表的字符集主库后续的相关字段更新在从库上就可能触发类型转换问题复制线程直接停掉。遇到这种情况我的建议是不要轻易跳过事务。跳过复制错误相当于给数据挖坑这次跳过的那个事务在从库上永远缺失后续所有依赖它的变更都会连锁出错。正确做法是把从库从复制链路中摘除重新初始化数据再搭复制。对一些非核心从库如果数据量不大重新导入数据比重修位点更稳妥。表结构变更也是复制中断的高发区。主库执行DDL时如果从库还在回放前序事务DDL会被排队执行期间从库延迟飙升。如果在主库先删除了某列从库还没完成这个DDL此时业务正好往该表插入数据从库回放就可能因为找不到列而报错。DDL变更最好选择低峰期并且保证主从都有足够的空间和时间窗口。4.3 读写分离下的边界问题先写后读问题在第三章提过再补充一个容易踩的边界自增主键的回读。应用插入一条数据后经常需要获取新记录的主键或流水号。LAST_INSERT_ID()是基于会话的如果插入走主库紧接着查询却走了从库拿到的很可能是0或者错误值。解决方式很明确获取自增值的动作必须和插入语句在同一个主库连接上完成。另一个边界是连接池的参数配置。主库和从库的连接池需要单独设置不能复用同一套配置。从库承担大量读流量连接数需求可能比主库还高但每个连接的内存消耗也要控制住。主库的连接数反而要更保守因为写事务持有连接的时间通常更长连接池太小容易排队太大会拖垮操作系统。还有一类容易被忽略的情况某些报表查询会把多张大表做关联SQL在从库上执行消耗了大量临时空间严重时会拖垮从库整个实例。这类查询应该单独拆分到专门的报表库或数仓不要和线上只读实例混在一起。简单说从库扛的是线上常规读不是重查询泄压阀。把重查询全部丢给从库从库早晚被压垮。最后提醒一下监控的完整性。读写分离链路里至少要监控主库的复制线程状态、从库的延迟时间、两端的数据位点差、从库的慢查询数量。不要等业务反馈页面加载变慢才察觉到问题日常巡检时就应该把这些指标纳入告警体系。很多团队把监控只做到主库存活从库延迟到十几个小时才被发现这种状态下的读写分离已经名存实亡了。4.4 上线前的验证事项与渐进演练读写分离改造上线前一定要做几轮针对性的压测和演练。第一轮验证基本的路由逻辑事务内读是否全部走主库、只读查询是否分流到从库、强制注解是否生效。可以通过在业务日志中打印数据源标识或者临时查询connection_id对应的实例来确认。第二轮要验证降级策略。把从库实例直接停掉观察路由层能否快速感知并切换核心接口是否受影响切回主库后主库的负载是否在可承受范围内。这一步极重要因为很多故障不是由于从库本身的问题而是降级切换瞬间把主库打挂最终演变成常说的“雪崩”。第三轮验证复制延迟场景。模拟一个大批量更新任务观察从库延迟是否飙升业务侧是否出现明显的数据不一致反馈。同时验证监控告警能否在延迟超过阈值时触发值班同学是否能通过现有工具快速定位到延迟源头。整个过程建议以分钟级粒度复盘记录每个时间点的系统状态和业务表现作为后续调优的基线。我在实际项目中做过几次读写分离改造最大的体会是路由代码本身不难写难的是把各种边界情况想全。事务穿透、延迟敏感查询、复制中断、降级策略每个点都需要和业务同学反复对齐。上线之前先把这些边界条件列成checklist逐项验证通过后再切流量能省掉很多半夜被叫醒的麻烦。我个人还建议上线后前两周保持高频观察。不要急着把全部读流量都切到从库先放开10%观察从库的延迟和负载曲线再逐步提升比例。数据库相关的架构调整往往不是设计出来的而是这样一步步试探着调优出来的。慢一点稳一点读写分离只是起点后续的监控优化和容灾演练才是真正拉开团队差距的地方。