1、前言
当我们需要搭建一个平台给不同的客户群体使用同一套应用时,又需要针对不同的客户群里数据隔离,这就迎来了多租户的时代。
多租户的数据库级别的隔离就需要根据不同租户配置不同的数据源。
Mybatis-plus中提供了动态数据源dynamic-datasource-spring-boot-starter
来实现,详细的使用文档确实收费的,有兴趣的可以付费学习一下。
今天自己实现动态数据源的切换以及新增。
2、关键信息
动态数据源的切换会使用到ThreadLocal
和 AbstractRoutingDataSource
。
ThreadLocal:
主要实现不同线程的资源隔离,用来储存当前线上的数据源标识。AbstractRoutingDataSource:
根据用户定义的规则选择当前的数据源,需要重写抽象方法determineCurrentLookupKey()
,决定使用哪一个数据源。
3、环境准备
- SpringBoot 2.6.13
- mybatis-plus 3.5.7
- lombok1.18.24
4、实现ThreadLocal
创建本地线程类,用来保存当前线程的需要使用的数据源简称或者标识。
/**
*
*
* @author: ws
* @date: 2024/9/18 10:09
*/
public class DataSourceContextHolder {
private static final ThreadLocal<String> DATASOURCE_CONTEXT = new ThreadLocal<>();
private DataSourceContextHolder(){
}
public static void setDataSource(String dataSourceName){
DATASOURCE_CONTEXT.set(dataSourceName);
}
public static String getDataSource(){
return DATASOURCE_CONTEXT.get();
}
public static void clearDataSource(){
DATASOURCE_CONTEXT.remove();
}
}
5、实现AbstractRoutingDataSource
类的全路径:org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
定义一个动态加载数据源的类,通过
determineCurrentLookupKey()
判断最终使用哪个数据源
/**
* 动态数据源
*
* @author: ws
* @date: 2024/9/6 13:08
*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
private final Map<Object, Object> targetDataSources = new HashMap<>();
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSource();
}
/**
* 加载所有的数据源
*
* @author ws
* @date 2024/9/18 15:35
*/
public void createDateSource(List<DataSourceEntity> entityList){
try{
if (CollectionUtils.isNotEmpty(entityList)) {
for (DataSourceEntity entity : entityList) {
// 校验数据库连接是否有效
log.info("校验数据库连接是否有效>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
Class.forName(entity.getDriverClassName());
Connection connection = DriverManager.getConnection(entity.getUrl(), entity.getUsername(), entity.getPassword());
if (connection != null) {
HikariDataSource dataSource = getHikariDataSource(entity);
targetDataSources.put(entity.getKey(), dataSource);
}
super.setTargetDataSources(targetDataSources);
//将TargetDataSources中的连接信息放入resolvedDataSources管理
super.afterPropertiesSet();
}
}
}catch (Exception e){
log.error("检查数据源异常:", e);
}
}
public HikariDataSource getHikariDataSource(DataSourceEntity entity) {
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setDriverClassName(entity.getDriverClassName());
hikariConfig.setJdbcUrl(entity.getUrl());
hikariConfig.setUsername(entity.getUsername());
hikariConfig.setPassword(entity.getPassword());
return new HikariDataSource(hikariConfig);
}
/**
* 动态新增数据源
*
* @author ws
* @date 2024/9/18 15:39
*/
public void addDateSource(DataSourceEntity entity){
try {
log.info("校验数据库连接是否有效>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
Class.forName(entity.getDriverClassName());
Connection connection = DriverManager.getConnection(entity.getUrl(), entity.getUsername(), entity.getPassword());
if (connection != null) {
// 关闭老的数据库链接
Map<Object, DataSource> resolvedDataSources = getResolvedDataSources();
if (CollectionUtils.isNotEmpty(resolvedDataSources)) {
resolvedDataSources.forEach((k, v) -> {
// 因为这里使用的是HikariDataSource数据源,所以可以直接强转
HikariDataSource hikariDataSource = (HikariDataSource) v;
hikariDataSource.close();
});
}
HikariDataSource dataSource = getHikariDataSource(entity);
targetDataSources.put(entity.getKey(), dataSource);
}
super.setTargetDataSources(targetDataSources);
//将TargetDataSources中的连接信息放入resolvedDataSources管理
super.afterPropertiesSet();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
其中createDateSource()
用来加来所有的数据源,博文中使用的数据源是HikariDataSource
。
addDateSource()
是动态的新增一个新的数据源,并重新初始化所有的数据源信息。
determineCurrentLookupKey()
直接从DataSourceContextHolder
中获取当前数据源的简称。
6、数据源信息实体DataSourceEntity
DataSourceEntity
记录数据源的链接信息以及别名简称
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DataSourceEntity implements Serializable {
private static final long serialVersionUID = 1584256444573613974L;
/**
* 数据源的Key
*/
private String key;
/**
* 数据库连接地址
*/
private String url;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 驱动
*/
private String driverClassName;
}
7、数据源的配置
Spring容器启动的时候加载一个数据源
@Configuration
public class DataSourceConfig {
@Bean
public DynamicDataSource dynamicDataSource(){
DynamicDataSource dynamicDataSource = new DynamicDataSource();
List<DataSourceEntity> dsList = new ArrayList<>();
dsList.add(new DataSourceEntity("dev", "jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf-8", "root", "root", "com.mysql.cj.jdbc.Driver"));
dynamicDataSource.createDateSource(dsList);
return dynamicDataSource;
}
}
8、测试控制层
其中类似
AuctionPlanMapper
使用的Mybatis-plus,可以自行生成。dev
和test
笔者在测试的时候分别代表的不同的开发测试库和测试数据库。
/**
* TODO
*
* @author: ws
* @date: 2024/9/18 10:20
*/
@RestController
public class TestController {
@Resource
private AuctionPlanMapper auctionPlanMapper;
@Resource
private DynamicDataSource dynamicDataSource;
@GetMapping("/testDev")
public Object testDev(){
DataSourceContextHolder.setDataSource("dev");
AuctionPlanExt auctionPlanExt = auctionPlanMapper.selectById(3);
DataSourceContextHolder.clearDataSource();
return auctionPlanExt;
}
@GetMapping("/testInt")
public Object testInt(){
DataSourceContextHolder.setDataSource("test");
AuctionPlanExt auctionPlanExt = auctionPlanMapper.selectById(3);
DataSourceContextHolder.clearDataSource();
return auctionPlanExt;
}
@GetMapping("/addDataSource")
public String addDataSource(){
DataSourceEntity dataSourceEntity = new DataSourceEntity("test", "jdbc:mysql://127.0.0.1:3306/test02?characterEncoding=utf-8", "root", "root", "com.mysql.cj.jdbc.Driver");
dynamicDataSource.addDateSource(dataSourceEntity);
return "success";
}
}
9、项目测试
因为配置中只加载了一个数据源,所以只检测了一个数据源。
9.1 测试/testDev
9.2 测试/testInt
报错了,因为笔者没有配置
test
的地址。
9.3 测试/addDataSource
从日志可以看出,加载了两个数据源。
9.4 重新测试/testInt
结果有了。返回单额数据和dev
的数据不一致,确实是两个库的数据。