springboot項目使用spring-data-jpa如何連接多數(shù)據(jù)源

一. 為什么要連接多數(shù)據(jù)源

springboot下使用spring-data-jpa連接數(shù)據(jù)庫配置非常方便,只需要在application.properties簡單的幾行配置就能搞定。
有些時候我們需要在一個項目里面連接多個數(shù)據(jù)庫炎功,如常見的數(shù)據(jù)庫主從分離,將部分查詢請求分流到只讀從庫里邻寿,降低主庫的壓力伯复。
這種時候搞莺,就不能通過簡單的幾行配置來搞定了撵溃;需要手動進行一些配置才行疚鲤。

二. springboot下連接多數(shù)據(jù)源的兩種方案

目前有兩種方案可以解決這個問題

  1. 為每個數(shù)據(jù)源配置一套dataSource,并針對每個dataSource配置一套jpa和事務管理器缘挑。
  2. 為每個數(shù)據(jù)源配置一套dataSource集歇,使用AbstractRoutingDataSource將所有數(shù)據(jù)源集成到一起成為動態(tài)數(shù)據(jù)源,在代碼調(diào)用的時候隨時切換數(shù)據(jù)源语淘。

這兩套方案都可以滿足日常使用需要诲宇,各位看官可以根據(jù)個人喜好選用。

三. 配置多套entityManagerFactory

先來講解為每個數(shù)據(jù)源配置一套dataSource惶翻,并針對每個dataSource配置一套jpa和事務管理器的方案焕窝。廢話不多說,直接上代碼维贺。具體樣例代碼點此查看

首先巴帮,我們要有兩個數(shù)據(jù)庫溯泣。在application.properties中如下配置。

#數(shù)據(jù)庫通用配置
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.maximum-pool-size=5
spring.jpa.database=MYSQL
spring.jpa.hibernate.dll-auto=none
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
#主庫配置
spring.datasource.primary.url=jdbc:mysql://wuxiaodong.mysql.rds.aliyuncs.com:3306/test_for_blog?serverTimezone=GMT%2B8
spring.datasource.primary.username=test_for_blog
spring.datasource.primary.password=A1b2c3d4e5
#二庫配置
spring.datasource.secondary.url=jdbc:mysql://wuxiaodong.mysql.rds.aliyuncs.com:3306/test_for_blog2?serverTimezone=GMT%2B8
spring.datasource.secondary.username=test_for_blog
spring.datasource.secondary.password=A1b2c3d4e5

主庫配置

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(entityManagerFactoryRef="entityManagerFactoryPrimary", transactionManagerRef="transactionManagerPrimary", basePackages= {"com.test.dao.primary"})
public class DataSourcePrimaryConfig
{
    @Value("${spring.datasource.driverClassName}")
    private String driverClassName;

    @Value("${spring.datasource.hikari.maximum-pool-size}")
    private Integer maximumPoolSize;

    @Value("${spring.datasource.primary.url}")
    private String primaryUrl;

    @Value("${spring.datasource.primary.username}")
    private String primaryUsername;

    @Value("${spring.datasource.primary.password}")
    private String primaryPassword;

    /**
     * 主庫數(shù)據(jù)源配置
     * @return
     */
    @Primary
    @Bean(name = "dataSourcePrimary")
    public DataSource dataSourcePrimary()
    {
        HikariDataSource dataSourcePrimary = new HikariDataSource();
        dataSourcePrimary.setDriverClassName(driverClassName);
        dataSourcePrimary.setJdbcUrl(primaryUrl);
        dataSourcePrimary.setUsername(primaryUsername);
        dataSourcePrimary.setPassword(primaryPassword);
        dataSourcePrimary.setMaximumPoolSize(maximumPoolSize);

        return dataSourcePrimary;
    }

    /**
     * 主庫jpa 實例管理器工廠配置
     */
    @Primary
    @Bean(name = "entityManagerFactoryPrimary")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary(EntityManagerFactoryBuilder builder)
    {
        LocalContainerEntityManagerFactoryBean em = builder
                .dataSource(dataSourcePrimary())
                .packages("com.test.model")
                .build();
        Properties properties = new Properties();
        properties.setProperty("hibernate.physical_naming_strategy", "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
        em.setJpaProperties(properties);
        return em;
    }

    /**
     * 主庫事務管理器配置
     */
    @Primary
    @Bean(name = "transactionManagerPrimary")
    public PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder)
    {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactoryPrimary(builder).getObject());
        return txManager;
    }
}

第二數(shù)據(jù)庫配置

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(entityManagerFactoryRef="entityManagerFactorySecondary", transactionManagerRef="transactionManagerSecondary", basePackages= {"com.test.dao.secondary"})
public class DataSourceSecondaryConfig
{
    @Value("${spring.datasource.driverClassName}")
    private String driverClassName;

    @Value("${spring.datasource.hikari.maximum-pool-size}")
    private Integer maximumPoolSize;

    @Value("${spring.datasource.secondary.url}")
    private String secondaryUrl;

    @Value("${spring.datasource.secondary.username}")
    private String secondaryUsername;

    @Value("${spring.datasource.secondary.password}")
    private String secondaryPassword;

    /**
     * 二庫數(shù)據(jù)源配置
     * @return
     */
    @Bean(name = "dataSourceSecondary")
    public DataSource dataSourceSecondary()
    {
        HikariDataSource dataSourceSecondary = new HikariDataSource();
        dataSourceSecondary.setDriverClassName(driverClassName);
        dataSourceSecondary.setJdbcUrl(secondaryUrl);
        dataSourceSecondary.setUsername(secondaryUsername);
        dataSourceSecondary.setPassword(secondaryPassword);
        dataSourceSecondary.setMaximumPoolSize(maximumPoolSize);

        return dataSourceSecondary;
    }

    /**
     * 二庫jpa 實例管理器工廠配置
     */
    @Bean(name = "entityManagerFactorySecondary")
    public LocalContainerEntityManagerFactoryBean entityManagerFactorySecondary(EntityManagerFactoryBuilder builder)
    {
        LocalContainerEntityManagerFactoryBean em = builder
                .dataSource(dataSourceSecondary())
                .packages("com.test.model")
                .build();
        Properties properties = new Properties();
        properties.setProperty("hibernate.physical_naming_strategy", "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
        em.setJpaProperties(properties);
        return em;
    }

    /**
     * 二庫事務管理器配置
     */
    @Bean(name = "transactionManagerSecondary")
    public PlatformTransactionManager transactionManagerSecondary(EntityManagerFactoryBuilder builder)
    {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactorySecondary(builder).getObject());
        return txManager;
    }
}

大部分代碼相信各位看官一眼就能看明白榕茧,不過有幾個關鍵單還是要額外說明下垃沦。

@EnableJpaRepositories(entityManagerFactoryRef="entityManagerFactoryPrimary", 
        transactionManagerRef="transactionManagerPrimary", 
        basePackages= {"com.test.dao.primary"})

這里是指定使用我們自定義的jpa實體管理工廠entityManagerFactoryPrimary,事務管理器transactionManagerPrimary來自定義jpa實現(xiàn)用押。這個jpa只掃描com.test.dao.primary這個包下的Repository肢簿。也就是com.test.dao.primary這個包下面的JpaRepository使用我們在配置文件中定義的主庫。

主庫配置中蜻拨,我們定義的幾個bean都加上了@Primary注解池充;而二庫的配置中,并沒有加上缎讼。這是因為spring中收夸,dataSourceentityManagerFactory血崭、transactionManager這幾個如果初始化的時候?qū)嵗硕鄠€卧惜,項目直接無法啟動厘灼,會給出如下這樣的提示

Parameter 0 of constructor in org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration required a single bean, but 2 were found:
    - dataSourcePrimary: defined by method 'dataSourcePrimary' in class path resource [com/test/config/DataSourcePrimaryConfig.class]
    - dataSourceSecondary: defined by method 'dataSourceSecondary' in class path resource [com/test/config/DataSourceSecondaryConfig.class]

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

但是如果你把主庫和二庫都加上了@Primary,又會給出這樣的錯誤提示咽瓷。

No qualifying bean of type 'javax.sql.DataSource' available: more than one 'primary' bean found among candidates: [dataSourcePrimary, dataSourceSecondary]

dataSource设凹、entityManagerFactorytransactionManager這幾個如果必須實例化多個的話茅姜,必須使用使用@Primary指定其中一個為默認值闪朱。一般,建議使用主庫為默認數(shù)據(jù)源匈睁。

當我們需要使用事務的時候监透,單數(shù)據(jù)源的時候,是直接使用@Transactional航唆。但是因為我們配置了多數(shù)據(jù)源胀蛮,然后配置主庫事務管理器的時候,加上了@Primary的將其指定為默認事務管理器糯钙。所有這時候使用@Transactional其實是開啟了主庫的事務粪狼,如果你這時候試圖對二庫進行事務管理,會發(fā)現(xiàn)完全不會生效任岸。
如果你希望對二庫進行事務管理再榄,需要指定使用二庫的事務管理器@Transactional(transactionManager="transactionManagerSecondary")
代碼如下

@Transactional
    public void updateTestTable1()
    {
        TestTable testTable = new TestTable();
        testTable.setName("test");
        testTable.setStatus(1);
        testTablePrimaryRepository.save(testTable);

        testTable = testTablePrimaryRepository.findOne(1l);
        testTable.setName("1");
        testTablePrimaryRepository.save(testTable);
    }

    @Transactional(transactionManager="transactionManagerSecondary")
    public void updateTestTable2()
    {
        TestTable testTable = new TestTable();
        testTable.setName("test");
        testTable.setStatus(1);
        testTableSecondaryRepository.save(testTable);

        testTable = testTableSecondaryRepository.findOne(1l);
        testTable.setName("1");
        testTableSecondaryRepository.save(testTable);
    }

四. 使用AbstractRoutingDataSource實現(xiàn)動態(tài)數(shù)據(jù)源切換

下面來講解動態(tài)數(shù)據(jù)源的方案享潜。廢話不多說困鸥,繼續(xù)上代碼。具體樣例代碼點此查看剑按。

public class DBContextHolder {

    /**
     * 動態(tài)數(shù)據(jù)源key holder
     */
    private static ThreadLocal<String> contextHolder = new ThreadLocal<String>();

    public static final String DB_TYPE_PRIMARY = "dataSourceKeyPrimary";
    public static final String DB_TYPE_SECONDARY = "dataSourceKeySecondary";

    public static String getDbType() {
        String db = contextHolder.get();
        if (db == null) {
            db = DB_TYPE_PRIMARY;// 默認是主庫
        }
        return db;
    }

    /**
     * 設置本線程的dbtype
     */
    public static void setDbType(String str) {
        contextHolder.set(str);
    }

    /**
     * 清理連接類型
     */
    public static void clearDBType() {
        contextHolder.remove();
    }
}
public class DynamicDataSource extends AbstractRoutingDataSource
{
    @Override
    protected Object determineCurrentLookupKey()
    {
        String dbType = DBContextHolder.getDbType();
        return dbType;
    }
}
@Configuration
public class DataSourceConfig
{
    @Value("${spring.datasource.driverClassName}")
    private String driverClassName;

    @Value("${spring.datasource.hikari.maximum-pool-size}")
    private Integer maximumPoolSize;

    @Value("${spring.datasource.primary.url}")
    private String primaryUrl;

    @Value("${spring.datasource.primary.username}")
    private String primaryUsername;

    @Value("${spring.datasource.primary.password}")
    private String primaryPassword;

    @Value("${spring.datasource.secondary.url}")
    private String secondaryUrl;

    @Value("${spring.datasource.secondary.username}")
    private String secondaryUsername;

    @Value("${spring.datasource.secondary.password}")
    private String secondaryPassword;

    @Bean(name = "dataSource")
    public DataSource dynamicDataSource()
    {
        //配置主庫數(shù)據(jù)源
        HikariDataSource dataSourcePrimary = new HikariDataSource();
        dataSourcePrimary.setDriverClassName(driverClassName);
        dataSourcePrimary.setJdbcUrl(primaryUrl);
        dataSourcePrimary.setUsername(primaryUsername);
        dataSourcePrimary.setPassword(primaryPassword);
        dataSourcePrimary.setMaximumPoolSize(maximumPoolSize);

        //配置二庫數(shù)據(jù)源
        HikariDataSource dataSourceSecondary = new HikariDataSource();
        dataSourceSecondary.setDriverClassName(driverClassName);
        dataSourceSecondary.setJdbcUrl(secondaryUrl);
        dataSourceSecondary.setUsername(secondaryUsername);
        dataSourceSecondary.setPassword(secondaryPassword);
        dataSourceSecondary.setMaximumPoolSize(maximumPoolSize);

        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("dataSourceKeyPrimary", dataSourcePrimary);
        targetDataSources.put("dataSourceKeySecondary", dataSourceSecondary);

        //配置動態(tài)數(shù)據(jù)源
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }
}

下面講解一下重要的代碼疾就。動態(tài)數(shù)據(jù)源方案的核心是spring的抽象類AbstractRoutingDataSource。將多個數(shù)據(jù)源配置到自定義的動態(tài)數(shù)據(jù)源中

        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("dataSourceKeyPrimary", dataSourcePrimary);
        targetDataSources.put("dataSourceKeySecondary", dataSourceSecondary);

        //配置動態(tài)數(shù)據(jù)源
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);

在動態(tài)數(shù)據(jù)源類中艺蝴,要重寫一個方法猬腰,來告訴每次調(diào)用動態(tài)數(shù)據(jù)源的時候,使用哪個key對應的數(shù)據(jù)源猜敢。

@Override
    protected Object determineCurrentLookupKey()
    {
        String dbType = DBContextHolder.getDbType();
        return dbType;
    }

DBContextHolder中姑荷,我們使用ThreadLocal來針對每個線程使用哪個數(shù)據(jù)源來進行控制。默認使用主庫的數(shù)據(jù)源缩擂。如果需要進行切換鼠冕,如下代碼進行切換。

    public List<TestTable> getTestTables2()
    {
        //切換數(shù)據(jù)源至二庫
        DBContextHolder.setDbType(DBContextHolder.DB_TYPE_SECONDARY);
        List<TestTable> testTables = testTableRepository.findAll();

        return testTables;
    }

這套動態(tài)數(shù)據(jù)源切換方案撇叁,在使用jdbcTemplate供鸠、Mybatis的時候非常好用;但是配合jpa的時候陨闹,卻發(fā)現(xiàn)個問題楞捂。jpa在一個線程中拿過一個數(shù)據(jù)源后薄坏,后續(xù)使用就一直用那個數(shù)據(jù)源,即使你加上切換數(shù)據(jù)源的代碼要求切換寨闹,但因為jpa根本就沒有走動態(tài)數(shù)據(jù)源獲取第二次胶坠,所以根本切換不了。

    public List<TestTable> getTestTables3()
    {
        //拿到默認數(shù)據(jù)源繁堡,即主庫數(shù)據(jù)源
        List<TestTable> testTables1 = testTableRepository.findAll();
        //要求切換到二庫數(shù)據(jù)源
        DBContextHolder.setDbType(DBContextHolder.DB_TYPE_SECONDARY);
        //因為jpa只拿一次數(shù)據(jù)源沈善,所以這里依然沿用上一個數(shù)據(jù)源,即主庫數(shù)據(jù)源
        List<TestTable> testTables2 = testTableRepository.findAll();

        List<TestTable> testTables = new ArrayList<>();
        testTables.addAll(testTables1);
        testTables.addAll(testTables2);

        return testTables;
    }

五. 兩套方案各自的應用場景
動態(tài)數(shù)據(jù)源方案椭蹄,配置完成后闻牡,只需要簡單加上一行代碼就可以隨意切換使用哪個數(shù)據(jù)源,不需要對原有代碼結構進行很大的變動绳矩。為優(yōu)先考慮方案罩润。但因為在jpahibernate中翼馆,框架幫我們做了很多事割以,有時候數(shù)據(jù)源并不能自由的切換。所以应媚,建議如下:

  • jdbcTemplate严沥、mybaits框架下,推薦使用動態(tài)數(shù)據(jù)源方案中姜。
  • jpa消玄、hibernate框架下,推薦使用配置多套entityManagerFactory的方案丢胚。
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末莱找,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子嗜桌,更是在濱河造成了極大的恐慌,老刑警劉巖辞色,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件骨宠,死亡現(xiàn)場離奇詭異,居然都是意外死亡相满,警方通過查閱死者的電腦和手機层亿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來立美,“玉大人匿又,你說我怎么就攤上這事〗ㄌ悖” “怎么了碌更?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵裕偿,是天一觀的道長。 經(jīng)常有香客問我痛单,道長嘿棘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任旭绒,我火速辦了婚禮鸟妙,結果婚禮上,老公的妹妹穿的比我還像新娘挥吵。我一直安慰自己重父,他們只是感情好,可當我...
    茶點故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布忽匈。 她就那樣靜靜地躺著房午,像睡著了一般。 火紅的嫁衣襯著肌膚如雪脉幢。 梳的紋絲不亂的頭發(fā)上歪沃,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天,我揣著相機與錄音嫌松,去河邊找鬼沪曙。 笑死,一個胖子當著我的面吹牛萎羔,可吹牛的內(nèi)容都是我干的液走。 我是一名探鬼主播,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼贾陷,長吁一口氣:“原來是場噩夢啊……” “哼缘眶!你這毒婦竟也來了?” 一聲冷哼從身側響起髓废,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤巷懈,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后慌洪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體顶燕,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年冈爹,在試婚紗的時候發(fā)現(xiàn)自己被綠了涌攻。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,625評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡频伤,死狀恐怖恳谎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情憋肖,我是刑警寧澤因痛,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布婚苹,位于F島的核電站,受9級特大地震影響婚肆,放射性物質(zhì)發(fā)生泄漏租副。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一较性、第九天 我趴在偏房一處隱蔽的房頂上張望甚淡。 院中可真熱鬧瓣颅,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽金矛。三九已至毙玻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間速和,已是汗流浹背歹垫。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留颠放,地道東北人排惨。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像碰凶,于是被迫代替她去往敵國和親暮芭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,492評論 2 348

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