ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

ShardingSphere-jdbc与Spring Boot分库分表实战:从配置到踩坑全记录

ShardingSphere-jdbc与Spring Boot分库分表实战:从配置到踩坑全记录 分库分表这个话题聊的人多真正落地的人少。中间件选型、分片键设计、配置写法每一步都有坑。最近在一个新项目里把 ShardingSphere-jdbc 5.5.0 和 Spring Boot 完整集成了一遍从依赖引入到分片规则配置再到读写分离和问题排查踩了不少雷。这篇就当作一份实战记录给准备入手的兄弟做个参考。先说结论如果你的项目是 Java 技术栈、Spring Boot 应用数据量已经涨到单表扛不住的程度ShardingSphere-jdbc也就是 JDBC 模式是目前最平滑的切入方式。它不要求你改 SQL、不引入额外的中间件节点以一个增强版数据源的形式嵌在应用里配置好分片规则之后逻辑表映射到物理表、SQL 路由改写这些脏活累活都交给它处理。这套东西的入门门槛不高但配置项里的细节非常多网上很多文章停留在跑通 demo 的阶段真正接近生产环境的配置和坑点很少有人写透。1. 为什么选 ShardingSphere-jdbc两条技术路线的取舍1.1 代理模式与驱动模式选哪种分库分表中间件大致分两类。一类是代理模式典型代表是 MyCat 和 ShardingSphere-Proxy。应用连接的是代理代理再转发到真实的数据库。这种方案对应用侵入性最小但多了一层网络转发延迟会增加而且代理节点本身成了高可用架构里的一环运维成本不低。另一类是驱动模式也就是 ShardingSphere-jdbc。它本质上是一个增强版的 JDBC 驱动和数据源包装器。应用直接连真实库SQL 从发出到执行这一整条路径上解析、改写、路由都是在应用内存里完成的。没有额外的网络跳转性能损耗远小于代理模式部署上就是加一个依赖、写一段配置的事。我选驱动模式的另一个理由是调试方便。代理模式下SQL 从应用到代理再到数据库链路长了出了问题很难判断是哪一段出的岔子。JDBC 模式下控制台能把改写后的真实 SQL 直接打出来一眼就能看出 SQL 被路由到了哪个库、哪张表排查问题爽快得多。如果你的团队没有专职的 DBA 和中间件运维人员JDBC 模式更容易接受因为它的形态就是一个数据源Spring Boot 里替换成本极低。1.2 5.x 的配置体系变化想特别提醒一点ShardingSphere 在 4.x 升到 5.x 的时候配置方式是推倒重来的。4.x 时期用的是 properties 风格一堆sharding.rules.table...前缀的键值对可读性差而且很多命名在 5.x 里已经废弃。5.x 全面转向 YAML 风格分片算法做成了可插拔模式——每个算法定义在sharding-algorithms节点下起个名字表规则里通过sharding-algorithm-name去引用。这个设计比 4.x 干净很多算法复用、调整都方便。5.5.0 是 5.x 系列里比较成熟的版本对 Spring Boot 2.x 和 3.x 都有适配。如果你直接用 Spring Boot 3.x不用太担心老版本那种 javax 和 jakarta 的兼容问题但还是要确认没有其他老依赖在拖后腿这个后面细说。1.3 你的数据量真的需要分片吗这话可能有些人不爱听但该说还是要说分库分表不是银弹它解决的问题相对单一就是单表数据量过大导致的操作性能下降。如果你的库还在单表百万级别索引加上去、慢查询优化一下、Redis 缓存顶一顶大概率能撑很久。这个投入产出比远高于上一套分库分表中间件。真正需要分片的场景一般满足这几条单表数据量几千万甚至上亿、写入吞吐上不去了、单库连接数不够或者磁盘容量顶不住。更关键的是一旦定了分片规则分片键的选择就是个架构决策后面想改基本等于重做一套。所以动手之前一定把业务查询模式想清楚把两年内的数据量增长也算进去。2. 工程搭建版本组合与依赖引入2.1 版本选型避坑先把环境交代清楚我这次项目的组合是组件版本Spring Boot2.7.18ShardingSphere-jdbc5.5.0MyBatis-Plus3.5.5MySQL8.0.xJDK1.8为什么没直接上 Spring Boot 3.x因为项目里还有不少老依赖从 javax 迁到 jakarta 的工作量不小。如果你是新项目Spring Boot 3.2 配 5.5.0 完全没问题官方已经适配了新命名空间。版本这里有个容易被坑的点ShardingSphere 的版本号和 Spring Boot 的版本号不是简单的“一一对应”关系中间隔了好几个小版本。建议用 5.4.x 或 5.5.x这两个版本对 Spring Boot 2.7 和 3.x 的兼容性调整都做完了别去用 5.0.0 这种早古版本生产环境会教做人。2.2 Maven 坐标的正确姿势依赖坐标是个大坑我必须单独拿出来说。网上大量文章写的还是老坐标!-- 网上常见的旧写法5.5.0 里不要用 -- dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.5.0/version /dependency这个坐标在很多 5.x 小版本里已经调整了。5.5.0 官方推荐的是直接引入shardingsphere-jdbc这个聚合坐标它会自动带上 Spring Boot 集成需要的模块dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc/artifactId version5.5.0/version /dependency我第一次就是照着旧文章拉依赖IDE 里包怎么都拉不下来最后去翻官方文档才发现坐标换了。这种坑完全可以通过看官方文档避免别偷懒。2.3 单数据源约束与 ORM 配合配置 ShardingSphere 之后一个隐含的约束要记在心里Spring 容器里的 DataSource 会被 ShardingSphere 接管你不能在项目里自己定义第二个数据源。如果你原来的项目用了 dynamic-datasource 这种多数据源框架要先停掉把多个库统一挪到 ShardingSphere 的datasource配置块里管理。不然会出现数据源互相覆盖、bean 循环依赖等莫名其妙的问题。和 MyBatis-Plus 配合倒是很顺利。MyBatis-Plus 的selectById、selectPage这些方法在逻辑表上正常工作ShardingSphere 会把 SQL 改写成对真实表的操作。但注意一点MyBatis-Plus 的代码生成器或者手写的 SQL 里表名一定要写逻辑表名比如t_order不能写t_order_0这种物理表名。写了物理表名ShardingSphere 就不会帮你路由这条 SQL 会直接打到它解析出的那个数据源上等于绕过了分片规则。3. 分片规则配置从分片键到表规则3.1 分片键怎么定才合理动手配置之前先把分片逻辑想清楚。我习惯用订单系统举例来推演。假设有 2 个物理库ds0、ds1订单表逻辑上叫t_order物理上拆成 4 张表每个库 2 张t_order_0、t_order_1。这里最关键的选择是分片键。订单表的高频查询场景一般就是两类查某用户的订单列表WHERE user_id ?查某笔订单详情WHERE order_id ?。如果只拿user_id一个字段做分片键按order_id查详情就会变成全路由——ShardingSphere 不知道这个订单在哪个分片上只能所有分片都查一遍。如果只拿order_id做分片键按用户查列表又会全路由。所以在真实项目里我倾向于采用“复合分片”的思路库分片键用user_id表分片键也用user_id或者和用户维度强相关的字段。这样一来同一个用户的所有订单都在同一个库同一张表里查询走精确路由不会跨库 join。代价是单个用户的数据量特别大时会存在热点但绝大多数业务场景里单用户数据量远达不到这个量级这个取舍是划算的。另外记住一个原则分片键最好选择分布均匀、不会频繁更新的字段。手机号、用户 ID 这类就很合适。状态、类型这种枚举值少的字段千万别拿来分片否则数据都堆在几个分片上分片形同虚设。3.2 一份完整 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://192.168.1.10:3306/db_order_0?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai username: root password: root123 max-pool-size: 20 min-pool-size: 5 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.11:3306/db_order_1?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai username: root password: root123 max-pool-size: 20 min-pool-size: 5 rules: sharding: tables: t_order: actual-data-nodes: ds$-{0..1}.t_order_$-{0..1} 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: id key-generator-name: snowflake_id t_order_item: actual-data-nodes: ds$-{0..1}.t_order_item_$-{0..1} database-strategy: standard: sharding-column: user_id sharding-algorithm-name: db_mod table-strategy: standard: sharding-column: user_id sharding-algorithm-name: table_mod binding-tables: - t_order, t_order_item broadcast-tables: - t_dict sharding-algorithms: db_mod: type: MOD props: sharding-count: 2 table_mod: type: MOD props: sharding-count: 2 key-generators: snowflake_id: type: SNOWFLAKE props: worker-id: 1 max-tolerate-time-difference-seconds: 5 props: sql-show: true数据源部分ds0、ds1是数据源标识符必须和规则里的ds$-{0..1}对应上。$-{0..1}本质是 Groovy 表达式展开就是ds0、ds1。如果数据源起名不规范比如叫primary_order_datasource表达式就得写成$-{[primary_order_datasource, secondary_order_datasource]}这种可读性很差。我建议一律用ds0、ds1这种短命名。真实表映射actual-data-nodes: ds$-{0..1}.t_order_$-{0..1}表示每个库里有t_order_0和t_order_1两张表。注意这个表达式只负责路由不负责建表。数据库里没有对应的物理表的话启动可能不报错但 SQL 一执行就会报“表不存在”所以建表脚本必须和表达式一致。分片策略database-strategy和table-strategy下面我都用了standard标准分片策略意思是用单一分片键决定路由。sharding-column指定分片键是user_idsharding-algorithm-name引用下面定义的db_mod取模算法。key-generate-strategy分布式主键生成。分库分表之后绝对不能用数据库自增主键多分片之间主键必冲突。这里用 ShardingSphere 内置的SNOWFLAKE雪花算法生成器worker-id在同一个分片环境里要全局唯一多个应用实例如果配了相同的 worker-id生成的 ID 会重复这是个生产环境的隐患。props 配置sql-show: true打开 SQL 日志开发阶段必开排查路由问题靠它。生产环境记得关掉日志量非常大而且会把物理表结构暴露在日志里。3.3 分片算法选型对比5.x 内置的算法类型很多我用一个表把常用的列出来方便你选型算法类型说明适用场景MOD分片键直接取模分片键是数字且分布均匀比如数字 IDHASH_MOD分片键先哈希再取模分片键是字符串比如手机号、订单号需要打散分布INLINEGroovy 行表达式支持条件判断需要按状态、按区间定制路由逻辑灵活度高INTERVAL按时间区间分片日志表、流水表按天或按月分片CLASS_BASED自定义算法实现类内置算法满足不了复杂业务逻辑时兜底我上面配置按user_id % 2分库、user_id % 2分表是因为user_id是个从用户表带过来的数字 ID本身分布均匀MOD 就够了。如果分片键是订单号这种字符串建议用 HASH_MOD让哈希先把分布打散再取模不然字符串直接取模可能全挤在同一个分片里。分片策略除了 standard5.x 还支持complex复合分片键和hint强制路由。complex 需要自己实现ComplexKeysShardingAlgorithm适合一个表里多个分片键配合路由的场景。hint 不走 SQL 的 WHERE 条件而是由代码显式指定要路由到哪个分片适合分片键不在 SQL 里、但在调用上下文中能拿到的场景。基础配置阶段standard 加 MOD/HASH_MOD 已经能覆盖大部分需求。3.4 绑定表与广播表绑定表这个配置能解决一个很大的性能问题必须专门讲。订单表和订单明细表是高频 join 的场景。如果两张表的分片键都是user_id分片数量一致那么配置了binding-tables之后ShardingSphere 就知道这两张表中同一个用户的数据一定落在同一个分片上join 的时候不会跨库跨表。如果不配置绑定表会怎样ShardingSphere 会按笛卡尔积来路由。4 个订单分片 × 4 个明细分片一次 join 就变成 16 次路由查询慢到你怀疑人生。这是实际项目里最容易踩的隐形性能坑。广播表是另一类表字典表、配置表这种所有分片都要保留一份完整数据的表。比如t_dict每个库都建一张一模一样的表配置了broadcast-tables之后INSERT/UPDATE/DELETE 会自动同步到所有数据源查询只会路由到其中一个分片。这让字典表的读写都不用你做额外处理。3.5 雪花主键与前端精度问题这里有个特别容易被忽略的坑ShardingSphere 雪花算法生成的 ID 是 19 位 Long精度超出了 JavaScript 的 Number 安全整数范围2^53。也就是说后端 ID 传给前端之后前端拿这个 ID 再传回来做查询末位数字已经变了根本查不到数据。这个坑在前后端分离的项目里特别常见。解决方案不复杂接口返回时把 Long 类型的主键序列化成字符串。如果你用 Jackson可以给主键字段加JsonSerialize(using ToStringSerializer.class)或者全局配置一个 Long 转 String 的序列化器。数据库里继续用 bigintJava 里继续用 Long只是 JSON 传输层转成字符串两边都舒服。4. 读写分离叠加配置4.1 主从数据源配置如果你的分片库每个都做了主从复制ShardingSphere 可以在分片之上再叠加读写分离。每个分片的数据源从“单个库”变成“一组主从库”。配置上是在rules层加一个readwrite-splitting规则spring: shardingsphere: rules: readwrite-splitting: >t_order: actual-data-nodes: rw_ds$-{0..1}.t_order_$-{0..1}这样分片和读写分离就叠加起来了。要理解的是读写分离里的read-data-source-names可以配多个从库但写库只有一个。从库挂了的话需要配置动态感知或者依赖负载均衡策略健康检查这是生产环境要额外考虑的点。4.2 负载均衡与事务的取舍读库负载均衡算法有三种主流选择ROUND_ROBIN轮询请求均匀分布到各个从库适合从库配置一样的场景RANDOM随机分配适合从库数量多、连接压力分散的场景WEIGHT按权重分配适合从库硬件性能有差异的场景实际项目里如果两个从库配置一样直接 ROUND_ROBIN。如果从库是新旧机器混用的用 WEIGHT给性能好的机器更高权重。这里有个事务相关的铁律必须知道服务里一旦开启事务读写分离就失效了所有操作都会走主库。原因很现实——从库和主库之间有复制延迟事务内读从库可能读到旧数据这会造成逻辑错误。所以如果你在 Service 方法上加了Transactional哪怕方法里只有一条 SELECT这条 SELECT 也会打在主库上。不要试图去关掉这个特性这是为了保证数据一致性必须做的取舍。如果你接受不了可以从业务上避免长事务、避免在事务里做查询把读操作尽量放到事务外面。5. 常见问题与排查技巧实录5.1 启动失败排查最常见的问题是启动直接报数据源配置错误集中在下面几个点依赖坐标错误。5.5.0 要用shardingsphere-jdbc这个坐标不是老文章里的-starter结尾那个。依赖拉不下来先查坐标对不对。YAML 缩进层级错乱。datasource和rules都在spring.shardingsphere下面缩进错了会导致配置被静默忽略Spring Boot 找不到数据源就崩了。YAML 的报错信息经常不明确优先检查缩进对齐。项目里残留其他数据源配置。比如spring.datasource.url、spring.datasource.druid.*这类配置只要存在就会被 Spring Boot 自动配置抢走数据源初始化流程。ShardingSphere 模式下把其他数据源配置全部删掉只留spring.shardingsphere.datasource这一套。5.2 SQL 路由日志怎么看打开sql-show: true之后控制台会打印类似这样的日志ShardingSphere-SQL: Logic SQL: SELECT * FROM t_order WHERE user_id 123 ShardingSphere-SQL: Actual SQL: ds0 ::: SELECT * FROM t_order_0 WHERE user_id 123这种日志是排查问题的核心工具。:::左边是数据源名右边是改写后的真实 SQL。看到这条日志你就能确认逻辑 SQL 被正确路由到了 ds0表也被改写成了t_order_0。排查问题的思路是四步走看逻辑 SQL 是否就是你写的原始 SQL看:::左边的数据源对不对不对就是库分片规则有问题看真实表名对不对不对就是表分片规则有问题看 WHERE 条件里分片键的值是否能命中正确的分片5.3 分页查询踩坑分库分表之后分页查询性能问题会被放大。比如这条 SQLSELECT * FROM t_order ORDER BY create_time LIMIT 100000, 20ShardingSphere 的处理方式是每个分片都取前 100020 条数据然后归并排序最后取第 100000 到 100020 条。4 个分片就是扫描 4 份 100020 条数据深分页时性能断崖式下跌。这个问题的解法一般有三种业务层面限制最大翻页数超过 100 页就提示用户改用条件筛选用游标分页替代 offset 分页WHERE create_time ? ORDER BY create_time LIMIT 20每次把上一次查询的最大 create_time 传进去深分页场景直接交给 Elasticsearch 这类搜索引擎MySQL 只负责承接最近的热数据查询另外如果你用的是 MyBatis-Plus 的selectPage记得确认分页插件版本和 ShardingSphere 兼容。我的经验是MyBatis-Plus 3.5.x 配合 ShardingSphere 5.5.0 可以正常工作但如果你自己写拦截器再做一次分页改写很容易造成双重分页这种问题排查起来非常费劲。5.4 分片键更新限制与 DDL 同步分片键不能出现在 UPDATE 语句的 SET 子句里。比如UPDATE t_order SET user_id 100 WHERE id 1这条 SQL 会被 ShardingSphere 直接拒绝因为改了分片键等于数据要从一个分片迁移到另一个分片中间的链路极其复杂。如果业务上真的需要变更用户归属正确做法是先查旧数据、新分片插入、旧分片删除、处理分布式事务千万别指望一条 UPDATE 搞定。还有一个日常要用的注意事项分库分表之后DDL 操作不会自动同步。你在t_order_0上加了字段t_order_1不会自动跟着变。所有建表、加字段、加索引的操作都需要对每个真实表执行一遍。表少的时候手动执行还行表多了建议写成循环脚本统一执行不然漏掉一张表线上跑 SQL 就开始报“列不存在”这种线上事故我见过太多次了。事务方面再补一句ShardingSphere 默认的本地事务不支持跨库分布式事务。如果单个事务里同时操作了 ds0 和 ds1 的表一致性是没法保证的。跨库写操作一定要引入 Seata 或者 ShardingSphere 的 XA 事务模式那是另一套配置和架构决策了基础阶段先记住这个边界。做这套配置的时候我最大的体会是ShardingSphere 不复杂但它的配置项和概念非常多逻辑表、物理表、分片算法、分片策略、绑定表、广播表、分布式主键每一个都值得花时间吃透。先把基础的分片跑通再逐步叠加读写分离、分布式事务这些能力比自己上来就一把梭要稳得多。最后再分享一个建议配置上线之前一定要写一个校验脚本确认每个分片上的物理表结构完全一致字段缺失这种问题在分片环境下会造成数据错乱这是无论怎么强调都不为过的习惯。
返回列表