SpringBoot 手动实现动态切换数据源 DynamicSource (上)

大家好,我是此林。

在实际开发中,经常可能遇到在一个SpringBoot Web应用中需要访问多个数据源的情况。

下面来介绍一下多数据源的使用场景、底层原理和手动实现。

一、 多数据源经典使用场景

场景一:业务复杂,数据量过大

1. 业务初期,开发的SpringBoot应用只需要访问一个数据库。

2. 随着业务的复杂度和数据量的不断增长,一台Mysql服务器容量可能存不下了,或者说业务复杂需要对接多个数据源。

3. 此时,一个SpringBoot 需要访问多个数据源。

上文所述也就是应用没有拆分,数据库进行了拆分。

有人可能会说,为什么不把应用也拆分成微服务,每个服务可以使用自己的独立的数据库。

答:要看实际业务,SpringBoot拆分成微服务也需要成本。

场景二:读写分离

虽然一个SpringBoot应用使用一个数据源,但为了保证Mysql的性能和高可用性,采用了Mysql主从集群的方式部署,主数据库只进行写操作,从数据库负责读操作。

此时,需要进行数据源的动态切换。

常用中间件:ShardingSphere、MyCat。

Mysql主从集群、读写分离,解决了以下问题:

1. 提高并发量,因为写锁会阻塞读操作。

2. 保证高可用性,数据备份。

二、动态切换数据源的底层原理 

我们先来想下,要用Spring 去操作mysql,配置流程。

比如我们执行了 userMapper.insert(user) ,见下图。

简而言之,ORM框架底层通过Spring-jdbc拿到我们配置的DataSource,再调用getConnection() 方法拿到数据库connection连接。

那么,Mybatis 通过 connection 就可以对数据库进行 CRUD 操作了。

观察发现,要实现动态数据源切换,我们能配置的只有DataSource这个扩展点。

所以,更改如下。

这里我们自定义了DynamicDataSource类,实现了DataSource接口,重写了getConnection() 方法。

读操作(用R标识) ,写操作(用W标识)。

根据不同的业务标识(R 或 W),来返回不同的注入的datsource bean(最左侧)。

因为前面也说了,只要执行了userMapper.insert() 方法,那么它底层就会先去 getConnection 得到数据库连接,才能对mysql 进行 CRUD操作。

三、手动实现

pom.xml

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.3.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.24</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

application.yml

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    datasource1:
      url: jdbc:mysql://localhost:3306/datasource1?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
    datasource2:
      url: jdbc:mysql://localhost:3307/datasource2?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver

DataSourceConfig.java

@Configuration
public class DataSourceConfig {

    @Bean(name = "dataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.datasource1")
    public DataSource dataSource1(){
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dataSource2")
    @ConfigurationProperties(prefix = "spring.datasource.datasource2")
    public DataSource dataSource2(){
        return DruidDataSourceBuilder.create().build();
    }
}

DynamicDataSource.java

@Component
@Primary // 将该Bean设置为主要注入Bean
public class DynamicDataSource implements DataSource, InitializingBean {

    // 当前使用的数据源标识
    public static ThreadLocal<String> name = new ThreadLocal<>();

    // 写
    @Autowired
    @Qualifier("dataSource1")
    DataSource dataSource1;

    // 读
    @Autowired
    @Qualifier("dataSource2")
    DataSource dataSource2;

    @Override
    public Connection getConnection() throws SQLException {
        if (name.get().equals("R")) {
            return dataSource1.getConnection();
        } else {
            return dataSource2.getConnection();
        }
    }

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

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

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

    @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 void afterPropertiesSet() throws Exception {
        name.set("W");
    }
}

由于我们注入了三个DataSource:datasource1、datasource2、DynamicDatasource

1. Spring 首先根据DataSource类型去IOC 容器中找,找到了三个DataSource。

2. 找到了后再根据beanName = "datasource" 去找

3. 我们这里没有bean的名字叫datasource的,所以Spring不知道使用哪个DataSource。

解决:在DynamicDatasource类上加@Primary,将其作为主要的Bean优先注入使用。

(即:当出现相同的DataSource类型,优先使用DynamicDatasource)

测试:

通过浏览器访问,查看mysql数据库检验是否进行了对应的数据源切换即可。

当然,这只是粗糙的实现了以下动态数据源的切换,为了讲明白原理,简化了很多步骤。

后续会出SpringBoot 手动实现动态切换数据源 DynamicSource (中)、SpringBoot 手动实现动态切换数据源 DynamicSource (下),讲述进阶版、多数据源事务管理、及主流框架使用,持续更新!

关注我吧,我是此林,带你看不一样的世界!

更新后续:

2024.12.12

SpringBoot 手动实现动态切换数据源 DynamicSource (中)-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值