简介
DataSource 是自 JDK 1.4 提供的一个标准接口,用于获取访问物理数据库的 Connection 对象。
JDK 不提供其具体实现,而它的实现来源于各个驱动程序供应商或数据库访问框架,例如 Spring JDBC、Tomcat JDBC、MyBatis、Druid、C3P0、Seata 等。
从 Oracle JDK 的 JavaDoc 文档中得知,它的实现一般有三类:
- 基本实现 —— 生成标准 Connection 对象
- 连接池实现 —— 生成一个 Connection 自动参与连接池的对象。此实现与中间层连接池管理器一起使用。
- 分布式事务实现 —— 产生一个 Connection 可用于分布式事务并且几乎总是参与连接池的对象。此实现与中间层事务管理器一起使用,并且几乎总是与连接池管理器一起使用。
下面是 DataSource 接口源码摘要:
public interface DataSource extends CommonDataSource, Wrapper {
Connection getConnection() throws SQLException;
Connection getConnection(String username, String password) throws SQLException;
}
应用
在 MyBatis 中的应用
熟悉 MyBatis 的人应该知道,每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。和数据的交互,都是通过其获取数据访问会话 SqlSession 对象。
在 MyBatis 中提供了一个 DefaultSqlSessionFactory 实现,其中包含一个 Configuration 对象属性,而 Configuration 对象中的 Environment 对象属性又包含 DataSource 对象。
因此 DefaultSqlSessionFactory 中 openSession 方法最终会调用到 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 构造得到。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。
XML 配置 DataSource
其中 type=“POOLED” 代表使用了 PooledDataSource 作为数据源的类型,提供数据库连接池功能。内置的除 POOLED 外还有 UNPOOLED、JNDI,如果想使用其他自定义类型 DataSource,具体查看:官方文档。
内置的三种 type 的注册源码在 Configuration 类的构造方法中。
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 中的应用
Spring Data JPA 访问数据库的核心在于 EntityManager 接口。Spring Boot 利用 AutoConfiguration 自动配置进行实例化 EntityManager,在这个过程中 DataSource 作为其中的一个配置参数。
自动配置 DataSource
通过 application.yml 配置文件配置:
spring:
datasource:
url: jdbc:mysql://localhost:3306/db
username: root
password: 123456
其中主要涉及到的类有:
DataSourceAutoConfiguration
DataSource 自动配置类,导入 DataSourceConfiguration 配置类。-
DataSourceConfiguration
根据配置自动实例化 DataSource 对象。 -
HibernateJpaAutoConfiguration
JPA 自动配置类,导入 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
场景
多数据源
MyBatis 多数据源
从上面配置数据源的代码段可以看到,SqlSessionFactory 可配置指定的 Mapper 类或包扫描路径。因此,配置多个 SqlSessionFactory 可对不同的 Mapper 生效,以此实现多数据源的功能。例如:
// 第一个数据源
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);
// 第二个数据源
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 情况下的配置方式,如果结合 Spring 或 Spring Boot。配置方式略有不同,但最终效果会和上面的一致,这里不展开细讲。
对于结合 Spring Boot 使用,可以参考使用:baomidou / dynamic-datasource-spring-boot-starter
Spring Data JPA 多数据源
和 Mybatis 原理类似,配置多个 LocalContainerEntityManagerFactoryBean 对不同路径下的 Entity、Repository 起作用即可。
具体查看:Spring JPA – Multiple Databases
动态数据源
实现动态数据源的关键点在于需要实现一个 DataSource,支持在 getConnection 方法调用时,能够取到需要的数据源 Connection 对象。
根据此业务场景 Spring 框架在早期 2.0 版的时候提供了一个 AbstractRoutingDataSource 抽象类(建议阅读该类源码),开发者可对其实现抽象方法,来实现动态数据源切换。
示例代码:
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,因此在多线程操作数据库时,可能需要特殊处理。
读写分离
可以在动态数据源的基础上实现读写分离,在读操作和写操作方法上设置不同的 AOP 切面进行 DataSource 切换,进而实现读和写拿到不同的数据源 Connection 对象。
除编程方式实现读写分离外,还有分布式数据库中间件,通过判断 SQL 同样能实现读写分离,例如:ShardingSphere、Mycat、Cobar
多租户
可以在动态数据源的基础上实现多租户,判断当前上下文(例如 ThreadLocal)中租户信息,进而实现 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,进行切换数据源
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
缺陷和动态数据源一样,要小心多线程情况。
参考
- Oracle JDK 17 JavaDoc - DataSource
- MyBatis - Getting Started
- Spring Boot - Data Access
- Spring Data JPA - Reference
- Spring - Dynamic DataSource Routing