MySQL - 从单机到分布式
前面 9 篇讲的都是单机 MySQL 的内核索引、事务、锁、日志……把这些吃透能让一台 MySQL 跑得又快又稳。但业务总会长到一台机器扛不住的那天——读请求太多、写请求太密、单表数据太大。这篇就讲扛不住之后怎么办读写分离、高可用、分库分表。先立一个最重要的观点也是面试最能体现工程判断力的一点不要过早分布式。扩展是有代价的顺序应该是——优化 SQL / 加索引 → 加缓存(Redis) → 读写分离 → 冷数据归档 → 分库分表 最便宜 最贵、最复杂能靠前面几步扛住就绝不上分库分表。它引入的分布式 ID、跨库 join/事务/分页等一堆复杂度是最后的手段不是首选。下面按这个演进顺序展开。目录单机为什么会扛不住读写分离与主从复制进阶主从延迟绕不开的坑高可用主库挂了怎么办分库分表垂直与水平拆分分库分表带来的问题演进路线小结一、单机为什么会扛不住单机 MySQL 的瓶颈通常来自三个方向对应三种不同的扩展手段读压力大读多写少比如商品详情、订单查询→ 主要靠读写分离加从库分摊读。写压力大 / 单表数据量大单表几千万上亿行写入也扛不住→ 靠分库分表把数据横向切开分散到多台机器。可用性要求高主库不能挂挂了要能快速顶上→ 靠高可用架构主从 自动故障切换。这三者往往是叠加的一个成熟系统通常既有读写分离、又做了分库分表、还配了高可用切换。但记住第一节开头那句——先把单机优化和缓存做到位很多以为要分库分表的场景加个索引、上个 Redis 就解决了。二、读写分离与主从复制进阶读写分离的思路很直接写走主库读走从库。一主多从主库只承担写多个从库分摊读压力。它的基础是主从复制——主库把变更写进 binlog从库拉过来重放复制的完整链路在《binlog 与两阶段提交》那篇讲过主库 binlog → 从库 IO 线程拉取 → relay log → SQL 线程重放。主从复制按主库要不要等从库分成三种模式这是高频考点模式主库提交时数据安全性能异步复制默认写完 binlog 立刻返回不等从库弱主库刚提交就挂、从库还没拉到 → 丢数据最好半同步复制提交后要等至少一个从库确认收到 binlog才返回较强至少一个从库有这条数据略降组复制MGR基于 Paxos 协议多数派确认强一致自带故障切换更重异步复制是默认性能最好但有丢数据风险——主库 commit 后还没来得及把 binlog 发给从库就宕机这条数据在从库上就永远没有了。大多数对一致性要求不极致的业务用它。半同步复制semi-sync靠插件开启牺牲一点性能换安全主库必须等到有从库回复我收到 binlog 了才算提交成功保证宕机时至少有一个从库不丢数据。金融等对数据安全敏感的场景常用。**组复制MGRMySQL Group Replication**是官方 8.0 力推的高可用方案基于 Paxos 做多副本强一致还内置了故障自动检测和切换是复制 高可用的一体化方案。读写分离怎么落地两种方式中间件层应用连一个代理ProxySQL、MyCat、ShardingSphere-Proxy由它把写路由到主库、读路由到从库。应用无感知。应用层组件用 Sharding-JDBC 这类客户端组件在应用内部完成读写路由。少一层网络转发但和应用耦合。三、主从延迟绕不开的坑读写分离最大的坑是主从延迟——从库重放跟不上主库写入导致刚在主库写完立刻去从库读却读不到。这是面试必问的延伸点。成因主库是多线程并发写从库早期是单线程重放天然追不上。大事务主库一个大事务执行很久从库也得原样重放很久。从库机器配置比主库差、或从库还要扛查询流量。DDL 这种大操作在从库上阻塞后面的重放。解决并行复制让从库多线程重放。MySQL 5.7 起支持基于组提交的并行LOGICAL_CLOCK8.0 的WRITESET模式并行度更高——通过slave_parallel_workers配置线程数。这是缓解延迟最有效的手段。拆分大事务别让一个事务改太多数据这条在《SQL 优化实战》和《锁》里也反复出现——大事务哪儿哪儿都是麻烦。关键读走主库对写完必须立刻读到的场景比如下单后马上查订单强制这类读走主库绕开延迟。半同步复制能保证从库至少收到了 binlog缩小延迟窗口。四、高可用主库挂了怎么办读写分离解决了读扩展但主库依然是单点——它一挂整个系统就写不了了。高可用要解决的就是主库故障时自动从从库里选一个提升为新主库让系统快速恢复。核心动作是故障检测 主从切换 路由更新。常见方案MHAMaster High Availability老牌方案一个管理节点监控主库主库挂了自动把数据最新的从库提升为新主、并让其他从库指向它。成熟但需要额外部署且本身也可能成为单点。MGR MySQL Router官方组合。MGR 负责多副本强一致和自动选主Router 负责把流量路由到当前的主节点。8.0 后越来越主流。OrchestratorGitHub 开源的拓扑管理和故障切换工具可视化管理复制拓扑。云数据库RDS如果用云厂商的托管 MySQL高可用基本是开箱即用的——主备切换、故障转移都由云平台托管中小团队最省心的选择。切换时有两个绕不开的权衡RPO能容忍丢多少数据和RTO能容忍停多久。异步复制切换快但可能丢数据RPO 0半同步/MGR 更安全但更重。选哪种取决于业务对这两个指标的要求。五、分库分表垂直与水平拆分当单表数据量太大B 树层数变高、查询变慢参见《B 树索引》的层数估算经验值单表别超 2000 万行左右或单库写压力太大读写分离只分摊了读写还是全压在主库时就要动拆这一刀了。分两个维度垂直拆分——按业务/列拆垂直分库按业务把不同的表拆到不同的库。订单表放订单库、用户表放用户库、商品表放商品库各库独立部署。这是最自然的第一步拆分也符合微服务的划分。垂直分表把一张宽表按列拆开。比如把不常用的、或占空间大的字段长文本、详情拆到一张附属表让主表更瘦、单页能放更多行、查询更快。水平拆分——按行拆真正解决数据量和写压力水平分表同一张表按某个规则把行拆成多张结构相同的表order_0、order_1……order_n。水平分库再把这些表分散到不同的库/机器上写压力也就分散了。水平拆分的关键是分片键sharding key——按哪个字段、什么规则来决定一行数据落到哪个分片。常见规则取模user_id % 分片数。分布均匀但扩容时几乎所有数据都要重新分布迁移量大。范围range按 id 区间或时间段分比如按月分表。扩容方便加新分片即可但容易出热点最新的分片最忙。一致性哈希扩容时只迁移一小部分数据缓解取模扩容的痛点。分片键选得好不好直接决定成败——要选查询最常用、能让数据均匀分布的字段。比如订单表用user_id分片那查某用户的所有订单就能落在单个分片里但如果经常要按订单号查,而订单号不是分片键就得扫所有分片性能很差。六、分库分表带来的问题分库分表不是免费的——数据一旦被切开分散到多台机器很多单机上理所当然的操作都变难了。这些坑正是它被称为最后手段的原因也是面试的深挖点① 分布式 ID分表后主键不能再用AUTO_INCREMENT——每张分表各自自增会撞车。得换全局唯一 ID 方案雪花算法Snowflake64 位 时间戳 机器号 序列号趋势递增、无需中心节点最常用。号段模式如美团 Leaf从数据库批量取一段 ID 缓存在本地慢慢发减少数据库压力。RedisINCR简单但依赖 Redis 可用性。UUID 全局唯一但无序做主键会引起页分裂、还大不推荐——原因见《B 树索引》。② 跨库 join数据分散在不同库没法直接 join。解法尽量避免跨库 join改成先查一个库拿到结果再拿结果去另一个库查应用层拼装或者用冗余字段把常用的关联字段冗余到本表空间换查询。③ 跨库分页LIMIT深分页本来就麻烦见《SQL 优化实战》跨库更甚——要每个库都查出前 N 条再在应用层归并排序取真正的 N 条,偏移量越大越吃内存。常见做法是业务上限制翻页深度或改用游标。④ 跨库事务一个操作跨了多个库就成了分布式事务。要么用重量级的XA/2PC性能差要么退而求其次用最终一致性方案TCC、本地消息表、事务消息在一致性和性能间做取舍。⑤ 跨库聚合count、SUM、GROUP BY没法一条语句搞定得每个分片各算一遍再汇总。⑥ 扩容分片数不够了要加机器取模分片会导致大量数据迁移。缓解办法有一致性哈希、预分片一开始就分足够多的逻辑分片物理上先合并、后拆分、翻倍扩容等。看完这一串就明白为什么开头要强调不要过早分库分表了——它把单机上一条 SQL 就能搞定的事拆成了一堆分布式难题。七、演进路线小结把整篇收成一条演进线也是一个系统随着体量增长的典型成长路径阶段手段解决什么1优化 SQL、加索引让单机跑到极致前面 9 篇的主题2加缓存Redis挡住大部分读减轻数据库压力3读写分离一主多从分摊读压力4主从 高可用切换主库不再是单点5冷数据归档让单表瘦下来能不拆就不拆6分库分表分摊写压力、解决单表过大最后手段每一步都是被业务体量逼出来的越往后代价越大。架构没有银弹只有权衡——面试时能说清我会先做 1~5只有确实到了单机写不动、单表压不住的地步才上分库分表并且清楚它带来的分布式 ID、跨库事务这些代价,比背出多少种分片算法都更能体现你的工程判断力。回望整个 MySQL 系列前 9 篇带你从理解一台 MySQL 怎么把数据又快又稳又并发地存取在磁盘上这一篇则告诉你当一台不够用时如何有代价、有取舍地扩展成一套分布式的数据存储。从一条记录、一个数据页到一台实例、一个集群——这就是 MySQL 的全景。