你好呀,我是小羊。
你一定听说过Sass 平台这个概念,比如你在淘宝上开了一家店,对于淘宝来说,新开的店铺可以发布商品,售卖商品,发放优惠券等等功能。每个人都能开自己独立的店铺互不影响,这就是多租户的功能。那么这种架构是怎么设计的呢,今天我们就来聊一聊多租户。

1.什么是Sass ?
多租户技术或称多重租赁技术,简称SaaS,是一种软件架构模式,其中单个实例的软件服务可以同时为多个用户(通常是不同的组织或实体)提供服务。每个用户被称为一个“租户”,他们可以独立地访问和使用软件服务,而且彼此之间相互隔离,就好像每个租户都在使用自己的独立实例一样。
2.多租户的实现方案有哪几种?
既然知道了什么是多租户,就可以知道多租户的核心就是把不同的租户进行数据隔离。有哪几种方式呢?一般有四种。
1. 分库
每个租户有自己独立的数据库,应用程序根据登录的租户去不同的数据库处理数据,数据隔离级别最高,即便其中一个数据库挂掉了,也不会影响到其他租户,数据不会互相影响。但是开发维护成本很高。

2. Schema
共享数据库、隔离数据架构:多租户使用同一个数据裤,但是每个租户对应一个Schema(数据库user)。优点是可以共享硬件资源,但是也有不少的改造量,每一个租户都必须新增一个schema。而且主流的mysql 不支持这种模式。

3. 分表或者分区
在同一个数据库创建不同的表用于区分,每新建一个租户,就新建对应的表或者分区,应用程序根据租户去不同的表或者分区处理数据,相对第一种方案,成本低一点,但是维护起来也很麻烦。

4. 字段区分
在同样的业务表增加租户id用于区分不同租户,应用程序只需要传入对应的租户id当成条件来处理数据,数据隔离级别较低。优点是成本较低,比较好维护。数据迁移等工作也比较好处理。

3.方案选择
综上几种实现方式,对应金融等数据隔离要求非常高的系统来说,使用分库分表会比较合适,普通的业务系统,使用表字段来实现多租户会比较合适。表字段来实现多租户有几大优势:
-
成本最低 -
扩展性最好 -
适合快速扩展 -
易于维护 -
逻辑简单
而表字段来区分的缺陷也可以通过其他方式来克服,比如数据量大的情况可以通过增加硬件配置或者分库分表。数据隔离可以让应用程序来保证。
表字段的方式来做多租户实现非常简单,我们可以建几张表来做一个简单的多租户管理。
用户表,使用 tenant_id 来区分不同的租户
CREATE TABLE sys_user
(
id BIGINT NOT NULL COMMENT '主键ID',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
PRIMARY KEY (id)
);
插入一些数据
INSERT INTO sys_user (id, tenant_id, name) VALUES
(1, 1, 'Jone'),(2, 1, 'Jack'),(3, 1, 'Tom'),
(4, 0, 'Sandy'),(5, 0, 'Billie');
如果我们想查不同租户的数据,就可以把 tenant_id 当成一个条件来筛选。
select * from sys_user where tenant_id = 1
这样很简单,但是也面临一个问题,对应业务系统来说,sql操作是非常多的,如果每次都需要加上这个条件,这就变的非常麻烦,而且也增加了很多测试的工作,有没有办法简化呢?mybatisplus 框架提供了多租户的功能。它可以做到后面加上租户的查询条件,不需要人工加上 tenant_id 的查询条件。自动帮你在sql。
mybatisplus 多租户配置
多租户实体类。
@Data
public class TenantDto {
private Integer tenantId;
}
把租户id注入spring容器
@Configuration
public class TenantConfig {
@Bean
public TenantDto setTenant(){
TenantDto tenantDto = new TenantDto();
tenantDto.setTenantId(1);
return tenantDto;
}
}
mybatisplus 配置
@Configuration
@MapperScan("com.example.mybatisplustenant.mapper")
public class MybatisPlusConfig {
@Autowired
private TenantDto tenantDto;
/**
* 新多租户插件配置,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new LongValue(tenantDto.getTenantId());
}
// 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
@Override
public boolean ignoreTable(String tableName) {
return !"sys_user".equalsIgnoreCase(tableName);
}
}));
// 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
// 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
用户实体类
@Data
@Accessors(chain = true)
@TableName("sys_user")
public class User {
private Long id;
/**
* 租户 ID
*/
private Long tenantId;
private String name;
@TableField(exist = false)
private String addrName;
}
mapper 方法
public interface UserMapper extends BaseMapper<User> {
/**
* 自定义SQL:默认也会增加多租户条件
* 参考打印的SQL
* @return
*/
Integer myCount();
List<User> getUserAndAddr(@Param("username") String username);
List<User> getAddrAndUser(@Param("name") String name);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.mybatisplustenant.mapper.UserMapper">
<select id="myCount" resultType="java.lang.Integer">
select count(1) from sys_user
</select>
<select id="getUserAndAddr" resultType="com.example.mybatisplustenant.entity.User">
select u.id, u.name, a.name as addr_name
from sys_user u
left join user_addr a on a.user_id=u.id
<where>
<if test="username!=null">
u.name like concat(concat('%',#{username}),'%')
</if>
</where>
</select>
<select id="getAddrAndUser" resultType="com.example.mybatisplustenant.entity.User">
select a.name as addr_name, u.id, u.name
from user_addr a
left join sys_user u on u.id=a.user_id
<where>
<if test="name!=null">
a.name like concat(concat('%',#{name}),'%')
</if>
</where>
</select>
</mapper>
测试,动态设置tenantId 可以动态的在sql 后面增加条件,达到多租户的效果。
@Autowired
private UserMapper mapper;
@Autowired
private TenantDto tenantDto;
@Test
public void aInsert() {
tenantDto.setTenantId(123);
User user = new User();
user.setName("一一");
Assertions.assertTrue(mapper.insert(user) > 0);
user = mapper.selectById(user.getId());
Assertions.assertTrue(123 == user.getTenantId());
}
如果在代码中不想使用多租户的查询,也可以用下面的方法进行忽略
TenantHelper.ignore(() -> xxxx )
或者直接忽略不需要使用多租户的表。
public interface TenantLineHandler {
/**
* 获取租户 ID 值表达式,只支持单个 ID 值
* <p>
*
* @return 租户 ID 值表达式
*/
Expression getTenantId();
/**
* 获取租户字段名
* <p>
* 默认字段名叫: tenant_id
*
* @return 租户字段名
*/
default String getTenantIdColumn() {
// 如果该字段你不是固定的,请使用 SqlInjectionUtils.check 检查安全性
return "tenant_id";
}
/**
* 根据表名判断是否忽略拼接多租户条件
* <p>
* 默认都要进行解析并拼接多租户条件
*
* @param tableName 表名
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
*/
default boolean ignoreTable(String tableName) {
return false;
}
}
4.总结
Sass 平台多租户的实现有多种,对于普通项目来讲,使用表字段进行数据隔离比较常见。对于业务开发来说,可以使用mybatisplus 的多租户功能进行开发,减少开发的工作量。
5.相关链接
表字段多租户项目
https://gitee.com/yangzheng1/mybatisplus-tenant
mybatisplus 多租户文档
https://baomidou.com/pages/aef2f2/
6.往期精彩推荐




原文始发于微信公众号(小羊架构):Sass多租户的4种实现方案,简单易懂,附上源码
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/260029.html