springboot项目使用spring-data-jpa如何连接多数据源

一. 为什么要连接多数据源

springboot下使用spring-data-jpa连接数据库配置非常方便,只需要在application.properties简单的几行配置就能搞定。
有些时候我们需要在一个项目里面连接多个数据库,如常见的数据库主从分离,将部分查询请求分流到只读从库里,降低主库的压力。
这种时候,就不能通过简单的几行配置来搞定了;需要手动进行一些配置才行。

二. springboot下连接多数据源的两种方案

目前有两种方案可以解决这个问题

  1. 为每个数据源配置一套dataSource,并针对每个dataSource配置一套jpa和事务管理器。
  2. 为每个数据源配置一套dataSource,使用AbstractRoutingDataSource将所有数据源集成到一起成为动态数据源,在代码调用的时候随时切换数据源。

这两套方案都可以满足日常使用需要,各位看官可以根据个人喜好选用。

三. 配置多套entityManagerFactory

先来讲解为每个数据源配置一套dataSource,并针对每个dataSource配置一套jpa和事务管理器的方案。废话不多说,直接上代码。具体样例代码点此查看

首先,我们要有两个数据库。在application.properties中如下配置。

#数据库通用配置
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;

    /**
     * 主库数据源配置
     * @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;
    }
}

第二数据库配置

@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;

    /**
     * 二库数据源配置
     * @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实现。这个jpa只扫描com.test.dao.primary这个包下的Repository。也就是com.test.dao.primary这个包下面的JpaRepository使用我们在配置文件中定义的主库。

主库配置中,我们定义的几个bean都加上了@Primary注解;而二库的配置中,并没有加上。这是因为spring中,dataSourceentityManagerFactorytransactionManager这几个如果初始化的时候实例化了多个,项目直接无法启动,会给出如下这样的提示

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]

dataSourceentityManagerFactorytransactionManager这几个如果必须实例化多个的话,必须使用使用@Primary指定其中一个为默认值。一般,建议使用主库为默认数据源。

当我们需要使用事务的时候,单数据源的时候,是直接使用@Transactional。但是因为我们配置了多数据源,然后配置主库事务管理器的时候,加上了@Primary的将其指定为默认事务管理器。所有这时候使用@Transactional其实是开启了主库的事务,如果你这时候试图对二库进行事务管理,会发现完全不会生效。
如果你希望对二库进行事务管理,需要指定使用二库的事务管理器@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实现动态数据源切换

下面来讲解动态数据源的方案。废话不多说,继续上代码。具体样例代码点此查看

public class DBContextHolder {

    /**
     * 动态数据源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()
    {
        //配置主库数据源
        HikariDataSource dataSourcePrimary = new HikariDataSource();
        dataSourcePrimary.setDriverClassName(driverClassName);
        dataSourcePrimary.setJdbcUrl(primaryUrl);
        dataSourcePrimary.setUsername(primaryUsername);
        dataSourcePrimary.setPassword(primaryPassword);
        dataSourcePrimary.setMaximumPoolSize(maximumPoolSize);

        //配置二库数据源
        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);

        //配置动态数据源
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }
}

下面讲解一下重要的代码。动态数据源方案的核心是spring的抽象类AbstractRoutingDataSource。将多个数据源配置到自定义的动态数据源中

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

        //配置动态数据源
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);

在动态数据源类中,要重写一个方法,来告诉每次调用动态数据源的时候,使用哪个key对应的数据源。

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

DBContextHolder中,我们使用ThreadLocal来针对每个线程使用哪个数据源来进行控制。默认使用主库的数据源。如果需要进行切换,如下代码进行切换。

    public List<TestTable> getTestTables2()
    {
        //切换数据源至二库
        DBContextHolder.setDbType(DBContextHolder.DB_TYPE_SECONDARY);
        List<TestTable> testTables = testTableRepository.findAll();

        return testTables;
    }

这套动态数据源切换方案,在使用jdbcTemplateMybatis的时候非常好用;但是配合jpa的时候,却发现个问题。jpa在一个线程中拿过一个数据源后,后续使用就一直用那个数据源,即使你加上切换数据源的代码要求切换,但因为jpa根本就没有走动态数据源获取第二次,所以根本切换不了。

    public List<TestTable> getTestTables3()
    {
        //拿到默认数据源,即主库数据源
        List<TestTable> testTables1 = testTableRepository.findAll();
        //要求切换到二库数据源
        DBContextHolder.setDbType(DBContextHolder.DB_TYPE_SECONDARY);
        //因为jpa只拿一次数据源,所以这里依然沿用上一个数据源,即主库数据源
        List<TestTable> testTables2 = testTableRepository.findAll();

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

        return testTables;
    }

五. 两套方案各自的应用场景
动态数据源方案,配置完成后,只需要简单加上一行代码就可以随意切换使用哪个数据源,不需要对原有代码结构进行很大的变动。为优先考虑方案。但因为在jpahibernate中,框架帮我们做了很多事,有时候数据源并不能自由的切换。所以,建议如下:

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

推荐阅读更多精彩内容