Mybatis-Plus 多數據源動態切換


多數據源解決方案

目前在SpringBoot框架基礎上多數據源的解決方案大多手動創建多個DataSource,后續方案有三:

  1. 繼承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,使用AOP切面注入相應的數據源 ,但是這種做法僅僅適用單Service方法使用一個數據源可行,如果單Service方法有多個數據源執行會造成誤讀。
  2. 通過DataSource配置 JdbcTemplateBean,直接使用 JdbcTemplate操控數據源。
  3. 分別通過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

參考:

dynamic-datasource

MybatisPlus多數據源及事務解決思路

mybatis-plus多數據源解析

mybatis-plus多數據源切換失敗


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM