☰
SpringBoot多数据源三大落地方案:静态配置、动态路由与ShardingSphere集成
2026/10/17 1:11:35 网站建设 项目流程

1. 项目概述:为什么多数据源不是“选做题”,而是SpringBoot项目的必答题

在某高校实验室做后端系统开发时,我接手了一个典型的业务中台项目:前端要展示销售数据、库存数据和用户行为日志,三类数据分别存放在MySQL、PostgreSQL和MongoDB里。当时第一反应是——写三个独立服务?但运维成本、接口协调、事务一致性全会崩盘。后来发现,真正能落地的方案,其实是让一个SpringBoot应用原生支持多个数据库连接,按需路由、隔离管理、互不干扰。这就是“多数据源”的真实战场:它不是教科书里的概念演示,而是每天都在发生的生产级需求。

核心关键词“SpringBoot”“多数据源”“三种方式”,背后对应的是三类典型场景:

  • 读写分离:主库写、从库读,扛住高并发查询;
  • 异构数据库共存:MySQL存订单、Elasticsearch做搜索、Redis缓存热点;
  • 租户隔离或业务域拆分:不同客户/部门的数据物理隔离,避免SQL注入穿透或误删。

这三种方式不是并列选项,而是层层递进的技术演进路径:最轻量的是静态配置+手动切换,适合单体架构初期验证;进阶是动态数据源路由(AbstractRoutingDataSource),支撑运行时按规则切换;最高阶是JDBC代理层集成(如ShardingSphere-JDBC),把分库分表、读写分离、加密脱敏全收编。很多人卡在第二步就止步不前,结果上线后遇到事务跨库失败、MyBatis拦截器失效、Druid监控指标错乱等问题,最后被迫推倒重来。

我试过用@Primary注解硬编码两个DataSource Bean,结果单元测试跑通,一上预发环境就报Cannot determine embedded database driver class for database type NONE——因为Spring Boot自动配置在扫描到多个DataSource时直接放弃了内嵌数据库推断逻辑。后来才明白:多数据源的本质,不是“加几个Bean”,而是重构Spring的JDBC生命周期管理链路。从DataSource初始化、TransactionManager绑定、JdbcTemplate装配,到MyBatis的SqlSessionFactory构建,每个环节都得重新对齐。这篇文章不讲API调用,只拆解这三条路径的真实落地细节:每种方式的启动原理、事务边界怎么划、MyBatis怎么适配、Druid监控怎么看、以及我踩过的7个致命坑——比如动态路由下@Transactional失效的底层原因,还有ShardingSphere里spring.shardingsphere.props.sql-show=true开启后日志刷屏导致磁盘打满的事故复盘。

2. 方案一:静态配置+手动切换——最简但最易翻车的入门姿势

2.1 核心设计逻辑:用@Bean硬编码隔离,靠@Qualifier精准投送

静态配置的本质,是放弃Spring Boot的自动配置魔法,回归Spring原始的Bean管理思维。当项目里存在MySQL和PostgreSQL两个库时,我们不依赖spring.datasource.*的默认配置,而是显式声明两个DataSource Bean,并用@Qualifier标注区分。这样做的好处是零学习成本、调试直观;坏处是完全丧失运行时灵活性——切换数据库必须改代码、重启服务。

关键在于理解Spring的Bean注入机制:@Autowired默认按类型匹配,当容器里有多个DataSource类型Bean时,必须用@Qualifier("mysqlDataSource")指定具体实例。而@Primary只能标记一个“首选Bean”,无法解决多实例共存问题。我最初犯的错误,就是给两个DataSource都加了@Primary,结果Spring直接抛出NoUniqueBeanDefinitionException。

提示:@Primary不是“设为默认”,而是“当@Autowired没加@Qualifier时,优先选这个”。多数据源场景下,它反而会制造歧义。

2.2 实操步骤与配置细节:从application.yml到Mapper层穿透

第一步,在application.yml中定义两套独立配置:

spring: datasource: mysql: url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver postgres: url: jdbc:postgresql://localhost:5432/log_db username: postgres password: 123456 driver-class-name: org.postgresql.Driver

注意:这里不能写成spring.datasource.url这种顶层配置,否则Spring Boot会尝试用同一套参数初始化两个DataSource,必然失败。

第二步,编写配置类DataSourceConfig.java,手动构建Bean:

@Configuration public class DataSourceConfig { @Bean("mysqlDataSource") @ConfigurationProperties("spring.datasource.mysql") public DataSource mysqlDataSource() { return DataSourceBuilder.create().build(); } @Bean("postgresDataSource") @ConfigurationProperties("spring.datasource.postgres") public DataSource postgresDataSource() { return DataSourceBuilder.create().build(); } }

@ConfigurationProperties的作用是把yml里spring.datasource.mysql.*的值自动注入到DataSourceBuilder中,比手写setUrl()安全得多——它会校验driver-class-name是否存在,避免运行时报ClassNotFoundException。

第三步,声明对应的JdbcTemplate和TransactionManager:

@Bean("mysqlJdbcTemplate") public JdbcTemplate mysqlJdbcTemplate(@Qualifier("mysqlDataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); } @Bean("postgresJdbcTemplate") public JdbcTemplate postgresJdbcTemplate(@Qualifier("postgresDataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); } @Bean("mysqlTransactionManager") public PlatformTransactionManager mysqlTransactionManager(@Qualifier("mysqlDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } @Bean("postgresTransactionManager") public PlatformTransactionManager postgresTransactionManager(@Qualifier("postgresDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }

这里有个隐藏陷阱:DataSourceTransactionManager必须和对应DataSource强绑定。如果把mysqlDataSource传给postgresTransactionManager,事务注解会静默失效——因为事务管理器根本不知道该管哪个连接。

第四步,Mapper层使用:以MyBatis为例,需要为每个数据源单独配置SqlSessionFactory:

@Bean("mysqlSqlSessionFactory") public SqlSessionFactory mysqlSqlSessionFactory(@Qualifier("mysqlDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/mysql/*.xml")); return bean.getObject(); } @Bean("postgresSqlSessionFactory") public SqlSessionFactory postgresSqlSessionFactory(@Qualifier("postgresDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/postgres/*.xml")); return bean.getObject(); }

setMapperLocations指定了XML映射文件路径,确保MySQL的Mapper只加载mapper/mysql/下的文件,避免SQL语句错配数据库方言。

2.3 实操心得:新手最容易忽略的5个细节

  1. Druid监控页面只显示一个数据源:默认Druid会监控dataSource这个Bean,但你有两个DataSource。解决方案是为每个DataSource单独配置DruidFilter:

    @Bean("mysqlDataSource") @ConfigurationProperties("spring.datasource.mysql") public DataSource mysqlDataSource() { DruidDataSource dataSource = new DruidDataSource(); // 手动设置Druid属性 dataSource.setFilters("stat,wall"); dataSource.setConnectionProperties("druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000"); return dataSource; }

    然后在application.yml中关闭自动配置:spring.datasource.druid.enabled=false,否则Druid会抢注自己的Bean。

  2. @Transactional注解必须指定transactionManager:

    @Transactional(transactionManager = "mysqlTransactionManager") public void createOrder(Order order) { ... }

    如果不指定,Spring会找@Primary的TransactionManager,而你可能没标——结果事务根本没生效。

  3. MyBatis的@MapperScan要分包隔离:

    @MapperScan(basePackages = "com.example.mapper.mysql", sqlSessionFactoryRef = "mysqlSqlSessionFactory") @MapperScan(basePackages = "com.example.mapper.postgres", sqlSessionFactoryRef = "postgresSqlSessionFactory")

    否则MyBatis会把所有Mapper扫进同一个SqlSessionFactory,导致PostgreSQL的SQL在MySQL里执行报错。

  4. 单元测试时HikariCP连接池初始化失败:
    原因是@Test方法启动时,Spring Boot会尝试加载所有DataSource,但测试环境可能没配PostgreSQL。解决方案是在测试配置里用@Profile("test")隔离:

    @Configuration @Profile("test") public class TestDataSourceConfig { @Bean("postgresDataSource") public DataSource testPostgresDataSource() { return new HikariDataSource(); // 返回空池,避免连接失败 } }
  5. Lombok的@Data在实体类里引发序列化冲突:
    当MySQL实体用@Table(name="order"),PostgreSQL实体也用同名注解,Jackson序列化时会混淆。必须为每个实体指定@JsonTypeName:

    @JsonTypeName("mysql-order") public class MysqlOrder { ... } @JsonTypeName("postgres-log") public class PostgresLog { ... }

3. 方案二:动态数据源路由——用AbstractRoutingDataSource实现运行时智能分发

3.1 核心原理:ThreadLocal + 查表路由,让数据源选择变成“查字典”

静态配置的硬伤在于无法应对动态场景:比如根据HTTP Header里的X-Tenant-ID决定查哪个库,或者按订单ID哈希值路由到16个分库中的某一个。这时必须升级到动态路由模式。Spring官方提供的AbstractRoutingDataSource是唯一正解——它本身不持有真实连接,而是一个“路由代理”,每次getConnection()时,通过determineCurrentLookupKey()方法返回一个key,再从内部targetDataSourcesMap里查出对应的真实DataSource。

关键设计在于determineCurrentLookupKey()的实现逻辑。我最初想用HttpServletRequest获取Header,结果发现AbstractRoutingDataSource是单例Bean,而Request对象是线程级的——必须用ThreadLocal暂存上下文。于是架构变成:
Controller层→TenantContext.setTenantId("tenant-a")(存入ThreadLocal)
RoutingDataSource→determineCurrentLookupKey()→return TenantContext.getTenantId()
目标DataSource→ 从Map里取出tenant-a对应的DataSource

整个过程像快递分拣:包裹(SQL请求)进来,扫描单号(ThreadLocal里的tenantId),扔进对应格子(targetDataSources里的DataSource)。

3.2 完整实现:从上下文管理到MyBatis拦截器适配

第一步,定义TenantContext工具类:

public class TenantContext { private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); public static void setTenantId(String tenantId) { CONTEXT_HOLDER.set(tenantId); } public static String getTenantId() { return CONTEXT_HOLDER.get(); } public static void clear() { CONTEXT_HOLDER.remove(); } }

第二步,实现动态路由数据源:

public class DynamicRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { // 返回null时,走defaultTargetDataSource return TenantContext.getTenantId(); } }

第三步,在配置类中组装:

@Configuration public class DynamicDataSourceConfig { @Bean public DataSource dynamicDataSource() { DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource(); // 构建目标数据源Map Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("tenant-a", mysqlDataSourceA()); targetDataSources.put("tenant-b", mysqlDataSourceB()); targetDataSources.put("tenant-c", postgresDataSourceC()); routingDataSource.setTargetDataSources(targetDataSources); routingDataSource.setDefaultTargetDataSource(mysqlDataSourceA()); // 默认走tenant-a routingDataSource.afterPropertiesSet(); // 必须调用,否则targetDataSources不生效 return routingDataSource; } @Bean @ConfigurationProperties("spring.datasource.tenant-a") public DataSource mysqlDataSourceA() { return DataSourceBuilder.create().build(); } // 其他数据源同理... }

afterPropertiesSet()是关键:它触发AbstractRoutingDataSource的初始化,把targetDataSources复制到内部resolvedDataSources缓存里。漏掉这行,路由永远走默认数据源。

第四步,解决MyBatis的SqlSessionFactory适配问题。动态路由下,所有Mapper都走同一个SqlSessionFactory,但SqlSessionFactory在创建时已绑定固定DataSource。解决方案是用MyBatis的插件机制,在Executor执行前切换数据源:

@Intercepts(@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}))) public class DynamicDataSourcePlugin implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; // 根据SQL的namespace判断归属,比如"com.example.mapper.UserMapper.insert" if (ms.getNamespace().contains("UserMapper")) { TenantContext.setTenantId("tenant-a"); } else if (ms.getNamespace().contains("LogMapper")) { TenantContext.setTenantId("tenant-b"); } return invocation.proceed(); } }

但这种方式耦合太重。更优雅的是在Service层显式声明:

@Service public class OrderService { @Autowired private JdbcTemplate jdbcTemplate; // 注入动态数据源的JdbcTemplate public void createOrderForTenant(String tenantId, Order order) { TenantContext.setTenantId(tenantId); // 切换上下文 try { jdbcTemplate.update("INSERT INTO orders VALUES (?, ?)", order.getId(), order.getAmount()); } finally { TenantContext.clear(); // 必须清理,避免线程复用污染 } } }

finally块里的clear()是生死线:Tomcat线程池会复用线程,如果不清除,下一个请求可能继承上一个tenantId,导致数据写错库。

3.3 事务管理的致命陷阱与绕过方案

动态路由最大的雷区是@Transactional。Spring事务基于DataSourceTransactionManager,而它在事务开始时就锁定了DataSource。但AbstractRoutingDataSource的determineCurrentLookupKey()在每次getConnection()时才调用——这意味着:

  • 事务开启时,getConnection()被调用一次,拿到tenant-a的连接;
  • Service里调用另一个Mapper时,再次getConnection(),又调用determineCurrentLookupKey(),可能返回tenant-b;
  • 结果一个事务里混用了两个库的连接,事务彻底失效。

实测案例:我写了一个方法,先插入订单(tenant-a),再记录日志(tenant-b),加了@Transactional。结果订单成功,日志也成功,但手动抛异常后,订单没回滚——因为两个操作用了不同连接,根本不在同一个事务上下文里。

解决方案只有两个:

  1. 强制单库事务:所有操作限定在同一tenantId下,用@Transactional(transactionManager = "dynamicTransactionManager"),但DynamicRoutingDataSource本身不能直接当TransactionManager用,必须包装:

    @Bean public PlatformTransactionManager dynamicTransactionManager() { return new DataSourceTransactionManager(dynamicDataSource()); }

    这样事务管理器会绑定dynamicDataSource,但实际执行时仍会路由到具体库——前提是determineCurrentLookupKey()返回的key在整个事务周期内不变。

  2. 放弃声明式事务,改用编程式:

    @Autowired private TransactionTemplate transactionTemplate; public void createOrderWithLog(String tenantId, Order order) { TenantContext.setTenantId(tenantId); transactionTemplate.execute(status -> { try { // 插入订单 jdbcTemplate.update("INSERT INTO orders...", order.getId()); // 切换到日志库 TenantContext.setTenantId("log-tenant"); jdbcTemplate.update("INSERT INTO logs...", order.getId()); } catch (Exception e) { status.setRollbackOnly(); throw e; } return null; }); }

    这种写法明确控制事务边界,但代码侵入性强,且status.setRollbackOnly()只能回滚当前连接,跨库依然无解。

注意:跨库事务本质是分布式事务,必须用Seata或XA协议。动态路由只解决“单事务单库”场景,别试图用它实现跨库ACID。

4. 方案三:JDBC代理层集成——用ShardingSphere-JDBC接管全链路

4.1 架构定位:不是替代Spring,而是成为JDBC协议的“中间件”

当业务发展到分库分表、读写分离、影子库压测阶段,自己手写路由逻辑就力不从心了。这时该引入ShardingSphere-JDBC——它不是ORM框架,而是JDBC Driver的增强实现。它的核心价值在于:把数据库访问的复杂性下沉到协议层,上层代码完全无感。你写的还是jdbcTemplate.query("SELECT * FROM users WHERE id = ?", 1),但背后可能经历了:

  • SQL解析 → 路由到users_001分表 → 主库写入 → 从库同步 → 缓存更新

ShardingSphere-JDBC的定位很清晰:它替换掉你原来的MySQL Driver(com.mysql.cj.jdbc.Driver),自己注册一个org.apache.shardingsphere.driver.ShardingSphereDriver,然后在getConnection()时,把原始JDBC URL(如jdbc:mysql://127.0.0.1:3306/demo)解析成逻辑库名,再根据分片规则映射到真实物理库。

这带来两个革命性变化:

  • 零代码改造:不用改任何Mapper、Service,连application.yml里的spring.datasource.url都只需换Driver类;
  • 能力可插拔:分库分表、读写分离、数据脱敏、影子库,全靠YAML配置开关,不用写一行Java代码。

我参与过一个电商订单系统迁移:原单库单表,峰值QPS 2000,响应延迟超800ms。接入ShardingSphere后,把orders表按order_id % 4分4库8表,同时配置一主两从读写分离。上线后QPS提升到1.2万,平均延迟降到98ms——整个过程只改了3行配置,没动一行业务代码。

4.2 配置实战:从分库分表到读写分离的完整YAML清单

application.yml配置如下(精简版):

spring: shardingsphere: props: sql-show: true # 开启SQL日志,调试必备 executor-size: 16 # JDBC执行线程数 datasource: names: ds-master-0,ds-slave-0,ds-slave-1,ds-master-1 ds-master-0: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.10:3306/order_db_0?serverTimezone=Asia/Shanghai username: root password: 123456 ds-slave-0: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.11:3306/order_db_0?serverTimezone=Asia/Shanghai username: root password: 123456 ds-slave-1: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.12:3306/order_db_0?serverTimezone=Asia/Shanghai username: root password: 123456 ds-master-1: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://192.168.1.13:3306/order_db_1?serverTimezone=Asia/Shanghai username: root password: 123456 rules: - !SHARDING tables: orders: actual-data-nodes: ds-master-$->{0..1}.orders_$->{0..3} # ds-master-0.orders_000 ~ ds-master-1.orders_003 table-strategy: standard: sharding-column: order_id sharding-algorithm-name: table-inline database-strategy: standard: sharding-column: user_id sharding-algorithm-name: db-inline default-database-strategy: none: # 默认不路由,走ds-master-0 sharding-algorithms: table-inline: type: INLINE props: algorithm-expression: orders_$->{order_id % 4} db-inline: type: INLINE props: algorithm-expression: ds-master-$->{user_id % 2} - !READWRITE_SPLITTING >// 在Service里 if (params.getOrderId() == null) { throw new IllegalArgumentException("分片键order_id不能为空"); }
  • JOIN查询只支持单库关联:
    SELECT o.*, u.name FROM orders o JOIN users u ON o.user_id = u.id会失败,因为orders和users可能在不同库。必须拆成两条SQL,用应用层组装。

  • ORDER BY和LIMIT分页必须带分片键:
    SELECT * FROM orders ORDER BY create_time LIMIT 10会从每个分表取10条,再内存合并——结果可能漏掉数据。正确写法:

    SELECT * FROM orders WHERE order_id IN (1,2,3,...) ORDER BY create_time LIMIT 10
  • Druid监控看到的连接数是“逻辑连接”,不是物理连接:
    ShardingSphere-JDBC会维护自己的连接池,Druid监控的ActiveCount显示的是逻辑连接数,而PhysicalConnectCount才是真实数据库连接。排查连接泄漏时,必须看ShardingSphere的sharding_sphere_metrics端点。

  • sql-show: true在生产环境必须关闭:
    我们曾在线上开启此配置,结果日志每秒写入2GB,磁盘10分钟打满。正确做法是用sharding-sphere-agent做采样监控,或通过/actuator/shardingsphere/metrics端点查慢SQL。

  • 5. 常见问题与排查技巧实录:从启动失败到线上事故的全链路诊断

    5.1 启动阶段高频报错与根因分析

    报错信息根本原因解决方案
    Failed to configure a DataSource: 'url' attribute is not specifiedSpring Boot自动配置未关闭,仍在扫描spring.datasource.*顶层配置在application.yml中添加spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
    Cannot resolve reference to bean 'dataSource' while setting constructor argument多个DataSource Bean未用@Qualifier标注,导致注入歧义检查所有@Autowired DataSource字段,必须加@Qualifier("xxx")
    java.lang.IllegalStateException: No target DataSource configuredAbstractRoutingDataSource.setTargetDataSources()未调用afterPropertiesSet()在配置类中显式调用routingDataSource.afterPropertiesSet()
    Caused by: java.sql.SQLException: Access denied for user ''@'localhost'@ConfigurationProperties未正确绑定,用户名密码为空检查yml缩进是否正确,spring.datasource.mysql.username不能写成spring.datasource.mysql .username(多空格)

    5.2 运行时典型故障与现场诊断法

    故障1:事务不回滚,但日志显示Transaction rolled back because it has been marked as rollback-only

    • 现象:Service方法里抛异常,@Transactional没生效,数据库数据已提交。
    • 诊断:用Arthaswatch命令监控DataSourceTransactionManager.doBegin():
      watch org.springframework.jdbc.datasource.DataSourceTransactionManager doBegin '{params[0].toString(), params[1].isReadOnly()}' -n 5
      如果发现params[0](DataSource)和你的动态路由数据源不一致,说明事务管理器绑错了Bean。
    • 根因:@Transactional默认用@Primary的TransactionManager,而你没标或标错了。
    • 修复:显式指定@Transactional(transactionManager = "dynamicTransactionManager")。

    故障2:MyBatis查询返回空,但日志显示SQL已执行

    • 现象:jdbcTemplate.queryForObject()正常,userMapper.selectById(1)返回null,日志里却有SELECT * FROM users WHERE id = 1。
    • 诊断:检查MyBatis的SqlSessionFactory是否绑定了正确的DataSource。用@PostConstruct打印:
      @PostConstruct public void checkDataSource() { System.out.println("Mapper DataSource: " + sqlSessionFactory.getConfiguration().getEnvironment().getDataSource()); }
      如果输出的是HikariDataSource而非你的DynamicRoutingDataSource,说明SqlSessionFactory配置错了。
    • 根因:@MapperScan的sqlSessionFactoryRef指向了静态配置的Bean。
    • 修复:确保sqlSessionFactoryRef指向动态数据源的SqlSessionFactory。

    故障3:ShardingSphere路由到错误分表,SELECT * FROM orders WHERE order_id = 1001查不到数据

    • 现象:计算1001 % 4 = 1,应该在orders_001表,但实际查的是orders_000。
    • 诊断:开启sql-show: true,看日志里执行的SQL是SELECT * FROM orders_000 WHERE order_id = 1001。
    • 根因:分片算法配置错误。algorithm-expression: orders_$->{order_id % 4}生成的是orders_0,orders_1,orders_2,orders_3,但物理表名是orders_000~orders_003。
    • 修复:改成orders_$->{String.format("%03d", order_id % 4)},或统一物理表名为orders_0~orders_3。

    5.3 性能压测专项排查清单

    当多数据源上线后QPS骤降,按此顺序排查:

    1. 连接池耗尽:用jstack查线程堆栈,看是否大量线程卡在HikariPool.getConnection();
    2. SQL未走索引:ShardingSphere路由后,物理表的索引是否覆盖分片键?用EXPLAIN验证;
    3. Druid监控延迟飙升:检查DataSource的maxWait参数,默认30秒,应设为5000ms;
    4. ShardingSphere解析开销:开启props.sql-show后,看SQL解析耗时是否超10ms,超则需优化分片算法复杂度;
    5. JVM GC频繁:ShardingSphere的SQL解析会创建大量临时对象,建议用G1垃圾收集器,-XX:+UseG1GC。

    最后分享一个小技巧:在DynamicRoutingDataSource.determineCurrentLookupKey()里加日志,打印Thread.currentThread().getName()和TenantContext.getTenantId(),能快速定位线程上下文污染问题。我靠这行日志揪出过三次线程复用导致的数据错乱事故。

    需要专业的网站建设服务?

    联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

    立即咨询