多租户技术或称多重租赁技术,简称SaaS
,是一种软件架构技术,是实现如何在多用户环境下(此处的多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。
简单讲:在一台服务器上运行单个应用实例,它为多个租户(客户)提供服务。从定义中我们可以理解:多租户是一种架构,目的是为了让多用户环境下使用同一套程序,且保证用户间数据隔离。
不同系统间共用同个数据库或者数据库表,如大企业具有内部的多个系统,但数据结构是相同的,可采用公共数据库,数据不同。
租户共享同一个Database、同一个Schema,但在表中增加TenantID多租户的数据字段。这是共享程度最高、隔离级别最低的模式。
除了一些系统共用的表以外,其他租户相关的表,我们都需要在sql不厌其烦的加上AND t.provider_id = ?
查询条件,稍不注意就会导致数据越界,数据安全问题让人担忧。好在有了MybatisPlus这个神器,可以极为方便的实现多租户SQL解析器
每个数据表都需要有一个租户标识(provider_id)
字段名 字段类型 描述
id BIGINT(20) 主键
provider_id BIGINT(20) 服务商ID
name VARCHAR(30) 姓名
provider_id作为租户Id,用于识别租户间的数据。
初始化数据如下
DROP TABLE IF EXISTS user;
CREATE TABLE user
(
id BIGINT(20) NOT NULL COMMENT '主键',
provider_id BIGINT(20) NOT NULL COMMENT '服务商ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
PRIMARY KEY (id)
);
INSERT INTO user (id, provider_id, name) VALUES (1, 1, 'Tony老师');
INSERT INTO user (id, provider_id, name) VALUES (2, 1, 'William老师');
INSERT INTO user (id, provider_id, name) VALUES (3, 2, '路人甲');
INSERT INTO user (id, provider_id, name) VALUES (4, 2, '路人乙');
INSERT INTO user (id, provider_id, name) VALUES (5, 2, '路人丙');
INSERT INTO user (id, provider_id, name) VALUES (6, 2, '路人丁');
集成mybatisPlus及msql数据库
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<scope>providedscope>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>19.0version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.0.5version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plusartifactId>
<version>3.0.5version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>3.0.5version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.38version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.10version>
dependency>
spring:
datasource:
name: db
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/user?characterEncoding=utf-8
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
用户实体类
package com.syd.entity;
import lombok.Data;
import lombok.ToString;
import lombok.experimental.Accessors;
@Data
@ToString
@Accessors(chain = true)
public class User {
private Long id;
private Long providerId;
private String name;
}
用户UserMapper接口
package com.syd.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wuwenze.entity.User;
public interface UserMapper extends BaseMapper<User> {
}
模拟当前系统上下文,对 provider_id 进行取值、存值。
import com.google.common.collect.Maps;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public 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);
}
}
package com.syd.config;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser;
import com.google.common.collect.Lists;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
@MapperScan("com.syd.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();
}
}
@Slf4j
@RunWith(SpringRunner.class)
@FixMethodOrder(MethodSorters.JVM)
@SpringBootTest(classes = MybatisPlusMultiTenancyApplication.class)
public class MybatisPlusMultiTenancyApplicationTests {
@Test
public void contextLoads() {
}
@Autowired
private ApiContext apiContext;
@Autowired
private UserMapper userMapper;
@Before
public void before() {
// 在上下文中设置当前服务商的ID
apiContext.setCurrentProviderId(1L);
}
@Test
public void selectList() {
userMapper.selectList(null).forEach((e) -> {
log.info("#selectList, e={}", e);
// 验证查询的数据是否超出范围
Assert.assertEquals(apiContext.getCurrentProviderId(),
e.getProviderId());
});
}
@Test
public void insert() {
User user = new User().setName("新来的Tom老师");
Assert.assertTrue(userMapper.insert(user) > 0);
user = userMapper.selectById(user.getId());
log.info("#insert user={}", user);
// 检查插入的数据是否自动填充了租户ID
Assert.assertEquals(apiContext.getCurrentProviderId(), user.getProviderId());
}
}
自动加上过滤条件
使用 mybatis-plus 实现多租户只需要要在数据表里加上provider_id 识别字段,然后即可在查询、新增、删除、更新等SQL语句中自动加上where provider_id 字段。