资讯动态

ShardingSphere与@DS注解共存方案:数据源切换与分片路由的职责分离

发布时间:2026/10/22 4:16:12 来源:尧图企业网站定制
最近在做一个订单类的演示项目技术选型时同时用到了 ShardingSphere 和DS注解。本来觉得这两者各管各的没什么问题结果一跑起来就出事要么 ShardingSphere 的分片规则完全不生效要么DS切数据源像没写一样日志里 SQL 全部打到默认库。后来顺着数据源链路排查才发现问题不在业务代码而在两个框架对 DataSource 的争夺和职责边界上。这篇文章就把这套整合方案掰开揉碎讲清楚适合正在用 ShardingSphere 做分库分表、同时又依赖DS做多数据源切换的团队参考。先说结论ShardingSphere 和DS完全可以共存但前提是别把两者放在同一层“抢 DataSource 控制权”。正确做法是让DS负责“切哪个逻辑数据源”让 ShardingSphere 负责“在逻辑数据源内部做分片路由”。下面先从底层机制说起。1. 两条链路各管什么DS 的切源和 ShardingSphere 的路由为什么不能互相替代很多第一次遇到这个组合的人脑子里会有一个直觉ShardingSphere 已经管理了多个物理库DS又能在多个数据源之间切换那我是不是可以在DS(ds_0)和DS(ds_1)之间切换顺便实现分库这个想法看着合理实际上会把 ShardingSphere 完全绕过去造成分片逻辑失效、SQL 路由错乱、甚至表都找不到。1.1 DS 的切源原理DS注解来自 dynamic-datasource 框架核心机制并不复杂通过 AOP 拦截被注解修饰的方法把注解里指定的数据源 key 放到一个 ThreadLocal 里。然后业务代码通过注入的 DataSource 获取连接时动态路由数据源会去一个MapString, DataSource里根据 key 找到对应的真实数据源再返回它的 Connection。整个过程就是“切 DataSource”它不关心 SQL 是 INSERT 还是 SELECT也不关心表名落在哪个库。只要 key 存在它就把连接切换到那个数据源key 不存在默认落到 primary 数据源。1.2 ShardingSphere 的路由原理ShardingSphere-JDBC 的本质是一个代理数据源它也实现了javax.sql.DataSource接口但它内部不直接持有单一物理连接而是持有多个真实数据源比如 ds_0、ds_1同时维护一套分片规则。当业务 SQL 穿过它的时候它会解析 SQL、根据分片键计算路由结果再把 SQL 改写成目标库目标表上的 SQL最后执行并归并结果。也就是说ShardingSphere 的工作范围是“在数据源内部做路由”它控制的是 SQL 级别的物理路由。它和DS并不在一个维度上。1.3 冲突本质两个框架都在抢 DataSource真正让两者打架的不是注解本身而是 Spring 容器里的 DataSource Bean 归属问题。用 dynamic-datasource 时它会把一个动态路由数据源作为 primary DataSource 注册进容器。用 ShardingSphere 的 Spring Boot Starter 时它也会尝试注册一个 ShardingSphereDataSource 作为数据源。如果两个 starter 同时引入又没有做排除Spring 容器里就会出现两个“疑似” DataSource 的 Bean注入时谁被选中就成了看运气的事。即便 Bean 不冲突另一个常见问题是职责错位。有的项目把DS(ds_0)写在 Service 方法上以为这样就能把 ShardingSphere 绕开手动选库这等于放弃了分片规则。ShardingSphere 一旦发现 SQL 路由后的表不存在就会报出各种奇怪的表解析错误而且这类问题最难排查。给这两者做个简单对比维度DS 注解ShardingSphere控制粒度方法级数据源切换SQL 级分片路由管理对象多个逻辑 DataSource多个物理 DataSource切源时机AOP 拦截ThreadLocal 传 keySQL 解析时计算路由常见误区用来指定分片库用来做业务级数据源切换所以整合的关键不是让其中一个适应另一个而是把它们的职责分开。ShardingSphere 应该作为一个整体数据源被DS管理而不是让DS去管理 ShardingSphere 里的每一个分片库。2. 整合架构选型把 ShardingSphere 包成普通数据源塞给动态路由明确了职责边界后后面的问题就简单了我们到底让谁当 Spring 容器里的 “DataSource 总入口”2.1 方案 AShardingSphere 当总入口DS 只做静态选库这个方案思路是应用只暴露一个 ShardingSphereDataSource把需要分库分表的表全部纳入 ShardingSphere 的规则配置中DS只用来切换“非 ShardingSphere 管理的普通库”。听起来很干净但实际会碰到一个尴尬情况ShardingSphere 的规则是全局配置的它管理的表会按照规则路由。假如业务里有 t_order 这类分片表也有不需要分片的 t_user、t_log你就得在 ShardingSphere 里给每张表都配置规则。不需要分片的表也要配成 broadcast table 或者在规则里做处理否则 SQL 路由会把它当成单表直接往一个默认数据源上执行行为不可控。我见过不少项目为了把不相关表塞进 ShardingSphere 规则里配置越写越厚维护成本直线上升。这个方案适合“几乎全部表都需要分片”的项目不适合有多套独立业务库的混合场景。2.2 方案 Bdynamic-datasource 当总入口ShardingSphere 作为其中一个逻辑数据源这是我最推荐的做法也是后面配置示例里采用的方式。具体思路dynamic-datasource 继续担任动态路由数据源负责管理所有逻辑数据源。那些不需要分片的库直接用普通连接池配置即可。而需要分库分表的库用代码手动构建一个 ShardingSphereDataSource然后通过 dynamic-datasource 的注册接口把它挂到某个 key 下面比如sharding-order。业务调用时想走分片数据源就写DS(sharding-order)想走普通日志库就写DS(log-db)默认不写注解落到 primary 数据源。这个方案的好处是层次清晰DS只做“逻辑数据源选择”ShardingSphere 只做“物理分片路由”。两者各自在自己的层次里工作互不干扰。ShardingSphere 对上层应用来说就是一个普通的 DataSource它可以被放进任何支持 DataSource 管理的框架里。2.3 方案 CShardingSphere 只做分表不做分库跨库交给 DS有些项目的数据量还没到需要分库的程度只是想解决单表数据量过大的问题。这种情况下可以让 ShardingSphere 只做单库分表多个业务库之间用DS切换。这种组合也很实用尤其适合“订单库单独分表 其他库普通管理”的场景。需要注意 ShardingSphere 的规则配置里只需要配置表规则数据源名只有一个不需要配置分库策略。2.4 方案取舍方案配置复杂度灵活性适用场景AShardingSphere 总入口高所有表都要纳入规则低普通表处理麻烦全库分片项目Bdynamic 总入口sharding 作为一个数据源中手动构建 sharding 数据源一次高混合业务库友好分片库和普通库共存的项目Csharding 只分表DS 负责跨库低无分库策略中单库分表为主跨库较少我在实际项目里选择的是方案 B。理由很简单项目里既有需要分片的订单表又有大量不需要分片的配置表和日志表单独一套 ShardingSphere 规则不可能同时满足两类表的路由需求而动态数据源的 key 管理方式可以让业务代码非常直观。3. 最小可运行整合配置依赖排除、数据源构建与注册选型定了下面直接上可运行的配置。这里以 Spring Boot 2.7 dynamic-datasource 4.x ShardingSphere-JDBC 5.x 为例注意 ShardingSphere 各版本之间的 API 差异比较大尤其是分片算法配置这一块下面代码中的写法在 5.4 版本下是可以用的。3.1 依赖引入避免两个 Starter 自动配置互抢ShardingSphere 官方提供了 Spring Boot Starter引入后会自动配置一个 ShardingSphereDataSource但在 dynamic-datasource 同时存在时容易发生 DataSource Bean 冲突。所以我建议只引入 ShardingSphere 的 core 依赖不使用它的 Spring Boot Starter用它提供的 API 手动构建数据源。dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version4.3.1/version /dependency dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core/artifactId version5.4.1/version /dependency如果你之前已经引入了shardingsphere-jdbc-spring-boot-starter建议先移除或者把它的自动配置排除掉。我自己的经验是与其在排除配置里来回折腾不如直接用 core 手动构建至少数据源是怎么创建出来的心里是清楚的。3.2 手动构建 ShardingSphere 数据源下面是创建一个分库分表数据源的配置类。这里模拟两个物理库 ds_0、ds_1每个库里各有一张订单分表 t_order_0、t_order_1。分库键用 user_id 取模分表键用 order_id 取模。Configuration public class ShardingDataSourceConfig { Bean(shardingOrderDataSource) public DataSource shardingOrderDataSource() throws SQLException { MapString, DataSource dataSourceMap new LinkedHashMap(); dataSourceMap.put(ds_0, createDataSource(jdbc:mysql://127.0.0.1:3306/ds_0?useSSLfalseserverTimezoneAsia/Shanghai)); dataSourceMap.put(ds_1, createDataSource(jdbc:mysql://127.0.0.1:3306/ds_1?useSSLfalseserverTimezoneAsia/Shanghai)); ShardingRuleConfiguration shardingRuleConfig new ShardingRuleConfiguration(); ShardingTableRuleConfiguration orderTableRule new ShardingTableRuleConfiguration( t_order, ds_${0..1}.t_order_${0..1} ); shardingRuleConfig.getTables().add(orderTableRule); shardingRuleConfig.setDefaultDatabaseStrategy( new InlineShardingStrategyConfiguration(user_id, ds_${user_id % 2}) ); shardingRuleConfig.setDefaultTableStrategy( new InlineShardingStrategyConfiguration(order_id, t_order_${order_id % 2}) ); Properties props new Properties(); props.setProperty(sql-show, true); return ShardingSphereDataSourceFactory.createDataSource( dataSourceMap, Collections.singleton(shardingRuleConfig), props ); } private DataSource createDataSource(String jdbcUrl) { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbcUrl); config.setUsername(root); config.setPassword(123456); config.setDriverClassName(com.mysql.cj.jdbc.Driver); return new HikariDataSource(config); } }这里有几个细节值得注意。数据源 map 里的 keyds_0、ds_1是给 ShardingSphere 路由用的名字和业务代码里的DSkey 没有关系。分片表达式里的${0..1}是范围枚举${user_id % 2}是 inline 取模这两条表达式可以直接帮助 ShardingSphere 算出目标库和目标表。sql-show打开后ShardingSphere 会在控制台打印逻辑 SQL 和实际 SQL这是后面验证路由结果最重要的手段开发阶段建议打开性能敏感时再关闭。3.3 把 ShardingSphere 数据源注册进动态路由手动构建的 ShardingSphere 数据源并不会自动被 dynamic-datasource 识别必须把它注册进动态路由数据源的 key 仓库里。我的做法是写一个启动后的注册器而不是在配置类构建阶段直接注入 dynamic 数据源因为这时候 Bean 可能还没有初始化完容易踩循环依赖。Component public class ShardingDataSourceRegistrar implements ApplicationRunner { private final DynamicRoutingDataSource dynamicRoutingDataSource; private final DataSource shardingOrderDataSource; public ShardingDataSourceRegistrar( DynamicRoutingDataSource dynamicRoutingDataSource, Qualifier(shardingOrderDataSource) DataSource shardingOrderDataSource) { this.dynamicRoutingDataSource dynamicRoutingDataSource; this.shardingOrderDataSource shardingOrderDataSource; } Override public void run(ApplicationArguments args) { String dsName sharding-order; if (dynamicRoutingDataSource.getDataSource(dsName) null) { dynamicRoutingDataSource.addDataSource(dsName, shardingOrderDataSource); } } }这段代码把 keysharding-order绑定到 ShardingSphere 数据源上。之后业务代码里的DS(sharding-order)就会拿到这个整体数据源SQL 进入后会由 ShardingSphere 完成分片路由。注意这里的DynamicRoutingDataSource是 dynamic-datasource 内部实现类。在 dynamic-datasource 4.x 版本中默认注册的 DataSource Bean 就是它构造器里直接注入即可。如果当前容器里有多个 DataSource Bean记得给注入的数据源加上Qualifier避免注入到别的数据源。3.4 application.yml普通库与分片库的共存配置普通数据源仍然走 dynamic-datasource 的 yml 配置。这里我配置了一个日志库 log-db 作为 primary这样所有没写DS的方法都会默认落到 log-db。spring: datasource: dynamic: primary: log-db strict: false datasource: log-db: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/log_db?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456strict 设置为 false 是为了保证 key 不存在时能优雅回退到 primary而不是直接抛异常。开发阶段可以先开着 strict 调试避免悄悄回退掩盖问题。3.5 Service 层使用方式注册完成后使用起来就很直接了。这里是两个典型的示例方法Service public class OrderService { DS(sharding-order) public void addOrder(OrderDO order) { orderMapper.insert(order); } DS(sharding-order) public ListOrderDO queryByUserId(Long userId) { return orderMapper.selectByUserId(userId); } }日志操作走普通库Service public class LogService { DS(log-db) public void addLog(LogDO log) { logMapper.insert(log); } }当执行addOrder时DS拿到的是 ShardingSphere 数据源SQL 进入 ShardingSphere 后被解析、改写最终落到 ds_0 或 ds_1 的 t_order_0 或 t_order_1。当执行addLog时直接走 log-db完全绕开分片逻辑。这就是整个整合方案最核心的使用方式业务层通过DS选择逻辑数据源分片 SQL 的内部路由全部交给 ShardingSphere。4. 事务、代理与初始化顺序三个高频翻车点及处理方式配置跑通之后紧接着会遇到三个比较隐蔽的问题分别出在事务、代理对象和 Bean 初始化顺序上。这些问题在功能简单时不会暴露一旦加上事务或者并发访问就容易出现“偶发但致命”的故障。4.1 事务边界与 DS 切换顺序这是一个非常经典的坑。很多人这样写Transactional public void doBiz() { orderService.addOrder(order); logService.addLog(log); }Transactional注解的事务管理器在方法执行开始时就会从 DataSource 获取连接并把它绑定到当前线程。等执行到orderService.addOrder时DS再切数据源已经晚了因为事务管理器手里的连接还是最开始那个。如果这个事务入口本身用的是 primary 数据源那么即使方法内部写了DS(sharding-order)SQL 也还是会落到 primary 对应的物理连接上。这种问题日志看不出来因为代码确实执行了只是数据跑到了错误的库里非常难排查。处理方式有三种拆分事务边界把Transactional放在真正需要事务的单个数据源操作上让DS切换发生在事务边界之外使用 dynamic-datasource 提供的DSTransactional它能感知路由数据源的切换在同一个事务里支持多数据源连接绑定如果必须使用原生Transactional就保证事务方法内部只访问同一个逻辑数据源。我在项目里更倾向于第一种。把“跨库操作不能共享本地事务”这件事想在前面远比后面去排查诡异数据要省事。ShardingSphere 的 Connection 本身是逻辑连接真正的物理连接在 SQL 执行时才确定跨分片的本地事务并不能像单库事务那样保证强一致。如果业务对一致性要求高就需要考虑分布式事务方案了。4.2 数据源 Bean 初始化顺序与循环依赖如果尝试在配置类方法里直接把 ShardingSphereDataSource 注入 DynamicRoutingDataSource可能会遇到BeanCurrentlyInCreationException这类循环依赖问题。因为 dynamic-datasource 的自动配置和手动创建的 sharding 数据源之间存在初始化先后关系不好靠不容易控制的Order去解决。我建议不要在 Bean 创建阶段做注册而是放到ApplicationRunner里也就是前面示例代码的写法。这个时机点里所有 Bean 都已经创建完成Spring 容器已经处于可用状态调用 addDataSource 是安全的。这样既避开了循环依赖也避免了数据源 key 在业务还没开始时就不可用的问题。4.3 事务管理器到底该绑定哪一个数据源Spring Boot 自动配置的事务管理器默认绑定的是容器里的 primary DataSource。在我们的整合方案里dynamic-datasource 的动态路由数据源应该是 primary所以事务管理器绑定的也就是这个动态路由数据源。事务开始时从路由数据源拿连接路由数据源再根据当前线程里的 key 去决定返回哪个真实连接。这也是为什么事务管理器只能有一套。如果你又额外给 ShardingSphere 数据源单独创建了一个事务管理器业务代码里又混着使用很容易出现事务不回滚、连接被提前释放的问题。实测下来保持“一个动态路由数据源 一个事务管理器”是最简单的。内部多数据源的事务一致性需求用 dynamic-datasource 提供的DSTransactional兜底不要自己造多事务管理器。5. 实测验证与经验清单路由日志、异常表现和最终建议配置完成后我用一个模拟订单系统做了验证核心目标是确认两点分片路由是否真的生效以及普通库和分片库之间是否能通过DS正常切换。5.1 测试环境与用例搭建了两套物理库ds_0 和 ds_1每个库里创建了 t_order_0、t_order_1 两张分表另一个独立库 log_db 存放日志。表结构保持一致分表字段为 order_id分库字段为 user_id。测试用例包括插入订单观察路由日志能否正确计算到 ds_0 / ds_1 以及对应的 t_order_0 / t_order_1查询时使用 userId 作为条件观察是否只命中一个分片插入日志到 log_db确认没有经过 ShardingSphere 路由在一个方法里同时操作两个数据源验证DS切换是否生效。5.2 路由日志验证打开sql-show后执行插入订单时控制台会输出类似下面的内容Logic SQL: INSERT INTO t_order (order_id, user_id, amount) VALUES (?, ?, ?) Actual SQL: ds_0 ::: INSERT INTO t_order_0 (order_id, user_id, amount) VALUES (?, ?, ?)这条日志非常关键。看到 Actual SQL 里的ds_0 ::: t_order_0就说明 ShardingSphere 已经按照 user_id 取模计算出了目标库按照 order_id 取模计算出了目标表。如果这里输出的是ds_0 ::: t_order说明表规则没有生效大概率是分表表达式写错了。执行日志插入时控制台不会出现 ShardingSphere 的 Logic SQL 输出说明这条 SQL 没有通过 ShardingSphere 数据源DS(log-db)的切换是生效的。5.3 常见异常与原因速查异常或现象可能原因处理方式DataSource Bean 冲突注入报错ShardingSphere Starter 和 dynamic-datasource 同时注册数据源改用 shardingsphere-jdbc-core手动构建 sharding 数据源DS 开了但 SQL 仍走 primary事务先开启连接已绑定调整事务边界或用 DSTransactional出现”表不存在”但表明明建了分表表达式把表路由到了不存在的物理表检查 inline 表达式和实际建表名是否一致ShardingSphere 规则完全没生效业务方法没有走 sharding 数据源确认 DS key 对应的是 ShardingSphereDataSource动态路由 key 找不到注册时机太早addDataSource 没执行移到 ApplicationRunner 阶段注册5.4 最终建议整套方案跑通后我对这类整合场景的体会是先画数据源关系图再写代码。把“逻辑数据源”和“物理数据源”分开画标注哪些库由 ShardingSphere 管理哪些库由动态路由直接切哪些表需要分片哪些表不能碰分片规则。这张图画清楚了后面所有配置和注解都是水到渠成的事。最后再分享一个小技巧开发阶段把 dynamic-datasource 的 strict 设为 true把 ShardingSphere 的 sql-show 打开一旦 SQL 路由到意料之外的库日志能第一时间暴露问题。上线前再关掉这两个开关性能和安全都更稳妥。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑