简介:一份基于Spring Boot的多数据源与分库分表实战代码包,面向需要处理高并发读写、水平拆表扩库的Java后端开发者。项目采用MyBatis-Plus的dynamic-datasource统一管理多数据源,引入Sharding-JDBC完成分库分表,配合Druid连接池监控数据库连接,并通过Lombok简化实体类,整体工程结构清晰,适合作为微服务数据层架构的参考实现。压缩包共162个文件,以115个XML映射文件为主,另有14个Java核心类与14个Class编译产物、6套YML配置声明数据源和分片规则,同时附带JAR依赖、Maven构建脚本、properties与README等文档,包体仅164KB,下载后即可快速导入本地复现,并在测试用例中验证分库分表是否生效。内容预览显示资源内含完整测试用例与示例接口,读者可结合JUnit运行结果观察分片逻辑的执行路径。目前已吸引623人学习下载,适合想深入理解Sharding-JDBC分片策略并需要可运行示例的Spring Boot开发者。
1. 多数据源+分库分表:单库到瓶颈时,别急着上缓存
“多数据源+数据库分库分表”这个组合,解决的不是慢查询优化那一层的问题,而是数据库结构性瓶颈:当核心表数据量到了千万级、连接数被占满、磁盘 IO 和锁竞争把单实例压到极限,加缓存只能延缓,没法治本。多数据源负责让同一个应用按业务域或读写角色连不同的数据库实例,分库分表负责把一张大表按分片键拆到多个物理节点上,两者通常一起出现,因为拆完库之后必然要面对多数据源切换。这套方案适合订单、流水、用户、消息这类持续增长的 OLTP 业务,也适合正在做微服务拆分、需要把老单体里的库按域隔离出去的团队。下面这篇按“先想清楚、再落地、后避坑”的顺序讲透。
2. 分库分表前先做容量评估:分片键、拆分策略与数据源边界
2.1 先别引入框架,把容量账算明白
不少团队是被慢查询逼着上分库分表的,结果框架引入了一堆,分片键随便选个时间字段,三个月后热点分片又把单库打爆。我一般的做法是:动手前先把当前库的真实容量算出来,包括表行数、日增长量、单行平均长度、高峰期 QPS、读写比例,以及连接数峰值。算完之后你会得到一个关键数字——单一 MySQL 实例在业务高峰期到底还能撑多久。
有个粗略但实用的经验值:单表行数超过 2000 万,或者单表容量超过 50GB,或者单实例连接数长期超过 70%,就值得认真考虑分库分表了。这个数字不是绝对红线,MyISAM 和 InnoDB、机械盘和 SSD 的表现差异很大,关键在于“当前增长速度是否会在一个业务周期内击穿单实例上限”。如果按日增 10 万行估算,2000 万行的表只需要半年就到临界点,这时候应该提前规划分片方案,而不是等报警了再熬夜迁移。
算清楚容量之后,还要区分两个概念:分库解决的是连接数和 IO 瓶颈,分表解决的是单表锁竞争和索引深度问题。如果一个实例的连接数还没打满,只是单表查询慢,先做分表就够了;反过来,如果每张表都不大但实例连接被打满,那要优先分库。两者混在一起改,排查问题时变量太多,容易翻车。
2.2 分片键与拆分策略:hash、range、时间分片怎么选
分片键是整个分库分表方案里最不能拍脑袋决定的参数,因为它直接决定数据是否均匀分布、后续扩容要不要重排数据。常见分片键有两类:
- 业务主键类:订单 ID、用户 ID。适合用 hash 取模,数据均匀,但扩容时要变更模数。
- 时间类:创建时间、订单时间。适合用 range 分片,按月或按年切分,天然支持按时间归档,但容易产生热点分区——当天的数据永远集中在最新分片上。
| 策略 | 数据分布 | 扩容难度 | 典型场景 |
|---|---|---|---|
| hash 取模(MOD) | 均匀 | 难,mod 数变化要重分布 | 订单表、用户表按 ID 分 |
| range 范围 | 不均匀,按业务量分布 | 易,新增分区即可 | 日志表、流水表按时间分 |
| 一致性 hash | 较均匀 | 相对易,只迁移少量数据 | 分布式缓存映射到库 |
| 按业务租户分 | 天然隔离 | 中 | SaaS 多租户场景 |
实际项目中,订单这类核心业务表我基本不用纯时间分片,因为热点太集中,下单高峰期所有写请求都打到最后一个月的那几张表上,分片的意义就打了折扣。常见做法是“user_id 或 order_id 取模定库+定表”,时间字段只作为二级索引。如果业务一定要按时间维度查,可以用“用户维度分片 + 时间字段索引”的组合,牺牲一部分跨月查询性能换写入均衡。
分片键还必须是查询条件里稳定出现的字段。如果 SQL 经常不带分片键查询,比如“查最近十分钟所有用户的订单”,这个分片键就不合格,因为无法路由到具体分片,只能全库广播。选分片键前,把线上 TOP 20 高频 SQL 拉出来,确认其中的 WHERE 条件、JOIN 关联字段、事务里的查询路径都落在同一个分片键上,才算过关。
2.3 多数据源的概念边界:异构数据库与同构分片实例
多数据源这个词容易让人误解成“连多个 MySQL 实例”或者“同时连 MySQL 和 Oracle”。按我的理解,多数据源在工程上有两种形态,混着用经常出问题。
第一种是读写分离型多源:同一个业务库,主库负责写,从库负责读,数据源配置里定义 primary 和 replica 两套连接,通过路由在代码层切换。这种“多源”数据是一份,逻辑上仍是同一个库。第二种是业务域隔离型多源:一个应用同时连接订单库、用户库、商品库,甚至同时连 MySQL 和 PostgreSQL、达梦这样的异构数据库。这种“多源”各管一份数据,数据模型和业务边界都不同。
分库分表属于第二类的延伸——拆分后得到的多个分片实例,在应用层看起来是多个数据源,但它们持有的是同一张逻辑表的数据。这里最常见的误区是把它们当成普通的多数据源去手工切换,SQL 路由逻辑散落在业务代码里,时间一长根本维护不动。所以行业里才会出现 ShardingSphere 这类中间件:把分库分表的多源路由收敛在配置层,业务代码尽量无感。下一章先讲不依赖中间件、用 Spring 自身能力实现多数据源的最小方案,把它理解透了你才能看懂那些中间件帮你做了什么。
3. 用 AbstractRoutingDataSource 实现多数据源路由:最小配置与切换规则
3.1 多数据源切换的核心机制:路由发生在拿到连接的那一刻
Spring 里实现多数据源,绕不开 AbstractRoutingDataSource 这个抽象类。它本身不自带任何数据源,而是持有一个 targetDataSources 映射表和一个默认数据源,在对外的 getConnection 被调用时,通过 determineCurrentLookupKey 来决定返回哪个真实数据源的连接。理解这个时机很重要:路由发生在“拿连接”的瞬间,而不是 SQL 执行的瞬间。
这意味着只要你在进入 DAO 或 Service 之前把路由 key 放进一个上下文,数据源切换就能做到对 MyBatis、JdbcTemplate 透明。常见的落地方式是用 ThreadLocal 存当前线程的路由 key,再用一个注解加 AOP 切面在方法调用前设值、调用后清理。事务边界要特别小心,如果 @Transactional 在方法进入时已经拿到了连接,你再切换路由 key 是无效的,这一点到第 5 章展开讲。
3.2 最小配置:application.yml 定义数据源与路由规则
先看一份可用的最小配置。这里以两个订单库分片为例:ds0 和 ds1,都是 MySQL 实例。生产环境连接池我一般用 HikariCP,Spring Boot 2.x 和 3.x 都默认集成,不用额外引依赖。
spring: datasource: hikari: pool-name: HikariCP minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 3000 idle-timeout: 600000 dynamic: # 自定义的扩展配置,不是 Spring Boot 原生配置 datasource: ds0: jdbc-url: jdbc:mysql://192.168.1.10:3306/order_db_0?useSSL=false&serverTimezone=Asia/Shanghai username: order_app password: change_me driver-class-name: com.mysql.cj.jdbc.Driver ds1: jdbc-url: jdbc:mysql://192.168.1.11:3306/order_db_1?useSSL=false&serverTimezone=Asia/Shanghai username: order_app password: change_me driver-class-name: com.mysql.cj.jdbc.Driver注意这里 dynamic.datasource 是我自定义的配置段,Spring Boot 原生并不认识它,需要你写一个配置类去读取。很多团队直接用它配合 baomidou 的 dynamic-datasource-spring-boot-starter,省去自己写配置类。不过我还是建议先自己写一次,理解背后的机制,后面出问题才知道从哪排查。上面的 jdbc-url 是 HikariCP 的标准写法,driver-class-name 对应 MySQL 8 的驱动包。
连接池参数里,maximum-pool-size 是最容易拍脑袋的。单实例 20 个连接看起来不多,但如果你有 8 个数据源,每个默认 20,高峰期就是 160 个连接同时挂在各库上。先按业务并发估算:每个事务占用一个连接,事务平均执行时间 50ms,并发 200 个请求同时进来,理论需要 200×0.05=10 个连接;再算上慢查询和连接等待重试的余量,单库 20 是合理的起步值。
3.3 路由类、注解与 AOP:把切换动作收敛成一行注解
有了数据源定义,下一步是让代码能切换。先写路由 key 的持有者,再写真正的路由器,然后写注解和切面。
public class DataSourceContextHolder { private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>(); public static void setDataSource(String key) { CONTEXT.set(key); } public static String getDataSource() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }ThreadLocal 的作用是隔离线程间的路由状态。这里必须用 remove 而不是简单的 set null,因为 Web 容器会复用线程,如果线程执行完不清理,下一个请求复用这个线程时会读到上一个请求的路由 key,造成数据源错乱。这个坑非常隐蔽,排查一次至少半天。
public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface DataSource { String value() default "ds0"; }@Aspect @Component public class DataSourceAspect { @Around("@annotation(dataSource)") public Object switchDataSource(ProceedingJoinPoint joinPoint, DataSource dataSource) throws Throwable { DataSourceContextHolder.setDataSource(dataSource.value()); try { return joinPoint.proceed(); } finally { DataSourceContextHolder.clear(); } } }切面的作用域放在 finally 里清理,保证即使业务方法抛异常,路由状态也能复位。注解可以直接标在 Service 方法上:@DataSource("ds1"),底层 DAO 执行 SQL 时会自动拿到 ds1 的连接。如果方法上没有注解,AbstractRoutingDataSource 会回落到默认数据源,所以一定要在配置里指定默认源。
@Bean public DataSource dataSource() { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("ds0", buildDataSource("ds0")); targetDataSources.put("ds1", buildDataSource("ds1")); DynamicDataSource dynamicDataSource = new DynamicDataSource(); dynamicDataSource.setTargetDataSources(targetDataSources); dynamicDataSource.setDefaultTargetDataSource(targetDataSources.get("ds0")); return dynamicDataSource; }默认数据源建议选只读压力最小的库,或者统一指向一个专门用于兜底的实例。很多团队默认指向分片 0,结果所有没标注解的 SQL 全打到 ds0,时间一长 ds0 的连接和 IO 比 ds1 高一大截,从监控上看像是数据倾斜,其实是默认路由设计疏忽。
3.3 读写分离场景下的路由规则:key 不只有数据源名,还可以是角色
多数据源如果是为了读写分离,路由 key 的定义思路就不同了。此时不再是“订单走 ds0,用户走 ds1”的业务域逻辑,而是“读走 slave,写走 master”。常见做法是在路由 key 里带上读写角色:query 开头的方法走从库,save/update/delete 开头的方法走主库。
这里有两个容易踩的细节。一是主从延迟,刚写入的数据立即读可能读不到,业务上对强一致有要求的场景,必须走主库,或者用“先写主库再查主库”的兜底逻辑。二是从库之间要负载均衡,AbstractRoutingDataSource 本身只支持按 key 选择一个源,你需要在路由上下文里自己做轮询或随机分配,比如用 AtomicLong 计数取模。更省事的做法是换成熟中间件,但理解原理之后你会发现,这些框架做的事就是这套模型的工程化封装。
4. ShardingSphere 做分库分表:从分片算法到全局表的一步步配置
4.1 选 ShardingSphere-JDBC 还是 Proxy:两种形态的取舍
分库分表到了落地阶段,几乎没人还用手写路由+手工拼接表名。业界最常见的方案是 ShardingSphere,它有两种形态:ShardingSphere-JDBC 以 jar 包方式嵌入应用,SQL 经过它解析、改写、路由、执行,对应用来说像连了一个普通数据源;ShardingSphere-Proxy 则是一个独立服务,应用通过 MySQL 协议连它,它再把 SQL 分发到底层分片。
| 对比项 | ShardingSphere-JDBC | ShardingSphere-Proxy |
|---|---|---|
| 部署方式 | 嵌入应用进程 | 独立进程 |
| 性能损耗 | 较低,本地解析 | 多一跳网络,损耗略高 |
| 改造成本 | 改数据源配置即可 | 改连接地址,兼容任意语言 |
| 运维复杂度 | 随应用发布 | 独立部署与升级 |
| 适用场景 | Java 应用、原生日志 | 多语言混部、需要集中管控 |
我一般建议 Java 服务优先用 JDBC 形态,理由只有一个:少一个故障节点。Proxy 在管控能力和多语言接入上有优势,但生产环境里 Proxy 自身的连接管理和 SQL 解析性能也是要额外维护的成本。如果你的团队连 MySQL 主从都还没理清,先别上 Proxy,把 JDBC 形态跑通再说。
4.2 分片配置实例:按用户 ID 哈希拆成 2 库 × 4 表
用 Spring Boot + ShardingSphere-JDBC 5.x 的 YAML 配置做实例,目标是实现:用户订单表 t_order 按 user_id 取模,分成 2 个库(ds0、ds1),每库 4 张表(t_order_0 到 t_order_3),共 8 张物理表。
spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.10:3306/order_db_0 username: order_app password: change_me ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.11:3306/order_db_1 username: order_app password: change_me rules: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order_$->{0..3} database-strategy: standard: sharding-column: user_id sharding-algorithm-name: db_mod table-strategy: standard: sharding-column: user_id sharding-algorithm-name: table_mod key-generate-strategy: column: order_id key-generator-name: snowflake sharding-algorithms: db_mod: type: MOD props: sharding-count: 2 table_mod: type: MOD props: sharding-count: 4 key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1 props: sql-show: trueactual-data-nodes 这一段是关键,它用表达式声明了 8 张物理表的完整位置。database-strategy 里的 sharding-column 是 user_id,算法名指向 db_mod,算法类型 MOD、取模数 2,意思是 user_id % 2 决定进哪个库;table_strategy 同理,user_id % 4 决定进哪张表。库和表都用同一个分片键,保证同一条订单数据在路由时能精确落到唯一的物理表。
为什么要用 MOD 而不是 hash 后再模?MOD 的实现简单直接,适用于分片键本身就是数字的场景。如果分片键是字符串,比如用户手机号或 UUID,通常先对字符串做 hash 再取模,否则字符串转数字后取模的分布性很难保证。ShardingSphere 的 HASH_MOD 算法内部会先用 MD5 计算 hash 再做取模,字符串分片键我建议直接用它。
key-generate-strategy 里的 snowflake 是分库分表绕不开的环节:数据库自增主键在分片环境下无法保证全局唯一,业务主键建议用分布式 ID。雪花算法生成的 ID 是 64 位长整型,包含时间戳、机器位、序号,趋势递增且全局唯一。要注意的是,雪花 ID 作为表主键没问题,但作为分片键时别拿它取模——时间戳高位的影响会让取模分布产生倾斜。
4.3 全局表与绑定表:分片后 JOIN 查询的两个关键配置
分库分表之后,SQL 的 JOIN 查询会遇到两种情况,需要单独配置才能正常路由。
第一种是广播表(全局表),比如省份、字典、商品类目这类数据量小、几乎不更新的表。它们不需要分片,每个库里都放一份全量数据。配置上把它声明为 broadcast-table,查询时 ShardingSphere 会在当前路由到的库内直接 JOIN,不会因为表不在当前分片里而报错。代价是每次更新这个表时,所有分片都要同步更新,所以这类表必须严格控制更新频率。
第二种是绑定表,指的是分片键完全一致、分片规则也一致的两张业务表。比如 t_order 和 t_order_item,两张表都按 user_id 分片,那么 user_id=100 的订单和它的明细一定在同一个物理库、同一张逻辑表上。把两者配置成 binding-tables 后,JOIN 查询会强制在两个分片规则对应的表之间做关联,不会出现跨库 JOIN。
rules: sharding: binding-tables: - t_order,t_order_item broadcast-tables: - t_dict_province这两个配置的价值体现在查询性能上:没有绑定关系时,ShardingSphere 只能做笛卡尔积式路由——把 order 的 8 个分片和 order_item 的 8 个分片两两组合,理论上要查 64 组;配置绑定后,只查 user_id 对应模数一致的 8 组(order_0 配 item_0,order_1 配 item_1……)。业务表建模时如果主表、明细表分片键不一致,分库分表后所有关联查询都是灾难,所以库表设计阶段就要强制这一点。这也解释了为什么“先设计好分片规则再拆库”比“拆完再补规则”靠谱得多。
4.4 跨分片查询与分布式事务:能规避就规避,不能规避再上中间件
分库分表后,不带分片键的查询会退化成全分片广播,性能取决于最慢的那个分片,这是物理规律,任何中间件都优化不了。常见的解决思路是“分片键找不到,就再建一个索引表”。比如订单表按 user_id 分片,但运营后台要按商户 merchant_id 查订单,怎么办?单独建一张 merchant_order_index 表,字段是 merchant_id 和 order_id,这张表也分片,分片键换成 merchant_id。查询时先查索引表拿到 order_id,再用 order_id 对应的 user_id 回订单表。这是以额外的存储换取路由确定性。
跨分片事务则是另一个复杂话题。分片后一个事务可能涉及多个库,普通事务的 ACID 语义无法跨库保证。轻量业务可以用本地消息表+最终一致性方案;强一致场景需要引入类似 SEATA 的分布式事务框架。这里不展开细节,只提醒一句:分库分表后事务范围会被物理拆散,排查问题时要习惯把“分布式事务未提交/回滚失败”列入怀疑列表。
5. 多数据源与分库分表的坑清单:路由、事务、DDL 与连接池
5.1 分片字段没进 WHERE,SQL 被广播到全部分片
现象:一条单表查询平时 20ms,分片后偶尔 400ms 甚至超时;慢日志里同一个 SQL 出现在所有分片节点上。
原因:分片键没有出现在 WHERE 条件里,ShardingSphere 无法推断目标分片,只能把 SQL 下发到所有物理表,然后合并结果。比如 t_order 按 user_id 分片,你按 order_id 精确查询,这条 SQL 只能全分片扫描。
解决:两条路——要么把查询条件改写成分片键+业务键组合(先查出 user_id 再回表),要么像上面说的建一张按 order_id 分片的索引映射表。开发规范上应当强制规定:访问分片表的 SQL 必须携带分片键,上线前用 sql-show 打开路由日志,人工抽查 SQL 的路由结果。
5.2 多数据源下事务失效,回滚只回滚了默认数据源
现象:方法上标了 @DataSource("ds1") 和 @Transactional,业务在 ds1 上写入成功,但后面的异常回滚后,ds1 的数据还是写进去了。
原因:Spring 事务管理器在事务开始时就从数据源上下文拿连接。如果事务切面先于数据源切面执行,连接在路由 key 设置之前就被绑定到了默认库,你的 @DataSource 注解晚了,事务连接已经和默认数据源绑死。
解决:控制切面执行顺序,让数据源切面的 order 值小于事务切面的 order 值。用 @Order 或 @AutoConfigureOrder 显式声明:DataSourceAspect 的 order 设为 0,Spring 事务切面的默认 order 是 Integer.MAX_VALUE 附近,前者先执行,路由 key 先设置,事务再拿连接就能拿到正确的数据源。这种时序问题在单数据源项目里根本不存在,多源改造后务必检查切面顺序。
5.3 分片后修改表结构:一次 DDL 发布要逐库逐表执行
现象:发布脚本只对 order_db_0.t_order_0 执行了 ALTER TABLE,业务流量被路由到 order_db_1.t_order_3 时直接报 column not found;或者在主库改了表结构,从库没同步,引发随机性报错。
原因:分库分表后,逻辑表背后是 N 个物理表,研发习惯性地把单库的 DDL 直接粘贴执行,脑子里的表模型还是“一张表”。
解决:改成脚本化 DDL,遍历所有物理节点执行,执行完做一次 schema 差异校验。常见做法是把数据源地址、分片规则写进一个变量表,用 Python 或 Bash 脚本遍历连接,逐个执行同一个 DDL,然后在所有节点上跑 information_schema.COLUMNS 查询,比对字段版本。发布清单里必须包含这个校验步骤,不能只靠“执行成功”就宣称发布完成。
5.4 连接池给了默认值,多数据源叠加后线程池被打满
现象:业务高峰期应用日志频繁出现 connectionTimeout、HikariPool 连接获取超时,但数据库侧负载并不高,CPU 和 IO 都很空闲。
原因:每个数据源都按单库经验配置了 maximum-pool-size=20,8 个分片就是 160 个连接。如果线程池核心线程数是 50,每个线程的一次调用只持有 1 个连接,理论上够;但如果有嵌套事务——一个 Service 里调多个 DAO,每个 DAO 又切了不同的数据源,连接会被占更多,50 个线程请求 160 个连接的池子也会挤爆。
解决:连接池参数必须按“数据源数量同时被一个线程占用的最大可能值”来估算。事务方法里如果会串行访问 ds0 和 ds1,那么一个线程最多占 2 个连接,核心线程数×单线程最大占用连接数就是连接池下限。还要注意 ShardingSphere-JDBC 下,连接是逻辑连接对应的物理连接,路由到 8 张表时可能同时打开 8 个物理连接,这个场景要单独压测确认。
5.5 扩容与数据迁移:hash 分片不是一锤子买卖
现象:分片数定成 2 库×4 表后,业务半年涨了一倍,需要扩容到 4 库×8 表,结果发现老数据全要按新模数重算位置,迁移期间写入和查询互相打架。
原因:MOD 取模分片天然和分片数强绑定,mod 从 2 变成 4,老数据的归属全部变化,必须全量重分布。这是方案设计阶段留下的债,很多团队把分片数当成静态配置,等到扩容时才发现是“数据重洗”级别的工程。
解决:设计阶段就预留分片余量,按未来三年的数据量定分片数,一次性给足比如 32 库×64 表,避免 1 年后扩容。如果一定要扩容,用“双倍数扩容”或一致性 hash 的方案,只迁移部分数据。上线前把扩容演练加入应急预案,别把首次扩容放在生产事故当天做。
6. 分片验证与灰度迁移:用 EXPLAIN 看路由,用双写保回滚
6.1 用 EXPLAIN 验证路由结果:确认 SQL 落在哪张物理表
ShardingSphere 的 sql-show 日志能看到改写后的 SQL,但生产环境不会一直开着。跑分片验证时,我习惯直接把 sql-show 打开,在测试环境对每个核心 SQL 执行 EXPLAIN,确认改写后的 SQL 落到预期物理表。
EXPLAIN SELECT * FROM t_order WHERE user_id = 100 AND order_id = 20250701001;ShardingSphere 会把这段 SQL 改写为:
SELECT * FROM t_order_0 WHERE user_id = 100 AND order_id = 20250701001;只要 user_id=100 时 user_id % 2=0、user_id % 4=0,路由到 ds0.t_order_0 就是正确的。这类验证要和自动化测试绑定,不然每个分片键的余数组合都要人工算一遍,漏掉user_id=101这个奇数分片就会在线上翻车。
6.2 迁移上线的顺序:影子表双写、对账、再切读
分库分表上线不推荐“一把梭”,我见过最稳的顺序是:先在新分片上建立影子表,应用侧写双份——老单库写一份、新分片写一份,跑一段时间对账,确认两边数据一致,再切读流量,最后把老库降级只读。双写期间的失败要记录到补偿队列,定时任务把失败数据重新同步到新分片。对账脚本按主键分批拉数据,比对行数和关键字段的 hash,这一步不能省,因为“新表结构和旧表完全一致”这个假设往往在字段默认值、时区转换上破功。
6.3 切流后的 watch 清单
切到分片后的前三天,重点盯着这几个指标:分片间数据增长是否均匀、各库连接数是否有单个被压满、慢查询日志里有没有“无分片键广播”的 SQL。踩坑记录里最常见的还是 5.1——历史代码里藏着一条没带分片键的报表查询,平时压测压不到,凌晨跑批时把所有分片全部打满。我会把这类 SQL 汇总成黑名单,在监控里设置单独告警,一冒头就有人处理。
分库分表这个方向,真正考验人的不是配置写法,而是对数据分布和路由时序的敏感度。我早期交过的学费都集中在 ThreadLocal 清理和事务切面顺序这些“代码之外的细节”上,只能说多数据源这种设计,天然把复杂度从数据库转移到了应用层。希望这份从评估到落地的路径能帮你在动手前把账算清楚,上线后少熬几个夜。
本文还有配套的精品资源,点击获取