0. 寫在前面
到底遇到了什么問題?
簡潔版:
在 Oracle 與 Neo4j 共存的多數據源項目中,一個僅涉及 Oracle 操作的請求,卻因為 Neo4j 連接失敗而報錯。根本原因是 Spring 的默認事務管理器錯誤地指向了 Neo4j,導致不相關的請求也受到了 Neo4j 連接狀態的影響。
詳細版:
在包含 Oracle 和 Neo4j 數據庫的多數據源 Spring Boot 項目中,一個業務邏輯上僅需訪問 Oracle 數據庫的 API 請求(標記了 @Transactional ),在執行時卻意外地嘗試啟動 Neo4j 事務。當 Neo4j 數據庫無法連接時,這個本應只與 Oracle 交互的請求,反而因為 Neo4j 的連接或事務錯誤而失敗。
1. 背景
本項目是一個基于 Spring Boot 的應用,集成了多種數據源:
- 兩個 Neo4j 圖數據庫實例(分別用于開發/生產環境,通過 spring.dev.neo4j.* 和 spring.prod.neo4j.* 配置)。
- 一個 Oracle 關系型數據庫(通過 dynamic-datasource-spring-boot-starter 管理,主數據源名為 dsPrimary )。
- 使用 Mybatis-Plus 作為 Oracle 數據庫的 ORM 框架。
- 使用 Spring 的 @Transactional 注解進行事務管理。
2. 問題描述
在開發過程中,當兩個 Neo4j 數據庫實例宕機或無法連接時,調用一個 僅涉及 Oracle 數據庫 的 API(例如 /xxx/xx/saveXxx )時,應用程序拋出異常,導致該 API 不可用。
初始錯誤:
org.springframework.transaction.TransactionSystemException: Could not open a new Neo4j session: Unable to connect to [Neo4j IP]:7687...; nested exception is org.neo4j.driver.exceptions.ServiceUnavailableException: Unable to connect to [Neo4j IP]:7687...at org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager.doBegin(Neo4jTransactionManager.java:313)...
這表明即使 API 不直接操作 Neo4j,Spring 仍然嘗試啟動一個 Neo4j 事務。
3. 分析過程
- 初步診斷 : 錯誤發生在
Neo4jTransactionManager
的 doBegin 方法中。這通常意味著 Neo4j 的事務管理器被配置為 Spring 的 默認(Primary)事務管理器 。當 Spring 遇到 @Transactional 注解且未指定特定事務管理器時,它會嘗試使用默認的事務管理器,即使該方法本身不涉及 Neo4j。檢查發現,ProdNeo4jConfig.java
中的 prodTransactionManager Bean 可能被標記了 @Primary 。 - 嘗試移除 Neo4j 的 @Primary : 移除了 prodTransactionManager Bean 上的 @Primary 注解。
- 出現新錯誤 : 移除后,再次調用該 API,出現 NoUniqueBeanDefinitionException 。
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.transaction.TransactionManager' available: expected single matching bean but found 2: devTransactionManager,prodTransactionManager
這表明:
- 該 API 確實需要事務管理(其對應 Service 方法上有 @Transactional 注解)。
- Spring 容器中存在多個 PlatformTransactionManager 類型的 Bean(至少有 devTransactionManager 和 prodTransactionManager )。
- 由于沒有 Bean 被標記為 @Primary ,Spring 無法確定默認使用哪一個。
- 區分默認數據源與主事務管理器 : 我在
application-dev.yml
中配置了 spring.datasource.dynamic.primary: dsPrimary 。需要明確,此配置僅告知 dynamic-datasource-spring-boot-starter 庫哪個數據源是默認的, 并不能 指定哪個 PlatformTransactionManager Bean 是 Spring 事務管理的 @Primary Bean。 - 嘗試切換數據源配置 : 為了簡化問題,嘗試將數據源配置從 dynamic-datasource 改回標準的 spring.datasource.druid.* 。
- 問題 5.1 : 啟動報錯 CannotFindDataSourceException: dynamic-datasource can not find primary datasource 。原因是
application.yml
中排除了 com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure ,導致 Spring Boot 無法根據 spring.datasource.druid.* 自動創建 DataSource 。 - 解決 5.1 : 移除對 DruidDataSourceAutoConfigure 的排除。
- 問題 5.1 : 啟動報錯 CannotFindDataSourceException: dynamic-datasource can not find primary datasource 。原因是
spring:autoconfigure:exclude: # - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
- 問題 5.2 : 啟動后仍然報錯 NoUniqueBeanDefinitionException ,且錯誤信息中 只列出了 Neo4j 的事務管理器 ( devTransactionManager , prodTransactionManager )。這表明即使啟用了 Druid 自動配置,Oracle 對應的 DataSourceTransactionManager 也沒有被成功創建或注冊為 Bean,或者 Spring 因某種原因未能找到它。
- 確定最終方向 : 無論是使用標準 Druid 配置還是 dynamic-datasource ,最可靠的方法是 顯式地在 Java 配置中定義 Oracle 數據庫(即 dsPrimary )對應的事務管理器,并將其標記為 @Primary 。
4. 解決方案
決定繼續使用 dynamic-datasource-spring-boot-starter 以保留其靈活性,并通過 Java 配置顯式定義主事務管理器。
在 對應工程的對應目錄下新增(或者修改對應的)一個配置類 DataSourceConfig.java :
package com.xxx.xxxx.config; // 使用項目實際的包路徑import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;@Configuration
public class DataSourceConfig {/*** 顯式定義與動態數據源關聯的事務管理器。* @param dataSource Spring 容器會自動注入由 dynamic-datasource-spring-boot-starter 創建的代理 DataSource Bean。* 這個代理 DataSource 知道如何根據上下文切換到 dsPrimary 或其他數據源。* @return 標記為 @Primary 的事務管理器*/@Bean("transactionManager") // 使用標準的 "transactionManager" 作為 Bean 名稱@Primary // <--- 關鍵:標記為主要事務管理器public PlatformTransactionManager transactionManager(DataSource dataSource) {// 使用注入的動態數據源代理來創建事務管理器return new DataSourceTransactionManager(dataSource);}
}
實施效果:
添加此配置類后,Spring 容器中存在三個 PlatformTransactionManager Bean:
- devTransactionManager (Neo4j)
- prodTransactionManager (Neo4j)
- transactionManager (Oracle, 使用動態數據源代理, @Primary )
當調用僅涉及 Oracle 且標記了 @Transactional 的 API 時,Spring 會自動選用被 @Primary 標記的 transactionManager ,不再嘗試使用 Neo4j 的事務管理器,也解決了 NoUniqueBeanDefinitionException 。應用程序在 Neo4j 宕機時,涉及 Oracle 的 API 可以正常工作。
5. 關鍵點總結
- 在 Spring Boot 中, @Primary 注解用于指定在存在多個同類型 Bean 時應優先注入或使用的 Bean。對于事務管理,它指定了默認的 PlatformTransactionManager 。
- dynamic-datasource-spring-boot-starter 的 spring.datasource.dynamic.primary 配置項用于指定該庫內部的默認數據源,與 Spring 的 @Primary 事務管理器是兩個不同的概念。
- 在包含多個事務管理器(例如,連接不同類型數據庫)的環境中,必須明確指定一個事務管理器為 @Primary ,以供未顯式指定事務管理器名稱的 @Transactional 注解使用。
- 當使用 dynamic-datasource-spring-boot-starter 時,配置 @Primary 的 DataSourceTransactionManager 需要注入由該庫提供的 代理 DataSource Bean 。
- 注意檢查 spring.autoconfigure.exclude 配置,避免意外排除了必要的自動配置類。
6. 涉及文件
application.yml
server:port: 8080servlet:context-path: /xxxspring:profiles:active: devautoconfigure:exclude: - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure- org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveDataAutoConfigurationdata:neo4j:database: neo4jprod:neo4j:uri: bolt://ip:port1authentication:username: xxxpassword: xxxxdatabase: xxxdev:neo4j:uri: bolt://ip:port2authentication:username: xxxpassword: xxxxxdatabase: xxxdatasource: dynamic: strict: falseprimary: dsPrimarydruid: validation-query: SELECT 1 FROM DUALinitial-size: 5min-idle: 0max-active: 100max-wait: 10000# 配置間隔多久才進行一次檢測,檢測需要關閉的空閑連接,單位是毫秒time-between-eviction-runs-millis: 30000# 配置一個連接在池中最小生存的時間,單位是毫秒min-evictable-idle-time-millis: 1800000test-while-idle: truetest-on-borrow: falsetest-on-return: false#線程溢出檢測控制remove-abandoned: true#線程溢出時間控制(秒)remove-abandoned-timeout-millis: 120#線程溢出日志log-abandoned: false# 是否緩存preparedStatement,也就是PSCachepool-prepared-statements: falsemax-pool-prepared-statement-per-connection-size: 0# 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄connection-properties: druid: stat: # 合并參數化的SQLmergeSql: trueslowSqlMillis: 5000# 配置監控統計攔截的filters,去掉后監控界面sql無法統計,'wall'用于防火墻,'log4j'是用來輸出統計數據的filters: statmybatis-plus:configuration:# 駝峰命名,默認true-開啟map-underscore-to-camel-case: falsejdbc-type-for-null: 'null'global-config:db-config:# 字段驗證策略,not-null默認策略,不會對null做處理update-strategy: ignoredinsert-strategy: not-nullmapper-locations: classpath*:/mapper/*Mapper.xml,classpath*:/mapper/**/*Mapper.xml
application-dev.yml
spring: neo4j:uri: bolt://ip:portdata:neo4j:database: xxxdatasource: dynamic: datasource: dsPrimary: driver-class-name: oracle.jdbc.OracleDriverurl: jdbc:oracle:thin:@ip:port/xxxusername: xxxpassword: xxxxxx
neo4j:authentication:username: xxxpassword: xxxxx
DevNeo4jConfig.java
import org.neo4j.driver.AuthToken;
import org.neo4j.driver.Config;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.transaction.PlatformTransactionManager;import java.net.URI;@Configuration
@ConditionalOnProperty(prefix = "spring.dev.neo4j", name = "uri")
public class DevNeo4jConfig extends AbstractMultiNeo4jConfig {@Bean("devCypherService")public CypherService devCypherService(@Qualifier("devNeo4jClient") Neo4jClient neo4jClient) {return new CypherServiceImpl(neo4jClient);}@Bean("devCypherQueryService")public CypherQueryService devCypherQueryService(@Qualifier("devNeo4jClient") Neo4jClient neo4jClient) {return new CypherQueryServiceImpl(neo4jClient);}@Bean("devNeo4jClient")public Neo4jClient neo4jClient(@Qualifier("devDriver") Driver driver,@Qualifier("devDatabaseSelectionProvider") DatabaseSelectionProvider databaseNameProvider) {return Neo4jClient.create(driver, databaseNameProvider);}@Bean@ConfigurationProperties(prefix = "spring.dev.neo4j")public KbNeo4jProperties devNeo4jProperties() {return new KbNeo4jProperties();}/*** The driver to be used for interacting with Neo4j.** @return the Neo4j Java driver instance to work with.*/@Bean("devDriver")@Overridepublic Driver driver() {AuthToken authToken = mapAuthToken(devNeo4jProperties().getAuthentication());Config config = mapDriverConfig(devNeo4jProperties());URI serverUri = determineServerUri(devNeo4jProperties());return GraphDatabase.driver(serverUri, authToken, config);}@Bean("devNeo4jTemplate")@Overridepublic Neo4jTemplate neo4jTemplate(final @Qualifier("devNeo4jClient") Neo4jClient neo4jClient,final Neo4jMappingContext mappingContext,@Qualifier("devDatabaseSelectionProvider") DatabaseSelectionProvider databaseNameProvider) {return new Neo4jTemplate(neo4jClient, mappingContext, databaseNameProvider);}@Bean("devTransactionManager")@Overridepublic PlatformTransactionManager transactionManager(@Qualifier("devDriver") Driver driver,@Qualifier("devDatabaseSelectionProvider") DatabaseSelectionProvider databaseNameProvider) {return super.transactionManager(driver, databaseNameProvider);}@Bean("devDatabaseSelectionProvider")@Overrideprotected DatabaseSelectionProvider databaseSelectionProvider() {String database = devNeo4jProperties().getDatabase();return (database != null) ? DatabaseSelectionProvider.createStaticDatabaseSelectionProvider(database): DatabaseSelectionProvider.getDefaultSelectionProvider();}@Bean("devNeo4jImportService")public Neo4jImportServiceImpl neo4jImportService(@Qualifier("devDriver") Driver driver) {return new Neo4jImportServiceImpl(driver);}
}
ProdNeo4jConfig.java
import org.neo4j.driver.AuthToken;
import org.neo4j.driver.Config;
import org.neo4j.driver.Driver;
import org.neo4j.driver.GraphDatabase;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.neo4j.core.DatabaseSelectionProvider;
import org.springframework.data.neo4j.core.Neo4jClient;
import org.springframework.data.neo4j.core.Neo4jTemplate;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.transaction.PlatformTransactionManager;import java.net.URI;@Configuration
@ConditionalOnProperty(prefix = "spring.prod.neo4j", name = "uri")
public class ProdNeo4jConfig extends AbstractMultiNeo4jConfig {@Bean("prodCypherService")public CypherService prodCypherService(@Qualifier("prodNeo4jClient") Neo4jClient neo4jClient) {return new CypherServiceImpl(neo4jClient);}@Bean("prodCypherQueryService")public CypherQueryService prodCypherQueryService(@Qualifier("prodNeo4jClient") Neo4jClient neo4jClient) {return new CypherQueryServiceImpl(neo4jClient);}@Bean("prodNeo4jClient")public Neo4jClient neo4jClient(@Qualifier("prodDriver") Driver driver,@Qualifier("prodDatabaseSelectionProvider") DatabaseSelectionProvider databaseNameProvider) {return Neo4jClient.create(driver, databaseNameProvider);}@Bean@ConfigurationProperties(prefix = "spring.prod.neo4j")public KbNeo4jProperties prodNeo4jProperties() {return new KbNeo4jProperties();}/*** The driver to be used for interacting with Neo4j.** @return the Neo4j Java driver instance to work with.*/@Bean("prodDriver")@Overridepublic Driver driver() {AuthToken authToken = mapAuthToken(prodNeo4jProperties().getAuthentication());Config config = mapDriverConfig(prodNeo4jProperties());URI serverUri = determineServerUri(prodNeo4jProperties());return GraphDatabase.driver(serverUri, authToken, config);}@Bean("prodNeo4jTemplate")@Overridepublic Neo4jTemplate neo4jTemplate(final @Qualifier("prodNeo4jClient") Neo4jClient neo4jClient,final Neo4jMappingContext mappingContext,@Qualifier("prodDatabaseSelectionProvider") DatabaseSelectionProvider databaseNameProvider) {return new Neo4jTemplate(neo4jClient, mappingContext, databaseNameProvider);}@Bean("prodTransactionManager")
// @Primary@Overridepublic PlatformTransactionManager transactionManager(@Qualifier("prodDriver") Driver driver,@Qualifier("prodDatabaseSelectionProvider") DatabaseSelectionProvider databaseNameProvider) {return super.transactionManager(driver, databaseNameProvider);}@Bean("prodDatabaseSelectionProvider")@Overrideprotected DatabaseSelectionProvider databaseSelectionProvider() {String database = prodNeo4jProperties().getDatabase();return (database != null) ? DatabaseSelectionProvider.createStaticDatabaseSelectionProvider(database): DatabaseSelectionProvider.getDefaultSelectionProvider();}@Bean("prodNeo4jImportService")public Neo4jImportServiceImpl neo4jImportService(@Qualifier("prodDriver") Driver driver) {return new Neo4jImportServiceImpl(driver);}
}
DataSourceConfig.java
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;@Configuration
public class DataSourceConfig {/*** 顯式定義與動態數據源關聯的事務管理器。* @param dataSource Spring 容器會自動注入由 dynamic-datasource-spring-boot-starter 創建的代理 DataSource Bean。* 這個代理 DataSource 知道如何根據上下文切換到 dsPrimary 或其他數據源。* @return 標記為 @Primary 的事務管理器*/@Bean("transactionManager") // 使用標準的 "transactionManager" 作為 Bean 名稱@Primary // <--- 關鍵:標記為主要事務管理器public PlatformTransactionManager transactionManager(DataSource dataSource) {// 使用注入的動態數據源代理來創建事務管理器return new DataSourceTransactionManager(dataSource);}// 通常不需要在這里手動配置 DataSource Bean,// dynamic-datasource-spring-boot-starter 會根據 application-dev.yml 中的配置自動完成。
}