SpringBoot + BeetlSQL3 多租户改造

Posted Start_liyi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot + BeetlSQL3 多租户改造相关的知识,希望对你有一定的参考价值。

最近听闻beetlSQL升级到3了,正好最近在研究多租户,于是乎就做了一下beetlSQL3的实现(分库分表)。
[BeetlSQL3官方文档](https://www.kancloud.cn/xiandafu/beetlsql3_guide/1945631)

基本的思路是将租户id与数据源连接信息作为Map加载到动态数据源中,
过来的请求将租户id放到header上,通过配置过滤器获取当前请求的tenantId,然后设置当前Thread。
通过重写determineCurrentLookupKey方法根据当前租户id获取Datasource实现分库。

1. pom追加beetlSQL3的引用
<!-- https://mvnrepository.com/artifact/com.ibeetl/sql-springboot-starter -->
    <dependency>
        <groupId>com.ibeetl</groupId>
        <artifactId>sql-springboot-starter</artifactId>
        <version>3.3.9-RELEASE</version>
    </dependency>
2. application.yml
   *这里设置主数据源,主数据源数据库里有租户管理表。*
   
spring:
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://略
    username: root
    password: 
    hikari:
      connection-timeout: 15000
      connection-test-query: SELECT 1
      max-lifetime: 200000
      idle-timeout: 200000
beetlsql:
  sqlManagers: sqlManager1
  sqlManager1:
    ds: master
    basePackage: org.beetl.sql.springboot.simple
    daoSuffix: Dao
3. DynamicDataSource 这个类用来继承AbstractRoutingDataSource,原理就是通过lookupKey去找当前数据源,详细说明网上应该有很多,这里只做实现。

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    public DataSource determineTargetDataSource() {
        return super.determineTargetDataSource();
    }

    /**
     * 底层根据lookupKey去找数据源,所以这里重写,去ThreadLocal里去取
     * 
     * @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()
     */
    @Override
    public Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceKey();
    }

    /**
     * 设置默认数据源
     * 
     * @param defaultDataSource
     */
    public void setDefaultDataSource(Object defaultDataSource) {
        super.setDefaultTargetDataSource(defaultDataSource);
    }

    /**
     * 设置数据源
     * 
     * @param dataSources
     */
    public void setDataSources(Map<Object, Object> dataSources) {
        super.setTargetDataSources(dataSources);
        // 将数据源的 key 放到数据源上下文的 key 集合中,用于切换时判断数据源是否有效
        DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
    }

}
4. DynamicDataSourceContextHolder ThreadLocal控制

public class DynamicDataSourceContextHolder {

    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>() {
        /**
         * 将 master 数据源的 key作为默认数据源的 key
         */
        @Override
        protected String initialValue() {
            return "master";
        }

    };

    /**
     * 数据源的 key集合,用于切换时判断数据源是否存在
     */
    public static List<Object> dataSourceKeys = new ArrayList<>();

    /**
     * 切换数据源
     * 
     * @param key
     */
    public static void setDataSourceKey(String key) {
        contextHolder.set(key);
    }

    /**
     * 获取数据源
     * 
     * @return
     */
    public static String getDataSourceKey() {
        return contextHolder.get();
    }

    /**
     * 重置数据源
     */
    public static void clearDataSourceKey() {
        contextHolder.remove();
    }

    /**
     * 判断是否包含数据源
     * 
     * @param key 数据源key
     * @return
     */
    public static boolean containDataSourceKey(String key) {
        return dataSourceKeys.contains(key);
    }

    /**
     * 添加数据源keys
     * 
     * @param keys
     * @return
     */
    public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
        return dataSourceKeys.addAll(keys);
    }
5. DataSourceConfig 数据源注入

@Configuration
public class DataSourceConfig {

    /**
     * 环境
     */
    @Autowired
    private Environment env;

    /**
     * 主数据源
     * 
     * @return
     */
    @Primary
    @Bean(name = "master")
    public DataSource master() {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl(env.getProperty("spring.datasource.url"));
        ds.setUsername(env.getProperty("spring.datasource.username"));
        ds.setPassword(env.getProperty("spring.datasource.password"));
        ds.setDriverClassName(env.getProperty("spring.datasource.driver-class-name"));
        Properties dsProperties = hikariProperties();
        ds.setDataSourceProperties(nameConvert(dsProperties));
        return ds;
    }

    /**
     * 动态数据源加载
     * 
     * @return DataSource
     */
    @Bean("dynamicDataSource")
    public DataSource dynamicDataSource() {

        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>();
        // 将 master 数据源作为默认指定的数据源
        dataSourceMap.put("master", master());
        dynamicDataSource.setDefaultDataSource(master());

        //从数据库中获取其他动态数据源
        SQLManager sqlManager = SpringUtil.getBean(SQLManager.class);
        HikariDataSource master = (HikariDataSource) master();
        SysTenant query = new SysTenant();
        query.setDelFlag(0);
        List<SysTenant> tenantList = sqlManager.template(query);

        for (SysTenant dto : tenantList) {
            HikariDataSource dataSource = new HikariDataSource();
            dataSource.setDriverClassName(dto.getDriver());
            dataSource.setJdbcUrl(dto.getUrl());
            dataSource.setUsername(dto.getUsername());
            dataSource.setPassword(dto.getPassword());
            dataSource.setDataSourceProperties(master.getDataSourceProperties());
            dataSourceMap.put(dto.getTenantId(), dataSource);
        }

        //设置数据源
        dynamicDataSource.setDataSources(dataSourceMap);

        //执行生效
        dynamicDataSource.afterPropertiesSet();

        return dynamicDataSource;
    }

  

    /**
     * hikari连接池
     * 
     * @return Properties
     */
    @Bean(name = "hikariProperties")
    @ConfigurationProperties("spring.datasource.hikari")
    public Properties hikariProperties() {
        Properties dsProperties = new Properties();
        return dsProperties;
    }

    /**
     * 转换
     * 
     * @param srcPorp
     * @return Properties
     */
    private Properties nameConvert(Properties srcPorp) {
        Properties descProp = new Properties();
        if (srcPorp == null) {
            return descProp;
        }

        Enumeration<?> enu = srcPorp.propertyNames();
        while (enu.hasMoreElements()) {
            String key = (String) enu.nextElement();
            String underLineStr = StrUtil.replace(key, "-", "_");
            String underLineKey = StrUtil.toCamelCase(underLineStr);
            descProp.put(underLineKey, srcPorp.get(key));
        }

        return descProp;
    }

}
6. TenantFilter 多租户过滤器

@Configuration
@WebFilter(urlPatterns = "/*", filterName = "TenantFilter")
public class TenantFilter implements Filter {

    /**
     * LOGGER.
     */
    private static final Logger log = LoggerFactory.getLogger(TenantFilter.class);

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String tenantId = request.getHeader("tenantId");
        log.info("当前租户ID:" + tenantId);
        if (StrUtil.isEmpty(tenantId)) {
            tenantId = "master";
        }

        //设置key
        DynamicDataSourceContextHolder.setDataSourceKey(tenantId);

        //放行
        chain.doFilter(request, response);
    }

}
7. HelpUtil  帮助类,用来获取当前租户激活的数据源,创建 SQLManager 对象。

public class HelpUtil {

    /**
     * 获取当前的SQLManager
     * 
     * @return SQLManager
     */
    public static SQLManager getCurrentSqlManaget() {

        SQLManager sqlManager = null;
        try {
            String key = DynamicDataSourceContextHolder.getDataSourceKey();

            DynamicDataSource dynamicDataSource = (DynamicDataSource) SpringUtil.getBean("dynamicDataSource");

            //获取当前DataSource
            DataSource dataSource = dynamicDataSource.determineTargetDataSource();

            //构建SQLManager
            ConnectionSource source = ConnectionSourceHelper.getSingle(dataSource);
            SQLManagerBuilder builder = new SQLManagerBuilder(source);
            builder.setNc(new UnderlinedNameConversion());
            builder.setInters(new Interceptor[] { new DebugInterceptor() });
            builder.setDbStyle(new MySqlStyle());
            builder.setName(key);
            sqlManager = builder.build();
        }
        catch (Exception e) {

        }

        return sqlManager;
    }
}
8. 查询示例

 /**
     * 通过登录名获取系统用户信息
     * 
     * @param loginName 用户登录名
     * @return 用户信息
     */
    public LogonUserDto getUserInfoByName(String loginName) {

        SQLManager sqlManager = HelpUtil.getCurrentSqlManaget();

        SysUser query = new SysUser();
        query.setLoginName(loginName);
        SysUser user = sqlManager.templateOne(query);
        if (user == null) {
            return null;
        }

        LogonUserDto dto = new LogonUserDto(user);

        return dto;
    }

以上是关于SpringBoot + BeetlSQL3 多租户改造的主要内容,如果未能解决你的问题,请参考以下文章

华为云企业级Redis:集群搭载多DB,多租隔离更降本

GaussDB(for Redis)新特性发布:前缀搜索千倍提升与集群版多租隔离

大数据资源管理方案研究

MaxCompute安全管理指南-基础篇

MaxCompute安全管理指南-基础篇

管理多租户数据库连接