美文网首页
SpringBoot+Mybatis Plus多租户动态数据源

SpringBoot+Mybatis Plus多租户动态数据源

作者: 龙小草 | 来源:发表于2020-04-27 13:55 被阅读0次

    背景

    需求场景是需要实现一个支持多租户多数据源的系统,每个租户的数据库完全隔离。并且系统需求通过区分不同租户的请求进行动态数据源的切换。

    系统底层框架是使用的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. 如果一个租户下也有多个数据怎么实现

    大家可以思考一下这些问题。

    相关文章

      网友评论

          本文标题:SpringBoot+Mybatis Plus多租户动态数据源

          本文链接:https://www.haomeiwen.com/subject/agpywhtx.html