本节摘要:单机数据库是高并发系统最先遇到的天花板——数据量大了查不动、写多了锁冲突。本节讲两种数据库扩展手段:读写分离(主库写、从库读,分散压力)和分库分表(把数据拆到多个库多个表)。重点是它们的代价——读写分离有主从延迟的一致性问题,分库分表引入跨分片查询和分布式事务的复杂性。扩展不是免费的,每一步都要权衡。
阅读完本节,你应当能够:
单机数据库在高并发下有两个天花板。第一是写瓶颈:所有写都落在一个主库,写多了锁冲突严重、IO 打满。第二是数据量瓶颈:单表数据量到几千万行后,查询变慢、索引维护开销大、备份恢复慢。
这两个天花板,靠加机器配置(垂直扩展)能延缓,但终究会到极限。要根本解决,得水平扩展——把读写压力或数据分散到多个数据库实例。这就是读写分离和分库分表的来历。
读写分离解决写瓶颈:主库专门写,读分散到多个从库,读压力不再全压主库。分库分表解决数据量和写并发瓶颈:把数据按某种规则拆到多个库多个表,每个分片只承担一部分数据和压力。两者都是把单点的压力分散,但引入了各自的复杂性。
读写分离的原理:数据库配置一个主库和多个从库,主库负责写,从库负责读,主库的变更通过复制机制(binlog 同步)传到从库。
这个架构直接缓解了读压力——读请求分散到多个从库,不再全压主库。对一个读多写少的系统(大多数互联网应用),加几个从库就能把读压力摊薄。
但读写分离有个绕不开的问题:主从延迟。主库写入后,复制到从库有时间差(毫秒到秒级)。这意味着“写完立即读”可能读到旧数据——用户刚改了昵称,刷新页面还是旧的,因为读落在了还没同步完的从库。
应对主从延迟有几种策略。关键写后读走主库:写操作完成后,短时间内(如 1 秒)的读请求强制走主库,确保读到最新。会话粘性:同一用户的读写都走同一个库。业务容忍:有些场景(如查看他人资料)容忍短暂不一致,读从库即可。
| 场景 | 一致性要求 | 读路由 |
|---|---|---|
| 自己写后立即读 | 强 | 走主库 |
| 查看他人数据 | 弱 | 走从库 |
| 报表统计 | 弱 | 走从库 |
当读写分离也扛不住(写瓶颈仍在单主库,或数据量太大单库存不下),就要分库分表——把数据拆到多个库多个表。
垂直拆分:按字段拆。把一个宽表(几十个字段)拆成多个窄表,按业务相关性分组。比如用户表里,基本信息(昵称、头像)一组,隐私信息(身份证、手机)一组,分到不同库。垂直拆分降低单表字段数,但每个库仍存全量数据的对应字段,数据量瓶颈没解决。
水平分片(Sharding):按行拆。把一个表的数据按某种规则分散到多个库多个表。比如按用户 ID 取模,ID 对 4 取模为 0 的用户数据放分片 0,为 1 的放分片 1,以此类推。每个分片只存一部分数据,数据量和写压力都被分散。这是解决数据量和写并发瓶颈的主力手段。
水平分片的关键是分片键的选择——按哪个字段来分。分片键选得好,大多数请求只查一个分片(带分片键的查询能定位到具体分片);选得差,大量请求要查所有分片再合并(跨片查询,性能差)。
比如电商订单按用户 ID 分片,那么“查某用户的订单”只查一个分片(带分片键,快),但“查某商品的所有订单”要查全部分片(不带分片键,慢)。分片键要选查询最频繁的那个维度。
分片键选错,整个分库分表方案就失败一半。选分片键考虑两点:高频查询要带分片键(避免跨片),数据分布要均匀(避免某些分片过热)。
常用的分片策略有几种。取模分片:分片键取模分到各片,分布均匀,但加片麻烦(要重新分布所有数据)。范围分片:按分片键的范围分(如 ID 1-10000 一片),加片容易,但可能数据倾斜(热门范围过载)。一致性哈希分片:用一致性哈希环,加片时只影响相邻片,兼顾均匀和扩展性。
⚠️ 常见坑:分片键选了一个查询不常用的字段,结果绝大多数查询都要跨片。跨片查询性能差不说,还要在应用层合并结果,复杂度暴增。分片键必须是最频繁查询带的条件。
分库分表前,一个事务里更新多张表,它们在同一个库,数据库自带事务保证。分库分表后,这几张表可能在不同库,跨库事务数据库保证不了,要用分布式事务方案(如两阶段提交、TCC、Saga)。
分布式事务的开销和复杂度远高于本地事务。两阶段提交性能差(要锁资源等协调),TCC 要写大量补偿代码,Saga 要处理中间状态。所以分库分表后,要尽量规避跨片事务——把强相关的数据分到同一片(按同一分片键),让事务落在本地。
分库分表是最后的手段,它能扩容但代价大(跨片查询、分布式事务、运维复杂)。在分之前,先把其他手段用足:索引优化、SQL 优化、读写分离、缓存。这些做完单库还扛不住,才考虑分库分表。
💡 关键直觉:数据库扩展的每一步,都是用“复杂度”换“容量”。读写分离用主从延迟换读扩展,分库分表用跨片和分布式事务换写和数据扩展。扩展前先衡量,这笔复杂度值不值得换这点容量。很多情况优化 SQL 和加索引就够了,不必分。
下一章往上走到系统级,讲怎么让整个系统扛得住故障——容灾多活和混沌工程。
分库分表的所有后续能力都被分片键锁定,这个决策值得用最高的谨慎度对待。选键的标准有三条:分布均匀(避免某片过热)、查询命中(多数查询能定位到单片)、不随时间漂移(用户 ID 稳定,订单号会增长)。三条经常冲突:按用户 ID 分片分布均匀,但"查某商品的所有订单"就要扫全片;按订单时间分片利于归档,但写入热点集中在最新片。冲突的本质是访问模式多元,没有一个键对所有人友好。工程解法有三种:冗余双写(按用户分一片、按商家再冗余一份,存储换查询)、异构索引(用搜索引擎承接跨片查询)、以及基因法(把用户 ID 嵌入订单号,两种查询都能路由)。三种都要写两份数据或维护索引,没有免费的午餐。
扩容策略也要提前想:分片数一旦定下,翻倍扩容要迁移一半数据。主流做法是一开始就把分片数定为终局规模的预估(宁可单片小、片数多),或者采用一致性哈希减少迁移量。从一片扩到十六片的迁移演练,应该在分片设计定稿前就在测试环境跑通——迁移工具的坑(大事务、双写一致性、切换瞬间)远比设计文档里写的多。
再补分布式事务的选型对比,因为分片之后跨片事务无法回避。可靠消息最终一致适合长流程,实现简单但只能保证最终态,中间态可见;事务消息把一致性绑在消息中间件上,开发友好但耦合深;两阶段提交强一致但同步阻塞、吞吐低,互联网高并发场景基本弃用;柔性事务框架把上述模式工程化,学习成本换来的是团队统一的用法。选型的锚点还是业务:资金类操作要强一致或对账兜底,普通状态流转用最终一致绰绰有余——用两阶段提交去保护一个通知消息,是典型的杀鸡用牛刀还把鸡吓死了。
分库分表之后还有一类被低估的工作量:数据运维。原来单库的一条更新语句,分片后变成跨片任务;原来简单的全表统计,现在要聚合几十个分片的结果。批处理任务要重写为分片感知的版本,备份恢复要按分片编排,数据订正要带路由计算——这些日常运维的复杂度膨胀,在架构评审时经常被完全遗漏,上线后才以运维同学加班的形式显现。预留一个工具建设周期给数据运维平台,是分片项目规划里的必要项,不是锦上添花。
数据订正是分片库运维里最考验纪律的操作:订正脚本必须先在影子库演练、必须有回滚方案、必须分批执行并在批间校验。分片环境下订正的爆炸半径比单库大得多,一条路由计算错误的订正语句会同时污染多个分片。把订正流程工具化——路由计算、批次控制、校验报告全部自动化——是分片规模超过十个之后必然要做的事,人工执行在这个量级上迟早出事。
再补一个分库后唯一性的细节:自增主键在分片间会冲突,常用解法是号段模式(应用批量领取区间)或雪花算法(时间戳加机器位)。号段模式简单但重启会浪费区间,雪花算法依赖时钟(时钟回拨会出重复),两者都要求数据库层面再加唯一索引兜底。选哪个都可以,没兜底就上线是事故预约。