简介

DataSource 是自 JDK 1.4 提供的一个标准接口,用于获取访问物理数据库的 Connection 对象。

JDK 不提供其具体实现,而它的实现来源于各个驱动程序供应商或数据库访问框架,例如 Spring JDBC、Tomcat JDBC、MyBatis、Druid、C3P0、Seata 等。

从 Oracle JDK 的 JavaDoc 文档中得知,它的实现一般有三类:

  1. 基本实现 —— 生成标准 Connection 对象
  2. 连接池实现 —— 生成一个 Connection 自动参与连接池的对象。此实现与中间层连接池管理器一起使用。
  3. 分布式事务实现 —— 产生一个 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 同样能实现读写分离,例如:ShardingSphereMycatCobar

多租户

可以在动态数据源的基础上实现多租户,判断当前上下文(例如 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

缺陷和动态数据源一样,要小心多线程情况。

参考