1.评估可行性
首先想一想为什么选择进行分库分表?是因为觉得现在数据量这么大,SQL查询太慢了,得优化一下,而且慢SQL,索引优化,表结构优化,数据库参数调优,缓存扛热点读和削峰填谷这些都已经使用过了。
在开始分库分表之前一定要提前分析好!分库分表应该是“最后的大招”,因为带来的复杂度是永久性的。
看几个点:
如果当前没有瓶颈、未来也看不到瓶颈,就不要分库分表了,优先上面的尝试别的操作。
- 分表解决的是单表数据量过大导致的查询慢的问题
- 分库解决的是单机资源瓶颈,把压力分摊到多台服务器
2.设计方案
①定拆分方向
- 垂直分表:按列拆分,宽表变窄,解决是“表太宽”。把一张宽表的列拆开,常用字段放一张表,不常用的大字段单独拎出去。比如用户表拆成user_base存用户名、手机号,user_detail存个人简介、头像这些占空间的字段
- 垂直分库:按照业务边界进行拆分,把一整个库拆分成订单库order_db、用户库user_db、商品库goods_db,通常跟着微服务走,一个服务模块对应一个库。
- 水平分表:同一张表的数据按行拆分到一个库内,解决“单表行数太多”,比如按用户ID取模,user_0、user_1、user_2这样分。每张表结构一模一样,只是数据不同。
- 水平分库:按行拆到多个数据库实例,解决“单库写入/连接数等单机资源瓶颈”。表结构复制到多个数据库实例,数据按规则分散存储。比如电商订单按用户ID哈希到order_db_0、order_db_1、order_db_2三个库
垂直,是从业务的视角来分析的,解决的是“边界混乱”,跟领域划分、微服务拆分绑定。
水平,是从数据视角来分析的,解决的是“量太大”,跟QPS、行数、容量绑定。
实际演进顺序通常是:
垂直分库(随微服务按照业务分库)->垂直分表(优化表结构,把不常用的大字段单拎出去)->水平分表(表太大)->水平分库分表(单库也扛不住)。
②定分片键与分片策略和数量
这个阶段要确定分片键、分片策略和分片数量。
分片键,给每条数据一个“门牌规则”,决定“用哪个字段做路由依据”。这是因为分库分表之后,写入和查询都要先回答“这条数据在哪个库哪张表”。
每张被水平拆分的逻辑表,只能有一条路由规则(通常落在分片键字段上),因为每行是数据的物理住址必须唯一。
通常分片键要选择查询频率高、数据分布均匀的字段,比如电商系统就选user_id/order_id。
分片策略,门牌号的换算公式,决定“怎么把键值换算成表编号”。
主要有以下策略:
- 哈希取模,hash(user_id) % 64->分布最均匀。代价是扩容的时候需要搬数据
- 范围分片,按照时间(比如2026年的订单放这一批表里面),最为方便,好归档,好扩容。代价是热点集中(最新月份的表最忙,老表闲着)
- 查表法,维护一张“哪个值在哪”的映射表,灵活。但是每次路由要多一次查询。
分片数量,是给未来留的位置,建议一步到位。
最好是根据未来的3~5年的预估数据量进行反推。
③定全局唯一ID
单表的时候,主键主要是靠数据库自增。
那么分库分表之后,如果每个库都从1开始自增,出现主键撞车的情况,解决起来比较麻烦,所以需要一个独立于数据库的、全局统一的发号器:
- UUID,应用自己随机生成一个36位字符串。
- 优点:简单,一行代码搞定
- 致命伤:无序。新插入的主键随机落点,导致B+树频繁分裂页(写性能差);36字符太长,做索引又大又慢
- 所以它可以用做唯一标识,但不适合做MySQL主键
- 雪花算法,实际最主流。生成的是一个64位long
[1位符号 | 41位时间戳 | 10位机器ID | 12位序列号]- 同一毫秒、同一机器能发4096个号,理论峰值400万+/秒
- 优点:不依赖数据库,本地内存生成,性能极高。趋势递增,对B+树友好
- 最大的坑:时钟回拨,机器NTP校时回退后可能发出重复号。需要有回拨检测
- 10位机器ID意味着最多1024个节点,需要一套分配机制
- 号段模式(美团Leaf-segment),数据库里面放一张发号表,每次从库里面批量取一段(比如一次领1000个号),应用在内存里面慢慢发,发完了再领下一段。
- 优点:递增、简单可靠、不依赖ZK
- 代价:每次领号有一次DB访问(但批量1000,摊薄后可忽略);双buffer预加载可消除毛刺
- 缺点:严格连续,重启会浪费一段号,这个无所谓,号段会暴露业务量。
- Redis自增,直接INCR一把梭
- 优点:实现最快
- 缺点:持久化策略尴尬,Redis挂了发号就停
- 一般只用于并发不高的小场景
④中间件选型
我们写的SQL不改变,还是 INSERT INTO order … 这种样子,是中间件帮我们做了一些事情。
中间件主要是负责:
主要有以下形态:
- ShardingSphere-JDBC(客户端模式),性能最好但入侵代码,适合Java应用,追求性能。
- 缺点:只支持java,每个微服务都要引入配置,连接数会放大
- ShardingSphere-Proxy(代理模式)。独立部署一个“假MySQL”,应用拿它当普通数据库连,它背后再连真实的16个库。适合多语言,集中管控。
- 语言无关,对应用完全透明
- 改分片规则只改代理的配置,应用零感知
- 代价:每次查询多跳一次网络
- MyCat。独立部署,适用于存量系统,现在社区基本停更,新项目不建议选
还有其他的一些方案【Vitess,自研等】,这里不再展开。
⑤非分片键查询方案
分片键的设计是为了让最频繁的操作都走高速,日常操作尽量都走分片键,但是总会有一些低频的查询支线。
- 基因法
- 解决:按照订单号、支付单号这类“自己生成的单号”查单
- 原理:把路由信息(user_id的基因)提前埋进单号里面,让单号自己“报路”
- 两步走,用映射换算
- 解决:查询条件和user_id一对一的场景(手机号、身份证、邮箱)
- 也就是说先换算成分片键,再拿着分片键进行查询。
- 注意:映射本身要在下单/注册时同步维护。如果映射表没有命中,回源到用户表查一次再回填。
- 异构索引表
- 解决一个新维度对应一堆订单的高频查询(商家查“我的订单”、骑手查“我送的单”)
- 用一个窄表索引型,只存储 [merchant_id | order_id | user_id] 。这样的话查找详细的再拿 user_id 会主表取完整详情就可以。
- 怎么写进去呢?
- 像这种双写主流的做法是异步:
下单 -> 写主表(user_id 分片)->发一条MQ消息->消费者写异构表
或者使用Canal订阅主库binlog,监听到订单插入,自动搬运到异构表
- 像这种双写主流的做法是异步:
- ES异构
- 解决:多维组合查询(运营后台:时间 × 省份 × 类目等等)
主表 ->binlog -> Cancal/Kafka ->ES
- 解决:多维组合查询(运营后台:时间 × 省份 × 类目等等)
- 广播兜底查询:针对日常几乎不会查询的就可以搬出来广播了,直接全表扫描
3.数据库迁移
这是最容易出事的环节。用Canal监听老库binlog做增量同步,用存量脚本只做存量迁移。
迁移完必须做数据校验,行数对不对、关键字段值对不对、业务逻辑跑一遍结果对不对。
4.灰度切换
先切读流量到新库,观察一周没问题再切写流量。
切换期间保持双写,万一新库有问题能快速回滚到老库。
5.一些问题
①迁移过程中怎么保证数据一致性?
关键是增量同步和数据校验。增量用Canal监听binlog,保证老库的每一条变更都能同步到新库。
存量迁移期间可能会有数据被改,所以迁移完要跑一遍全量校验,比对行数、关键字段的MD5、抽样查询业务数据。
校验发现不一致的记录打标记,单独补偿修复。
切流量之前再跑一遍校验,确认没有问题才敢切。
②索引和分片键有什么区别?
| 索引 | 分片键 | |
| 回答的问题 | 这行数据在表内的哪个位置 | 这行数据在哪张表(哪个库) |
| 本质 | 有指路的作用,不移动数据 | 直接决定了数据的物理位置 |
| 没命中的后果 | 全表扫描 | 需要采取一些措施,最差的结果广播所有表 |
③灰度切换期间老库和新库同时写,怎么处理主键冲突?
双写期间主键生成必须用分布式ID,不能用数据库自增。
Snowflake或者Leaf这类方案都行,保证全局唯一。
如果老系统用的是自增主键,迁移前得先把主键生成逻辑改掉,跑一段时间确认没问题再开始迁移。这个改造最好提前几周做,别跟迁移混在一起,出了问题不好定位。



