大家好,我是此林。
在实际开发中,经常可能遇到在一个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