赞
踩
多租户:多租户技术又称多重租赁技术,是一种软件即服务的软件服务架构(简称SaaS)
。同一个系统开放给多个组织/用户使用,每个组织/用户需要进行数据隔离,并且每个组织/用户可以自定义自己租用系统的个性化配置。使用多重租赁技术还有PaaS,IaaS等。
注意:多租户 != 权限过滤,不要乱用,租户之间是完全隔离的!!!启用多租户后所有执行的method的sql都会进行处理.
自写的sql请按规范书写(sql涉及到多个表的每个表都要给别名,特别是 inner join 的要写标准的 inner join)
TenantLineInnerInterceptor 租户数据隔离内置拦截器(插件)
属性名 | 类型 | 描述 |
---|---|---|
tenantLineHandler | TenantLineHandler | 租户处理器( TenantId 行级 ) |
public interface TenantLineHandler { /** * 获取租户 ID 值表达式,只支持单个 ID 值 * <p> * * @return 租户 ID 值表达式 */ Expression getTenantId(); /** * 获取租户字段名 * <p> * 默认字段名叫: tenant_id * * @return 租户字段名 */ default String getTenantIdColumn() { return "tenant_id"; } /** * 根据表名判断是否忽略拼接多租户条件 * <p> * 默认都要进行解析并拼接多租户条件 * * @param tableName 表名 * @return 是否忽略, true:表示忽略,false:需要解析并拼接多租户条件 */ default boolean ignoreTable(String tableName) { return false; } }
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); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。