一. 為什么要連接多數(shù)據(jù)源
springboot下使用spring-data-jpa連接數(shù)據(jù)庫配置非常方便,只需要在application.properties
簡單的幾行配置就能搞定。
有些時候我們需要在一個項目里面連接多個數(shù)據(jù)庫炎功,如常見的數(shù)據(jù)庫主從分離,將部分查詢請求分流到只讀從庫里邻寿,降低主庫的壓力伯复。
這種時候搞莺,就不能通過簡單的幾行配置來搞定了撵溃;需要手動進行一些配置才行疚鲤。
二. springboot下連接多數(shù)據(jù)源的兩種方案
目前有兩種方案可以解決這個問題
- 為每個數(shù)據(jù)源配置一套
dataSource
,并針對每個dataSource
配置一套jpa和事務管理器缘挑。 - 為每個數(shù)據(jù)源配置一套
dataSource
集歇,使用AbstractRoutingDataSource
將所有數(shù)據(jù)源集成到一起成為動態(tài)數(shù)據(jù)源,在代碼調(diào)用的時候隨時切換數(shù)據(jù)源语淘。
這兩套方案都可以滿足日常使用需要诲宇,各位看官可以根據(jù)個人喜好選用。
三. 配置多套entityManagerFactory
先來講解為每個數(shù)據(jù)源配置一套dataSource
惶翻,并針對每個dataSource
配置一套jpa和事務管理器的方案焕窝。廢話不多說,直接上代碼维贺。具體樣例代碼點此查看。
首先巴帮,我們要有兩個數(shù)據(jù)庫溯泣。在application.properties
中如下配置。
#數(shù)據(jù)庫通用配置
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.maximum-pool-size=5
spring.jpa.database=MYSQL
spring.jpa.hibernate.dll-auto=none
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
#主庫配置
spring.datasource.primary.url=jdbc:mysql://wuxiaodong.mysql.rds.aliyuncs.com:3306/test_for_blog?serverTimezone=GMT%2B8
spring.datasource.primary.username=test_for_blog
spring.datasource.primary.password=A1b2c3d4e5
#二庫配置
spring.datasource.secondary.url=jdbc:mysql://wuxiaodong.mysql.rds.aliyuncs.com:3306/test_for_blog2?serverTimezone=GMT%2B8
spring.datasource.secondary.username=test_for_blog
spring.datasource.secondary.password=A1b2c3d4e5
主庫配置
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(entityManagerFactoryRef="entityManagerFactoryPrimary", transactionManagerRef="transactionManagerPrimary", basePackages= {"com.test.dao.primary"})
public class DataSourcePrimaryConfig
{
@Value("${spring.datasource.driverClassName}")
private String driverClassName;
@Value("${spring.datasource.hikari.maximum-pool-size}")
private Integer maximumPoolSize;
@Value("${spring.datasource.primary.url}")
private String primaryUrl;
@Value("${spring.datasource.primary.username}")
private String primaryUsername;
@Value("${spring.datasource.primary.password}")
private String primaryPassword;
/**
* 主庫數(shù)據(jù)源配置
* @return
*/
@Primary
@Bean(name = "dataSourcePrimary")
public DataSource dataSourcePrimary()
{
HikariDataSource dataSourcePrimary = new HikariDataSource();
dataSourcePrimary.setDriverClassName(driverClassName);
dataSourcePrimary.setJdbcUrl(primaryUrl);
dataSourcePrimary.setUsername(primaryUsername);
dataSourcePrimary.setPassword(primaryPassword);
dataSourcePrimary.setMaximumPoolSize(maximumPoolSize);
return dataSourcePrimary;
}
/**
* 主庫jpa 實例管理器工廠配置
*/
@Primary
@Bean(name = "entityManagerFactoryPrimary")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary(EntityManagerFactoryBuilder builder)
{
LocalContainerEntityManagerFactoryBean em = builder
.dataSource(dataSourcePrimary())
.packages("com.test.model")
.build();
Properties properties = new Properties();
properties.setProperty("hibernate.physical_naming_strategy", "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
em.setJpaProperties(properties);
return em;
}
/**
* 主庫事務管理器配置
*/
@Primary
@Bean(name = "transactionManagerPrimary")
public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder)
{
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactoryPrimary(builder).getObject());
return txManager;
}
}
第二數(shù)據(jù)庫配置
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(entityManagerFactoryRef="entityManagerFactorySecondary", transactionManagerRef="transactionManagerSecondary", basePackages= {"com.test.dao.secondary"})
public class DataSourceSecondaryConfig
{
@Value("${spring.datasource.driverClassName}")
private String driverClassName;
@Value("${spring.datasource.hikari.maximum-pool-size}")
private Integer maximumPoolSize;
@Value("${spring.datasource.secondary.url}")
private String secondaryUrl;
@Value("${spring.datasource.secondary.username}")
private String secondaryUsername;
@Value("${spring.datasource.secondary.password}")
private String secondaryPassword;
/**
* 二庫數(shù)據(jù)源配置
* @return
*/
@Bean(name = "dataSourceSecondary")
public DataSource dataSourceSecondary()
{
HikariDataSource dataSourceSecondary = new HikariDataSource();
dataSourceSecondary.setDriverClassName(driverClassName);
dataSourceSecondary.setJdbcUrl(secondaryUrl);
dataSourceSecondary.setUsername(secondaryUsername);
dataSourceSecondary.setPassword(secondaryPassword);
dataSourceSecondary.setMaximumPoolSize(maximumPoolSize);
return dataSourceSecondary;
}
/**
* 二庫jpa 實例管理器工廠配置
*/
@Bean(name = "entityManagerFactorySecondary")
public LocalContainerEntityManagerFactoryBean entityManagerFactorySecondary(EntityManagerFactoryBuilder builder)
{
LocalContainerEntityManagerFactoryBean em = builder
.dataSource(dataSourceSecondary())
.packages("com.test.model")
.build();
Properties properties = new Properties();
properties.setProperty("hibernate.physical_naming_strategy", "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
em.setJpaProperties(properties);
return em;
}
/**
* 二庫事務管理器配置
*/
@Bean(name = "transactionManagerSecondary")
public PlatformTransactionManager transactionManagerSecondary(EntityManagerFactoryBuilder builder)
{
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactorySecondary(builder).getObject());
return txManager;
}
}
大部分代碼相信各位看官一眼就能看明白榕茧,不過有幾個關鍵單還是要額外說明下垃沦。
@EnableJpaRepositories(entityManagerFactoryRef="entityManagerFactoryPrimary",
transactionManagerRef="transactionManagerPrimary",
basePackages= {"com.test.dao.primary"})
這里是指定使用我們自定義的jpa實體管理工廠entityManagerFactoryPrimary
,事務管理器transactionManagerPrimary
來自定義jpa實現(xiàn)用押。這個jpa只掃描com.test.dao.primary
這個包下的Repository
肢簿。也就是com.test.dao.primary
這個包下面的JpaRepository
使用我們在配置文件中定義的主庫。
主庫配置中蜻拨,我們定義的幾個bean
都加上了@Primary
注解池充;而二庫的配置中,并沒有加上缎讼。這是因為spring中收夸,dataSource
、entityManagerFactory
血崭、transactionManager
這幾個如果初始化的時候?qū)嵗硕鄠€卧惜,項目直接無法啟動厘灼,會給出如下這樣的提示
Parameter 0 of constructor in org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration required a single bean, but 2 were found:
- dataSourcePrimary: defined by method 'dataSourcePrimary' in class path resource [com/test/config/DataSourcePrimaryConfig.class]
- dataSourceSecondary: defined by method 'dataSourceSecondary' in class path resource [com/test/config/DataSourceSecondaryConfig.class]
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
但是如果你把主庫和二庫都加上了@Primary
,又會給出這樣的錯誤提示咽瓷。
No qualifying bean of type 'javax.sql.DataSource' available: more than one 'primary' bean found among candidates: [dataSourcePrimary, dataSourceSecondary]
dataSource
设凹、entityManagerFactory
、transactionManager
這幾個如果必須實例化多個的話茅姜,必須使用使用@Primary
指定其中一個為默認值闪朱。一般,建議使用主庫為默認數(shù)據(jù)源匈睁。
當我們需要使用事務的時候监透,單數(shù)據(jù)源的時候,是直接使用@Transactional
航唆。但是因為我們配置了多數(shù)據(jù)源胀蛮,然后配置主庫事務管理器的時候,加上了@Primary
的將其指定為默認事務管理器糯钙。所有這時候使用@Transactional
其實是開啟了主庫的事務粪狼,如果你這時候試圖對二庫進行事務管理,會發(fā)現(xiàn)完全不會生效任岸。
如果你希望對二庫進行事務管理再榄,需要指定使用二庫的事務管理器@Transactional(transactionManager="transactionManagerSecondary")
。
代碼如下
@Transactional
public void updateTestTable1()
{
TestTable testTable = new TestTable();
testTable.setName("test");
testTable.setStatus(1);
testTablePrimaryRepository.save(testTable);
testTable = testTablePrimaryRepository.findOne(1l);
testTable.setName("1");
testTablePrimaryRepository.save(testTable);
}
@Transactional(transactionManager="transactionManagerSecondary")
public void updateTestTable2()
{
TestTable testTable = new TestTable();
testTable.setName("test");
testTable.setStatus(1);
testTableSecondaryRepository.save(testTable);
testTable = testTableSecondaryRepository.findOne(1l);
testTable.setName("1");
testTableSecondaryRepository.save(testTable);
}
四. 使用AbstractRoutingDataSource實現(xiàn)動態(tài)數(shù)據(jù)源切換
下面來講解動態(tài)數(shù)據(jù)源的方案享潜。廢話不多說困鸥,繼續(xù)上代碼。具體樣例代碼點此查看剑按。
public class DBContextHolder {
/**
* 動態(tài)數(shù)據(jù)源key holder
*/
private static ThreadLocal<String> contextHolder = new ThreadLocal<String>();
public static final String DB_TYPE_PRIMARY = "dataSourceKeyPrimary";
public static final String DB_TYPE_SECONDARY = "dataSourceKeySecondary";
public static String getDbType() {
String db = contextHolder.get();
if (db == null) {
db = DB_TYPE_PRIMARY;// 默認是主庫
}
return db;
}
/**
* 設置本線程的dbtype
*/
public static void setDbType(String str) {
contextHolder.set(str);
}
/**
* 清理連接類型
*/
public static void clearDBType() {
contextHolder.remove();
}
}
public class DynamicDataSource extends AbstractRoutingDataSource
{
@Override
protected Object determineCurrentLookupKey()
{
String dbType = DBContextHolder.getDbType();
return dbType;
}
}
@Configuration
public class DataSourceConfig
{
@Value("${spring.datasource.driverClassName}")
private String driverClassName;
@Value("${spring.datasource.hikari.maximum-pool-size}")
private Integer maximumPoolSize;
@Value("${spring.datasource.primary.url}")
private String primaryUrl;
@Value("${spring.datasource.primary.username}")
private String primaryUsername;
@Value("${spring.datasource.primary.password}")
private String primaryPassword;
@Value("${spring.datasource.secondary.url}")
private String secondaryUrl;
@Value("${spring.datasource.secondary.username}")
private String secondaryUsername;
@Value("${spring.datasource.secondary.password}")
private String secondaryPassword;
@Bean(name = "dataSource")
public DataSource dynamicDataSource()
{
//配置主庫數(shù)據(jù)源
HikariDataSource dataSourcePrimary = new HikariDataSource();
dataSourcePrimary.setDriverClassName(driverClassName);
dataSourcePrimary.setJdbcUrl(primaryUrl);
dataSourcePrimary.setUsername(primaryUsername);
dataSourcePrimary.setPassword(primaryPassword);
dataSourcePrimary.setMaximumPoolSize(maximumPoolSize);
//配置二庫數(shù)據(jù)源
HikariDataSource dataSourceSecondary = new HikariDataSource();
dataSourceSecondary.setDriverClassName(driverClassName);
dataSourceSecondary.setJdbcUrl(secondaryUrl);
dataSourceSecondary.setUsername(secondaryUsername);
dataSourceSecondary.setPassword(secondaryPassword);
dataSourceSecondary.setMaximumPoolSize(maximumPoolSize);
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("dataSourceKeyPrimary", dataSourcePrimary);
targetDataSources.put("dataSourceKeySecondary", dataSourceSecondary);
//配置動態(tài)數(shù)據(jù)源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
}
下面講解一下重要的代碼疾就。動態(tài)數(shù)據(jù)源方案的核心是spring的抽象類AbstractRoutingDataSource
。將多個數(shù)據(jù)源配置到自定義的動態(tài)數(shù)據(jù)源中
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("dataSourceKeyPrimary", dataSourcePrimary);
targetDataSources.put("dataSourceKeySecondary", dataSourceSecondary);
//配置動態(tài)數(shù)據(jù)源
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
在動態(tài)數(shù)據(jù)源類中艺蝴,要重寫一個方法猬腰,來告訴每次調(diào)用動態(tài)數(shù)據(jù)源的時候,使用哪個key對應的數(shù)據(jù)源猜敢。
@Override
protected Object determineCurrentLookupKey()
{
String dbType = DBContextHolder.getDbType();
return dbType;
}
在DBContextHolder
中姑荷,我們使用ThreadLocal
來針對每個線程使用哪個數(shù)據(jù)源來進行控制。默認使用主庫的數(shù)據(jù)源缩擂。如果需要進行切換鼠冕,如下代碼進行切換。
public List<TestTable> getTestTables2()
{
//切換數(shù)據(jù)源至二庫
DBContextHolder.setDbType(DBContextHolder.DB_TYPE_SECONDARY);
List<TestTable> testTables = testTableRepository.findAll();
return testTables;
}
這套動態(tài)數(shù)據(jù)源切換方案撇叁,在使用jdbcTemplate
供鸠、Mybatis
的時候非常好用;但是配合jpa的時候陨闹,卻發(fā)現(xiàn)個問題楞捂。jpa在一個線程中拿過一個數(shù)據(jù)源后薄坏,后續(xù)使用就一直用那個數(shù)據(jù)源,即使你加上切換數(shù)據(jù)源的代碼要求切換寨闹,但因為jpa根本就沒有走動態(tài)數(shù)據(jù)源獲取第二次胶坠,所以根本切換不了。
public List<TestTable> getTestTables3()
{
//拿到默認數(shù)據(jù)源繁堡,即主庫數(shù)據(jù)源
List<TestTable> testTables1 = testTableRepository.findAll();
//要求切換到二庫數(shù)據(jù)源
DBContextHolder.setDbType(DBContextHolder.DB_TYPE_SECONDARY);
//因為jpa只拿一次數(shù)據(jù)源沈善,所以這里依然沿用上一個數(shù)據(jù)源,即主庫數(shù)據(jù)源
List<TestTable> testTables2 = testTableRepository.findAll();
List<TestTable> testTables = new ArrayList<>();
testTables.addAll(testTables1);
testTables.addAll(testTables2);
return testTables;
}
五. 兩套方案各自的應用場景
動態(tài)數(shù)據(jù)源方案椭蹄,配置完成后闻牡,只需要簡單加上一行代碼就可以隨意切換使用哪個數(shù)據(jù)源,不需要對原有代碼結構進行很大的變動绳矩。為優(yōu)先考慮方案罩润。但因為在jpa
,hibernate
中翼馆,框架幫我們做了很多事割以,有時候數(shù)據(jù)源并不能自由的切換。所以应媚,建議如下:
-
jdbcTemplate
严沥、mybaits
框架下,推薦使用動態(tài)數(shù)據(jù)源方案中姜。 -
jpa
消玄、hibernate
框架下,推薦使用配置多套entityManagerFactory的方案丢胚。