场景介绍

开发中需要碰到的场景,需要将现有的一套系统提供给另外一个项目组进行使用,但是服务器还是用我们现有的,只是多配置两个数据库进行数据隔离,因此需要用到多数据源进行数据切换。

配置多数据源两种方法,我目前的场景需要用到第一种进行配置

  1. 通过aop在请求中定义标识符,告诉spring该注入哪个数据源,请求对应的数据库
  2. 通过mybatis-plus进行配置

spring底层如何操作数据库-原理

实现多数据源的配置其实并不复杂,很多博客也有具体的实现步骤,为了透彻的理解整个实现原理,这里介绍下spring底层是如何操作数据库的。
当我们发起一个请求涉及到数据库操作时,spring会调用ORM持久层框架(例:mybatis)对应的api;我们都知道任何的持久层框架要连接数据库,都离不开JDBC。因此我们需要用到spring-jdbc模块,spring-jdbc模块提供了一个connection给ORM框架,在调用getConnection方法时注入数据源,数据源内则配置了jdbc连接需要的参数,此时,ORM就可以连接并操作数据库了。
原理示意图

多数据源具体实现

上一步讲述了spring连接数据库的原理步骤,通过上述步骤可以看出,我们需要连接不同的数据库时,最好的改动点就是在注入数据源的时候进行修改,因为数据源是我们自己配置的,具体要怎么操作呢?
在这里插入图片描述

配置步骤

配置说明

首先,我们需要自定义一个动态的数据源DynamicDataSource实现DataSource,然后结合自己的实际业务需求,定义一个标识来区分需要获取哪个数据源,比如我的系统每个用户都有对应的角色和项目组,那么我就可以根据他对应的角色或分组进行判断,来注入不同的dataSource。

初始版本demo代码示例
前提:建议新建一个demo项目,写一个简单的查库接口,然后跟着下面代码进行操作(如果本地或服务器装有mysql,建议新建两个database,在database下创建一张同名的表,方便同一套代码切换数据源查询不同库,可以更直观的看到效果。
我这边创建了名为wp和wpa的两个database,在databse下建立了一张user表)

代码结构如下
在这里插入图片描述

步骤一:在yml文件中配置两个数据源 datasource1 datasource2
server:
  port: 8099

spring:
  datasource:
    datasource1:
      username: root
      password: 123456
      url: jdbc:mysql://22.12.193.64:3306/datasource1?serverTimezone=GMT&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&allowMultiQueries=true
      driver-class-name: com.mysql.jdbc.Driver

    datasource2:
      username: root
      password: 123456
      url: jdbc:mysql://22.12.193.64:3306/datasource2?serverTimezone=GMT&useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&allowMultiQueries=true
      driver-class-name: com.mysql.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapping/*.xml
  type-aliases-package: com.wp.demo.entity
步骤二:新建DatasourceConfig配置文件,读取两个datasource配置
@Configuration
public class DatasourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.datasource1")
    public DataSource dataSource1() {
        // 通过配置地址拿到spring.datasource中的配置,创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.datasource2")
    public DataSource dataSource2() {
        // 通过配置地址拿到spring.datasource中的配置,创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

}
步骤三:新建DynamicDatasource动态数据源文件
@Component
@Primary // 有多个datasource时,将此打他source设置为主要注入的bean,primary
public class DynamicDatasource implements DataSource, InitializingBean {

    // 数据源标识,为了保证线程安全使用threadLocal
    public static ThreadLocal<String> name = new ThreadLocal<>();

    @Autowired
    DataSource dataSource1;

    @Autowired
    DataSource dataSource2;

    @Override
    public Connection getConnection() throws SQLException {
    // 通过不同的标识动态分配数据源
        if ("R".equals(name.get())) {
            return dataSource1.getConnection();
        }
        if ("W".equals(name.get())) {
            return dataSource2.getConnection();
        }
        return dataSource1.getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return null;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return null;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {

    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {

    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }

    /**
     * 实现了InitializingBean的默认方法
     * spring Bean初始化回调
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化
        name.set("W");
    }
}

到这里,基本就配置好了,我们只需要在controller内请求之前,加上判断数据源的标识,就可以动态切换数据源,来请求wp和wpa两个不同库的数据了,controller代码示例如下:

	@GetMapping("/user")
    public Result getUser(@RequestParam("id") int id){
    	// 设置数据源标识
        DynamicDatasource.name.set("R");
        log.info(String.format("查询datasource1数据源的%s号用户", id));
        return userService.selectById(id);
    }

    @GetMapping("/user2")
    public Result getUser2(@RequestParam("id") int id){
        DynamicDatasource.name.set("W");
        log.info(String.format("查询datasource2数据源的%s号用户", id));
        return userService.selectById(id);
    }
缺点

当然,这只是个demo帮助我们理解流程的,这套代码并不完美,缺点如下。

  1. DynamicDatasource中有很多方法并没有写具体的实现,当spring调用到这些方法时,返回值不能符合预期,代码不稳定。
  2. 数据源切换的标识是耦合在controller中的,这个方案不符合现实逻辑,最好通过aop来实现动态切换。

代码优化

优化版本代码示例

其实上述的一些问题,spring已经为我们提供了解决方案,我们可以用到AbstractRoutingDataSource抽象类,这个类并不复杂,下面简单介绍下这个类。

// 重点看这个类的几个成员变量
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
	// 需要自己去指定的,所有数据源(targetDataSources)
    @Nullable
    private Map<Object, Object> targetDataSources;
    // 需要指定一个默认数据源(defaultTargetDataSource)
    @Nullable
    private Object defaultTargetDataSource;
    // 会自动将targetDataSources赋值给自己(resolvedDataSources)
    @Nullable
    private Map<Object, DataSource> resolvedDataSources;// 是负责最终切换数据源的map
}

了解了这个类需要赋值的一些属性,我们就可以开始使用了。

优化步骤一:对DynamicDatasource类进行优化 ,继承AbstractRoutingDataSource
@Component
@Primary // 有多个datasource时,将此打他source设置为主要注入的bean,primary
public class DynamicDatasource extends AbstractRoutingDataSource {

    // 数据源标识,为了保证线程安全使用threadLocal
    public static ThreadLocal<String> name = new ThreadLocal<>();

    @Autowired
    DataSource dataSource1;

    @Autowired
    DataSource dataSource2;

    /**
     * 此方法作用是返回当前数据源标识
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return name.get();
    }

    /**
     * 重写父类方法之前,为父类基础属性进行赋值
     */
    @Override
    public void afterPropertiesSet() {
        // 为AbstractRoutingDataSource的主要参数进行赋值

        // targetDataSources初始化所有数据源
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("W", dataSource1);
        targetDataSources.put("R", dataSource2);
        super.setTargetDataSources(targetDataSources);

        // defaultTargetDataSource设置默认数据源
        super.setDefaultTargetDataSource(dataSource1);
        
        super.afterPropertiesSet();
    }
}
优化步骤二:通过AOP动态切换

新建DynamicDatasourceAspect切面类,拦截所有请求,根据具体业务解析请求头,来实现分配不同的数据源

@Component
@Aspect
@Slf4j
public class DynamicDatasourceAspect {

    @Before(value = "execution(public * com.example.demo.controller.*.*(..))")
    public void before(JoinPoint jp) {
        String name = "解析token获取用户对应的数据源";
        // 通过反射拿到请求的相关信息,进行解析,获取相应的标识
        // 根据具体业务对应的标识设置请求哪个数据源
        DynamicDatasource.name.set("W");
        log.info(name);
    }

}

多数据源事务控制

spring编程式事务

spring声明式事务

DynamicDataSource多数据源框架

框架介绍

使用步骤

其他说明

框架自动配置原理

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐