
做后端这行的迟早会撞上数据量暴涨这堵墙。我在去年接手了一个订单系统核心订单表三个月就冲到了1600万行写入高峰时期主库IO直接被打满慢查询日志里全是扫几百万行的恐怖记录。当时摆在我面前的无非那几条路换更强的物理机、上读写分离、或者咬牙做分库分表。权衡了一周之后我们选了Apache ShardingSphere中的ShardingSphere-JDBC落地方式就是一条yml配置加少量代码改造。整篇文章我会把这次改造的完整链路——从核心概念到配置逐行拆解从业务代码适配到生产环境踩坑——一次性写清楚给正在分库分表门口犹豫或者已经卡在yml配置里起不来的同学一份能直接抄的作业。1. 分库分表这件事为什么绕不开ShardingSphere1.1 单表数据量膨胀后的真实困境很多团队对分库分表的第一反应是太重了先加索引、再上缓存。这个判断在小规模时正确但数据量到了千万级以后你会看到几个很现实的问题。第一索引的收益在递减。一棵B树的层级随着数据量增长会从3层涨到4层、5层磁盘IO次数随之增加。就算命中了二级索引回表和页分裂的开销也压不住高并发写入第二写入瓶颈在单机上是有物理上限的你不可能靠换一台服务器无限堆IOPS第三备份和DDL成本失控一张千万行的大表做一次在线表结构变更生成的临时表可能比你业务高峰期跑的任务还重。所以当数据量还在增长曲线上又没有归档策略兜底的时候分库分表不是要不要做的问题而是哪天做的问题。ShardingSphere做的就是把这件复杂的事从业务代码里抽离出去让开发者面对逻辑表写SQL底层路由到真实的物理库表。1.2 ShardingSphere-JDBC和ShardingSphere-Proxy怎么选Apache ShardingSphere提供两种形态很多刚接触的人会在这个选择上纠结很久。ShardingSphere-JDBC是一个jar包直接嵌入你的应用进程使用。你的应用和MySQL之间多了一层轻量的分片引擎SQL带着分片条件进来它负责解析、路由、改写、归并然后把结果返回。这种模式不需要额外部署中间件性能开销主要在应用进程内单机QPS损耗在可接受范围内。ShardingSphere-Proxy则是独立部署的代理服务模拟一个MySQL服务端应用通过普通客户端驱动连上它就行。好处是不用改应用代码坏处是多一次网络跳转延迟会有损耗另外需要单独维护一套代理集群。我基于实际经验给的参考维度维度ShardingSphere-JDBCShardingSphere-Proxy部署形态嵌入应用进程独立中间件集群代码侵入需要引入依赖并适配数据源应用无感知性能损耗较低进程内路由有额外网络开销协议兼容仅支持Java应用任意语言客户端运维成本随应用发版单独维护和扩容如果你们的团队主要是Java技术栈、对延迟敏感、希望分片逻辑跟随应用版本一起发布我建议直接用ShardingSphere-JDBC。我们生产环境就是这种形态单实例支撑了几千QPS的核心订单链路非常稳。1.3 为什么yml配置模式最流行ShardingSphere-JDBC支持Java API配置、Spring Boot Starter配置、YAML配置三种方式。早期版本大量人用Java配置类代码里全是Config Bean看起来是类型安全了但改一个分片算法就得重新编译发版非常不灵活。yml配置模式的核心优势是配置与代码分离。数据源、分片算法、分片策略全部沉淀在application.yml里调整分片表达式只需要改配置、重启应用不用动一行Java代码。再加上yml天然具备层次结构和可读性团队之间的配置评审也直观。像我们这次订单表的分片键、广播表清单、雪花ID生成策略一目了然哪怕后续交接给新同学半小时就能看懂整个分片拓扑。2. 动手配置前先把这些概念吃透直接贴一份yml配置不难但如果不懂背后的概念配置错了根本没法排查。我先把分库分表里最核心的几个概念讲清楚。2.1 逻辑表、物理表与真实数据节点这是所有分片配置的基础。逻辑表是你在业务代码里看到的表名比如t_order物理表是实际落在MySQL里的表比如t_order_0、t_order_1。ShardingSphere做的事就是让SQL里的t_order在运行期被改写为具体物理表的名字然后再去执行。真实数据节点用一句话就能表达分片拓扑ds$-{0..1}.t_order$-{0..1}这行表达式表示数据源ds0和ds1下各有一张t_order_0和t_order_1一共2库×2表4个物理分片。$-{...}是行表达式语法里面可以写0..1这种范围也可以写0,2这种枚举。我第一次配置时就在这上面栽过跟头写成ds$-{0..1}.t_order$-{0,0,1,1}虽然YAML校验能过但运行期实际产生的节点列表会翻倍初始化时多建了重复的实际表映射。记住行表达式一定要能准确表达你真实的库表分布别为了偷懒写重复项。2.2 分片键与分片算法不是随便选个字段就行分片键决定了数据被路由到哪个分片。它必须满足一个铁律查询条件里几乎必然出现且分布足够均匀。我们选择了订单号order_id作为分片键因为所有订单维度的查询都会带上订单号天然规避了全路由问题。ShardingSphere 5.x内置了很多分片算法我常用和推荐的是这三种INLINE表达式算法配置简单通过Groovy表达式计算分片结果适合取模、哈希、枚举等规则MOD取模算法按分片键对分片总数取模是最经典的做法HASH_MOD哈希取模算法先对分片键做哈希再取模解决字符串等分片键分布不均的问题。订单号是字符串但底层已经转成了Snowflake生成的长整型所以直接用order_id % 2取模就够了。表达式写法如下sharding-algorithms: db_inline: type: INLINE props: algorithm-expression: ds$-{order_id % 2} table_inline: type: INLINE props: algorithm-expression: t_order_$-{order_id % 2}这里的order_id不是Java变量而是SQL结果归并后的元数据列名。ShardingSphere会根据SQL里的分片键值动态代入计算。注意如果SQL里没有分片键的条件INLINE算法默认会直接报错我在后面踩坑部分会专门讲。2.3 绑定表、广播表和分布式主键这三个概念很多人容易忽略但它们往往决定了分库分表之后的查询正确性。绑定表是具备一致分片规则的两张或多张表比如t_order和t_order_item它们的分片键都是order_id且分片策略一致。配置绑定关系以后两张表在做JOIN时会被路由到同一个分片内完成避免跨库JOIN的生猛操作。广播表是那些数据量小、但每个分片都需要完整副本的表比如配置表、字典表。路由时ShardingSphere会把操作广播到所有分片执行查询时再合并结果。我们系统里的t_config就配成了广播表完全避免了跨库关联的问题。分布式主键是分库分表之后另一个容易翻车的地方。单体库里一张表可以靠自增主键过得很好分片后如果用自增ID两个分片很可能生成重复主键。ShardingSphere提供了内置的雪花算法Snowflake主键生成器只需要在yml里声明一个key-generator然后给对应表挂上即可。这样生成的ID全局唯一、趋势递增非常适合作为分片键也天然规避了分布式环境下的主键冲突。3. 基于yml的ShardingSphere分库分表全配置实录3.1 环境准备依赖和版本选型先说版本的事。ShardingSphere的配置结构在5.x和4.x之间有不小的变化4.x的spring.shardingsphere.sharding在5.x里被改成了spring.shardingsphere.rules.sharding。如果你在网上随手搜到一篇旧教程硬套启动阶段就会报一堆找不到配置属性的错误。我们项目用的是Spring Boot 2.7.xJDK 8ShardingSphere对应选了5.3.2版本。Maven依赖只需要一个Starterdependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.3.2/version /dependency这里有个容易踩的坑这个Starter会接管spring.datasource相关的自动配置所以你不能再往yml里写spring.datasource.druid一类的普通数据源配置也不要继续用SpringBootApplication(exclude DataSourceAutoConfiguration.class)去排除数据源配置否则ShardingSphere的数据源注入会失败。正确姿势是把所有数据源配置全部放在spring.shardingsphere.datasource节点下完全交由其管理。数据库方面我用的是MySQL 5.7驱动用的com.mysql.cj.jdbc.Driver。初始化时先建好两个数据库每个库里建两张订单表表结构如下CREATE DATABASE IF NOT EXISTS ds0; CREATE DATABASE IF NOT EXISTS ds1; -- 在ds0和ds1中分别执行 CREATE TABLE t_order_0 ( order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(10,2), status TINYINT, create_time DATETIME, PRIMARY KEY(order_id) ) ENGINEInnoDB; CREATE TABLE t_order_1 LIKE t_order_0;如果是初次实验不想建表也可以在yml里配置spring.shardingsphere.rules.sharding.tables.t_order.actual-data-nodes指向真实节点后加入初始化SQL节点但生产环境我强烈建议用专门的数据库迁移工具建表不要依赖应用启动时建表。3.2 完整配置逐行拆解数据源、分片规则、算法下面是一份经过生产验证的完整application.yml配置片段。我把注释写在每段后面方便对照。spring: shardingsphere: # 数据源声明 datasource: names: ds0, ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/ds0?useSSLfalseserverTimezoneAsia/ShanghairewriteBatchedStatementstrue username: root password: root ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/ds1?useSSLfalseserverTimezoneAsia/ShanghairewriteBatchedStatementstrue username: root password: root # 分片规则 rules: sharding: tables: t_order: # 真实数据节点2个库 * 2张表 4个物理分片 actual-data-nodes: ds$-{0..1}.t_order$-{0..1} # 库分片策略 database-strategy: standard: sharding-column: order_id sharding-algorithm-name: db_inline # 表分片策略 table-strategy: standard: sharding-column: order_id sharding-algorithm-name: table_inline # 分布式主键策略 key-generate-strategy: column: order_id key-generator-name: order_snowflake t_order_item: actual-data-nodes: ds$-{0..1}.t_order_item$-{0..1} database-strategy: standard: sharding-column: order_id sharding-algorithm-name: db_inline table-strategy: standard: sharding-column: order_id sharding-algorithm-name: table_inline key-generate-strategy: column: order_id key-generator-name: order_snowflake # 绑定表保证订单表与订单明细表JOIN时分片一致 binding-tables: - t_order, t_order_item # 广播表配置表在每个分片都保留完整副本 broadcast-tables: - t_config # 分片算法定义 sharding-algorithms: db_inline: type: INLINE props: algorithm-expression: ds$-{order_id % 2} table_inline: type: INLINE props: algorithm-expression: t_order_$-{order_id % 2} # 主键生成器 key-generators: order_snowflake: type: SNOWFLAKE # 运行参数 props: # 打印路由结果和改写SQL排查问题必开 sql-show: truenames下面声明了两个数据源ds0和ds1它们对应真实数据库实例。这里要注意ds0、ds1只是路由标识不是数据库连接串里的库名。我的习惯是让数据源标识和实际库名保持一致这样后续排查时看日志就能直接对应上。database-strategy和table-strategy都配了standard类型对应标准分片策略。它的核心是以sharding-column指定的列作为分片键用sharding-algorithm-name引用的算法计算分片目标。这个引用关系属于定义-使用分离yml里的算法挂在sharding-algorithms节点下策略通过名字引用改算法时不需要动策略定义。关于分片键的类型还有个小细节ShardingSphere里标准策略的分片键解析依赖SQL语法中的字段类型。所以order_id列在表结构里是BIGINT在INLINE表达式中做取模运算时就会按Long处理。如果分片键是字符串表达式里直接做hashCode() % n之类的运算需要确认JVM的哈希算法分布是否足够均匀。3.3 多实例部署时的yml动态端口技巧配置写到这里我想顺带提一个Spring Boot和yml常见的小技巧它和分库分表没什么直接关系但你在部署多个ShardingSphere-JDBC应用实例时很可能会用到随机端口。比如我们在本地联调时需要同时起多个实例或者生产环境用Sidecar模式部署多个消费者进程如果每个实例都去手改server.port很容易冲突。这时yml里可以这样写server: port: ${random.int[10000,20000]}Spring Boot会用随机数生成一个10000到20000之间的端口每个实例启动时自动分配基本不会撞。配合注册中心做服务发现其他服务通过服务名调用完全不关心具体端口。这个技巧看着简单但确实能省掉不少多实例部署时的琐碎操作。回到正题。上面的配置保存后启动应用正常情况下控制台会打印ShardingSphere初始化的数据节点拓扑包括识别到了几个物理分片、加载了哪些分片算法。这时候再执行一条带order_id条件的SQLsql-show: true会把解析后的逻辑SQL和改写后的实际SQL都打出来你可以清楚地看到t_order被改写成了t_order_1并且路由到了ds1。4. 业务代码接入与读写链路的关键设计配置完成只是第一步。分库分表后业务代码的接入方式和链路设计同样需要重新审视。下面这几个点都是我在改造过程中实际踩过或重点评估过的。4.1 数据源切换了业务却无感知路由如何生效很多同事刚接触ShardingSphere-JDBC时会问我代码里用的是DataSource和MyBatis分库分表之后是不是要改很多SQL答案是不需要改SQL只需要保证分片键在条件里。ShardingSphere-JDBC内部通过实现DataSource接口对业务层完全透明。你的SqlSessionFactory在初始化时拿到的就是ShardingSphere包装后的DataSource。一条INSERT INTO t_order ...进来后它会做下面几步SQL解析识别语句类型、表名、条件里的分片键路由根据分片算法算出目标库和目标表SQL改写把逻辑表名替换成物理表名t_order变成t_order_1ds0变成真实连接执行与结果归并必要时跨分片拉取数据后归并比如全局COUNT、SUM、AVG等聚合函数。所以业务代码里照常写Insert(INSERT INTO t_order(order_id, user_id, amount, status, create_time) VALUES(#{orderId}, #{userId}, #{amount}, #{status}, #{createTime})) int insertOrder(Order order);唯一需要注意的是插入时order_id不能依赖数据库自增。因为分片键需要先在业务侧生成Snowflake主键已经在yml里挂到了key-generate-strategyShardingSphere拦截到INSERT时发现主键为空会自动调用雪花算法填充。但为了保险我建议在应用代码里显式生成ID再传入避免某些边缘路径下主键生成时机不可控。查询侧也一样。SELECT * FROM t_order WHERE order_id 123456这条SQL会被精确路由到唯一的分片性能在没有分片之前一模一样。但如果SQL里没有order_id条件比如SELECT * FROM t_order WHERE user_id 100ShardingSphere就会执行一次全路由把这条SQL广播到所有分片执行再合并结果。全路由不是不能用但它意味着每一笔查询都打了4次库并发一高就会放大压力所以在设计查询接口时我强烈建议尽量让分片键出现在查询条件中。4.2 分页、排序与关联查询在分片下的变化分库分表之后分页查询是最容易让用户产生数据不对感觉的地方。假设逻辑表t_order有4个物理分片你在代码里写SELECT * FROM t_order ORDER BY create_time DESC LIMIT 10 OFFSET 40;ShardingSphere会在每个分片上执行SELECT * FROM t_order_0 ORDER BY create_time DESC LIMIT 10 OFFSET 50;也就是把OFFSET LIMIT的总和作为每个分片的偏移量然后把4个分片返回的50条数据全部拉到中间层按create_time排序后再跳过前40条最终返回第41到50条。这个做法在语义上是正确的但offset越大内存消耗越可观。假设用户翻到第10000页就意味着每个分片要传回数万条数据再在应用进程里做大排序极易引发Full GC。处理办法无非三种限制最大页码深度超过某个深度就引导用户用下一页游标翻页用WHERE create_time ?配合LIMIT做游标分页让分片键或排序键参与条件裁剪如果排序键就是分片键ShardingSphere能更好地裁剪分片但ORDER BY create_time这种跨分片排序属于硬成本。关联查询方面绑定表配置生效后t_order JOIN t_order_item ON t_order.order_id t_order_item.order_id会被改写为同一分片内的JOIN性能基本和单库一致。但如果JOIN的两张表没有绑定关系且JOIN键不是相同的分片键ShardingSphere只能走全路由把数据拉到中间层做内存JOIN这种SQL一定要在压测阶段尽早发现并改写。4.3 分布式事务选型XA还是柔性事务分库分表之后跨库事务是绕不开的话题。一张订单主表和订单明细表如果落在了不同分片本地事务已经无法保证原子性。我们系统初期压测时发现偶发情况下会出现主表插入成功但明细表失败数据对不上账。这就是典型的跨分片事务问题。ShardingSphere 5.x提供了两种事务模式XA事务强一致通过XA协议协调多个数据源保证跨库事务的原子性。实现上基于Atomikos或Narayana性能比单库事务有损耗但适合对一致性要求极高的金融类链路SEATA柔性事务最终一致基于AT模式或TCC牺牲强一致性换取性能。适合订单状态可以异步对账补偿的业务。我个人的选型经验是核心交易链路用XA非核心的配置更新、日志写入不用跨库事务。XA配置只需要在yml里加一行spring: shardingsphere: transaction: defaultType: XA然后在需要事务的方法上使用标准SpringTransactional即可ShardingSphere会自动把多个数据源纳入一个全局事务。注意必须引入对应的XA事务依赖否则启动时默认实现不生效。另外我把事务粒度控制得很小避免长事务占用多个分片连接导致连接池耗尽。5. 生产环境踩过的坑与性能调优建议这段是全文最值钱的部分。配置跑通不难难的是在真实流量下不出问题。下面按我排查的顺序把几个典型的坑和调优经验整理出来。5.1 无分片键查询变成全路由一个慢SQL引发的血案上线第二周监控报警一个接口的P99从80ms涨到了1200ms。查了日志发现一条SELECT * FROM t_order WHERE user_id 123触发的是全路由4个分片全部被扫描了一次。原来这个查询接口是给用户查我的订单列表用的天然只有user_id没有order_id。这个问题的本质是分片键选的是order_id但高频查询维度却是user_id。我当时的处理方案是在物理分片上给user_id建索引至少保证单个分片内的查询走索引引入订单索引表t_order_user_index以user_id为分片键维护user_id - order_id的映射查询时先查索引表拿到order_id列表再按order_id精确路由到目标分片。方案2要额外维护一致性但对于高频率的用户订单列表场景收益非常明显。这里想提醒的就是分片键一定要覆盖最高频的查询路径如果确实没法覆盖在一开始就设计好索引表或者将查询键作为冗余分片键。5.2 常见配置错误与完整排查链路我总结了三个高概率出错的配置点以及排查思路希望能帮你省去读源码的时间。错误一5.x使用了4.x的配置坐标。表现是启动报IllegalArgumentException: 配置项 spring.shardingsphere.sharding 不存在。排查链路很简单看启动物理类是否来自org.apache.shardingsphere.spring.boot包确认依赖是5.x后把配置从spring.shardingsphere.sharding搬移到spring.shardingsphere.rules.sharding即可。错误二数据源标识和库名不一致导致路由结果不对。我曾经见过一个案例数据源标识叫ds_a但实际库名是ds0结果行表达式ds_a$-{0..1}在底层解析时产生了不存在的数据源应用启动时还会报数据源初始化错误。这里建议数据源标识、库名、行表达式三者保持统一命名遇到这类问题直接review这三个地方。错误三分片算法表达式里的列名写错。algorithm-expression里使用的列名必须和SQL中实际的列名一致而且大小写敏感。比如SQL里写的是ORDER_ID表达式里写order_id会导致运行期解析异常。排查方法是打开sql-show: true用一条带分片键的SQL触发路由观察改写后的SQL和路由目标是不是预期值。顺便说一个我一直保留的好习惯分库分表改动上线前一定用sql-show: true在预发环境跑一遍核心链路。把日志里的实际SQL抓出来和预期比对而不是只看功能是否正常。宁可多这一步也别等上了生产再翻日志。5.3 容量规划不是分了4个表就一直够用最后聊一个容易被忽略的长期问题分片数量规划。很多团队上线时定了2库×2表4个分片觉得分完就不用管了。但实际上分片扩容非常痛苦——ShardingSphere的INLINE表达式在运行期是静态的不支持平滑动态扩容。你要从4个分片扩到8个分片需要做数据迁移、修改配置、灰度切流代价远高于一开始就规划大分片数。我的建议是按未来3到5年的数据量预估来定初始分片数。假设单表超过500万行后性能明显下降你的业务每年新增1200万行5年就是6000万行那么至少需要12到16个分片才能保证单表在500万行以下。一次性分成16个分片前两年确实会有一些分片利用率不高的问题但相比最后做一次痛苦扩容这算很划算的取舍。另外连接数规划也要提前算ShardingSphere-JDBC每个分片都对应一个真实数据源连接池应用实例数乘以分片数乘以连接池大小就是后端数据库要承受的连接总量。我们一开始连接池配的108个实例、4个分片单库连接数就是8×4×10320MySQL默认连接数上限很容易被顶爆。后来把连接池调小到5并加上了maximum-pool-size的动态调整才算稳定下来。5.4 分库分表的下一步从ShardingSphere-JDBC到分布式中间件的演进很多团队以ShardingSphere-JDBC起步做久了以后会碰到新的问题多个微服务都要各自维护一份分片配置公共表的管理也开始重复。这时候就得考虑要不要往ShardingSphere-Proxy演进把分片规则收敛到独立代理层。我个人认可的思路是先JDBC后Proxy。小规模、强性能要求阶段JDBC侵入式方案能快速见效等到分片配置需要大规模统一管理、团队里有非Java服务也要访问分片数据时再平滑过渡到Proxy。但不要一上来就上Proxy否则中间层会成为新的性能瓶颈和运维负担。从实践角度看分库分表并没有想象中那么玄乎。把逻辑表、真实节点、分片算法、主键生成这几件事在yml里理顺再想清楚分片键的选择和事务边界大部分问题都能在配置层面解决。真正需要花功夫的永远是数据分布不均、慢查询和扩容这些工程问题。我在这次订单系统改造里最大的体会有两点一是分片键的选择一定要前置到业务分析阶段和产品、运营一起梳理查询路径不要等上线后再补救二是配置一定要保持可读性yml里每个算法、每个策略都写清注释因为半年后回来看这段配置的大概率是你自己。最后再分享一个小技巧改造前先给现有库表做一次完整的数据备份和压测基线改造后逐项对比核心接口的耗时和吞吐这样即使出了问题也能第一时间定位是分片路由的问题还是业务代码的问题。