Java DataSource 學(xué)習(xí)筆記

簡介

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)一般有三類:

  1. 基本實現(xiàn) —— 生成標準 Connection 對象
  2. 連接池實現(xiàn) —— 生成一個 Connection 自動參與連接池的對象斋攀。此實現(xiàn)與中間層連接池管理器一起使用侧蘸。
  3. 分布式事務(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)讀寫分離焊刹,例如:ShardingSphereMycat恳蹲、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ù)源一樣错忱,要小心多線程情況儡率。

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市以清,隨后出現(xiàn)的幾起案子儿普,更是在濱河造成了極大的恐慌,老刑警劉巖掷倔,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件眉孩,死亡現(xiàn)場離奇詭異,居然都是意外死亡勒葱,警方通過查閱死者的電腦和手機浪汪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凛虽,“玉大人死遭,你說我怎么就攤上這事】” “怎么了呀潭?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵钉迷,是天一觀的道長。 經(jīng)常有香客問我蜗侈,道長篷牌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任踏幻,我火速辦了婚禮,結(jié)果婚禮上戳杀,老公的妹妹穿的比我還像新娘该面。我一直安慰自己,他們只是感情好信卡,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布隔缀。 她就那樣靜靜地躺著,像睡著了一般傍菇。 火紅的嫁衣襯著肌膚如雪猾瘸。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天丢习,我揣著相機與錄音牵触,去河邊找鬼。 笑死咐低,一個胖子當著我的面吹牛揽思,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播见擦,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼钉汗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了鲤屡?” 一聲冷哼從身側(cè)響起损痰,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎酒来,沒想到半個月后卢未,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡役首,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年尝丐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衡奥。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡爹袁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出矮固,到底是詐尸還是另有隱情失息,我是刑警寧澤譬淳,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站盹兢,受9級特大地震影響邻梆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜绎秒,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一浦妄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧见芹,春花似錦剂娄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至徘铝,卻和暖如春耳胎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惕它。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工怕午, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人怠缸。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓诗轻,卻偏偏與公主長得像,于是被迫代替她去往敵國和親揭北。 傳聞我的和親對象是個殘疾皇子扳炬,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容