Spring 多数据源配置详解

Posted 大鹏cool

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring 多数据源配置详解相关的知识,希望对你有一定的参考价值。

前言

数据源是 JDBC 规范中用来获取关系型数据库连接的一个接口,主要通过池化技术来复用连接。

简单点的 Java 项目或者拆分比较彻底的微服务模块中只会用到一个数据库实例,对应一个数据源。稍复杂点的项目,由于各种原因可能会使用到多个数据库实例,这些数据库可能属于不同的业务模块,可能用于分库分表,也可能用于读写分离。

使用多个数据库的情况下需要为每个数据库配置一个数据源,这本身并没有什么技术难度,重点在于如何在使用多数据源的情况下还能加入 Spring 的事务管理,这篇文章会详细介绍各种持久化技术如何在 Spring 中达到这个目标。

方案选型

在实现多数据源加入 Spring 事务管理这个大目标的前提下,我们需要做进一步的拆解。

主要需要考虑到多个数据源是否需要加入到一个事务中。

  • 如果每个数据源对应一个事务可以使用单机事务,这个与在单数据源的情况下是类似的。
  • 如果多个数据源需要加入到一个事务则只能使用分布式事务。

单机事务

在单机事务的情况下,每个数据源可以分别对应一个 Spring 事务管理器,也可以多个数据源使用一个事务管理器。由于 Spring 的事务管理会把数据源作为 key 存入线程上下文,所以一个线程下只能有一个数据源加入事务。

多事务管理器

单机事务情况下使用多个事务管理器,可以让每个数据源分别对应一个事务管理器,这和在单数据源的情况下是类似的,可以使用如下的图来表述。

不管哪种持久化技术,多个数据源配置多个事务管理器,在具体配置和使用事务方面都是类似的,可以概括为如下的流程。

  1. 为每个数据库分别配置一个数据源。
  2. 为每个数据源配置具体持久化技术操作数据库的核心类。
  3. 为每个数据源配置一个事务管理器。
  4. 为要加入事务的方法指定使用哪个事务管理器。

Spring 中常用的持久化技术主要就是 JdbcTemplateMyBatisHibernateJPA,本篇中的示例假定单数据源情况下你对 Spring 整合这些持久化技术具有一定的了解,限于篇幅本篇不会在细节上涉及太多,可点击链接了解更多内容。

下面看各持久化技术的多数据源多事务管理器如何进行配置与使用,不感兴趣的内容可直接跳过。

JdbcTemplate

如果业务比较简单,又不想引入其他依赖,可以使用 Spring 自带的 JdbcTemplate,假定有两个数据源,可以做如下配置。

@Configuration
@EnableTransactionManagement
public class JdbcTemplateConfiguration 
    // 第一个数据源的相关配置
    @Bean
    public DataSource dataSource1() 
        DataSource dataSource = ...;
        return dataSource;
    
    @Bean
    public JdbcTemplate jdbcTemplate1() 
        return new JdbcTemplate(dataSource1());
    
    @Bean
    public TransactionManager transactionManager1() 
        PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource1());
        return transactionManager;
    

    // 第二个数据源的相关配置
    @Bean
    public DataSource dataSource2() 
        DataSource dataSource = ...;
        return dataSource;
    
    @Bean
    public JdbcTemplate jdbcTemplate2() 
        return new JdbcTemplate(dataSource2());
    
    @Bean
    public TransactionManager transactionManager2() 
        PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource2());
        return transactionManager;
    

主要就是每个数据源配置一套 DataSourceJdbcTemplateTransactionManager,对应关系如下。

数据源JdbcTemplate事务管理器
dataSource1jdbcTemplate1transactionManager1
dataSource2jdbcTemplate2transactionManager2

一个方法内可以使用不同的数据源操作数据库,那么具体加入哪个事务管理器管理的事务呢呢?可以在 @Transactional 注解上指定事务管理器。

@Service
public class UserService 

    @Qualifier("jdbcTemplate1")
    @Autowired
    private JdbcTemplate jdbcTemplate1;

    @Qualifier("jdbcTemplate2")
    @Autowired
    private JdbcTemplate jdbcTemplate2;

    @Transactional(transactionManager = "transactionManager1")
    public List<User> list1() 
        return jdbcTemplate1.query("select * from user", new UserRowMapper());
    

    @Transactional(transactionManager = "transactionManager2")
    public List<User> list2() 
        return jdbcTemplate2.query("select * from user", new UserRowMapper());
    

不过不建议在一个方法内使用多个数据源,Spring 基于线程上下文的事务设计导致只有指定的事务管理器内部的数据源会加入事务中。

MyBatis

除了 JdbcTemplate,另一个最常用的 ORM 框架是 MyBatis,多个数据源的情况下在 Spring 中可以做如下的配置。

@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = "com.zzuhkp.mybatis.mapper1", sqlSessionFactoryRef = "sqlSessionFactory1")
@MapperScan(basePackages = "com.zzuhkp.mybatis.mapper2", sqlSessionFactoryRef = "sqlSessionFactory2")
public class MyBatisConfiguration 

    // 第一个数据源的相关配置
    @Bean
    public DataSource dataSource1() 
        DataSource dataSource = ...;
        return dataSource;
    
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory1() 
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // 设置数据源
        sqlSessionFactoryBean.setDataSource(dataSource1());
        ...省略部分代码
        return sqlSessionFactoryBean;
    
    @Bean
    public TransactionManager transactionManager1() 
        TransactionManager transactionManager = new DataSourceTransactionManager(dataSource1());
        return transactionManager;
    

    // 第二个数据源的相关配置
    @Bean
    public DataSource dataSource2() 
        DataSource dataSource = ...;
        return dataSource;
    
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory2() 
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // 设置数据源
        sqlSessionFactoryBean.setDataSource(dataSource2());
        ...省略部分代码
        return sqlSessionFactoryBean;
    
    @Bean
    public TransactionManager transactionManager2() 
        TransactionManager transactionManager = new DataSourceTransactionManager(dataSource2());
        return transactionManager;
    

主要就是为每个数据源配置一套 DataSourceSqlSessionFactoryBeanTransactionManager,以及配置 @MapperScan 注解。

@MapperScan 中的 basePackagessqlSessionFactoryRef 属性将不同包下面 Mapper 底层的 SqlSession 区分开。

经过上面的配置后,各组件对应关系如下。

数据源SqlSessionFactory事务管理器Mapper
dataSource1sqlSessionFactory1transactionManager1com.zzuhkp.mybatis.mapper1
dataSource2sqlSessionFactory2transactionManager2com.zzuhkp.mybatis.mapper2

由于 Spring 容器中存在多个事务管理器,必须要在事务方法的 @Transactional 注解上指定使用哪个事务管理器,示例如下。

@Service
public class UserService 

    @Autowired
    private UserMapper1 userMapper1;

    @Autowired
    private UserMapper2 userMapper2;

    @Transactional(transactionManager = "transactionManager1")
    public List<User> list1() 
        return userMapper1.list();
    

    @Transactional(transactionManager = "transactionManager2")
    public List<User> list2() 
        return userMapper2.list();
    


Hibernate

Hibernate 在前些年使用比较多,两个数据源的情况下在 Spring 中可以做如下配置。

@Configuration
@EnableTransactionManagement
public class HibernateConfiguration 

    // 第一个数据源的相关配置
    @Bean
    public DataSource dataSource1() 
        DataSource dataSource = ...
        return dataSource;
    
    @Bean
    public LocalSessionFactoryBean sessionFactory1() 
        LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean();
        ...省略配置内容
        return factoryBean;
    
    @Bean
    public TransactionManager transactionManager1(@Qualifier("sessionFactory1") SessionFactory sessionFactory1) 
        HibernateTransactionManager transactionManager = new HibernateTransactionManager();
        transactionManager.setSessionFactory(sessionFactory1);
        transactionManager.setDataSource(dataSource1());
        return transactionManager;
    
    
    // 第二个数据源的相关配置
    @Bean
    public DataSource dataSource2() 
        DataSource dataSource = ...
        return dataSource;
    
    @Bean
    public LocalSessionFactoryBean sessionFactory2() 
        LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean();
        ...省略配置内容
        return factoryBean;
    
    @Bean
    public TransactionManager transactionManager2(@Qualifier("sessionFactory2") SessionFactory sessionFactory2) 
        HibernateTransactionManager transactionManager = new HibernateTransactionManager();
        transactionManager.setSessionFactory(sessionFactory2);
        transactionManager.setDataSource(dataSource1());
        return transactionManager;
    

为每套数据源分别配置了 DataSourceLocalSessionFactoryBeanTransactionManager,对应关系如下。

数据源LocalSessionFactoryBean事务管理器
dataSource1sessionFactory1transactionManager1
dataSource2sessionFactory2transactionManager2

同样需要在事务方法的 @Transactional 注解上指定要使用的事务管理器。

@Service
public class UserService 

    @Qualifier("sessionFactory1")
    @Autowired
    private SessionFactory sessionFactory1;

    @Qualifier("sessionFactory2")
    @Autowired
    private SessionFactory sessionFactory2;

    @Qualifier("jdbcTemplate2")
    @Autowired
    private JdbcTemplate jdbcTemplate2;

    @Transactional(transactionManager = "transactionManager1")
    public List<User> list1() 
        return sessionFactory1.getCurrentSession().createSQLQuery("select * from user").list();
    

    @Transactional(transactionManager = "transactionManager2")
    public List<User> list2() 
        return sessionFactory2.getCurrentSession().createSQLQuery("select * from user").list();
    

JPA

基于 JPA 规范操作数据库可以随时替换实现,是 Hibernate 的另一个选择。使用 spring-data-jpa 模块,第一个数据源的配置如下。

@Configuration
@EnableJpaRepositories(basePackages = "com.zzuhkp.jpa.repository1",
        entityManagerFactoryRef = "entityManagerFactory1",
        transactionManagerRef = "transactionManager1")
@EnableTransactionManagement
public class JpaConfiguration1 

    @Bean
    public DataSource dataSource1() 
        DataSource dataSource = ...
        return dataSource;
    
    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory1() 
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactoryBean.setDataSource(dataSource1());
        ...省略部分配置
        return entityManagerFactoryBean;
    
    @Bean
    public TransactionManager transactionManager1(@Qualifier("entityManagerFactory1") EntityManagerFactory entityManagerFactory1) 
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory1);
        transactionManager.setDataSource(dataSource1());
        return transactionManager;
    

第二个数据源与第一个数据源的配置类似,不再列出,主要就是为每个数据源配置一套 DataSourceEntityManagerFactoryTransactionManager,以及为创建 Repository 接口实例提供 @EnableJpaRepositories,由于 @EnableJpaRepositories 不支持重复注解,可为每个数据源分别提供一个配置类,各组件对应关系如下。

数据源EntityManagerFactory事务管理器Repository
dataSource1entityManagerFactory1transactionManager1com.zzuhkp.jap.repository1
dataSource2entityManagerFactory2transactionManager2com.zzuhkp.jap.repository2

事务方法配置事务管理器的示例如下。

@Service
public class UserService 

    @Autowired
    private UserRepository1 userRepository1;

    @Autowired
    private UserRepository2 userRepository2;

    @Transactional(transactionManager = "transactionManager1")
    public List<User> list1() 
        return userRepository1.findAll();
    

    @Transactional(transactionManager = "transactionManager2")
    public List<User> list2() 
        return userRepository2.findAll();
    

单事务管理器

多个数据源配置单个事务管理器的好处是可以避免在事务方法上指定要使用的事务管理器。由于每个事务管理器内部只有一个数据源,因此只能使用一个动态数据源,使得在进入事务方法前,Spring 事务管理使用动态数据源路由到的目标数据源获取连接。

多个事务管理器与单个事务管理器的区别可以用如下的图示来表示。
我们的重点也将放在如何在动态数据源内部路由目标数据源。Spring 内部提供了一个 AbstractRoutingDataSource 数据源类用于路由目标数据源,实现比较简单,主要就是将目标数据源以 key-value 的形式存入到内部,由用户决定获取目标数据源的 key。

JdbcTemplate 持久化技术为例,可以修改上述示例中的配置如下。

@Configuration
@EnableTransactionManagement
public class JdbcTemplateConfiguration 

    // 第一个数据源的相关配置
    @Bean
    public DataSource dataSource1() 
        DataSource dataSource = ...;
        return dataSource;
    
    @Bean
    public DataSource dataSource2() 
        DataSource dataSource = ...;
        return dataSource;
    

    @Bean
    @Primary
    public DataSource primaryDataSource() 
        // 目标数据源
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("dataSource1", dataSource1());
        targetDataSources.put("dataSource2", dataSource2());
        AbstractRoutingDataSource dataSource = new AbstractRoutingDataSource() 
            @Override
            protected Object determineCurrentLookupKey() 
                return DataSourceKeyHolder.get();
            
        ;
        dataSource.setTargetDataSources(targetDataSources);
        dataSource.setDefaultTargetDataSource(dataSource1());
        return dataSource;
    

    @Bean
    public JdbcTemplate jdbcTemplate1() 
        return new JdbcTemplate(primaryDataSource());
    

    @Bean
    public TransactionManager transactionManager() 
        PlatformTransactionManager transactionManager = new DataSourceTransactionManager(primaryDataSource());
        return transactionManager;
    


和前面的示例相比,主要是加了一个主数据源,由主数据源路由目标数据源,两个 JdbcTemplate 合并为一个使用主数据源的 JdbcTemplate,两个事务管理器合并为一个使用主数据源的事务管理器。

主数据源是一个实现 AbstractRoutingDataSource 抽象类的动态数据源,这个类只有一个用于获取目标数据源 key 值的 determineCurrentLookupKey 方法需要实现,此外再设置一下可用的目标数据源以及默认数据源就可以了。

在获取 key 值的时候则使用到了我们自定义的 DataSourceKeyHolder.get() 方法,这个方法也比较简单,主要就是将 key 放在线程上下文中,可以在进行事务方法前手动设置 key,也可以利用 AOP 拦截方法,根据方法所在的包名、类名、或者方法上的注解动态设置 key。

DataSourceKeyHolder 定义如下。

public class DataSourceKeyHolder 

    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static String get() 
        return threadLocal.get();
    

    public static void set(String key) 
        threadLocal.set(key);
    

假如我们想利用 AOP,在不同的包下面使用不同的数据源,可以配置如下的切面。

@Aspect
@Component
public class DataSourceAop 

    @Around("execution(* com.zzuhkp.template.business1..*.*(..))")
    public Object dataSource1(ProceedingJoinPoint joinPoint) throws Throwable 
        return advice(joinPoint, "datasource1");
    

    @Around("execution(* com.zzuhkp.template.business2..*.*(..))")
    public Object dataSource2(ProceedingJoinPoint joinPoint) throws Throwable 
        return advice(joinPoint, "datasource2");
    

    private Object advice(ProceedingJoinPoint joinPoint, String key) throws Throwable 
        String prevKey = DataSourceKeyHolder.get();
        DataSourceKeyHolder.set(key);
        try 
            return joinPoint.proceed();
         catch (Exception e) 
            throw e;
         finally 
            DataSourceKeyHolder.set(prevKey);
        
    

经过这样的配置,com.zzuhkp.template.business1 包将使用 dataSource1 数据源,com.zzuhkp.template.business2 包将使用 dataSource2 数据源。

service 示例如下。

package com.zzuhkp.template.business1

一. 多环境配置的好处:

1.不同环境配置可以配置不同的参数~

2.便于部署,提高效率,减少出错~

二. properties多环境配置

1. 配置激活选项

spring.profiles.active=dev

2.添加其他配置文件

技术分享图片

application.properties:

#激活哪一个环境的配置文件spring.profiles.active=dev#公共配置spring.jackson.date-format=yyyy-MM-dd HH:mm:ss:

运行的时候还可以制定激活的环境

java -jar myapp.jar --spring.profiles.active=prd

三.YAML多环境配置

1.配置激活选项

spring:   profiles:    active: dev  

2.在配置文件添加三个英文状态下的短横线即可区分

---spring:  profiles: dev

application.yml

#激活哪一个环境的配置文件spring: profiles:  active: prd#公共配置spring: jackson:  date-format: yyyy-MM-dd HH:mm:ss---spring: profiles: devserver: port: 8081---spring: profiles: testserver: port: 8082---spring: profiles: prdserver: port: 8083

这种情况是报错的:Duplicate key: spring

正确的配置:

#激活哪一个环境的配置文件#公共配置spring: profiles:  active: prd jackson:  date-format: yyyy-MM-dd HH:mm:ss---spring: profiles: devserver: port: 8081---spring: profiles: testserver: port: 8082---spring: profiles: prdserver: port: 8083

四.两种配置方式的比较

1. Properties配置多环境,需要添加多个配置文件,YAML只需要一个配件文件

yml文件本身具有区分不同环境的能力

2.书写格式的差异,yaml相对比较简洁,优雅

3. YAML的缺点:不能通过@PropertySource注解加载。如果需要使用@PropertySource注解的方式加载值,那就要使用properties文件。

讲真,还是喜欢properties的配置形式,yml形式的优雅?没啥感觉~哈哈~


以上是关于Spring 多数据源配置详解的主要内容,如果未能解决你的问题,请参考以下文章

Spring 多数据源配置详解

Spring 多数据源配置详解

spring连接池配置详解

详解Spring Boot配置文件之多环境配置

Spring Boot 之属性读写详解

Spring中管理数据库事务的配置使用详解