聊到“水平分库”这个词,熟悉后端开发的朋友应该都不陌生。简单来说,就是把数据按照某个维度打散到多台MySQL实例上,让每一台库只承担整体的一部分流量和容量。前两篇聊过整体思路和垂直方向的拆分,这篇专门讲水平分库的实现细节。网上讲分库分表的文章很多,但大部分要么停在理论层面讲概念,要么直接甩一段配置就完事,真正能把“为什么要这么拆”“数据库路由怎么设计”“扩容时数据怎么迁”讲透的并不多。我这篇就用实际项目里的经验,把水平分库从选型到落地到踩坑,完整走一遍。
先说一个我自己的感受:单库单表到底能撑多久,取决于业务形态。哪怕你索引建得再合理、SQL写得再优雅,一旦数据量突破千万级,连接数、磁盘IO、主从延迟这些问题会接踵而至,调参只能延缓爆发,解决不了根本矛盾。到那个节点再想搞分库分表,基本就是带着压力上生产线改造,心跳加速,风险极高。更好的做法,是在容量还没见顶之前就充分想清楚拆分方案,哪怕先不拆,也要把路由规则和迁移路径预留好。水平分库作为最常见的扩展手段,核心不只是“把一份数据变成多份”,而是整套数据访问体系的重构。
1. 水平分库到底拆的是什么:先分清分库和分表
1.1 分库和分表是两码事
很多刚接触这个方向的同学,容易把“分库”和“分表”混在一起。虽然它们经常同时出现,但它们解决的是完全不同的问题。
分表,是在同一个数据库实例里,把一张大表拆成多张小表。比如订单表有5000万行,拆成64张物理表,每张表大约80万行。这样做最大的好处是降低单表的数据量,减少B+树的索引深度,让单条SQL的扫描成本降下来。但要注意,分表并不解决实例本身的压力——数据还是在那台服务器上,CPU、内存、磁盘、连接数这些资源瓶颈依然存在。
分库,是把数据分散部署到不同的MySQL实例上。每个实例有自己独立的进程、内存、磁盘和连接上限。这样做直接解决的是“单机资源上限”的问题。比如原来的订单库一天要处理1个亿的写入,单实例撑不住,拆成8个库之后,每个实例只需要处理1250万,压力立刻下来。分库带来的次要收益是故障隔离——一个库挂了,不会拖垮整个业务。
实际生产中最常见的做法是“分库又分表”,就是先把数据拆到多个实例上,每个实例里再进一步拆表。举个例子,用户库拆成4个实例,每个实例里的用户表再按区间拆成4张分表,这样整体就是4个库乘4张表,共16个物理分片。虽然听起来复杂,但本质上是把两个维度的拆分叠加起来,互不冲突。
注意:分库是分库,分表是分表。不要以为拆了表就等于完成了分库,也不要以为分了库就不需要再拆表。两者解决的问题不同,经常需要配合使用。
1.2 水平分库适合什么场景
水平分库不是银弹,它有自己的适用边界。我个人判断一个业务是否需要上水平分库,主要看三个指标:
第一个指标是数据量。单表数据量到了500万行以上,或者单库总数据量到了几个TB的级别,MySQL的运维成本会明显上升——备份恢复变慢、DDL变更锁表时间变长、慢查询排查困难。这时候水平拆分能有效控制每个分片的体量。
第二个指标是写入吞吐。如果业务的写入TPS持续走高,单库的写入能力到顶,主从同步带来的延迟也会越来越明显。这种情况靠加从库解决不了根本问题,因为从库本质上还是共享同一份写入压力,只有把写入分散到多个主库,才能真正扩展写入能力。
第三个指标是数据访问模式。水平分库要求业务的大部分查询能带上明确的分片键,比如用户ID、订单ID、商户ID这类维度。如果查询条件高度不可控,经常需要跨全部分片扫描,那分库会非常痛苦。这时候可能要考虑其他方案,比如按业务领域垂直拆分、引入搜索引擎,或者使用分布式数据库产品。
2. 第一道选择题:分片键怎么选
2.1 分片键的三个硬指标
分片键是水平分库的灵魂。选对了,后续一路顺畅;选错了,后面想换就是伤筋动骨。我在实际项目里总结出三个硬指标,缺一不可。
第一个指标是数据分布均匀。分片键的取值要在各分片上尽可能均衡,不能出现某一个库数据量特别大、其他库很空闲的情况。最容易犯的错误是选“状态”这种枚举值有限的字段做分片键——比如订单按“已完成、待支付、退款中”分片,那所有已完成订单堆在一个库上,完全起不到分散压力的作用。拿用户ID、订单ID这种高基数字段做分片键,天然分布均匀,是最稳妥的选择。
第二个指标是查询亲和性。所谓亲和性,就是要让绝大多数查询都能通过分片键直接定位到目标库。比如电商场景,用户查询自己的订单,SQL条件是where user_id = 123,那分片键用user_id最合适,一次路由就能命中一个库。如果你选了order_id做主分片键,用户查询订单列表时没带上order_id条件的场景怎么办?这就很尴尬了。
第三个指标是不可变性。分片键的取值一旦写入,就不应该再变化。因为路由公式是拿分片键去计算目标库的,如果分片键本身改了,计算出来的库就变了,数据就相当于“归错位”了,需要迁移才能修正。像手机号这种可能过户变更的字段,就不适合直接做分片键。用户ID这种系统生成的稳定值,才是最佳候选。
2.2 常见分片键选型对比
我把实际工作中用到过和见到过的分片键类型做了个表格,方便对照:
| 分片键类型 | 分布均匀性 | 查询亲和性 | 改动风险 | 适用场景 |
|---|---|---|---|---|
| 用户ID | 高 | 高(用户维度查询多) | 低,基本不变 | C端业务、用户中心、订单中心 |
| 订单ID | 高 | 中(按订单号查精准,但按用户查要先映射) | 低,基本不变 | 订单详情、交易流水 |
| 商户ID | 中(商户大小差异大) | 高(商户维度查询多) | 低 | B端业务、开放平台 |
| 时间字段 | 低(新数据永远集中在最新分区) | 低(跨时间范围查询多) | 中(可能订正历史) | 日志类、时序数据 |
| 设备ID | 高 | 高(设备维度查询) | 中 | IOT、采集类 |
选型逻辑背后有一个关键推断:优先选业务系统里出现频率最高、且大部分查询天然带有的那个维度。C端系统几乎都是用户进来自助查询,所以user_id是第一顺位;B端系统是商户视角,merchant_id更合适;日志系统按时间归档,那就用时间范围切分。
有一个容易被忽略的坑:订单场景选了订单ID做分片键,用户查订单列表怎么办?常见解法是维护一张“用户与订单关联表”或者“用户维度路由表”,记录user_id到order_id的映射。但这张表本身就是一张大表,会被打回原形。更常见的做法,是在订单表里把user_id也带上作为冗余,同时用user_id作为路由键——也就是说,分片键不一定是主键或唯一键,只要大部分查询能带得上就行。
实操心得:如果业务里既有用户维度查询又有订单维度查询,优先选用户维度做分片键,订单ID作为冗余字段保存在表里。用户维度查询直接路由,订单号查询如果做不到带user_id条件,就要接受偶尔的全分片扫描,或者引入额外的二级索引映射。别指望一个分片键能解决所有查询模式。
3. 第二道选择题:路由规则与算法推导
分片键定下来之后,紧接着要决定的就是路由算法——给定一个分片键的值,怎么算出它落在哪个库。这块我重点讲三种:取模路由、范围路由、一致性哈希路由。它们各有适用场景,也有各自的坑。
3.1 取模路由:实现最简单
取模路由是最直观的方案:库编号 = hash(分片键) % 分片数量。如果分片键本身是整数,比如自增ID,就直接拿ID做取模;如果是字符串,先算哈希值再取模。
举个例子,假设有4个库,用户ID为13:13 % 4 = 1,落在1号库。用户ID为14:14 % 4 = 2,落在2号库。逻辑就这样简单,任何开发都能在半小时内实现。
取模的优点是实现简单、路由计算开销小、数据分布均匀(只要分片键基数够大)。缺点是扩容极痛苦。从4个库扩到8个库时,hash(key) % 4和hash(key) % 8的余数大概率不同,意味着大量数据要跨库迁移。
举个例子:原来user_id=5存在1号库(5%4=1),扩容成8个库后,5%8=5,数据应该落在5号库。如果不迁移,查user_id=5会路由到5号库,发现找不到数据,查user_id=13又会路由到1号库,能查到旧数据。这种错乱在线上是不可接受的。
缓解取模扩容痛点的办法是“翻倍扩容”:从4个库扩到8个库时,把每个老库的数据拆成两份——余数小于4的数据留在原库,余数大于等于4的数据迁到新库。这样每个老库只需要迁出一半数据,新增实例的数量等于原实例数。翻倍扩容比任意扩容平滑,但有个前提:你要保证分片数量始终是2的幂次,否则翻倍策略不成立。
3.2 范围路由:扩容友好但可能热点不均
范围路由是另一个极端:把分片键按取值范围划分到不同库。比如订单ID在[0, 1000万)放1号库,[1000万, 2000万)放2号库,依次类推。
范围路由最大的优点是扩容友好。新增一个库,只要把某一段区间切分出去就行,比如原本[0, 4000万)一个库承载不了,改成[0, 2000万)和[2000万, 4000万)两个库,把后半段数据迁移到新库即可,旧数据不需要大面积搬迁。另外一个好处是范围查询效率高,比如按时间范围查数据,连续区间大概率落在同一个分片内。
范围路由的缺点也很明显:热点不均。如果分片键是增长型的,比如订单ID和时间,新数据永远落在最后一个分片,导致最后一个库永远是最忙的,前面几个库越来越闲。这在写入密集的系统中是大忌。解决思路是“范围+取模”组合:先按时间范围分成季度库,季度内再按ID取模分多个分片,让新写入的流量在多个分片间分散。
实操理解:范围路由更适合“冷热分层明显、频繁访问的是最新数据”的场景,配合合理的分片粒度,可以让老数据所在的库提供较低配置也能扛住。但纯范围路由在写入均匀性上很难保证,需要结合其他策略一起用。
3.3 一致性哈希:均衡与扩容的折中
一致性哈希算法是为了解决取模扩容迁移量大的问题而出现的。它的核心思想是把哈希环组织成一个0到2^32-1的环形空间,把库节点映射到环上,数据按照哈希值顺时针找到最近的节点。
简化理解:还是拿4个库举例,每个库的某个标识(比如IP+端口,或者库名)做哈希,映射到环上的一个点。任何一条数据,先算分片键的哈希值,从该位置顺时针走,遇到的第一个节点就是它的归属库。当新加一个库时,只需要把新节点顺时针方向到下一个节点之间那段数据迁到新库,其他数据不受影响。
但一致性哈希有一个先天问题——节点少的时候分布不均匀,比如4个库在环上位置接近,数据容易集中到某一个库。解决办法是引入虚拟节点,把每个物理库映射成上百个虚拟节点均匀散落在环上,这样数据分布就均匀很多。
实际做下来,一致性哈希的路由逻辑比取模复杂,性能损耗稍高,但扩容时的迁移数据量远小于取模。业务量小、后续扩容预期明确的系统,我更推荐一致性哈希。不过需要注意,一致性哈希只能解决“分片键相同的数据如何定位”,并不能替代业务维度的查询设计。
为了让大家看得更直白,我做了一个对比:
| 路由方式 | 实现复杂度 | 数据均匀性 | 扩容代价 | 适用场景 |
|---|---|---|---|---|
| 取模路由 | 低 | 好 | 高,几乎全量迁移 | 分片数固定、扩容频率低 |
| 范围路由 | 低 | 差,热点集中 | 低,切分区间即可 | 时序数据、冷热分层 |
| 一致性哈希 | 中高 | 好(虚拟节点) | 低,只迁移邻接区间 | 长期发展、多次扩容的系统 |
4. 落地实现方案:应用层还是中间件
路由规则之后要解决的是:谁来执行路由逻辑,谁来真正连接和操作这些库。业内常用的三条路:应用层自己写路由、客户端中间件、代理中间件。做技术选型这事,没有最好,只有适不适合。
4.1 应用层Router:轻量场景自己写
所谓应用层路由,就是在业务代码里封装一个数据访问层,由它来计算分片键对应的库,动态选择数据源执行SQL。这适合分库数量不多、路由规则简单、团队人手充足、暂时不想引入额外组件的场景。
我见过一个订单系统的实现:在项目里定义了一个OrderRepository,底层维护了一个库列表,根据order_id % N选择数据源。逻辑不复杂,几十行代码就能搞定。但问题也接踵而来——SQL越来越多样,比如按用户查订单列表、按时间区间统计、多表关联查询,每次新增SQL都要自己改写路由逻辑,开发效率被拖累,而且一旦路由规则更换,改动范围会波及所有写SQL的地方。
自己写路由还有一个隐患:分库之后,单条SQL原本的普通分页LIMIT 10 OFFSET 20,现在需要在多个分片分别执行,再把结果合并排序才能得出正确结果。这个归并逻辑如果处理不好,很容易出现分页页数错乱、排序不对的线上事故。我的建议是:如果决定应用层路由,一定要把SQL规格约束好,能用单分片完成的简单查询优先保证,复杂的聚合查询能挡则挡。
4.2 客户端中间件:嵌入应用的轻量方案
客户端中间件,比如Sharding-JDBC这一类,本质上是一个嵌入式数据库中间件。它在应用内部解析SQL,根据配置的路由规则自动把SQL路由到正确的分片执行,对业务代码的侵入比手写路由低得多,因为中间件帮你处理了SQL改写和结果归并。
这种方案的优点是不需要额外部署服务,没有额外的网络跳转,性能损耗很小,和应用一起部署,运维熟悉成本低。它适合Java技术栈统一、希望快速落地分库分表的团队。缺点是要受限于中间件的SQL兼容性——如果你的业务SQL比较奇葩,用了很多特定写法、复杂嵌套子查询,中间件可能解析不了,强制要求你改写SQL。另外,中间件本身维护成本不低,版本升级、逃逸语法处理都需要专人跟进。
从实践角度看,客户端中间件是目前中小团队落地分库分表的最优解。它把路由和归并的脏活累活交给了成熟组件,团队只需要把精力投入在分片键设计和SQL规范化上。
实践提醒:无论用哪种中间件,都要对实际执行的SQL做一次全量Review。我见过不少案例是上线后慢查询反而变多——原因是中间件对分页和聚合处理有额外的网络开销,如果SQL本身写得差,分库分表只会放大问题。
4.3 代理中间件:对应用透明的另一种形态
代理中间件是独立部署一个服务层,应用程序只连接代理,由代理负责把SQL分发到后端的各个MySQL实例。这种方案对应用最透明——数据库的连接串从单库改成代理地址就行,业务代码几乎不用改。
代理方案有几个适用场景:比如多语言团队共用一套数据库访问入口,或者数据库变更不想让应用感知。但它的缺点也很明显:多了一层网络转发,性能损耗比客户端中间件高;代理本身成了新的单点,必须部署高可用集群;占用的内存和连接管理也需要专门运维。如果团队没有运维中间件的能力,谨慎选这个方案。
这里做一个横评表格,方便决策:
| 方案 | 部署复杂度 | 性能损耗 | 业务侵入 | SQL兼容性 | 适用规模 |
|---|---|---|---|---|---|
| 应用层Router | 低 | 低 | 高,代码侵入明显 | 自己控制 | 分片少、规则简单 |
| 客户端中间件 | 中低 | 低 | 中,需少量改造 | 依赖组件解析能力 | 中小团队Java栈 |
| 代理中间件 | 高 | 中高 | 低,应用无感 | 依赖代理解析能力 | 大型异构团队 |
4.4 从库读写分离要不要一起考虑
水平分库落地之后,很多团队的下一步追问是:要不要顺便把读写分离也做了?我的建议是:如果你拆分分库的初衷就是解决写入压力,那主从读写分离可以先放一放,把主库水平拆分做好才是核心;如果瓶颈更多在读取侧,可以先在单库上挂只读从库,做读写分离,缓一缓再考虑分库。
典型的心路历程是:数据库慢了,先加从库做读写分离,过半年又慢了,再下决心分库。这条路本身没错,但要注意分库和主从延迟是叠加的——分库之后,每个分片的主库各自挂从库,写主库再异步同步到从库,任何一分片的从库延迟都可能影响全局读取一致性。在业务侧要配合强制读主库的开关,比如交易、支付这种强一致场景一律走主库。
5. 单库到多库的平滑过渡:迁移流程拆解
水平分库最难的从来不是写路由规则,而是怎么把线上已有的数据安全、平滑地从单库搬到多库。现阶段我见过的主流方案有两种:停机迁移和双写过渡。
5.1 停机迁移:最简单但要有容忍窗口
停机迁移的流程就是:停掉写入流量,应用进入只读或下线状态,导出单库数据,按路由规则计算每条数据的目标分片,导入到各个新库,然后验证数据,切换应用连接,恢复流量。
这种做法的优点是逻辑简单、数据一致性好;缺点是线上服务需要停写一段时间。对内部管理系统、管理后台这类可容忍短暂停服的业务,这个方案完全够用。但对于面向C端的核心交易链路,哪怕停机30秒,业务方也未必接受。停机时长主要取决于数据量:导出一张千万级的大表,加上导入回多个分片,折腾几个小时也是常事。
实操过程中,我建议把导入步骤按分片并行执行,比如4个目标库就并行开4个导入任务,能明显压缩总时长。另外导出数据之前,应用一定要先停掉所有写操作,否则一边导一边写,数据对不齐,校验阶段必然暴雷。
5.2 双写过渡:不停服的平滑方案
双写方案是目前大型系统上分库的主流做法,核心思路是“新老并行、逐步切换”。整个过程分三步:
第一步,搭建新库环境,初始化分片结构。第二步,启动历史数据迁移任务,把单库已有数据按路由规则导入新库,这个阶段称为全量迁移。第三步,同时开启双写,所有新写入的数据既写入老库,也通过路由逻辑写入新库对应的分片。此时老库和新库在同时接收新增数据。
全量迁移完成之后,还需要把全量迁移期间老库新增的数据补到新库,这个过程叫增量追平。之后进入数据校验阶段,逐条核对老库数据和新库数据的一致性。校验通过后,把应用读流量切换到新库,观察一段时间确认无误,再停掉老库的写入,保留老库作为备份观察,最终完成下线。
双写方案的优点是业务不停服,迁移过程平滑;缺点是技术复杂度高,尤其是双写的一致性问题——老库写入成功、新库写入失败怎么办?所以双写过程必须有个可靠的消息机制或者补偿任务,定期扫描老库数据,发现不一致就触发补偿写入。如果做不到可靠的补偿,双写反而是埋雷。
实战经验:如果团队是第一次做双写,建议迁移期间安排专人盯增量补偿任务的监控指标。双写最容易出的问题不是双写逻辑本身,而是全量迁移和增量数据重叠导致的主键冲突——全量导入的数据和新写入的数据同时插入同一个分片,不做好去重策略就会大量报错。稳妥的做法是先用不存在的临时ID检查路由目标,再决定是插入还是跳过。
5.3 数据校验与灰度切换
迁移过程中,数据校验是最容易被低估的环节。数据量大时,不可能把每条数据都拉出来比一次,但完全靠抽样又不放心。我在项目里一般采用两层校验:
第一层是总量校验。统计每个新库的表行数和老库总数做对比,这个数字对不上就说明迁移有明显遗漏。第二层是基于业务特征的抽样校验,取某一连续时间段的订单,按路由要求分别从老库和新库查询,比对关键字段是否一致,比如订单状态、金额、支付时间等。
灰度切换分两步走:先让一部分读流量打到新库,观察业务日志和监控指标,确认没问题后再全量切读。写流量切到新库之后,建议保留一段时间的回退窗口,一旦发现严重问题,还可以把写流量切回老库。回滚能力是迁移安全性的最后一道保险,不要省。
6. 水平分库后的常见坑与排查记录
这块内容是从实际踩坑里长出来的,我按频率排个序,重点说四个高频问题。
6.1 全局主键冲突
分库之后,MySQL自增主键的天然保证失效了。每个库的自增都是从1开始,A库和B库都可能插入ID=1001的记录,主键冲突带到应用层就是严重的脏数据。解决办法是全局唯一ID方案,主流的做法有几种:基于数据库号段模式生成ID、使用分布式ID服务、使用雪花算法生成64位Long型ID。
雪花算法是事实标准。它把64位拆成三部分:时间戳、机器ID、序列号。只要保证每台机器的机器ID不重复,并且同一毫秒内的序列号不溢出,生成的ID就是全局唯一且趋势递增的。趋势递增对数据库索引友好,插入性能稳定,这是我推荐的原因。生产环境要注意正确配置机器ID,很多事故就出在机器ID配重复了,导致同一毫秒内不同节点生成重复ID。
6.2 跨分片查询与事务
分库之后,原本在单库里一条SQL能完成的JOIN、GROUP BY、ORDER BY、COUNT等操作,变成了跨库操作。客户端中间件或代理中间件能做归并,但归并带来的网络开销和内存消耗相当可观。比如全球购的订单表按用户分库,要按商品维度统计销量,就必须从全部分片拉数据再聚合,执行效率远低于单库查询。
我的建议是:在设计阶段就明确划分“单分片查询”和“全分片查询”,尽力让90%以上的核心链路命中单分片,剩下不能命中单分片的查询走独立的汇总表或搜索引擎。这不是偷懒,而是必要的架构妥协。
事务是另一个大坑。数据库分库之前,一个事务里可以同时修改多个表,靠本地事务保证原子性。分库之后,一条事务里的数据可能落在不同的物理库上,本地事务完全没法覆盖。比如转账场景,转出账户在1号库,转入账户在2号库,事务跨越两个库,只能引入分布式事务方案,比如基于消息的最终一致性、事务消息等。但这些方案实现复杂,不是随便接个框架就能搞定的事。业务上如果做不了最终一致,就要在分库键上花功夫,尽量做到同一个用户的钱都落在同一个分片内。
6.3 连接数翻倍与连接池参数调优
在没有分库之前,一个应用连接池配20个连接面对一个库。分库成8个之后,如果连接池还保留扩容前的配置,每个库可能只分到两三个连接,高并发下连接耗尽几乎是必然的。
分库后的核心原则是:连接池总连接数不变,但要把连接数平均分配到多个分片的数据源上。比如原来单库配置20个连接,8个分库后,每个分片配置5个连接,总计40个?不对,这里要仔细说——假设应用同时只会消费20个并发连接,往8个分片分发时,最理想情况是每个分片同时处理的并发数只有个位数,所以每分片配3~5个连接已经足够。反过来,如果每个分片都保持原来的20个上限,总连接数会膨胀到160,MySQL可能压根撑不住。
具体参数要看压测数据,我给一个初始建议:每分片连接数 = 原连接池连接数除以分片数量,再乘以1.5到2倍的冗余。上线之后结合慢查询和连接等待监控再做微调。
常见错误:应用启动时报“Too many connections”,很多时候不是MySQL实例真的撑不住,而是分库之后连接池配置没有跟着缩,导致每个分片的连接总和超过了MySQL的max_connections。排查时先看监控面板里每个分片的活跃连接数,再回头调整连接池参数。
6.4 扩容时的数据搬迁
水平分库最痛苦的运维操作是扩容。取模路由从4扩到8,数据要搬迁;一致性哈希加节点,也要迁移邻接区间数据。很多团队的扩容策略是“不扩容,直接用新库承载新数据”,也就是所谓“加库不迁移”:把新流量路由到新库,老数据留在老库。这个方案对查询逻辑要求极高,因为同一类数据可能分布在老库和新库,查询必须同时路由到新老两侧再归并。看起来省事,实则把复杂度永远留了下来。
我见过比较稳妥的扩容实操,是把翻倍扩容和双写结合起来:先搭建新库并追平老库数据,再通过双写把新写入同时打到新老库,校验数据后切读流量,最后数据稳定后停掉老库写入。这个流程本质上就是一次增量迁移,不涉及停服,是生产环境里最常用的平滑扩容方式。
关于水平分库的坑,能写的还有很多,比如分片内小表冗余、分片内流水号、路由元数据管理、分片键变更等。但抓住上面四个高频问题,基本上能覆盖80%的线上事故场景。
7. 路由表冗余与分片元数据的进阶设计
如果分库范围进一步扩大,还有几个进阶话题值得提前思考,它们不是第一轮上线必须做的,但往往半年后会变成刚性需求。
首先是分片元数据的集中管理。当应用实例数量变多、分片数量变多时,把分片配置写死在每个应用里会变得难以维护。比如今天调整了某个分片的访问权重,难道要重新发布所有应用?实际上不必。可以把分片配置放在配置中心或独立的元数据库里,应用启动时读取,运行时动态刷新。元数据管理还需要考虑分片与数据库实例的映射关系,例如一个物理库可以承载多个分片,调整分片位置时不用改路由公式。
其次是字典表冗余。业务中总有一些被频繁查询但几乎不变的数据,比如商品分类、地区列表、支付通道信息。这类数据如果只存在某一个分片里,其余分片的查询都得跨库查,效率极差。业界常用的办法是“全分片冗余”,把字典表复制一份到每个分片库里。这样每个分片都可以独立完成本地查询。坏处是写操作需要同步到所有分片,但这在字典类低变更频率的数据上完全可接受。
还有一种是引入路由映射表。前面提到过订单场景用用户维度做分片键的情况,那当用户想知道一个他自己的订单明细时,SQL自然带user_id,直接路由没问题。但假如用户A想知道用户B的订单呢?这类查询发生在客服后台、运营报表等场景。这时可以给每个订单冗余记录user_id和order_id的映射,让路由可以通过映射表找到order_id所在的库。这个映射表本身可能也很大,但它的体量通常小于主业务表,可以先分库再冗余,总比每次全分片扫描划算。
8. 我个人在水平分库这条路上的一些体会
做了这么多轮分库分表之后,我最大的感触是:分库分表不是一个技术改造动作,而是一个架构演进过程,它要从业务模型里长出来,不能靠某个中间件强行加持。
第一个体会是“宁愿晚一点,也不要错一点”。如果业务还在早期,数据量没到千万级,单库合理优化加缓存完全够用,没必要提前背上分库的维护成本。但一旦决定要做,一定要在设计期就把分片键选对、路由规则想透、扩容路径预留好。这三个点里任何一个事后返工,都等于推倒重来。
第二个体会是“再成熟的技术,也要自己把关”。中间件能帮你解析SQL、做归并,但你依然要清楚自己写的每一条SQL最终在哪几个分片上执行了哪些操作。我见过太多团队上线中间件之后,SQL执行日志也不看,慢查询也不分析,最后出了问题只能靠DBA一层层翻。分库分表是把复杂度分散了,但它没有消灭复杂度,只是把复杂度从数据库转移到了应用和中间件。
第三个体会是“迁移预案比迁移本身更重要”。不管采用停机迁移还是双写,都要提前准备好回滚方案,明确什么条件下切回老库、回切动作由谁来执行、回切之后增量数据怎么处理。没有回滚预案的迁移,等于在高空踩钢丝不系安全带。
最后分享一个我在实际项目中经常使用的小技巧:不要把分库分表这类底层架构变更直接塞进常规迭代里排期,最好单独开辟一个专项窗口,让核心研发和DBA全程参与,配合灰度发布和演练,尽量选在业务低峰期做切换。上线之后的前两周,每天盯一遍分片监控,包括各分片连接数、慢查询数、数据增长率,任何一个分片数据增长过快都可能是新路由规则引入的热点问题。等业务稳定之后,再逐步把监控项收敛到日常告警阈值。
水平分库这条路走起来确实不轻松,但数据量摆在那里,它是最务实的扩展手段之一。把这套逻辑想清楚、把落地方案做扎实,比盲目追新框架靠谱得多。希望这篇记录能给正在这条路上摸索的同学一些实质性的参考。