Java DataSource 学习笔记

简介

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

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

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容