一、概述
当把数据库进行分布分表等集群化部署后,在应用层就需要能够随时切换访问数据源,这就需要用到动态数据源的技术。应用是通过DataSource来访问数据库的,所以动态数据源实现的技术归根结底是在能够根据情况动态切换DataSource。
二、基于Spring的AbstractRoutingDataSource实现动态数据源
基于Spring提供的AbstractRoutingDataSource组件,实现快速切换后端访问的实际数据库,该类实质充当了DataSource的路由中介, 能有在运行时, 根据某种key值来动态切换到真正的DataSource上。
源码分析
在AbstractRoutingDataSource的源码中,其继承AbstractDataSource抽象类,其核心方法为getConnection(),又可以发现getConnection()主要调用determineTargetDataSource()方法,该方法是确定使用哪个DataSource的核心,该部分逻辑调用determineCurrentLookupKey()抽象方法,所以我们只需要实现determineCurrentLookupKey()抽象方法即可实现动态切换数据源。
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
//...省略
public Connection getConnection() throws SQLException {
return this.determineTargetDataSource().getConnection();
}
public Connection getConnection(String username, String password) throws SQLException {
return this.determineTargetDataSource().getConnection(username, password);
}
//...省略
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = this.determineCurrentLookupKey();
DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
} else {
return dataSource;
}
}
@Nullable
protected abstract Object determineCurrentLookupKey();
//...省略
}
代码实现
1.在配置文件application.yml中配置多数据源信息。
server:
port: 9000
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
#自定义第一个数据源
datasource1:
url: jdbc:mysql://xxxx:3306/test?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: xxxx
initial-size: 1
min-idle: 1application.yml
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
#自定义第二个数据源
datasource2:
url: jdbc:mysql://xxxx:3306/test1?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: xxxx
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
创建数据源DataSource的配置类,其中创建2个 DataSource的Bean实例。
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean(name = "dataSource1")
@ConfigurationProperties(prefix = "spring.datasource.datasource1")
public DataSource dataSource1() {
// 底层会自动拿到spring.datasource1中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dataSource2")
@ConfigurationProperties(prefix = "spring.datasource.datasource2")
public DataSource dataSource2() {
// 底层会自动拿到spring.datasource2中的配置, 创建一个DruidDataSource
return DruidDataSourceBuilder.create().build();
}
}
3.创建动态数据源DynamicDataSource的配置类。其中核心是实现determineCurrentLookupKey()方法,通过静态的ThreadLocal name变量,可以实现获取当前线程需要的数据源。
@Component("dynamicDataSource")
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {
public static ThreadLocal<String> name = new ThreadLocal<>();
@Override
protected Object determineCurrentLookupKey() {
return name.get();
}
@Resource(name = "dataSource1")
DataSource dataSource1;
@Resource(name = "dataSource2")
DataSource dataSource2;
@Override
public void afterPropertiesSet() {
// 为targetDataSources初始化所有数据源
Map<Object, Object> targetDataSources=new HashMap<>();
targetDataSources.put("ds1",dataSource1);
targetDataSources.put("ds2",dataSource2);
super.setTargetDataSources(targetDataSources);
// 为defaultTargetDataSource 设置默认的数据源
super.setDefaultTargetDataSource(dataSource1);
super.afterPropertiesSet();
}
}
4.接口测试动态数据源。
import com.yangnk.mybatisplusdemo.config.DynamicDataSource;
import com.yangnk.mybatisplusdemo.domain.UserInfo;
import com.yangnk.mybatisplusdemo.mapper.UserInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Random;
@Controller
@RequestMapping("/RDS")
public class MyRDSController {
@Autowired
UserInfoMapper userInfoMapper;
@ResponseBody
@RequestMapping("/add1")
public String add1(@RequestParam(value = "dsKey",defaultValue = "ds1") String dsKey){
DynamicDataSource.name.set(dsKey);
System.out.println("add1");
int nextInt = new Random().nextInt(100);
UserInfo c = new UserInfo();
c.setId(nextInt);
c.setUser_name("name" + nextInt);
c.setAge(nextInt);
userInfoMapper.insert(c);
System.out.println(c);
DynamicDataSource.name.remove();
return c.toString();
}
@ResponseBody
@RequestMapping("/add2")
public String add2(@RequestParam(value = "dsKey",defaultValue = "ds2") String dsKey){
DynamicDataSource.name.set(dsKey);
System.out.println("add2");
int nextInt = new Random().nextInt(100) + 100;
UserInfo c = new UserInfo();
c.setId(nextInt);
c.setUser_name("name" + nextInt);
c.setAge(nextInt);
userInfoMapper.insert(c);
System.out.println(c);
DynamicDataSource.name.remove();
return c.toString();
}
}
三、基于dynamic-datasource实现动态数据源
dynamic-datasource是一个能实现动态切换数据源的框架,相较于基于Spring的AbstractRoutingDataSource实现动态数据源,他还有其他非常丰富的功能。
特性
- 数据源分组,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
- 内置敏感参数加密和启动初始化表结构schema数据库database。
- 提供对Druid,Mybatis-Plus,P6sy,Jndi的快速集成。
- 简化Druid和HikariCp配置,提供全局参数配置。
- 提供自定义数据源来源接口(默认使用yml或properties配置)。
- 提供项目启动后增减数据源方案。
- 提供Mybatis环境下的 纯读写分离 方案。
- 使用spel动态参数解析数据源,如从session,header或参数中获取数据源。(多租户架构神器)
- 提供多层数据源嵌套切换。(ServiceA >>> ServiceB >>> ServiceC,每个Service都是不同的数据源)
- 提供 不使用注解 而 使用 正则 或 spel 来切换数据源方案(实验性功能)。
- 基于seata的分布式事务支持。
代码实现
以下只展示重要步骤和重要信息。
1.POM配置文件添加dynamic-datasource-spring-boot-starter依赖项。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.0</version>
</dependency>
2.配置application.yml,在datasource
配置项后添加多数据源。
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
#使用dynamicDatasource框架
dynamic:
#设置默认的数据源或者数据源组,read
primary: read
#严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
strict: false
datasource:
db1:
url: jdbc:mysql://xxxx:3306/test?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: xxxx
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
db2:
url: jdbc:mysql://xxxx:3306/test1?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: xxxx
initial-size: 1
min-idle: 1
max-active: 20
test-on-borrow: true
driver-class-name: com.mysql.cj.jdbc.Driver
server:
port: 9000
3.通过@DS("xx")注解选择需要的数据源datasource。
@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
implements UserInfoService{
@Autowired
UserInfoMapper userInfoMapper;
@Override
@DS("db1")
@Transactional
public void insert1() {
System.out.println("add1");
int nextInt = new Random().nextInt(100);
UserInfo c = new UserInfo();
c.setId(nextInt);
c.setUser_name("name" + nextInt);
c.setAge(nextInt);
userInfoMapper.insert(c);
System.out.println(c);
}
@Override
@DS("db2")
@Transactional
public void insert2() {
System.out.println("add2");
int nextInt = new Random().nextInt(100) + 100;
UserInfo c = new UserInfo();
c.setId(nextInt);
c.setUser_name("name" + nextInt);
c.setAge(nextInt);
userInfoMapper.insert(c);
System.out.println(c);
}
}
参考资料
- 使用dynamic-datasource-spring-boot-starter做多数据源及源码分析:https://blog.csdn.net/w57685321/article/details/106823660 (有详细用dynamic-datasource框架源码说明)