1. 先别急着分库分表:什么时候才真的需要这个方案
分库分表这个话题,这几年几乎成了后端技术讨论的"标配"。很多团队一聊到数据库性能问题,第一反应就是"我们是不是该分库分表了"。但以我做过的几个实际项目来看,分库分表是最不应该被当成"银弹"的方案——它是一个一旦上了就回不了头的架构决策,复杂度是永久性的。
先聊聊什么情况下才真的需要考虑分库分表。数据库出现瓶颈,通常分两种表现:一种是读写并发太高,数据库连接被打满,CPU和IO持续飙升;另一种是单表数据量太大,索引变深,查询变慢,写入也变慢。前者往往可以通过加缓存、读写分离、引入消息队列削峰来解决,这部分优化性价比高,实施成本也低。后者才是分库分表真正的用武之地。
那"数据量大"有没有一个可以量化的判断标准?我在实际工作中一般会盯几个指标:单表行数超过2000万到3000万(这个数字和表的字段数量、索引数量强相关,不是绝对的);单表占用空间超过50GB;日常查询响应时间明显劣化,即使已经做了索引优化和SQL改写也救不回来。这里有个容易被忽视的点——如果你的表只有几个字段、索引也不多,5000万行可能依然跑得飞快;反之如果一张表有几十个字段、十来个索引,可能1000万行就已经开始卡了。所以不要迷信网上流传的"千万级数据分库分表"这种说法,判断依据应该是你的实际业务场景。
另外,在考虑分库分表之前,有几条路一定要先走一遍:
- 加缓存。热点数据的读压力,用 Redis 兜住,通常能扛掉80%以上的读流量。
- 归档冷数据。订单、日志、操作记录这类有时间属性的数据,按时间维度把历史数据搬到归档表或冷存储,主表数据量立刻降下来。
- 读写分离。主库只有写流量,读流量全部走只读从库。
- SQL 优化。把慢查询捞出来,看看是不是索引没建对、是不是查询条件里写了函数导致索引失效。
我遇到过不止一次的情况是——团队嚷嚷着要分库分表,结果一查慢查询日志,发现就是几个 SQL 没走索引,或者前端分页接口一次性拉了全量数据。这种问题不解决,就算分了32个库,依然会慢。
那什么时候才算"真的需要"?我的判断标准很简单:上面这些常规手段都已经用过了,数据量还在涨,单表的性能瓶颈已经影响核心业务,且未来三到五年的数据增长趋势可以预期。这时候,分库分表才应该被提上日程。过早动手是给自己埋雷,过晚动手是把自己逼到墙角(后面数据量大了再迁移,成本成倍增加)。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 拆分方向的选择:垂直拆分和水平拆分到底怎么定
很多人以为分库分表就是"一个表拆成多张表",其实不完整。分库分表是分库和分表的组合,又分为垂直拆分和水平拆分。这四种方向解决的问题完全不同,设计阶段得先把这个想清楚。
先说垂直拆分。垂直分库指的是按业务域把数据库拆开,比如把用户信息、订单信息、商品信息、支付流水分别放到独立的库里管理。这种拆分解决的是"多个业务模块互相抢占数据库资源"的问题。比如电商系统,高峰期订单模块写入量巨大,如果它和商品模块、用户模块共用一个库,其他模块的查询也会被拖慢。拆开后各管各的,互不干扰。垂直分表则是把一张字段特别多的表,按字段拆开,把更新频繁的字段和不常访问的大字段分开。比如商品表,可以把基本属性、价格库存、详细描述(可能包含很长的富文本)拆成多张表,然后通过主键关联。
水平拆分解决的是另一个问题——单表数据量过大。水平分表是把同一张表的数据,按照某种规则分散到多张结构完全相同的表里,每张表只包含一部分数据。比如订单表按订单号取模,拆成 order_0、order_1、order_2……order_15 这16张表。水平分库也是同样的思路,只是把数据分散到不同的库实例上。相比分表,分库还能分摊CPU、内存、磁盘和网络IO的压力,所以效果通常更明显。
实际项目中,这几招经常是组合使用的。以订单系统为例,我比较推荐的演进路径是:
- 初期数据量不大,订单表就放主库,跟用户、商品放一起。
- 业务量上来后,先把订单单独拆到独立的订单库(垂直分库),避免订单的写入高峰拖垮其他模块。
- 订单数据继续增长,单表超过千万级别,再按用户维度做水平分库分表,比如拆32个库,每库64张表。
- 如果订单里还有需要长时间保留的历史流水,再按时间维度做冷热分离,把超过一定时限的订单归档到历史库。
这个路径的关键是"匹配业务发展阶段"。别在第一阶段就幻想一步到位把32库256表整出来——架构复杂度会直接拖垮研发效率,很多中小团队就是死在过度设计上。
补充一句,垂直拆分相对温和,不存在路由计算的问题;水平拆分才是后期复杂度的大头,分片键、路由算法、数据迁移、跨片查询,都是从这里开始引入的。所以决策的时候,优先做垂直拆分,水平拆分能晚点就晚点。
3. 分片键选不好,后面全是坑
如果说分库分表有什么决定成败的环节,分片键(sharding key)的选型绝对排第一。这一步选错,后面所有努力都白费,而且越到后面越难改。分片键决定了每条数据进哪张表、进哪个库,也是查询时计算路由的依据。一个好的分片键,需要满足几个条件:能被高频查询条件命中、数据分布均匀、尽量避免跨分片查询。
拿订单表来举例。这是最常见的业务表,也是分库分表最典型的场景。订单表有两个天然候选字段:订单号(order_id)和用户ID(user_id)。
如果按订单号分片,比如 order_id % 16,那么当用户要查"我最近一个月的订单列表"时,问题就来了——你没法从用户ID直接算出订单号在哪个分片,于是只能把请求广播到所有16张表,再把结果合并。订单量小的时候还好,量一大,这个"广播查询"就是噩梦,每一次用户打开"我的订单"页面,背后都是一次全分片扫描。
如果按用户ID分片,比如 user_id % 16,用户查询自己的订单时,可以直接定位到唯一分片,一条SQL搞定。代价是,按订单号查单条订单详情时(比如从订单列表点进详情页、或者支付回调要根据订单号更新订单状态),需要额外维护一个"订单号到用户ID"的映射关系,或者在订单号里直接编码用户ID信息。
我倾向于按用户ID来分片,原因很简单——电商场景下,"用户查自己的订单"是最高频的查询;而"根据订单号查单条订单"虽然也很多,但单条查询即使走广播或者走映射表,成本也远低于高频的列表查询。
这里有一个实战中很实用的技巧:如果订单号本身是雪花算法生成的,它里面其实可以编码一部分用户ID的信息(比如把生成规则设计为"用户ID的低位 + 时间戳 + 序列号"),这样依然可以按订单号算路由,同时也能按用户ID算路由,一箭双雕。
分片键选型还有一个很容易被忽略的维度:数据分布均匀性。如果你的分片键选了一个性别字段(只有两个值),那不管怎么分,都只会落到两个分片上——这等于没分。我见过有人按区域ID分片,结果大部分订单都集中在华东地区,其他分片几乎空闲,最终还是要返工。选分片键之前,务必要做一次数据分布分析:把现有数据的候选字段跑一遍统计,看看每个取值区间的数据量是否接近均匀。这一步不能省。
总结一下分片键选型的几个坑,都是我见过或踩过的:
- 选了更新频率很高的字段,导致分片键变更时要搬数据。
- 选了区分度很低的字段,导致数据倾斜,部分分片过热。
- 选了查询条件里不常出现的字段,导致大量查询需要广播。
- 忽略了分片键变更场景——比如用户换绑手机号,如果用手机号分片,那这个用户的所有订单都要迁移到新分片,复杂度直接爆炸。
所以我的建议是:优先选用户ID或租户ID这种天然具备归属关系、且不会经常变更的字段,查询条件能带上它,路由计算也简单。
4. 路由算法与容量规划:mod、range/时间段,各有各的代价
分片键定下来之后,下一步是设计路由算法——就是说,一条数据到底应该落到哪个库、哪张表。主流的路由算法就三种:取模(mod)、范围(range)、一致性哈希(consistent hashing)。这三种各有明显的特点,选错一个也会埋雷。
取模是应用最广泛的方案。库序号 = hash(sharding_key) % 库数量,表序号 = hash(sharding_key) / 库数量 % 表数量 或者直接 % 表数量。优点是:实现简单、数据分布足够均匀、查询时直接计算,性能开销极低。缺点是:扩容极其痛苦。你原来16个库,数据快满了要扩到32个库,那么原来 user_id % 16 的数据全部要重新计算路由,迁移量几乎是全量数据。老数据迁移期间还要处理双写、校验、回滚的问题,方案设计稍有不慎就是事故。
范围路由是另一种思路。比如按用户ID区间分片:1号库存1到1000万的用户,2号库存1000万到2000万,以此类推。也可以按时间分片:订单表按月分,每个月一张表。优点是:扩容简单,新数据写入新表就行;范围查询(比如查某个月的所有订单)可以精确定位到具体表,不用跨片。缺点是:数据分布天然不均匀。按月分片,大促月份的订单量可能是平时十倍;按用户区间分,老用户的活跃度可能远低于新用户,导致部分库空闲、部分库过热。
一致性哈希结合了前面两种方案的优点。它对分片键做哈希后映射到一个环形空间,每个物理分片在环上占据一段弧,数据沿着环顺时针找到最近的物理分片。扩容时只需要把环上部分数据迁移到新节点,不需要全量迁移。但实现复杂度高,而且要处理虚拟节点(否则分片不均匀),对中小团队来说运维成本不低。
从实战角度,我个人的偏好是:如果业务量是可预期增长的,优先用 mod 加足够的冗余(比如初期就规划好 32 库 128 表),避免中途扩容;如果数据有明显的时间热特性(比如日志、流水),用 range 更省心;一致性哈希适合节点数量频繁变动的场景,但绝大多数业务系统用不上。
容量规划这件事,和路由算法的选择强相关。规划的时候不能只看当前数据量,要按未来三到五年的增长量来倒推。一个简单的估算公式可以这样算:
code复制预估总数据量 = 当前数据量 × (1 + 年均增长率) ^ 年限
单分片建议数据量 ≤ 500万行
分片总数 = ceil(预估总数据量 / 单分片建议数据量)
假设当前订单表有800万行,年均增长率80%,看三年后的量:800万 × 1.8^3 ≈ 4665万行。按单分片500万行算,大概需要10个分片。考虑到写放大和预留空间,我一般会把分片数放大4倍,也就是规划到 32 或者 64 个分片——反正mod路由只要分片数是2的幂,后续在运维层面还有操作空间。很多人贪省事直接128个分片,实际上分片太多会导致跨片查询变多、线程资源浪费、连接数翻倍,反而不是好事。
4.1 表数量与库数量的搭配
分片总数确定后,库和表的搭配也有讲究。一般原则是:单库的表数量不要太少,否则库的存储和连接资源没有充分利用;也不要太多,否则单库的并发压力又回来了。常见的配比是每个库16到64张表,比如 32库 × 64表 = 2048 个分片,基本可以覆盖绝大多数中大型业务。库和表的分片数量都取2的幂有个额外好处:如果之后要做数据迁移或二次分片,按位与的计算比取模更快,而且逻辑更清晰。
5. 分库分表之后,那些"原本不是问题的问题"
分库分表的真正挑战,不是拆分本身,而是拆分之后带来的连锁问题。这些问题在设计阶段没有提前想好方案,上线后就会被业务方追着打。
分布式ID。 单库时一张表用自增主键就够了,分库分表后自增ID在各个分片会重复,必须引入全局唯一ID。业界主流方案就几个:UUID(纯字符串,太长,索引性能差,不推荐做主键);雪花算法(64位长整型,由时间戳、机器ID、序列号组成,趋势递增);号段模式(数据库生成一批连续ID发放给应用,应用内存中分配)。我通常用雪花算法的变种,因为它是数字类型、趋势递增、性能高,而且可以在里面编码业务信息(比如前面说的编码用户ID),方便做路由。要注意的是,雪花算法强依赖机器时钟,时钟回拨会导致ID重复,部署时要做好时钟同步(NTP)和回拨补偿机制。我还见过有团队用 Redis 的 INCR 生成ID,但Redis 本身挂了会出问题,不太推荐作为唯一方案。
跨库 JOIN。 这个最头疼。单库时一条 JOIN 就能查出来的数据,分库分表后可能要跨多个库去取,基本没法在数据库层面完成。常见的替代方案有三个:
- 冗余字段。比如订单表里冗余存储用户昵称、商品名称,查询时就不用再去用户库、商品库关联。用空间换时间,实际业务里最常用。
- 应用层组装。先查订单分片,拿到用户ID集合,再去用户库查出用户信息,最后在应用里合并。查询次数增加,但每次查询都是简单查询,性能可控。
- 宽表或数仓。如果做BI或报表,可以把从库数据同步到分析型数据库(如 ClickHouse),在分析库做 JOIN,不影响在线业务。
分布式事务。 分库分表后,一个操作可能涉及多个库的数据变更,比如下单要同时扣库存、减余额、写订单,原来在一个数据库里可以用本地事务保证原子性,现在跨库了怎么办?强一致方案是两阶段提交(XA),但性能和可用性都很差,互联网业务很少直接用。业界用的多的是柔性事务:可靠消息最终一致性、TCC(Try-Confirm-Cancel)、本地消息表。实际项目中,我会先看业务能不能接受最终一致,能接受就简单很多——发事务消息,下游消费;如果业务确实需要强一致,看能不能通过表设计或流程合并尽量减少跨库事务,实在不行再上TCC。
排序和分页。 这个也是隐性坑。分库分表之前,一条 ORDER BY create_time DESC LIMIT 10 就搞定了;分库分表之后,每个分片各返回10条,然后要在应用层做归并排序,最后才能拿到全局前10条。这里有两个注意点:一是分页越深越慢,因为每个分片都要把对应页的数据捞出来再合并。LIMIT 10000, 10 这种深分页在分库分表场景下是性能灾难,建议改成游标分页(记录上一次查询的最后一条数据的排序值);二是排序字段如果不在分片键上,所有分片都得参与排序,这点要提前和产品打好招呼,常用排序组合最好能和分片键带上关系。
唯一性约束。 原来单表可以建唯一索引保证"同一用户对同一商品只能有一条评价",分库分表后唯一性只能在分片内部保证,跨分片的唯一性需要靠应用层或额外表来控制。这个在表结构设计阶段就得评估,否则后续数据不一致了,排查成本极高。
我在项目里总结了一套应对这些连锁问题的原则:能用冗余解决的,不要用join;能用最终一致解决的,不要追求强一致;能接受游标分页的,不要碰深分页。很多时候,业务方在了解技术限制之后,是愿意调整产品方案的——怕的是开发自己不主动暴露问题,硬扛着用错误的方式实现,最后线上出了问题才知道要返工。
6. 数据倾斜:分片序号很均匀,流量不一定均匀
前面说过,选分片键时要看数据分布是否均匀。但这里有个更隐蔽的问题:即使数据在分片间均匀分布,流量也不一定均匀。比如电商订单表按用户ID分片,用户ID取模后分布是均匀的,但头部用户一个晚上产生的订单量,可能超过尾部几万个用户的总和。于是某些分片被高频访问,其他分片却很闲。这就是数据倾斜/热点问题。
应对数据倾斜,策略有两个方向:
加随机因子。 如果热点数据是写入层面的,给分片键拼接一个随机后缀,把压力分散到多个分片上。比如某用户写入量大,写入时生成 user_id + "_0" 到 user_id + "_9",这样该用户的数据被分散到10个分片。缺点是查询这个用户的数据时需要聚合多个分片,要额外存储映射关系。
热点冗余。 如果热点数据是读取层面的,可以把热点数据复制到所有分片,直接缩短查询路径。比如爆款商品的详情和库存,本来只在一个分片上,访问量大导致该分片CPU告警;可以把这份数据冗余到其他分片,读请求从任意一个分片都能取到,再通过异步机制保证各分片数据一致性。
这里也想提醒一个认知误区:很多人以为分库分表只解决容量问题,实际上它同时也要解决并发和热点问题。所以规划分片数的时候,除了看数据量,还要按峰值QPS估算一下单分片能不能扛住。假设你的订单系统峰值QPS是5万,规划了32个库,理想情况下单库压力是1562 QPS,如果每个库的容量上限是3000 QPS,那32库是够的;如果估算出来单库需要扛5000 QPS,就要增加库数或者用缓存削峰。
7. 上线迁移怎么做才不丢数据
分库分表方案设计好了,代码也写完了,最后一道坎是平滑迁移。这是整个项目里最容易出事故的环节。从单库迁移到多库多表,通常有三种策略,我按风险从低到高排一下。
停机迁移。 在低峰期(比如凌晨),停服,写一个脚本把旧库的数据按新的分片规则全量灌入新库,同步完成后切换数据源,然后开服。优点是简单可靠,数据一致性几乎不可能出问题;缺点是业务不可用,一般只适合允许短时间停机维护的系统。我做过的大多数项目,业务方其实都能接受十几分钟的停机维护,只要提前通告就行——很多团队非要搞不停机迁移,反而把自己搞得精神崩溃,我觉得没必要。
双写迁移。 适用于完全不能停机的场景。核心思路是:新旧库并行写入一段时间,存量数据平滑搬迁,最后切换。具体流程是这样的:
- 在旧库基础上,部署新库(分库分表后的库)。
- 代码改造,写入时同时写旧库和新库(双写),新库写入失败不影响旧库主流程。
- 把旧库存量数据按新分片规则分批导入新库。
- 写一个对账程序,比对旧库和新库的数据,找出差异并补偿。
- 观察一段时间的双写数据一致性,稳定后把读流量切到新库(灰度切流)。
- 确认没问题,去掉双写,彻底切到新库。
这里有很多细节值得展开。双写这个阶段,写旧库成功、写新库失败的情况会频繁发生,不能简单重试(可能重试导致新库重复写入)。我当时是引入了一个"补偿队列",新库写失败的请求(包括新增、更新、删除)先记录到本地消息表,后台任务不断扫描重放,并且写操作要尽量做幂等(比如带唯一业务键)。这个补偿机制是整个迁移的兜底,一定要在迁移前就测试好。
binlog 同步迁移。 双写的代码改造成本比较大,另一种思路是通过订阅旧库的 binlog,把数据变更解析后写入新库。这个方案几乎不影响业务代码,但需要引入一套数据同步组件(比如 Canal),还需要处理 DDL 变更、主键冲突、同步延迟等问题。我只有在新旧库表结构差异特别大、业务代码没法低成本改造(比如老系统已经是别人的代码了)的情况下才会选这个方案。
不管用哪一种迁移方案,灰度发布都是必须的。最稳妥的做法是:先让1%的读流量走新库,观察一段时间,确认无误后逐步提升到5%、20%、50%、100%。一旦某一步发现异常,立刻把流量切回旧库,保证业务不受伤。
我之前遇到过一次迁移事故,就是跳过灰度、直接全量切流量,结果分页查询因为归并逻辑有bug,接口超时率飙到40%。当时灰度的话,在5%流量阶段就能发现,完全不需要搞得那么惊险。
8. 技术选型和配套治理:中间件不是万能的
分库分表方案的落地,离不开具体的中间件或者框架。目前主流的选择有两个方向:客户端式的库(ShardingSphere-JDBC)和代理式的库(ShardingSphere-Proxy),以及已经不太活跃的 MyCat。这里我用 ShardingSphere 作为代表讲讲。
ShardingSphere-JDBC 是嵌在应用里的jar包,应用自己连接分片数据库,数据源的管理、SQL解析路由都在应用层完成。优点是性能好(没有额外的网络跳转)、支持度好(重构版文档清晰、活跃度高);缺点是每个语言都要有对应的实现(实际主要就是Java用的多,其他语言支持较弱)、升级版本时应用要重新发布。
ShardingSphere-Proxy 是一个独立的代理服务,应用连它就和连普通的 MySQL 一样,它在后端把请求路由到实际的分片库。优点是使用方无感知,很容易用现有的 MySQL 客户端或 BI 工具直接连;缺点是每一条SQL都要经过代理转发,多一层网络开销,性能有损耗,高并发下代理节点本身会成为瓶颈。
在实际项目中,我建议一个很务实的选型思路:新建的 Java 项目优先考虑 ShardingSphere-JDBC,因为它侵入性小、部署简单、性能好;如果想兼容异构系统(比如 Kafka 连接器、BI 工具),可以考虑部署一个 Proxy 做透明路由。
但是选中间件之前,要先想清楚一个更重要的问题:你要的是分片能力,还是治理能力。很多团队引入中间件之后,以为万事大吉,实际上真正耗精力的是配套治理:
- 数据迁移工具:无论用哪种方案,线上迁移都要有对应的工具链(双写、补偿、核对脚本)。
- 监控告警:要能实时看到每个分片的连接数、QPS、慢查询、主从延迟。很多分片出现热点,第一个发现者往往是监控系统而不是DBA。
- 分片元数据管理:分片规则、分片键、表结构变更,这些要有统一的配置中心管理,不能散落在各个应用的配置项里。表结构变更(DDL)也要走一套审核流程,确保所有分片都同步执行。
- 调用全链路梳理:分库分表后,应用之间互相调用时,要保证"同一用户的数据链路尽量落在同一分片",这就要求上下游系统使用相同的分片键和分片规则,不然联表查询和事务就会开倒车。
没有这些配套治理,分库分表带来的不是可扩展性,而是事故频发。我见过一个大项目,分片规则写了三份(应用里一份、迁移脚本一份、文档一份),三份规则对不上,迁移期间对账程序跑出几万条差异,最后靠人工逐条核对才收场。所以,分库分表从设计到落地的全过程,一定要把规则当作代码来管理,建立唯一的规则源,所有脚本和应用都从这个源生成配置。
9. 写在最后:分库分表是一次"架构体检"
按照分库分表的实战经验,最后分享几个我个人的体会。
第一,分库分表不是数据库问题的终点。数据拆分之后,你会发现真正成为瓶颈的从数据库变成了别的——可能是应用层的归并逻辑,可能是消息队列的积压,也可能是缓存的一致性处理。不要以为做了分库分表就一劳永逸,它只是把系统的容量天花板抬高了,不是消除了天花板。
第二,分片规则的演进要有预案。业务从初期到成熟期,分片规则很可能要调整(比如先按用户ID分片,之后想额外支持企业维度查询)。上生产之前,要设计好分片规则的版本管理机制,数据搬迁工具也要保留,以备将来二次分片、扩容、或分片键调整。
第三,如果想少走弯路,最有效的方式是让有经验的团队在早期介入做架构评审。分库分表这个领域,技术方案本身不难,难的是躲坑。很多坑是经验性问题,看再多的文档不如听踩过坑的人说一遍。我写这篇文章,也是希望把我在实战中总结的这些经验分享出来——至少,下次再有人跟你提"分库分表解决一切"的时候,你能多问一句:先讲讲你的分片键是什么,跨分片查询怎么办,数据迁移打算怎么弄。
如果这三个问题对方答不上来,那你们需要的可能不是分库分表,而是一次更彻底的需求梳理和架构体检。
