簡介
DataSource 是自 JDK 1.4 提供的一個標準接口,用于獲取訪問物理數(shù)據(jù)庫的 Connection 對象椎咧。
JDK 不提供其具體實現(xiàn),而它的實現(xiàn)來源于各個驅(qū)動程序供應(yīng)商或數(shù)據(jù)庫訪問框架脚牍,例如 Spring JDBC向臀、Tomcat JDBC飒硅、MyBatis三娩、Druid会前、C3P0临庇、Seata 等假夺。
從 Oracle JDK 的 JavaDoc 文檔中得知,它的實現(xiàn)一般有三類:
- 基本實現(xiàn) —— 生成標準 Connection 對象
- 連接池實現(xiàn) —— 生成一個 Connection 自動參與連接池的對象斋攀。此實現(xiàn)與中間層連接池管理器一起使用侧蘸。
- 分布式事務(wù)實現(xiàn) —— 產(chǎn)生一個 Connection 可用于分布式事務(wù)并且?guī)缀蹩偸菂⑴c連接池的對象。此實現(xiàn)與中間層事務(wù)管理器一起使用鹉梨,并且?guī)缀蹩偸桥c連接池管理器一起使用闺魏。
下面是 DataSource 接口源碼摘要:
public interface DataSource extends CommonDataSource, Wrapper {
Connection getConnection() throws SQLException;
Connection getConnection(String username, String password) throws SQLException;
}
應(yīng)用
在 MyBatis 中的應(yīng)用
熟悉 MyBatis 的人應(yīng)該知道,每個基于 MyBatis 的應(yīng)用都是以一個 SqlSessionFactory 的實例為核心的俯画。和數(shù)據(jù)的交互,都是通過其獲取數(shù)據(jù)訪問會話 SqlSession 對象司草。
在 MyBatis 中提供了一個 DefaultSqlSessionFactory 實現(xiàn)艰垂,其中包含一個 Configuration 對象屬性泡仗,而 Configuration 對象中的 Environment 對象屬性又包含 DataSource 對象。
因此 DefaultSqlSessionFactory 中 openSession 方法最終會調(diào)用到 DataSource 的 getConnection 方法猜憎。
下面是以上提到的源碼摘要:
public interface SqlSessionFactory {
SqlSession openSession();
// ...
}
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
// ...
}
public class Configuration {
protected Environment environment;
// ...
}
public final class Environment {
private final DataSource dataSource;
// ...
}
通過進一步閱讀官方文檔和源碼得知娩怎,SqlSessionFactory 實例是由 SqlSessionFactoryBuilder 構(gòu)造得到。而 SqlSessionFactoryBuilder 則可以從 XML 配置文件或一個預(yù)先配置的 Configuration 實例來構(gòu)建出 SqlSessionFactory 實例胰柑。
XML 配置 DataSource
其中 type="POOLED" 代表使用了 PooledDataSource 作為數(shù)據(jù)源的類型截亦,提供數(shù)據(jù)庫連接池功能。內(nèi)置的除 POOLED 外還有 UNPOOLED柬讨、JNDI崩瓤,如果想使用其他自定義類型 DataSource,具體查看:官方文檔踩官。
內(nèi)置的三種 type 的注冊源碼在 Configuration 類的構(gòu)造方法中却桶。
public Configuration() { typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class); typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class); typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class); // ... }
編程方式配置 DataSource
DataSource dataSource = new PooledDataSource(driver, url, username, password);
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
在 Spring Data JPA 中的應(yīng)用
Spring Data JPA 訪問數(shù)據(jù)庫的核心在于 EntityManager 接口。Spring Boot 利用 AutoConfiguration 自動配置進行實例化 EntityManager蔗牡,在這個過程中 DataSource 作為其中的一個配置參數(shù)颖系。
自動配置 DataSource
通過 application.yml 配置文件配置:
spring:
datasource:
url: jdbc:mysql://localhost:3306/db
username: root
password: 123456
其中主要涉及到的類有:
-
DataSourceAutoConfiguration
DataSource 自動配置類,導(dǎo)入 DataSourceConfiguration 配置類辩越。 -
DataSourceConfiguration
根據(jù)配置自動實例化 DataSource 對象嘁扼。 -
HibernateJpaAutoConfiguration
JPA 自動配置類,導(dǎo)入 HibernateJpaConfiguration(繼承于 JpaBaseConfiguration) 配置類黔攒。 -
JpaBaseConfiguration
自動實例化 LocalContainerEntityManagerFactoryBean 對象(EntityManager 的工廠)趁啸。
以下是源碼摘要:
public class DataSourceAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}
// ...
}
abstract class DataSourceConfiguration {
// Spring Boot 默認使用 HikariDataSource
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
// ...
}
}
}
@Import(HibernateJpaConfiguration.class)
public class HibernateJpaAutoConfiguration {
}
class HibernateJpaConfiguration extends JpaBaseConfiguration {
// ...
}
public abstract class JpaBaseConfiguration implements BeanFactoryAware {
private final DataSource dataSource;
@Bean
@Primary
@ConditionalOnMissingBean({ LocalContainerEntityManagerFactoryBean.class, EntityManagerFactory.class })
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder factoryBuilder) {
Map vendorProperties = getVendorProperties();
customizeVendorProperties(vendorProperties);
return factoryBuilder.dataSource(this.dataSource).packages(getPackagesToScan()).properties(vendorProperties)
.mappingResources(getMappingResources()).jta(isJta()).build();
}
}
編程配置 DataSource
自定義 DataSource Bean,使 DataSourceAutoConfiguration 跳過自動配置 DataSource
@Configuration
public class DataSourceConfig {
@Bean
public DataSource getDataSource() {
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.driverClassName("org.h2.Driver");
dataSourceBuilder.url("jdbc:h2:mem:test");
dataSourceBuilder.username("SA");
dataSourceBuilder.password("");
return dataSourceBuilder.build();
}
}
具體查看:Configuring a DataSource Programmatically in Spring Boot
場景
多數(shù)據(jù)源
MyBatis 多數(shù)據(jù)源
從上面配置數(shù)據(jù)源的代碼段可以看到亏钩,SqlSessionFactory 可配置指定的 Mapper 類或包掃描路徑莲绰。因此,配置多個 SqlSessionFactory 可對不同的 Mapper 生效姑丑,以此實現(xiàn)多數(shù)據(jù)源的功能蛤签。例如:
// 第一個數(shù)據(jù)源
DataSource dataSource1 = new PooledDataSource(driver, url, username, password);
TransactionFactory transactionFactory1 = new JdbcTransactionFactory();
Environment environment1 = new Environment("development", transactionFactory1, dataSource1);
Configuration configuration1 = new Configuration(environment1);
configuration1.addMapper(AMapper.class);
SqlSessionFactory sqlSessionFactory1 = new SqlSessionFactoryBuilder().build(configuration1);
// 第二個數(shù)據(jù)源
DataSource dataSource2 = new PooledDataSource(driver, url, username, password);
TransactionFactory transactionFactory2 = new JdbcTransactionFactory();
Environment environment2 = new Environment("development", transactionFactory2, dataSource2);
Configuration configuration2 = new Configuration(environment2);
configuration2.addMapper(BMapper.class);
SqlSessionFactory sqlSessionFactory2 = new SqlSessionFactoryBuilder().build(configuration2);
以上的示例是純 MyBatis 情況下的配置方式,如果結(jié)合 Spring 或 Spring Boot栅哀。配置方式略有不同震肮,但最終效果會和上面的一致,這里不展開細講留拾。
對于結(jié)合 Spring Boot 使用戳晌,可以參考使用:baomidou / dynamic-datasource-spring-boot-starter
Spring Data JPA 多數(shù)據(jù)源
和 Mybatis 原理類似,配置多個 LocalContainerEntityManagerFactoryBean 對不同路徑下的 Entity痴柔、Repository 起作用即可沦偎。
具體查看:Spring JPA – Multiple Databases
動態(tài)數(shù)據(jù)源
實現(xiàn)動態(tài)數(shù)據(jù)源的關(guān)鍵點在于需要實現(xiàn)一個 DataSource,支持在 getConnection 方法調(diào)用時,能夠取到需要的數(shù)據(jù)源 Connection 對象豪嚎。
根據(jù)此業(yè)務(wù)場景 Spring 框架在早期 2.0 版的時候提供了一個 AbstractRoutingDataSource 抽象類(建議閱讀該類源碼)搔驼,開發(fā)者可對其實現(xiàn)抽象方法,來實現(xiàn)動態(tài)數(shù)據(jù)源切換侈询。
示例代碼:
public class ClientDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return ClientDatabaseContextHolder.getClientDatabase();
}
}
public class ClientDatabaseContextHolder {
private static final ThreadLocal CONTEXT = new ThreadLocal<>();
public static void set(ClientDatabase clientDatabase) {
Assert.notNull(clientDatabase, "clientDatabase cannot be null");
CONTEXT.set(clientDatabase);
}
public static ClientDatabase getClientDatabase() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
具體查看:A Guide to Spring AbstractRoutingDatasource
這里有一個缺陷舌涨,就是使用了 ThreadLocal,因此在多線程操作數(shù)據(jù)庫時扔字,可能需要特殊處理囊嘉。
讀寫分離
可以在動態(tài)數(shù)據(jù)源的基礎(chǔ)上實現(xiàn)讀寫分離,在讀操作和寫操作方法上設(shè)置不同的 AOP 切面進行 DataSource 切換革为,進而實現(xiàn)讀和寫拿到不同的數(shù)據(jù)源 Connection 對象扭粱。
除編程方式實現(xiàn)讀寫分離外,還有分布式數(shù)據(jù)庫中間件篷角,通過判斷 SQL 同樣能實現(xiàn)讀寫分離焊刹,例如:ShardingSphere、Mycat恳蹲、Cobar
多租戶
可以在動態(tài)數(shù)據(jù)源的基礎(chǔ)上實現(xiàn)多租戶虐块,判斷當前上下文(例如 ThreadLocal)中租戶信息,進而實現(xiàn) DataSource 切換嘉蕾。
示例代碼:
public class MultitenantDataSource extends AbstractRoutingDataSource {
@Override
protected String determineCurrentLookupKey() {
return TenantContext.getCurrentTenant();
}
}
public class TenantContext {
private static final ThreadLocal CURRENT_TENANT = new ThreadLocal<>();
public static String getCurrentTenant() {
return CURRENT_TENANT.get();
}
public static void setCurrentTenant(String tenant) {
CURRENT_TENANT.set(tenant);
}
}
@Component
@Order(1)
class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 通過添加全局過濾器贺奠,判斷請求頭中的租戶 ID,進行切換數(shù)據(jù)源
HttpServletRequest req = (HttpServletRequest) request;
String tenantName = req.getHeader("X-TenantID");
TenantContext.setCurrentTenant(tenantName);
try {
chain.doFilter(request, response);
} finally {
TenantContext.setCurrentTenant("");
}
}
}
具體查看:Multitenancy With Spring Data JPA
缺陷和動態(tài)數(shù)據(jù)源一樣错忱,要小心多線程情況儡率。