多租户:多租户技术又称多重租赁技术,是一种软件即服务的软件服务架构(简称SaaS)
。同一个系统开放给多个组织/用户使用,每个组织/用户需要进行数据隔离,并且每个组织/用户可以自定义自己租用系统的个性化配置。使用多重租赁技术还有PaaS,IaaS等。
注意:多租户 != 权限过滤,不要乱用,租户之间是完全隔离的!!!启用多租户后所有执行的method的sql都会进行处理.
自写的sql请按规范书写(sql涉及到多个表的每个表都要给别名,特别是 inner join 的要写标准的 inner join)
TenantLineInnerInterceptor 租户数据隔离内置拦截器(插件)
属性名 | 类型 | 描述 |
---|---|---|
tenantLineHandler | TenantLineHandler | 租户处理器( TenantId 行级 ) |
public interface TenantLineHandler {
/**
* 获取租户 ID 值表达式,只支持单个 ID 值
*
*
* @return 租户 ID 值表达式
*/
Expression getTenantId();
/**
* 获取租户字段名
*
* 默认字段名叫: tenant_id
*
* @return 租户字段名
*/
default String getTenantIdColumn() {
return "tenant_id";
}
/**
* 根据表名判断是否忽略拼接多租户条件
*
* 默认都要进行解析并拼接多租户条件
*
* @param tableName 表名
* @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件
*/
default boolean ignoreTable(String tableName) {
return false;
}
}
mybatis-plus多租户插件官方文档
yml可配置具体的表不走多租户数据隔离(可设计成starter给其他服务使用)
ignore-tenant-tables: user_info,user1
MybatisConfig配置类
@Configuration
@EnableTransactionManagement(proxyTargetClass = true)
public class MybatisConfig {
//忽略租户过滤表集合
@Value("${ignore-tenant-tables:}")
private String ignoreTenantTables;
// 最新版
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
if (!StringUtils.isBlank(ignoreTenantTables)) {
List<String> ignoreTenantTableList = Arrays.asList(StringUtils.split(ignoreTenantTables, ","));
if (!CollectionUtils.isEmpty(ignoreTenantTableList)) {
//多租户插件
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new LongValue(1);
}
@Override
public boolean ignoreTable(String tableName) {
//这是我的一个上下文类,忽略当前线程使用租户数据隔离
if (MybatisTenantContextHolder.isNoTenant()) {
return true;
}
return ignoreTenantTableList.contains(tableName);
}
}));
}
}
//分页插件
//interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
@Bean
public MybatisAutoFillHandler mybatisAutoFillHandler() {
return new MybatisAutoFillHandler();
}
}
定义MybatisTenantContextHolder(可设计成注解加到方法定义前面,如下)
public class MybatisTenantContextHolder {
private static final ThreadLocal<TenantContext> TENANT_CONTEXT_THREAD_LOCAL = new ThreadLocal<>();
public static void set(TenantContext context) {
TENANT_CONTEXT_THREAD_LOCAL.set(context);
}
public static TenantContext get() {
return TENANT_CONTEXT_THREAD_LOCAL.get();
}
public static void clear() {
TENANT_CONTEXT_THREAD_LOCAL.remove();
}
public static boolean isNoTenant() {
TenantContext tenantContext = TENANT_CONTEXT_THREAD_LOCAL.get();
if (tenantContext == null) {
return false;
}
return tenantContext.isNoTenant();
}
}
注解类@NoTenant
/**
* 无租户注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface NoTenant {
String name() default "";
}
切面类NoTenantAspect
@Aspect
@Component
@Slf4j
public class NoTenantAspect {
/**
* 切入点
*/
@Pointcut("@annotation(com.example.NoTenant)")
public void pointcut() {
}
/**
* 环绕操作
*
* @param point 切入点
* @return 原方法返回值
* @throws Throwable 异常信息
*/
@Around(value = "pointcut()")
public Object aroundLog(ProceedingJoinPoint point) throws Throwable {
try {
MybatisTenantContextHolder.set(TenantContext.builder().noTenant(true).build());
return point.proceed();
} finally {
MybatisTenantContextHolder.clear();
}
}
}
SELECT
TABLE_NAME,
concat( 'ALTER TABLE ', TABLE_SCHEMA, '.', TABLE_NAME, " ADD COLUMN `tenant_id` bigint DEFAULT '0' COMMENT '租户id';" ) t_sql
FROM
information_schema.TABLES t
WHERE
TABLE_SCHEMA = 'test'
AND TABLE_NAME NOT IN (
'user')
//(on)不增加tenant_id的查询条件,默认是增加(off)
@InterceptorIgnore(tenantLine = "on")
Integer myCount();
有些数据需要初始化的时候执行,所有租户都需要执行,可以用@Subscribe在注册完毕之后通过RPC远程调用获取所有租户信息,然后for循环遍历执行。
@Subscribe
public void initCache(ServiceReadyEvent serviceReadyEvent) {
}
with recursive cte as
(
select id,parent_id,`name`,cast(id as char(128) ) order_field from department as d where delete_flag = 0 AND id = #{departmentId}
union all
select c.id,c.parent_id,c.name,concat(order_field,',',cte.parent_id) order_field from department c, cte where c.delete_flag = 0 AND c.id = cte.parent_id
)
select * from cte as c order by `order_field` desc;
无法解析复杂sql,可以设置忽略该sql多租户插件追加,自己补全租户id过滤
@InterceptorIgnore(tenantLine = "on")
@Select("复杂炫酷sql")
List<UserRoleExt> userRoleList(Long userId);
使用MybatisTenantContextHolder设置当前线程标志位:noTenant开关为true即可,用完记得掉clear()方法;
改进版用注解@NoTenant
@Scheduled(cron = "0 */15 * * * ?")
private void commit() {
MybatisTenantContextHolder.set(TenantContext.builder().noTenant(true).build());
reportCommitInnerService.commit();
MybatisTenantContextHolder.clear();
}
提示:这里写了单元测试类TenantTest,运行结果就不细说了
@SpringBootTest
public class TenantTest {
@Resource
private UserInfoMapper mapper;
@Resource
private UserAddrMapper userAddrMapper;
@Test
public void aInsert() {
UserInfo userInfo = new UserInfo();
userInfo.setName("一一");
Assertions.assertTrue(mapper.insert(userInfo) > 0);
userInfo = mapper.selectById(userInfo.getId());
Assertions.assertTrue(1 == userInfo.getTenantId());
}
@Test
public void bDelete() {
Assertions.assertTrue(mapper.deleteById(3L) > 0);
}
@Test
public void cUpdate() {
Assertions.assertTrue(mapper.updateById(new UserInfo().setId(1L).setName("mp")) > 0);
}
@Test
public void dSelect() {
List<UserInfo> userInfoList = mapper.selectList(null);
userInfoList.forEach(u -> Assertions.assertTrue(1 == u.getTenantId()));
}
@Test
public void addrSelect() {
List<UserAddr> userAddrList = userAddrMapper.selectList(null);
userAddrList.forEach(u -> System.out.println(u));
}
/**
* 自定义SQL:默认也会增加多租户条件
* 参考打印的SQL
*/
@Test
public void manualSqlTenantFilterTest() {
System.out.println(mapper.myCount());
}
@Test
public void testTenantFilter() {
mapper.getAddrAndUser(null).forEach(System.out::println);
mapper.getAddrAndUser("add").forEach(System.out::println);
mapper.getUserAndAddr(null).forEach(System.out::println);
mapper.getUserAndAddr("J").forEach(System.out::println);
}
}