1

多租户

 2 years ago
source link: https://xie.infoq.cn/article/dd8ac8438d31d24c332989392
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

http://mp.baomidou.com/guide/tenant.html

一般的程序应用当使用者访问不同,并且进入相对应的程序页面,则会把用户相关数据传输到后台这里。在传输的时候需要带上标识(租户 ID),以便程序将数据进行隔离。当不同的租户使用同一个程序服务,这里就需要考虑一个数据隔离的情况。

什么是多租户技术

  • 多租户技术或称多重租赁技术,是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业)共用相同的系统或程序组件,并且确保各用户间数据隔离性。

  • 在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。多租户的重点就是同程序下实现多用户数据的隔离。

数据隔离有三种方案:

  1. 独立数据库:简单来说就是一个租户使用一个数据库,这种数据隔离级别最高,安全性最好,但是提高成本。

  2. 共享数据库、隔离数据架构:多租户使用同一个数据裤,但是每个租户对应一个 Schema(数据库 user)。

  3. 共享数据库、共享数据架构:使用同一个数据库,同一个 Schema,但是在表中增加了租户 ID 的字段,这种共享数据程度最高,隔离级别最低。

多租户具体实现

按照综合实际考虑,一般都会采用方案三,即共享数据库,共享数据架构,因为这种方案服务器成本最低,但是提高了开发成本。

MybatisPlus 实现逻辑

Mybatis-plus 实现多租户方案

Mybatis-plus 就提供了一种多租户的解决方案,实现方式是基于分页插件(拦截器)进行实现的;

  • 第一步:应用添加维护一张 tenant(租户表),需要进行隔离的数据表上新增租户 id,例如,现在有数据库表(user)如下:

将 tenantId 视为租户 ID,用来隔离租户与租户之间的数据,如果要查询当前服务商的用户,SQL 大致如下:

SELECT * FROM table t WHERE  t.tenantId = 1;
  • 第二步:实现 TenantHandler 接口并实现它的方法:

public interface TenantHandler {  /**   * 获取租户 ID 值表达式,支持多个 ID 条件查询   * 支持自定义表达式,比如:tenant_id in (1,2) @since 2019-8-2   * @param where 参数 true 表示为 where 条件 false 表示为 insert 或者 select 条件   * @return 租户 ID 值表达式   */  Expression getTenantId(boolean where);  /**   * 获取租户字段名   * @return 租户字段名   */  String getTenantIdColumn();  /**   * 根据表名判断是否进行过滤   * @param tableName 表名   * @return 是否进行过滤, true:表示忽略,false:需要解析多租户字段   */  boolean doTableFilter(String tableName);}
PreTenantHandler 实现 TenantHandler
@Slf4j@Componentpublic class PreTenantHandler implements TenantHandler {  @Autowired  private PreTenantConfigProperties configProperties;  /**   * 租户Id   * @return   */  @Override  public Expression getTenantId(boolean where) {    //可以通过过滤器从请求中获取对应租户id     Long tenantId = PreTenantContextHolder.getCurrentTenantId();    log.debug("当前租户为{}", tenantId);    if (tenantId == null) {      return new NullValue();    }    return new LongValue(tenantId);  }  /**   * 租户字段名   * @return   */  @Override  public String getTenantIdColumn() {    return configProperties.getTenantIdColumn();  }  /**   * 根据表名判断是否进行过滤   * 忽略掉一些表:如租户表(sys_tenant)本身不需要执行这样的处理   * @param tableName   * @return   */  @Override  public boolean doTableFilter(String tableName) {    return configProperties.getIgnoreTenantTables().stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));  }}
  • 第三步:配置 mybatisPlus 的分页插件配置

租户相关的表,我们都需要不厌其烦的加上 AND t.tenantId = ?查询条件,稍不注意就会导致数据越界,数据安全问题让人担忧。好在有了 MybatisPlus 这个神器,可以极为方便的实现多租户 SQL 解析器。

核心配置:TenantSqlParser

@Configuration@MapperScan("com.wuwenze.mybatisplusmultitenancy.mapper")public class MybatisPlusConfig {    private static final String SYSTEM_TENANT_ID = "provider_id";    private static final List<String> IGNORE_TENANT_TABLES = Lists.newArrayList("provider");    @Autowired    private ApiContext apiContext;    @Bean    public PaginationInterceptor paginationInterceptor() {        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();        // SQL解析处理拦截:增加租户处理回调。        TenantSqlParser tenantSqlParser = new TenantSqlParser()                .setTenantHandler(new TenantHandler() {                    @Override                    public Expression getTenantId() {                     // 从当前系统上下文中取出当前请求的服务商ID,通过解析器注入到SQL中。                        Long currentProviderId = apiContext.getCurrentProviderId();                        if (null == currentProviderId) {                            throw new RuntimeException("#1129 getCurrentProviderId error.");                        }                        return new LongValue(currentProviderId);                    }                    @Override                    public String getTenantIdColumn() {                        return SYSTEM_TENANT_ID;                    }                    @Override                    public boolean doTableFilter(String tableName) {                        // 忽略掉一些表:如租户表(provider)本身不需要执行这样的处理。                        return IGNORE_TENANT_TABLES.stream().anyMatch((e) -> e.equalsIgnoreCase(tableName));                    }                });        paginationInterceptor.setSqlParserList(Lists.newArrayList(tenantSqlParser));        return paginationInterceptor;    }    @Bean(name = "performanceInterceptor")    public PerformanceInterceptor performanceInterceptor() {        return new PerformanceInterceptor();    }}

配置好之后,不管是查询、新增、修改删除方法,MP 都会自动加上租户 ID 的标识,测试如下:

@Testpublic void select(){  List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery().eq(User::getAge, 18));  users.forEach(System.out::println);}

运行 sql 实例:

DEBUG==> Preparing: SELECT id, login_name, name, password,       email, salt, sex, age, phone, user_type, status,     organization_id, create_time, update_time, version,     tenant_id FROM sys_user    WHERE sys_user.tenant_id = '001' AND is_delete = '0' AND age = ?

注:特定 SQL 过滤,如果在程序中,有部分 SQL 不需要加上租户 ID 的表示,需要过滤特定的 sql,可以通过如下两种方式:

在配置分页插件中加上配置 ISqlParserFilter 解析器,配置 SQL 很多,比较麻烦,不建议;

paginationInterceptor.setSqlParserFilter(new ISqlParserFilter() {      @Override      public boolean doFilter(MetaObject metaObject) {        MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);        // 对应Mapper、dao中的方法        if("com.example.demo.mapper.UserMapper.selectList".equals(ms.getId())){          return true;        }        return false;      }});

通过租户注解 @SqlParser(filter = true) 的形式,目前只能作用于 Mapper 的方法上:

public interface UserMapper extends BaseMapper<User> {  /**   * 自定Wrapper修改   *   * @param userWrapper 条件构造器   * @param user    修改的对象参数   * @return   */  @SqlParser(filter = true)  int updateByMyWrapper(@Param(Constants.WRAPPER) Wrapper<User> userWrapper, @Param("user") User user);}

ApiContext

@Componentpublic class ApiContext {    private static final String KEY_CURRENT_PROVIDER_ID = "KEY_CURRENT_PROVIDER_ID";    private static final Map<String, Object> mContext = Maps.newConcurrentMap();    public void setCurrentProviderId(Long providerId) {        mContext.put(KEY_CURRENT_PROVIDER_ID, providerId);    }    public Long getCurrentProviderId() {        return (Long) mContext.get(KEY_CURRENT_PROVIDER_ID);    }}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK