DsProcessor
配合@DS 动态切换数据源处理器,自己实现继承重新matches与 doDetermineDatasource 方法就可以了
@Component // 注入Spring中
@RequiredArgsConstructor
public class MyDsProcessor extends DsProcessor {
// 必须已#开头才生效
public final static String KEY = "#DS";
/**
* 抽象匹配条件 匹配才会走当前执行器否则走下一级执行器
* @param key DS注解里的内容
* @return 是否匹配
*/
@Override
public boolean matches(String key) {
return Objects.equals(KEY, key);
}
/**
* 抽象最终决定数据源
* @param invocation 方法执行信息
* @param key DS注解里的内容
* @return 数据源名称
*/
@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
return getDs();
}
/**
* 获取请求头中ds的值
* @return
*/
private String getDs() {
javax.servlet.http.HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String header = request.getHeader("ds");
return header;
}
}
@DS(value = MyDsProcessor.KEY)
@Documented
@Retention(RUNTIME)
@Target({ FIELD, METHOD })
public @interface TenantDS {
}
使用方式
启动后动态添加数据源
@Configuration
@RequiredArgsConstructor
public class DBConfig {
private final DynamicRoutingDataSource drds;
/**
* Hikari数据源创建器
* 配置详情取配置文件 spring.datasource.dynamic.hikari
*/
private final HikariDataSourceCreator dataSourceCreator;
@PostConstruct
public void initialization() {
System.err.println(drds);
createDataSource("sal");
System.err.println("dataSourceProperty = "+dataSourceCreator);
}
/**
* 添加数据源
* @param ds 数据源名称
*/
private void createDataSource(String ds) {
String url = "jdbc:mysql://127.0.0.1:3306/db_temp?autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8";
String clsName ="com.mysql.cj.jdbc.Driver";
String name = "root";
DataSourceProperty config = new DataSourceProperty();
config.setUrl(url);
config.setDriverClassName(clsName);
config.setUsername(name);
config.setPassword(name);
DataSource dataSource = dataSourceCreator.createDataSource(config);
drds.addDataSource(ds, dataSource);
}
}
spring:
datasource:
dynamic:
hikari:
# 池中维护的最小空闲连接数
min-idle: 5
# 池中最大连接数,包括闲置和使用中的连接
max-pool-size: 20
# 池中连接最长生命周期
max-lifetime: 1800000
# 自动提交从池中返回的连接
is-auto-commit: true
# 连接允许在池中闲置的最长时间
idle-timeout: 30000
# 等待来自池的连接的最大毫秒数
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://127.0.0.1:3306/tmp?autoReconnect=true&allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
DynamicDataSourceContextHolder
手动切换数据源
/**
* 核心基于ThreadLocal的切换数据源工具类
*
* @author TaoYu Kanyuxia
* @since 1.0.0
*/
public final class DynamicDataSourceContextHolder {
/**
* 为什么要用链表存储(准确的是栈)
* <pre>
* 为了支持嵌套切换,如ABC三个service都是不同的数据源
* 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
* 传统的只设置当前线程的方式不能满足此业务需求,必须使用栈,后进先出。
* </pre>
*/
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
@Override
protected Deque<String> initialValue() {
return new ArrayDeque<>();
}
};
private DynamicDataSourceContextHolder() {
}
/**
* 获得当前线程数据源
*
* @return 数据源名称
*/
public static String peek() {
return LOOKUP_KEY_HOLDER.get().peek();
}
/**
* 设置当前线程数据源
* <p>
* 如非必要不要手动调用,调用后确保最终清除
* </p>
*
* @param ds 数据源名称
*/
public static String push(String ds) {
String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;
LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
return dataSourceStr;
}
/**
* 清空当前线程数据源
* <p>
* 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称
* </p>
*/
public static void poll() {
Deque<String> deque = LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
/**
* 强制清空本地线程
* <p>
* 防止内存泄漏,如手动调用了push可调用此方法确保清除
* </p>
*/
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
}
工具封装
/**
* 数据源切换工具
*/
@Slf4j
public class DynamicDSExecute {
/**
* 切换到指定数据源并执行业务逻辑
*
* @param ds 目标数据源
* @param executor 业务逻辑执行器
*/
public static <T> T execute(String ds, Supplier<T> executor) {
DynamicDataSourceContextHolder.push(ds);
try {
return executor.get();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
/**
* 切换到指定数据源并执行业务逻辑
*
* @param ds 目标数据源
* @param executor 业务逻辑执行器
*/
public static <T> T execute(String ds, Supplier<T> executor, Supplier<T> failExecutor) {
DynamicDataSourceContextHolder.push(ds);
try {
return executor.get();
} catch (Throwable e) {
log.error("执行业务逻辑发生异常", e);
return failExecutor.get();
} finally {
DynamicDataSourceContextHolder.poll();
}
}
}
相关依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>