SpringCloud Alibaba分库分表实战:ShardingSphere选型、配置与性能调优 1. 分库分表到底在解决什么问题1.1 单库单表的性能天花板在哪里很多团队在项目初期都是单库单表跑得好好的业务量一上来就开始出问题。我经历过的一个典型场景是订单表数据量到了三千万行左右查询开始明显变慢写入也开始出现锁等待。这时候你去加索引、优化SQL能撑一阵子但撑不了太久。单库单表的天花板主要来自三个维度。第一是单表数据量MySQL单表在千万级别时B树索引的层高会增加磁盘IO次数变多查询延迟上升明显。第二是单库连接数一个数据库实例的连接数是有限的通常几百到几千高并发场景下连接池很快被打满。第三是单机磁盘和IOPS数据量大了之后备份、DDL操作都会变得极其痛苦加一个字段可能锁表几十分钟。所以分库分表本质上是在做两件事把数据分散到多个物理节点上突破单机存储和IO瓶颈把请求分散到多个数据库连接上突破单实例连接数限制。理解这一点很关键因为后面所有的方案选型都是围绕这两个目标展开的。1.2 垂直拆分与水平拆分的适用边界分库分表分两个方向垂直和水平。垂直拆分是按业务维度拆比如订单库、用户库、商品库各自独立。这种拆分方式改动相对小业务边界清晰但解决不了单表数据量过大的问题——如果订单表本身就是亿级垂直拆完它还是亿级。水平拆分才是真正解决单表数据量问题的方案按某个分片键把一张表的数据分散到多张结构相同的表中。比如订单表按用户ID取模分成16张表每张表只存一部分数据。实际项目中这两种方式往往是组合使用的。先垂直拆出不同业务库再对核心大表做水平拆分。我个人的经验是能垂直先垂直垂直解决不了再水平。因为水平拆分带来的复杂度是数量级的提升包括分布式事务、跨分片查询、全局ID、数据迁移等等。1.3 什么时候该动手什么时候再等等分库分表不是越早做越好。过早拆分会让系统复杂度飙升开发效率下降而收益在数据量没上来之前几乎为零。我的判断标准是这样的指标建议动作单表数据量 500万不用拆加索引优化SQL就行单表数据量 500万-2000万做好拆分预案可以先做读写分离单表数据量 2000万考虑水平拆分单库QPS 5000考虑垂直拆分或读写分离单库连接数经常打满必须拆分当然这不是绝对标准还要看你的硬件配置和业务特征。有些表字段少、查询简单几千万行也没问题有些表字段多、有大文本字段几百万行就开始吃力。注意分库分表是不可逆的重构一旦拆了再合回去成本极高。动手之前一定要确认业务增长趋势别为了技术而技术。2. SpringCloud Alibaba体系下的分片方案选型2.1 为什么选ShardingSphere而不是其他方案在SpringCloud Alibaba生态里做分库分表主流选择有三个ShardingSphere、MyCat、以及自己写路由逻辑。自己写路由逻辑基本不考虑维护成本太高。MyCat是代理层方案部署独立服务应用通过MySQL协议连接。ShardingSphere是客户端方案以JAR包形式嵌入应用。我选ShardingSphere的核心原因是它和SpringCloud Alibaba的集成更自然。MyCat作为独立代理层多了一次网络转发延迟会增加而且代理层本身可能成为瓶颈。ShardingSphere直接在应用层做SQL解析和路由少一次网络跳转性能更好。另外ShardingSphere的生态更活跃和SpringBoot、MyBatis的集成方案都很成熟。ShardingSphere有两个产品线ShardingSphere-JDBC和ShardingSphere-Proxy。JDBC是客户端模式Proxy是代理模式。在SpringCloud Alibaba项目里我推荐用JDBC模式因为它不需要额外部署运维跟着应用一起发布就行。2.2 分片键的选择策略与踩坑经验分片键选得好不好直接决定了分库分表方案能不能落地。选分片键的核心原则是尽量让查询能命中单个分片。以订单表为例常见的分片键候选有订单ID、用户ID、商户ID、创建时间。怎么选看你的核心查询场景。如果80%的查询都是查某个用户的订单列表那用户ID就是最佳分片键。如果查询场景很分散那就要考虑基因法或者双写冗余。我踩过的一个坑是用订单ID做分片键结果运营后台要按用户查订单每次都要扫全部分片性能极差。后来改成用用户ID做分片键订单ID里嵌入用户ID的基因这样既能按用户查也能按订单ID精确定位。基因法的做法是生成订单ID时把用户ID的后几位嵌入到订单ID中。比如用户ID后4位是分片依据那订单ID的后4位也保留这个信息。这样通过订单ID也能算出分片位置。2.3 分片数量怎么定才合理分片数量不是越多越好。分片太多会导致跨分片查询变慢、连接池膨胀、运维复杂度上升。分片太少又起不到分散压力的作用。我的经验公式是分片数 预估3年后的数据量 / 单表理想数据量。假设订单表3年后预计有2亿行单表理想控制在1000万行以内那分片数就是20。取2的幂次方选16或32。为什么建议取2的幂次方因为取模运算用位运算更快而且后续如果要扩容2的幂次方更容易做一致性哈希的迁移。当然这不是硬性要求取质数也有好处分布更均匀。还有一个容易忽略的点分片数要一次性规划好。虽然ShardingSphere支持后续扩容但数据迁移的代价很大。宁可初期多分一点也别后面频繁扩。2.4 分布式主键的生成方案对比分库分表之后数据库自增主键不能用了因为多个分片各自自增会产生重复ID。常见的分布式ID方案有方案优点缺点适用场景UUID简单本地生成无序索引性能差非核心表雪花算法有序性能高依赖时钟大多数场景号段模式性能高有序需要额外存储高并发场景Redis自增简单有序依赖Redis可用性中小规模在SpringCloud Alibaba项目里我通常用雪花算法ShardingSphere内置了雪花算法的实现配置一下就能用。但要注意时钟回拨问题生产环境建议开启时钟同步并且配置好回拨容忍策略。提示如果业务对ID有可读性要求可以考虑号段模式每次从数据库取一批ID缓存在内存里用完再取。这样既有序又高性能缺点就是应用重启会浪费一段ID。3. 从零搭建分库分表环境的完整实操3.1 环境准备与依赖配置先确认基础环境JDK 1.8、MySQL 5.7、SpringBoot 2.x、SpringCloud Alibaba 2.2.x。我用的具体版本是SpringBoot 2.3.12、SpringCloud Hoxton.SR12、SpringCloud Alibaba 2.2.7.RELEASE、ShardingSphere-JDBC 4.1.1。为什么用ShardingSphere 4.x而不是5.x因为4.x和SpringBoot 2.3的兼容性更稳定5.x对SpringBoot版本要求更高。如果你的项目用的是SpringBoot 2.6那可以直接上ShardingSphere 5.x。Maven依赖核心就一个dependency groupIdorg.apache.shardingsphere/groupId artifactIdsharding-jdbc-spring-boot-starter/artifactId version4.1.1/version /dependency注意这个starter会自动排除掉默认的DataSourceAutoConfiguration所以你的数据源配置要全部写在ShardingSphere的配置里不能再单独配spring.datasource。3.2 数据库与表的初始化脚本假设我们要把订单表分成2个库、每个库4张表共8个分片。先建库CREATE DATABASE order_db_0; CREATE DATABASE order_db_1;然后在每个库里建4张订单表CREATE TABLE order_0 ( order_id BIGINT NOT NULL, user_id BIGINT NOT NULL, order_no VARCHAR(64) NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (order_id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_1 (...); CREATE TABLE order_2 (...); CREATE TABLE order_3 (...);两个库都执行同样的建表语句表名分别是order_0到order_3。这里有个细节分片表的索引要单独建不能依赖全局索引。每个分片表上的索引只对该分片的数据有效。3.3 分片规则的YAML配置详解ShardingSphere支持YAML配置和Java配置两种方式。YAML更直观推荐用YAML。核心配置如下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/order_db_0?useSSLfalseserverTimezoneAsia/Shanghai username: root password: root ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/order_db_1?useSSLfalseserverTimezoneAsia/Shanghai username: root password: root sharding: tables: t_order: actual-data-nodes: ds$-{0..1}.order_$-{0..3} database-strategy: inline: sharding-column: user_id algorithm-expression: ds$-{user_id % 2} table-strategy: inline: sharding-column: user_id algorithm-expression: order_$-{user_id % 4} key-generator: column: order_id type: SNOWFLAKE default-database-strategy: inline: sharding-column: user_id algorithm-expression: ds$-{user_id % 2} props: sql: show: true这段配置的含义是逻辑表t_order对应ds0和ds1两个库每个库里有order_0到order_3四张表。分库规则是user_id对2取模分表规则是user_id对4取模。主键order_id用雪花算法生成。actual-data-nodes的表达式ds$-{0..1}.order_$-{0..3}是Groovy表达式会展开成ds0.order_0到ds1.order_3共8个节点。这种写法比一个个列出来简洁得多。3.4 分片算法背后的计算逻辑上面用的是inline表达式分片适合简单场景。但实际项目中分片逻辑往往更复杂比如要按用户ID的哈希值分片或者按时间范围分片。这时候就需要自定义分片算法。自定义分片算法要实现PreciseShardingAlgorithm接口public class UserIdShardingAlgorithm implements PreciseShardingAlgorithmLong { Override public String doSharding(CollectionString availableTargetNames, PreciseShardingValueLong shardingValue) { long userId shardingValue.getValue(); // 用哈希值代替取模分布更均匀 int hash Math.abs(Long.hashCode(userId)); int index hash % availableTargetNames.size(); return order_ index; } }这里为什么用哈希而不是直接取模因为用户ID往往是递增的直接取模在数据量不大时分布不均匀。比如用户ID从1开始递增对4取模前4个用户刚好落在4个分片上看起来很均匀。但如果用户ID是雪花算法生成的低位有规律取模也可能不均匀。哈希一下能打散这种规律性。不过哈希也有代价范围查询没法路由。比如你要查user_id在100到200之间的订单哈希之后没法确定落在哪些分片只能全分片扫描。所以哈希分片适合点查范围查询多的场景还是用范围分片。3.5 读写分离与分库分表的组合配置SpringCloud Alibaba项目里通常还会配读写分离。ShardingSphere支持读写分离和分库分表组合使用配置上就是在数据源层面做读写分离分片规则不变。spring: shardingsphere: masterslave: name: ms master-data-source-name: ds0 slave-data-source-names: ds0_slave sharding: tables: t_order: actual-data-nodes: ms.order_$-{0..3}这种配置下写操作走master读操作走slave。但要注意主从延迟问题如果业务对一致性要求高读操作也要走master可以通过HintManager强制路由。注意读写分离和分库分表组合时事务要特别小心。跨库事务本身就是分布式事务再加上主从切换问题会更复杂。建议核心业务先用单库分表等稳定了再考虑读写分离。4. 跨分片查询与分布式事务的应对策略4.1 跨分片查询的几种典型场景分库分表之后跨分片查询是最头疼的问题。常见的跨分片场景有分页查询、聚合统计、多条件组合查询。分页查询是最典型的。比如查询第100页的订单每页20条如果没有分片键ShardingSphere会把SQL改写成从每个分片取前(100*20)条然后在内存里归并排序最后取第100页的数据。这个代价随着页码增大而急剧上升。我的应对策略是限制最大可查询页数比如最多只能翻到第100页。超过100页的查询走离线报表或者搜索引擎。这样虽然牺牲了一些体验但保证了系统稳定性。聚合统计比如统计今天所有订单的总金额这种必须扫全部分片。如果实时性要求不高可以定时跑批把结果写到统计表里。如果实时性要求高可以考虑用ClickHouse之类的OLAP数据库做实时分析。4.2 分布式事务在分库分表下的处理分库分表之后一个业务操作可能涉及多个库就产生了分布式事务问题。比如创建订单要扣库存订单在订单库库存在商品库这就是跨库事务。SpringCloud Alibaba体系下分布式事务方案主要有Seata。Seata的AT模式对业务侵入小适合大多数场景。但AT模式依赖全局锁高并发下会有性能问题。我的建议是能避免分布式事务就避免。怎么避免通过合理的分片设计让同一个事务涉及的数据落在同一个库。比如订单和订单明细用同一个分片键保证它们落在同一个库这样本地事务就够了。如果实在避免不了用Seata的AT模式但要做好降级预案。全局事务超时时间不要设太长避免锁等待拖垮系统。4.3 全局ID与分片键的协同设计前面提到用雪花算法生成全局ID但雪花ID和分片键是两回事。雪花ID保证全局唯一但分片键决定数据落在哪个分片。如果分片键是用户ID那雪花ID只是主键不参与分片。这里有个优化技巧把分片键的信息嵌入到全局ID中。比如用用户ID做分片键生成订单ID时把用户ID的后几位编码进去。这样通过订单ID就能反推出分片位置不需要额外查询。具体做法是订单ID 时间戳 用户ID后4位 序列号。这样订单ID既全局唯一又包含了分片信息。查询时通过订单ID就能定位到具体分片避免了全分片扫描。4.4 数据迁移与扩容的平滑方案业务增长到一定程度原来的分片数不够用了需要扩容。扩容的核心难点是数据迁移要保证迁移过程中业务不中断。我的方案是双写迁移先加新的分片新数据同时写旧分片和新分片然后后台跑任务把旧数据迁移到新分片迁移完成后切读流量到新分片最后下线旧分片。这个过程听起来简单实操中有很多坑。比如双写时如何保证一致性我的做法是写旧分片成功后再写新分片如果新分片写失败就记录到补偿表后台任务重试。读的时候先读新分片读不到再读旧分片。迁移任务要控制速率别把数据库IO打满。我一般用限流工具控制迁移QPS比如每秒最多迁移1000条。迁移过程中要监控主从延迟延迟太高就暂停迁移。提示扩容迁移最好在业务低峰期做并且提前做好回滚预案。一旦新分片出问题能快速切回旧分片。5. 生产环境踩坑记录与性能调优5.1 分片键选择不当导致的性能问题我遇到过一个案例某项目用订单创建时间做分片键按月分片。结果查询某用户最近3个月的订单时要扫3个分片还能接受。但查询某用户所有订单时要扫全部分片随着时间推移分片越来越多查询越来越慢。后来改成用用户ID做分片键问题解决了。但新的问题来了运营后台要按时间范围查订单又变成全分片扫描了。最后的方案是双写订单表按用户ID分片同时同步一份到按时间分片的报表库后台查询走报表库。这个案例的教训是分片键要跟着核心查询场景走没有一种分片键能覆盖所有查询。对于次要查询场景用冗余数据或者离线方案解决。5.2 跨分片JOIN的替代方案分库分表之后JOIN操作基本废了。原来一条SQL能搞定的关联查询现在要拆成多次查询在内存里组装。比如查询订单和用户信息原来JOIN一下就行。现在订单在订单库用户在用户库只能先查订单再根据用户ID批量查用户信息然后在代码里组装。这种方案的问题是会带来N1查询。优化方法是批量查询先查出所有订单收集用户ID去重然后一次查询所有用户信息最后在内存里做映射。这样把N次查询变成2次查询。如果关联查询特别多可以考虑用宽表冗余把常用字段冗余到订单表里。代价是数据一致性维护成本高适合字段变化不频繁的场景。5.3 分页查询的性能优化技巧前面提到分页查询要归并排序页码越大越慢。除了限制最大页数还有几个优化技巧。第一个是二次查询法先在各分片查询满足条件的ID归并排序后取目标页的ID再根据ID回表查详情。这样避免了在每个分片取大量数据。第二个是游标分页不用offset用上一页最后一条记录的ID作为游标。这样每次查询都是where id last_id limit 20不需要归并排序性能稳定。第三个是禁止跳页只允许上一页下一页不允许直接跳到第100页。这样配合游标分页性能最好。5.4 连接池与线程池的参数调优分库分表之后连接池数量要重新计算。原来一个库一个连接池现在N个库N个连接池。如果每个连接池都配50个连接10个库就是500个连接数据库可能扛不住。我的经验公式是总连接数 数据库最大连接数 * 0.8 / 分片数。比如数据库最大连接数1000分8个分片那每个分片的连接池最多配100个。再考虑读写分离读库和写库要分开算。线程池也要调整。ShardingSphere的SQL解析和路由是在应用线程里做的会消耗CPU。如果应用线程池太小高并发时会出现任务排队。建议把核心线程数调大一些具体数值要压测确定。5.5 监控与告警的关键指标分库分表之后监控要覆盖到每个分片。关键指标包括每个分片的QPS、慢查询数量、连接池活跃数、主从延迟。我一般用Prometheus Grafana做监控ShardingSphere提供了Metrics接口可以暴露分片级别的指标。告警规则要针对每个分片单独设置不能只看平均值。因为某个分片可能因为数据倾斜导致压力特别大平均值看不出来。数据倾斜是分库分表常见问题。比如按用户ID取模如果大客户集中在某几个分片这些分片压力会特别大。监控要能发现这种倾斜及时调整分片策略。提示建议定期做数据分布检查统计每个分片的数据量和QPS发现倾斜及时处理。数据倾斜严重时可以考虑用一致性哈希或者自定义分片算法来均衡。6. 几个容易被忽略的细节问题6.1 DDL变更如何操作分库分表之后加字段、改索引这些DDL操作要在所有分片上执行。手动一个个执行容易漏而且容易出错。我的做法是写一个DDL管理工具把DDL语句配置化工具自动在所有分片上执行。执行前先检查每个分片的表结构是否一致执行后验证结果。对于大表DDL要用pt-online-schema-change之类的工具避免锁表。ShardingSphere本身不提供DDL管理功能这块要自己实现。建议在项目初期就把DDL管理流程建好别等到要改表结构时手忙脚乱。6.2 分布式事务的降级预案用了Seata之后如果Seata服务挂了怎么办必须有降级预案。我的做法是配置本地事务降级开关Seata不可用时自动降级为本地事务同时记录日志和告警。降级后数据一致性会受影响但至少保证业务不中断。等Seata恢复后通过补偿任务修复数据。补偿任务的逻辑要提前写好不能等出问题了再临时写。6.3 分片表的备份与恢复分库分表之后备份策略要调整。原来备份一个库就行现在要备份N个库。备份要保证一致性不能有的库备份了有的没备份。我一般用逻辑备份工具按分片并行备份最后合并成一个备份集。恢复的时候按分片并行恢复。备份文件要包含分片规则否则恢复后不知道数据怎么分片的。6.4 测试环境的搭建要点分库分表项目的测试环境搭建很麻烦因为要模拟多个库。我的做法是用Docker起多个MySQL实例用脚本自动初始化分片表结构。测试数据也要按分片规则生成保证数据分布均匀。集成测试要覆盖跨分片查询、分布式事务、分页查询这些场景。单元测试可以Mock掉ShardingSphere只测业务逻辑。但集成测试必须用真实的分片环境否则测不出问题。我个人在实际操作中的体会是分库分表最难的不是技术实现而是业务改造和运维管理。技术方案再完美如果业务代码没改到位照样出问题。所以动手之前一定要做好业务影响分析把所有涉及分片键的查询都梳理一遍该改的改该冗余的冗余。另外分片规则一旦确定就不要轻易改改一次伤筋动骨。宁可初期多花时间设计也别后面频繁调整。