多數據源解決方案
目前在SpringBoot
框架基礎上多數據源的解決方案大多手動創建多個DataSource
,后續方案有三:
- 繼承
org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource
,使用AOP
切面注入相應的數據源 ,但是這種做法僅僅適用單Service
方法使用一個數據源可行,如果單Service
方法有多個數據源執行會造成誤讀。 - 通過
DataSource
配置JdbcTemplate
Bean,直接使用JdbcTemplate
操控數據源。 - 分別通過
DataSource
創建SqlSessionFactory
並掃描相應的Mapper
文件和Mapper
接口。
MybatisPlus
的多數據源解決方案正是AOP
,繼承了org.springframework.jdbc.datasource.AbstractDataSource
,有自己對ThreadLocal
的處理。通過注解切換數據源。也就是說,MybatisPlus
只支持在單Service
方法內操作一個數據源,畢竟官網都指明——“強烈建議只注解在service實現上”。
而后,注意看com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder
,也就是MybatisPlus
是如何切換數據源的。
pom引用
<mybatisplus.version>3.5.0</mybatisplus.version>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-core</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-annotation</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/transmittable-thread-local -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.12.2</version>
</dependency>
數據庫配置
spring:
datasource:
dynamic:
primary: generali
strict: false
datasource:
generali:
url: jdbc:mysql://awschina-nx-xman-gis-rds-01.crag5sximeqt.rds.cn-northwest-1.amazonaws.com.cn:3306/leads_lms?allowMultiQueries=true&multiStatementAllow=true&serverTimezone=GMT%2B8&autoReconnect=true&failOverReadOnly=false&useSSL=false&rewriteBatchedStatements=true
username: admin
password: VP4tR5BmBVvaYGyc
gen:
url: jdbc:mysql://awschina-nx-xman-gis-rds-01.crag5sximeqt.rds.cn-northwest-1.amazonaws.com.cn:3306/leads_gen?allowMultiQueries=true&multiStatementAllow=true&serverTimezone=GMT%2B8&autoReconnect=true&failOverReadOnly=false&useSSL=false&rewriteBatchedStatements=true
username: admin
password: VP4tR5BmBVvaYGyc
配置租戶與租戶id
tenantcode:
ignoreUrl: /common/**,/nano/**,/test/**,/i18n/**,/**/v2/api-docs,/**/login,/**/token,/health
mapping:
generali: 1
gen: 2
代碼:
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.util.Map;
/**
* @author qhong
* @date 2022/2/16 11:53
**/
@Configuration
@Data
@ConfigurationProperties(prefix = "tenantcode")
public class TenantCodeConfig {
/**
* 不進行身份校驗的url配置
*/
private String ignoreUrl;
/**
* 租戶code與db中tenantId匹配關系 ,例如 generali:1
*/
private Map<String, Long> mapping;
/**
* 根據tenantCode獲取tenantId
* @param tenantCode
* @return
*/
public Long getTenantIdByTenantCode(String tenantCode) {
if (StringUtils.isEmpty(tenantCode)) {
return null;
}
return mapping.get(tenantCode);
}
/**
* 根據tenantId獲取tenantCode
* @param tenantId
* @return
*/
public String getTenantCodeByTenantId(Long tenantId) {
if (tenantId == null) {
return null;
}
return mapping.entrySet().stream().filter(x -> x.getValue().equals(tenantId)).map(x -> x.getKey()).findFirst().orElse(null);
}
}
租戶code全局獲取
import com.alibaba.ttl.TransmittableThreadLocal;
import org.springframework.util.StringUtils;
import java.util.ArrayDeque;
import java.util.Deque;
/**
* @author qhong
* @date 2022/1/26 16:30
**/
public class ThreadContextHolder {
private static final ThreadLocal<Deque<String>> CONTENT_HOLDER = new TransmittableThreadLocal<Deque<String>>() {
@Override
protected Deque<String> initialValue() {
return new ArrayDeque<>();
}
};
private ThreadContextHolder() {
}
/**
* 獲得當前線程數據源
* @return 數據源名稱
*/
// public static String peek() {
// return CONTENT_HOLDER.get().peek();
// }
/**
* 獲得當前線程數據源
*
* @return 數據源名稱
*/
public static String element() {
Deque<String> strings = CONTENT_HOLDER.get();
if (strings == null || strings.size() == 0) {
return null;
}
return strings.element();
}
/**
* 設置數據源信息
*/
public static void push(String ds) {
CONTENT_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds);
}
/**
* 清空當前線程數據源
*/
public static void poll() {
Deque<String> deque = CONTENT_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
CONTENT_HOLDER.remove();
}
}
/**
* 強制清空本地線程
*/
public static void clear() {
CONTENT_HOLDER.remove();
}
}
web全局請求攔截:
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.zhongan.leads.config.AuthManager;
import com.zhongan.leads.config.TenantCodeConfig;
import com.zhongan.leads.dto.UserInfo;
import com.zhongan.leads.utils.ThreadContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
/**
* @author qhong
* @date 2022/2/16 11:38
**/
@Component
@Slf4j
public class TenantInterceptor extends HandlerInterceptorAdapter {
@Autowired
private AuthManager authManager;
@Autowired
private TenantCodeConfig tenantCodeConfig;
private static final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (StringUtils.hasText(tenantCodeConfig.getIgnoreUrl()) && checkIgnoreUrl(tenantCodeConfig.getIgnoreUrl())) {
//不進行身份校驗的,直接通過
return true;
}
try {
UserInfo userInfo = authManager.getUserInfo();
String tenantCode = userInfo.getTenantId().toString();
DynamicDataSourceContextHolder.push(tenantCode);
ThreadContextHolder.push(tenantCode);
return true;
} catch (Exception e) {
throw e;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
DynamicDataSourceContextHolder.clear();
ThreadContextHolder.clear();
}
/**
* 校驗當前訪問的url是否忽略校驗的路徑
*
* @param url
* @return
*/
private boolean checkIgnoreUrl(String url) {
if (StringUtils.isEmpty(url)) {
return false;
}
String currentUrl = authManager.getRequestURI();
List<String> urlList = Arrays.asList(url.split(","));
for (int i = 0; i < urlList.size(); i++) {
if (antPathMatcher.match(urlList.get(i), currentUrl)) {
return true;
}
}
return false;
}
}
定時任務:
定時任務需要遍歷所有的租戶數據庫
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.zhongan.leads.config.TenantCodeConfig;
import com.zhongan.leads.utils.ThreadContextHolder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author qhong
* @date 2022/2/17 14:16
* 用於標志多租戶定時任務,對各個租戶db進行輪詢處理,return 為null
**/
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Aspect
@Component
@Slf4j
public class ScheduledMultiTenantAspect {
private final TenantCodeConfig tenantCodeConfig;
@Pointcut("@annotation(org.springframework.scheduling.annotation.Scheduled)")
public void scheduledMultiTenantAspect() {
}
@Around(value = "scheduledMultiTenantAspect()")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
int tenantSize = tenantCodeConfig.getMapping().size();
if (tenantSize == 1) {
pjp.proceed();
return null;
}
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(tenantSize);
tenantCodeConfig.getMapping().entrySet().forEach(x -> {
fixedThreadPool.execute(() -> {
ThreadContextHolder.push(x.getKey());
DynamicDataSourceContextHolder.push(x.getKey());
log.info("ScheduledMultiTenantAspect,method:{},tenantcode:{}", method.getName(), x.getKey());
try {
pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
ThreadContextHolder.clear();
DynamicDataSourceContextHolder.clear();
}
});
});
fixedThreadPool.shutdown();
while (true) {
if (fixedThreadPool.isTerminated()) {
//log.info("ScheduledMultiTenantAspect,method:{} terminated", method.getName());
break;
}
}
//log.info("ScheduledMultiTenantAspect,method:{} end", method.getName());
return null;
}
}
Async異步任務:
DynamicDataSourceContextHolder
不支持異步,線程池,所以異步任務需要自己處理
public class MultiTenantCodeHelper {
/**
* DynamicDataSourceContextHolder不支持異步,線程池,使用該方法賦值
*/
public static void setDynamicDataSourceByThreadHolderForAsync(){
String tenantCode = ThreadContextHolder.element();
DynamicDataSourceContextHolder.push(tenantCode);
}
}
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.zhongan.leads.utils.MultiTenantCodeHelper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author qhong
* @date 2022/2/17 19:18
**/
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Aspect
@Component
@Slf4j
public class AsyncMultiTenantAspect {
@Pointcut("@annotation(org.springframework.scheduling.annotation.Async)")
public void asyncMultiTenantAspect() {
}
@Around(value = "asyncMultiTenantAspect()")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
try {
MultiTenantCodeHelper.setDynamicDataSourceByThreadHolderForAsync();
return pjp.proceed();
} catch (Exception e) {
throw e;
} finally {
DynamicDataSourceContextHolder.clear();
}
}
}
Redis緩存:
需要區分各個租戶的緩存信息,防止重復
日志:
需要區分各個租戶的日志信息,方便后續查看
總結
mybatis-plus
的動態數據庫切換,必須在controller
方法之前,因為controller
初始化的時候就設置默認db