上篇我们分享了SpringBoot整合Mybatisplus 完成基本多数据源的配置,此篇我们从更高的层面去去分享 SpringBoot整合Mybatisplus实现完全动态获取多数据源,此方案适合更多的业务场景,比如每个用户一个数据源、每种类型以一个数据源、每种请求一个数据源等等,即此方案也就是常说的多租户、读写分离的业务场景等!下面开始切入正题:
1、核心pom 相关文件配置
<dependency>
<groupId></groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId></groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
2、yml配置默认数据源:
spring:
datasource:
dynamic:
primary: master# 配置默认数据库,及没有注解指定时走的数据库
#严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
strict: false
datasource:
master: # 数据源1配置
url: jdbc:mysql://localhost:3306/user_db1?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name:
durid:
initial-size: 1
max-active: 20
min-idle: 1
max-wait: 60000
autoconfigure:
exclude:
此数据源是默认的数据源,服务启动后会有一个 key 为master的主数据源;如果服务启动不设置默认数据源,启动时会报错,健康检查失败,错误提示核心是:
: dynamic-datasource can not find primary datasource
at (:91) ~[dynamic-datasource-spring-boot-starter-3.5.:3.5.1]
at (:120) ~[dynamic-datasource-spring-boot-starter-3.5.:3.5.1]
at (:78) ~[dynamic-datasource-spring-boot-starter-3.5.:3.5.1]
但是,服务最终是启动成功的,如果健康检查不拦截的话。
3、封装数据源处理类
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import .slf4j.Slf4j;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
/**
* @Author nandao
* @Date 2021/12/01
**/
@Service
@Slf4j
public class CustomDataSourceService {
@Resource
private DataSource dataSource;
@Resource
private DefaultDataSourceCreator dataSourceCreator;
@PostConstruct
public void init (){
//可以初始化数据源在此
}
/**
* 获取当前所有数据源的key
*/
public Set<String> getAllDataSourceKey() {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
return ().keySet();
}
/**
* 获取当前所有数据源url
*/
public String userCurrentDataSource(String tenantId) {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
Set<String> urlList = ().keySet();
if((tenantId)){
DataSource dataSource = (tenantId);
if((dataSource)){
ItemDataSource druidDataSource = (ItemDataSource) dataSource;
String name = ();
return null;
}
}
return null;
}
/**
* 添加数据源
*/
public Set<String> add(DataSourcePO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
(dto, dataSourceProperty);
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = (dataSourceProperty);
// 测试连接
try {
();
}catch (Exception e){
throw new IllegalStateException("[" + (dto) + "]" + ":数据源连接不上可能是连接参数有误或库未创建!");
}
((), dataSource);
return ().keySet();
}
/**
* 删除容器中数据源
*/
@DeleteMapping
public Boolean remove(String name) {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
(name);
return ;
}
}
4、业务切面处理:
import ;
import ;
import ;
import .slf4j.Slf4j;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
/**
* @Author nandao
* @Date 2021/11/5
**/
@Aspect
@Component
@Slf4j
@Order(-1)
public class DynamicDataSourceAspect {
@Resource
private CustomDataSourceService customDataSourceService ;
@Resource
private HttpServletRequest request;
//@Pointcut("@annotation()")
@Pointcut("execution(* .*.*(..))")
public void dbMoreProduct() {
}
@Around("dbMoreProduct()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 获取当前租户
String tenantId = ("tenant");
// 获取当前所有的数据源
Set<String> dateSourceKeyList = ();
if (!dateSourceKeyList .contains(tenantId)) {
// 获取租户对应数据库链接配置
DataSourceVO dataSourceInfo = getTenantDateSource();
// 添加数据源
(dataSourceInfo);
}
// 手动指定数据源
String push = (tenantId);
("租户:{},当前租户的数据源:{}",tenantId,push);
try {
return ();
} finally {
();
}
}
private DataSourceVO getTenantDateSource() {
String tenantId = ("tenant");
DatabaseUrl databaseUrl = checkDataSource(tenantId);
DataSourceVO dataSource = new DataSourceVO();
(tenantId);
(());
(());
(());
return dataSource;
}
private DatabaseUrl checkDataSource(String tenantId) {
//去数据库查询 返回databaseUrl,此处省略.....
DatabaseUrl databaseUrl ;
return databaseUrl;
}
}
5,注意事项:
/**
* 此处根据业务场景判断是否要加,放在finally中:尽量加,利大于弊!
* 好处:此线程数据源使用后,清楚,避免内存泄露,同时避免其他线程拿到此数据源(垃圾回收之前),造成数据库串用,后果很严重,因为此处是栈的原理;
* 弊端:如果扫描mapper文件,可能造成多次重置数据源,但是安全稳定起见,还是要清除;
*/
();
业务场景:是张三、李四分别操作A、B两个数据库,张三一直操作A,然后李四操作B,这期间李四很可能拿到A库的数据源执行!
到此大概业务逻辑分享清楚,用户和自己对应的数据源信息可以通过管理后台提前维护到数据库,用户首次登录获取对应的数据源存到数据源容器中,根据租户和线程调用相关的数据源;当然数据源也可以采用缓存预热的方案,服务启动时把所有用户的数据源加载到容器中,功能接口调用时就不用在去数据库查询了,进一步提高了性能!
注意:AOP切面可以用扫描自定义注解、包名等等,具体选择根据业务来判断哪种合适,所有技术方案脱离了业务就是耍流氓!!!下面我们详细分享多数据源的源码,敬请期待!