MySQL数据库的分库分表实战
理论概览和业界方案对比之后这一篇我们来聚焦可复制的代码级实战核心框架选用ShardingSphere-JDBC 5.x Spring Boot从依赖、配置、分片策略到数据迁移全流程覆盖。分库分表的理论概述为什么需要分库分表当单表数据量达到千万级或者单库的并发连接数、磁盘IO达到上限时会出现明显的性能下降。分表解决单表数据量大导致的查询慢问题。分库解决单个数据库实例的并发连接数、磁盘IO和CPU负载瓶颈。需要注意的是分库分表是最后的手段务必先尝试索引优化、读写分离、缓存等手段。二、核心拆分方式主要分为垂直拆分和水平拆分两种。1. 垂直拆分按业务拆垂直分库按业务模块将表拆分到不同数据库比如把订单、商品、用户分别放在独立的库里。作用微服务架构的基础解决了单一数据库的连接和IO争用问题。垂直分表将一张宽表按字段拆成多张表比如把访问频率低的字段如商品详情单独放在一张表。作用减少单行数据大小降低磁盘IO开销。2. 水平拆分按数据拆—— 最核心分表将同一张表的数据按某种规则如哈希、范围分散到多张同结构的表中。分库将数据分散到多个数据库实例中。作用解决单表数据量和单库写性能的瓶颈。常用的分片算法算法做法优点缺点哈希取模shard_key % 库/表数量数据分布均匀不易出现热点扩容时数据迁移量大需要翻倍扩容或一致性哈希范围划分按时间、ID区间如第1个月的在表1利于范围查询扩容简单加新表容易产生“热点”数据新数据集中在最新表一致性哈希构造哈希环确定数据位置扩容影响小只需迁移少量数据实现复杂需要维护虚拟节点基因法将分片键的一部分编码到其他字段中支持通过非分片键查询路由需要额外字段和代码逻辑分库分表面临的三大核心难题1. 分布式ID问题分表后无法依赖数据库自增ID需要全局唯一的ID。解决方案雪花算法(Snowflake)最常用的方案64位长整数趋势递增ID不重复。Leaf美团区段号段模式需要搭建服务。UUID不推荐占用空间大且无序导致索引频繁分裂。2. 跨库关联查询Join问题原来一条SQL能Join多张表现在数据在不同数据库无法直接Join。解决方案字段冗余把常用字段如用户名直接存到订单表中避免关联查询。全局表对于一些字典表、配置表在每一个数据库中都保存一份完整副本。应用层组装先查A库的结果再根据结果去B库查询最后在代码中组装。数据异构使用Elasticsearch等搜索引擎构建宽表索引。3. 跨节点分页、排序、聚合问题order by、limit、group by需要从多个节点拉取数据后在中间件中归并。解决方案尽量让查询都带上分片键。例如查询用户订单时带上user_id作为条件这样可以直接定位到具体的库。对于无分片键的复杂排序应用层或中间件需要做归并排序这可能带来很大的性能消耗。4. 分布式事务操作跨多个数据库时需要保证数据的一致性。解决方案最终一致性强推荐使用可靠消息队列如RocketMQ实现最终一致性。Seata提供AT模式自动补偿和TCC模式手动编码根据业务对性能的要求选择。实际执行路径评估与规划预估未来3-5年的数据量和QPS合理设置分片数。分片数建议设置为 2 的 N 次方便于扩容。选型中间件ShardingSphere-JDBC目前最主流作为轻量级的Jar包嵌入应用适合Java项目。MyCat独立的代理中间件支持多语言但多一层网络开销。制定分片键这是最重要的一步。分片键需要能覆盖80%以上核心查询场景比如订单表通常选user_id。确定扩容策略双写迁移不停机迁移。同时写旧库和新库数据核对无误后切读流量到新库最后停写旧库。停机迁移在业务低峰期公告停机导出旧库数据清洗后导入新分片。分库分表是一个业务逻辑侵入性很强的架构调整。最重要的原则是少用慎用如果要用早期规划好分片键和分片数量。一旦实施很多SQL操作会受到限制所以分库分表通常会配合能力下沉——也就是把复杂的查询、聚合交给Elasticsearch或大数据平台去做。如果你正在规划方案建议先画出业务核心流程图确定最关键的分片键。需要我帮你分析一下具体的业务场景吗一个实践参数参考MySQL / InnoDB单表数据量超过500万-1000万或占用空间超过10GB就可以考虑分表。单库实例的表总数据量超过200GB-300GB或磁盘IO达到上限可以考虑分库。TDSQL / OceanBase这类分布式数据库虽然声称支持PB级数据但达到TB级别时也建议提前规划分区或拆分。总之一、依赖配置与核心版本版本选择直接决定配置语法能否跑通。以下是经过多家公司生产验证的稳定组合parentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion3.2.5/version/parentpropertiesshardingsphere.version5.5.2/shardingsphere.versionmybatis-plus.version3.5.15/mybatis-plus.version/propertiesdependencies!-- ShardingSphere-JDBC Starter核心依赖 --dependencygroupIdorg.apache.shardingsphere/groupIdartifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactIdversion${shardingsphere.version}/version/dependency!-- 数据库驱动和连接池 --dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdscoperuntime/scope/dependencydependencygroupIdcom.zaxxer/groupIdartifactIdHikariCP/artifactId/dependency!-- 与 MyBatis-Plus 的组合使用 --dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-spring-boot3-starter/artifactIdversion${mybatis-plus.version}/version/dependency/dependencies版本避坑Spring Boot 3.x 必须使用 ShardingSphere 5.3 版本且需兼容 Jakarta EEjakarta包名而非javax。千万不要回退到 4.x 版本否则配置语法完全不兼容会走很多弯路。二、库表准备先建物理表分库分表运行时ShardingSphere 不负责自动建表物理库表必须提前手动创建且同一张逻辑表对应的所有物理表结构必须完全一致。-- 1. 创建分库假设分2个库CREATEDATABASEIFNOTEXISTSorder_db_0;CREATEDATABASEIFNOTEXISTSorder_db_1;-- 2. 在每个库中创建物理表每个库2张表共4张表USEorder_db_0;CREATETABLEt_order_0(idbigintNOTNULLCOMMENT订单主键分布式ID,order_novarchar(64)NOTNULLCOMMENT订单编号,user_idbigintNOTNULLCOMMENT用户ID分片键,order_amountdecimal(10,2)DEFAULTNULLCOMMENT订单金额,create_timedatetimeDEFAULTCURRENT_TIMESTAMPCOMMENT创建时间,PRIMARYKEY(id))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT订单表;CREATETABLEt_order_1LIKEt_order_0;-- 3. 同样在 order_db_1 中执行上面两条建表语句USEorder_db_1;CREATETABLEt_order_0LIKEorder_db_0.t_order_0;CREATETABLEt_order_1LIKEorder_db_0.t_order_0;库表命名规范物理表名须遵循统一模式以便 ShardingSphere 通过表达式自动路由。如t_order_$-{0..1}表示t_order_0、t_order_1两张表。另外如果原表缺少关键索引分表时务必补上。有团队在实际分表过程中发现原表更新时间字段没有索引分表时增加了该索引显著提升了查询效率。三、核心配置实战application.yml以下配置实现水平分库 水平分表的混合分片方案user_id决定数据落在哪个库order_id决定数据落在库内的哪张表。spring:shardingsphere:datasource:names:ds0,ds1ds0:type:com.zaxxer.hikari.HikariDataSourcedriver-class-name:com.mysql.cj.jdbc.Driverjdbc-url:jdbc:mysql://localhost:3306/order_db_0?useSSLfalseserverTimezoneUTCusername:rootpassword:123456ds1:type:com.zaxxer.hikari.HikariDataSourcedriver-class-name:com.mysql.cj.jdbc.Driverjdbc-url:jdbc:mysql://localhost:3306/order_db_1?useSSLfalseserverTimezoneUTCusername:rootpassword:123456rules:sharding:tables:t_order:# 逻辑表名代码中操作的表名actual-data-nodes:ds$-{0..1}.t_order_$-{0..1}# 物理节点映射2库 × 2表 4个节点# 分库策略按 user_id 取模database-strategy:standard:sharding-column:user_idsharding-algorithm-name:db_inline# 分表策略按 order_id 取模table-strategy:standard:sharding-column:order_idsharding-algorithm-name:table_inline# 分片算法定义sharding-algorithms:db_inline:type:INLINEprops:algorithm-expression:ds$-{user_id % 2}table_inline:type:INLINEprops:algorithm-expression:t_order_$-{order_id % 2}props:sql-show:true# 开发环境打印改写后的 SQL生产环境建议关闭配置的底层逻辑是当你执行INSERT INTO t_order (user_id, order_id, ...) VALUES (...)时ShardingSphere 会根据user_id的哈希值把数据分配到ds0或ds1库再根据order_id的哈希值分配到该库下的t_order_0或t_order_1表中。代码层完全无感知依然操作逻辑表t_orderMyBatis-Plus 等 ORM 框架像平常一样使用即可。四、分片键选择策略决定 80% 的查询性能分片键是分库分表最关键的决策需要遵循以下核心原则原则说明好例子坏例子高频查询80% 以上的查询应携带该字段订单表选user_id选低频字段高离散性字段值分布均匀避免数据倾斜user_idstatus枚举值少稳定性字段值不随业务频繁变更user_id手机号业务相关性经常一起出现的关联表应共享同一分片键订单表 订单明细表共用order_id—实战中的经典方案用户订单系统采用user_id分库 order_id分表的混合策略按user_id分库保证同一用户的所有订单落在同一数据库避免按用户查询时的跨库扫描。按order_id分表在库内均匀分布数据到多张物理表解决单表数据量过大的问题。采用user_id作为分库键后按用户查询订单的 SQLWHERE user_id xxx能够精准路由到单一库性能大幅提升。五、分布式 ID 生成解决跨库主键唯一性分库分表后数据库自增 ID 在不同库中会重复必须使用分布式 ID 生成方案。ShardingSphere-JDBC 提供了多种内置策略最推荐雪花算法Snowflakespring:shardingsphere:rules:sharding:tables:t_order:# 配置主键生成策略key-generate-strategy:column:id# 主键字段名key-generator-name:snowflake# 使用的生成器名称# 主键生成器定义key-generators:snowflake:type:SNOWFLAKEprops:worker-id:1# 工作节点 ID分布式环境下各节点不同max-vibration-offset:0# 关闭抖动保证严格递增其中雪花算法生成的ID是一个64位的长整型Long对应Java中的long在数据库中占用8字节。它的每一位都有明确的含义整体分为符号位1bit 时间戳41bit 机器ID10bit 序列号12bit。Snowflake 生成的 64 位 Long 型 ID 由时间戳、工作机器 ID、序列号三部分组成全局唯一且趋势递增既满足唯一性也对数据库 B 树索引友好。高级技巧——基因分片对于订单系统这类同时需要按user_id和order_id查询的场景可将分库基因嵌入到 Snowflake ID 中。生成订单 ID 时把user_id的哈希值编码到订单 ID 的特定 bit 位后续通过订单 ID 也能直接计算出分片位置无需二次查表定位。六、跨节点分页查询分页归并的深层原理分页查询ORDER BY ... LIMIT m, n在分库分表场景下会演变成复杂的多结果集归并问题理解其原理对性能优化至关重要。核心原理图解原始 SQL: SELECT * FROM t_order ORDER BY create_time LIMIT 5, 10 ↓ SQL 解析与改写 ↓ ┌───────────┼───────────┐ ↓ ↓ ↓ ds0.t_order_0 ds0.t_order_1 ds1.t_order_0 ds1.t_order_1 ↓ ↓ ↓ ↓ 返回 15 条 返回 15 条 返回 15 条 返回 15 条 └───────────┴───────────┴───────────┘ ↓ 归并排序 ↓ 取合并后的第 5-15 条 → 最终返回 10 条改写原理当 SQL 包含LIMIT 5, 10时ShardingSphere 会将5加上10向每个分片下发LIMIT 0, 15的查询从每个分片拉取 15 条数据。然后在应用层进行归并排序从 4 个分片共 60 条数据中筛选出目标 10 条。随着分页深度增加如LIMIT 10000, 10ShardingSphere 会向每个分片下发LIMIT 0, 10010性能急剧下降。实战优化策略场景推荐方案必须带分片键查询条件中确保包含user_idSQL 可路由到单一分片完全避免归并深度分页改用游标分页WHERE id last_id ORDER BY id LIMIT n搜索引擎兜底将复杂查询接入 Elasticsearch构建宽表索引数据异构将需要多表关联的冷数据同步至 ClickHouse 等 OLAP 引擎七、数据迁移不停机方案从单库单表迁移到分库分表关键挑战是在不影响业务的情况下完成过渡。业界最成熟的方案是双写 灰度切换。实施步骤概览准备阶段 → 双写阶段 → 历史数据迁移 → 数据校验 → 灰度切流 → 停用旧库具体实施方案准备阶段设计好分库分表方案分片键、分片规则创建新库表结构同时修改应用程序代码写入数据时同时写旧库和新库但读请求仍从旧库进行。历史数据迁移使用数据同步工具如 DataX、Canal、Percona XtraBackup将存量数据按分片规则批量导入新库。如果新库规模较大推荐使用支持断点续传和限流的分布式同步工具。数据校验与灰度切流确认新旧库数据一致后按比例将读流量从旧库切换到新库建议灰度比例 1% → 10% → 50% → 100%。停用旧库观察一段时间无异常后终止双写逻辑正式下线旧库。避坑提醒双写阶段可能引入分布式事务问题——两个数据库的一致性如何保证生产实践中通常采用最终一致性方案写旧库成功后通过消息队列异步写入新库配合定时任务扫描补偿缺失数据。务必在切换前做全量数据对账这是上线前最后一道也是最重要的一道关卡。八、分片数量设定过度分片不仅没有实际收益还会带来运维复杂度和性能损耗。以下是行业参考值指标建议阈值单表行数500万 ~ 1000万行分片总数总数据量 ÷ 500万向上取整单库分片数推荐 8、16、32、64 等 2 的 N 次方便于扩容总分片数上限建议不超过 256过度分片会导致跨分片查询归并开销增大九、生产环境踩坑清单以下是在多个生产项目中踩过的坑值得提前关注问题类别典型问题解决方案分片键缺失WHERE条件不带分片键SQL 全路由扫全表监控慢 SQL强制核心查询带上分片键分布式事务跨库写操作不一致能用最终一致性就别用 XA性能影响极大主键生成错误数据库表主键设置了AUTO_INCREMENT与 Snowflake 冲突物理表主键字段不要设置AUTO_INCREMENTJDBC URL 参数带rewriteBatchedStatementstrue时批量插入可能路由异常分片表慎用该参数建议单独验证版本兼容性Spring Boot 2.x 和 3.x 依赖冲突严格使用匹配版本见第二节版本表格十、最佳实践建议尽早规划但不必过早实施当单表达到 2000 万行且仍有高速增长时启动分库分表预案最为适宜。优先通过优化避免分片不要为了分片而分片。在数据量可控的范围内索引优化、读写分离、冷热数据分离往往是成本更低的解决方案。分片键就是兵家必争之地选错分片键分库分表就是白做。订单系统用user_id分库、order_id分表的混合策略是最经典的选择。从简单方案起步初期能只分表不分库就不要一开始就分库分表架构越复杂维护成本越高。配置即文档将分片规则分片算法、actual-data-nodes映射表等以配置文件形式版本化管理形成团队知识沉淀。配合props.sql-show: true观察 SQL 路由情况及时发现异常。如果你正在规划某个具体业务模块的分库分表可以把你的核心查询场景和数据增长预估发出来我帮你评估分片方案是否合理。