SpringBoot + MyBatis Plus多租戶動態數據源


背景

需求場景是需要實現一個支持多租戶多數據源的系統,每個租戶的數據庫完全隔離。並且系統需求通過區分不同租戶的請求進行動態數據源的切換。

系統底層框架是使用的SpringCloud + MyBatisPlus(一個mybatis的增強框架),數據庫連接池是Druid。

熟悉SpringBoot的同學都知道SpringBoot本身是可以配置多個數據源的,但是SpringBoot的多數據做不到動態的切換,只能在代碼里面通過注解或寫死。

基於以上情況,設計實現了一個動態切換數據源的實現方案。

實現功能

  • 通過域名進行租戶自動識別
  • 通過租戶識別信息,動態的選擇數據源
  • 各個spring微服務之間進行租戶信息傳遞
  • 通過注射方式進行強制數據源制定

下面介紹一下功能的核心實現。

核心實現

租戶識別

租戶信息的識別通過Nginx代理來實現,核心思路就是域名中包含租戶信息,然后通過Nginx代理時,在請求頭和相應頭中添加租戶的識別信息。

servier {
    listen 80;
    server_name ~^(?<sub>.+)\.zane\.com$;   #按后綴匹配域名,並截取租戶標示
    ...
    location / {
        ...
        proxy_set_header tenant $sub;       #請求頭添加租戶標示
        add_header tenant $sub;             #響應頭添加租戶標示
    }
}

如上配置后,通過Nginx代理后的請求都會帶上租戶信息。eg: abc.zane.com通過這個域名訪問系統時會識別出租戶為abc。

動態切換

這個是方案的核心部分,重寫了MyBatis的數據源初始化過程。
講解一下核心實現原理及核心的代碼部分。

主要通過以下步驟實現:

  1. 配置Spring攔截器,設置租戶標示
public class DataSourceInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        try{
            String tenantHeader = request.getHeader("tenant");
            if (StringUtils.hasText(tenantHeader)) {
                DataSourceTenantContextHolder.setCurrentTanent(tenantHeader);
            } else {
                DataSourceTenantContextHolder.setDefaultTenant();
            }
        }catch (Exception e){
            DataSourceTenantContextHolder.setDefaultTenant();
        }
        return true;
    }
}

自定義攔截器,攔截先前Nginx設置的租戶信息,並設置到DataSourceTenantContextHolder中。

public class DataSourceTenantContextHolder {
    public static final String DEFAULT_TENANT = "default";
    private static final InheritableThreadLocal<String> DATASOURCE_HOLDER = new InheritableThreadLocal<String>(){
        @Override
        protected String initialValue() {
            return DEFAULT_TENANT;
        }
    };
    //設置默認數據源
    public static void setDefaultTenant() {
        setCurrentTanent(DataSourceTenantContextHolder.DEFAULT_TENANT);
    }
    //獲取當前數據源配置租戶標識
    public static String getCurrentTenant() {
        return DATASOURCE_HOLDER.get();
    }
    //設置當前數據源配置租戶標識
    public static void setCurrentTanent(String tenant) {
        DATASOURCE_HOLDER.set(tenant);
    }
}

將租戶信息設置到InheritableThreadLocal中,實現線程內的租戶信息可見。

@Configuration
public class InterceptorConfiguration implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry){
       registry.addInterceptor(new DataSourceInterceptor());
    }
}

將自定義攔截器加到Spring容器中。

通過以上實現了一個請求內的租戶信息可見。

  1. 重寫MyBatis的數據源初始化
@Configuration
@MapperScan(sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfiguration {
    @Bean(name = "dataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
        dataSourceBuilder.type(DynamicDataSource.class);
        return dataSourceBuilder.build();
    }
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory getSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource)throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        ...
        return bean.getObject();
    }
}

使用自定義數據源實現DynamicDataSource代替原始的DruidDataSource。

將自定義的數據源設置到MyBatis的工產類中

  1. 通過租戶標示初始化數據
public class DynamicDataSource extends DruidDataSource {
    @Override
    public DruidPooledConnection getConnection() throws SQLException,PaaSException {
        String tenant = DataSourceTenantContextHolder.getCurrentTenant();
        // 根據當前id獲取數據源
        DruidDataSource datasource = initDatasource(tenant);
        if (null == datasource){
            throw new PaaSException(String.format("Error DynamicDataSource Config %s %s", tenant));
        }
        return datasource.getConnection();
    }
    
    prvate DruidDataSource initDatasource(String tenant){
        ...
    }
}

數據源需要實現通過租戶來初始化的邏輯,具體的初始化可以按需求實現initDatasource方法。

租戶傳遞

基於SpringCloud各個服務間是通過Feign來通信,那么只要實現一個簡單的Feign攔截器就可以。

@Component
public class FeignTenantInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        String tenant = DataSourceTenantContextHolder.getCurrentTenant();
        template.header("tenant",tenant);
    }
}

注釋指定數據源

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Tenant {
    String value() default DataSourceTenantContextHolder.DEFAULT_TENANT;
}

自定義一個租戶的注解類

@Aspect
@Component
@Order(0)
@Slf4j
public class DatasourceSelectorAspect {
    @Around("@annotation(tenant)")
    public Object beforeTenant(ProceedingJoinPoint joinPoint, Tenant tenant) throws Exception{
        String sourceTenant = DataSourceTenantContextHolder.getCurrentTenant();
        try{
            String tenantName = tenant.value();
            DataSourceTenantContextHolder.setCurrentTanent(tenantName);
        }catch (Exception e){
            log.warn("",e);
        }
        Object result;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            throw new Exception(e);
        } finally {
            DataSourceTenantContextHolder.setCurrentTanent(sourceTenant);
        }
        return result;
    }
 }

通過環繞型的AOP攔截, 在打了注解的方法上進行租戶的切換。實現注解指定數據源。

思考

上面方法基本已經滿足了前面的需求,可以做到不同租戶的動態數據源的切換。
但是還是有許多地方需要完善,比如:

  1. 不同租戶的數據源的緩存,避免重復初始化
  2. 重寫了MyBatis的工廠類,那么MyBatis plus的相關特性怎么保留。
  3. 如果一個租戶下也有多個數據怎么實現

大家可以思考一下這些問題。


免責聲明!

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



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