单机 MySQL 总有天花板:单表行数破千万后,B+ 树层数增加、索引膨胀、写入与查询都开始吃力;单库连接数、磁盘 IO、CPU 也终有上限。读写分离和分库分表,是数据库横向扩展的两把主要武器。但它们不是"加机器就完事"的银弹——每一种扩展都在用复杂度换容量,把原本数据库帮你兜住的一致性、事务、聚合,推回到应用层。理解这些代价,才能在该用时用、不该用时忍住。
场景:从读慢到写慢的演进
系统初期,瓶颈通常在读:报表、列表、搜索把数据库压满,但写入量不大。这时读写分离最划算——主库扛写,多个从库分摊读。
继续增长,写也扛不住了,或者单表大到查询普遍变慢。这时才轮到分库分表出场。两者解决的是不同阶段的问题,顺序通常是先读写分离、再分表、最后分库,按需逐步上,不要一上来就过度设计。
读写分离:原理与一致性陷阱
读写分离依赖主从复制:主库把变更写进 binlog,从库拉取并重放,从而拥有主库数据的副本。应用把写请求路由到主库,读请求分发到从库。
1 | 写 ──────► 主库 (Master) |
看似简单,最大的坑是**主从延迟(replication lag)**导致的"读不到自己刚写的数据"。从库重放 binlog 需要时间,主库写入后立刻去从库读,可能读到旧值。经典翻车:用户改完资料跳转详情页,详情页走从库读,显示的还是改之前的内容。
应对策略有几种:
- 强制走主库:对一致性敏感的读(如"提交后立即查看")直接打到主库。多数中间件支持用 hint 标注。
- 读自己写一致性:会话级别记录"我刚写过",短时间内该用户的读强制走主库。
- 等待复制位点:写完拿到主库 binlog 位点,读时确认从库已追到该位点再读(半同步思路)。
延迟的根因之一是从库单线程重放跟不上主库多线程并发写。开启并行复制(按库、按表或按事务依赖并行回放)能显著缓解,但配置不当也可能引入新问题,需要监控 Seconds_Behind_Master 持续观察。
分库分表:拆分维度与路由
当单表过大,就要把一张表拆成多张。拆分有两个正交的维度:
垂直拆分:按列拆。把不常用的大字段(如 content、detail)拆到单独的表/库,让主表更"瘦"、缓存命中更高。本质是按业务边界切分,比如把用户表和订单表分到不同库。
水平拆分:按行拆。同一张表的数据按某种规则分散到多个表/库,这是应对海量数据的主力。关键是选分片键(sharding key)和分片算法:
1 | 取模:shard = user_id % 16 |
分片键的选择是分库分表里最关键的决策。它必须是高频查询条件,否则查询无法定位到具体分片,只能广播查询所有分片再合并——这几乎抵消了分表的收益。比如订单表按 user_id 分片,那么"查某用户的订单"很高效,但"按订单号查"就得扫所有分片。
工程权衡与踩坑
跨分片事务退化。 数据落在不同库后,本地事务的 ACID 不再适用。跨分片要么上分布式事务(2PC/XA 性能差、TCC 侵入业务、Saga 最终一致),要么从设计上规避——尽量让一次事务涉及的数据落在同一分片(同一分片键)。设计阶段就该把"哪些数据需要一起改"考虑进分片键。
跨分片聚合与排序的代价。 ORDER BY ... LIMIT、COUNT、GROUP BY 在分库后变得昂贵:要在每个分片各查一遍,再在应用层(或中间件层)归并。分页尤其麻烦——查第 100 页每页 10 条,理论上每个分片都得取前 1000 条再归并取 10 条,深分页几乎不可用。常见做法是用 ES 等专门的聚合层承接复杂查询,让分片库只做主键路由。
全局唯一 ID。 自增主键在分表后会冲突(每个分片各自从 1 开始)。需要全局发号器:雪花算法(Snowflake,时间戳+机器号+序列号,趋势递增、对索引友好)、号段模式(数据库批量取一段 ID 缓存到本地),或 Redis 自增。不能再依赖单表 AUTO_INCREMENT。
扩容是分库分表最痛的环节。 取模分片一旦要扩容,几乎所有数据都要重新计算分片、迁移落位,迁移期间还要保证双写一致、可回滚。所以设计初期就要预留分片数(比如直接按 1024 个逻辑分片规划,物理上先映射到少量库,扩容时只挪逻辑分片而不改算法),避免日后大动干戈。
别为了分而分。 分库分表把数据库的能力大幅削弱:失去跨表 JOIN(要改成应用层组装或冗余字段)、失去强一致事务、查询模式被分片键绑死。能用读写分离 + 单库优化(归档冷数据、加缓存、优化索引)扛住的,就不要轻易上分片。很多团队过早分库分表,换来的是数倍的开发与运维复杂度,却没用上对应的数据规模。
小结
读写分离扩的是读能力,核心难点是主从延迟与"读自己所写"的一致性;分库分表扩的是写与存储能力,核心是选对分片键、接受跨分片事务/聚合/分页的退化、解决全局 ID 与扩容迁移。两者的共同本质是用应用层复杂度换数据库容量。落地顺序应循序渐进——先压榨单机、再读写分离、最后分库分表,每一步都确认上一层真的扛不住了再走。架构的智慧,常常不在于会不会拆,而在于忍得住不拆。