# 多租户架构详解 ## SaaS 多租户模式 ### 租户隔离方案 芋道框架采用**共享数据库、共享 Schema**的租户隔离模式: - 所有租户共享同一个数据库实例 - 通过 `tenant_id` 字段进行数据隔离 - 在应用层面实现租户数据的自动过滤 ### 核心表结构 ```sql -- 租户表 CREATE TABLE system_tenant ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '租户编号', name VARCHAR(30) NOT NULL COMMENT '租户名', contact_user_id BIGINT COMMENT '联系人用户编号', contact_name VARCHAR(30) NOT NULL COMMENT '联系人', contact_mobile VARCHAR(500) DEFAULT NULL COMMENT '联系手机', status TINYINT NOT NULL DEFAULT 0 COMMENT '租户状态', website VARCHAR(256) DEFAULT '' COMMENT '绑定域名', package_id BIGINT NOT NULL COMMENT '租户套餐编号', expire_time DATETIME NOT NULL COMMENT '过期时间', account_count INT NOT NULL COMMENT '账号数量', creator VARCHAR(64) DEFAULT '' COMMENT '创建者', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updater VARCHAR(64) DEFAULT '' COMMENT '更新者', update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除' ); -- 租户套餐表 CREATE TABLE system_tenant_package ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '套餐编号', name VARCHAR(30) NOT NULL COMMENT '套餐名', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态', remark VARCHAR(256) DEFAULT '' COMMENT '备注', menu_ids VARCHAR(4096) NOT NULL COMMENT '关联的菜单编号', creator VARCHAR(64) DEFAULT '' COMMENT '创建者', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updater VARCHAR(64) DEFAULT '' COMMENT '更新者', update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', deleted BIT NOT NULL DEFAULT 0 COMMENT '是否删除' ); ``` ## 租户上下文管理 ### 租户上下文类 ```java /** * 租户上下文 Holder */ public class TenantContextHolder { /** * 当前租户编号 */ private static final ThreadLocal TENANT_ID = new ThreadLocal<>(); /** * 是否忽略租户 */ private static final ThreadLocal IGNORE = new ThreadLocal<>(); public static void setTenantId(Long tenantId) { TENANT_ID.set(tenantId); } public static Long getTenantId() { return TENANT_ID.get(); } public static void setIgnore(Boolean ignore) { IGNORE.set(ignore); } public static Boolean getIgnore() { return IGNORE.get(); } public static void clear() { TENANT_ID.remove(); IGNORE.remove(); } } ``` ### 租户拦截器 ```java @Component public class TenantSecurityWebFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; // 获取租户 ID(从请求头、域名、用户信息等) Long tenantId = getTenantId(httpRequest); try { // 设置租户上下文 TenantContextHolder.setTenantId(tenantId); // 继续处理请求 chain.doFilter(request, response); } finally { // 清理租户上下文 TenantContextHolder.clear(); } } private Long getTenantId(HttpServletRequest request) { // 1. 优先从请求头获取 String tenantIdHeader = request.getHeader("Tenant-Id"); if (StrUtil.isNotBlank(tenantIdHeader)) { return Long.valueOf(tenantIdHeader); } // 2. 从域名获取 String domain = request.getServerName(); Long tenantId = getTenantIdByDomain(domain); if (tenantId != null) { return tenantId; } // 3. 从用户 Token 获取 String token = SecurityFrameworkUtils.obtainAuthorization(request); if (StrUtil.isNotBlank(token)) { return getTenantIdFromToken(token); } // 4. 默认租户 return TenantConstants.DEFAULT_TENANT_ID; } } ``` ## 数据库租户隔离 ### MyBatis Plus 租户插件 ```java @Configuration public class TenantConfiguration { @Bean public TenantLineInnerInterceptor tenantLineInnerInterceptor() { TenantLineInnerInterceptor interceptor = new TenantLineInnerInterceptor(); // 设置租户处理器 interceptor.setTenantLineHandler(new TenantLineHandler() { @Override public Expression getTenantId() { Long tenantId = TenantContextHolder.getTenantId(); if (tenantId == null) { return new LongValue(TenantConstants.DEFAULT_TENANT_ID); } return new LongValue(tenantId); } @Override public String getTenantIdColumn() { return "tenant_id"; } @Override public boolean ignoreTable(String tableName) { // 系统表不需要租户隔离 return TenantConstants.IGNORE_TABLES.contains(tableName); } }); return interceptor; } } ``` ### 租户注解 ```java /** * 忽略租户 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface TenantIgnore { } /** * 租户信息 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface TenantInfo { } ``` ### 租户 AOP 切面 ```java @Aspect @Component public class TenantAspect { @Around("@annotation(tenantIgnore)") public Object aroundTenantIgnore(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable { Boolean oldIgnore = TenantContextHolder.getIgnore(); try { TenantContextHolder.setIgnore(true); return joinPoint.proceed(); } finally { TenantContextHolder.setIgnore(oldIgnore); } } } ``` ## 租户管理服务 ### 租户服务接口 ```java @Service public class TenantServiceImpl implements TenantService { @Resource private TenantMapper tenantMapper; @Override @TenantIgnore // 系统级操作,忽略租户 public Long createTenant(TenantCreateReqVO reqVO) { // 校验租户套餐 TenantPackageDO tenantPackage = validateTenantPackage(reqVO.getPackageId()); // 创建租户 TenantDO tenant = TenantConvert.INSTANCE.convert(reqVO); tenant.setAccountCount(tenantPackage.getAccountCount()); tenant.setExpireTime(calculateExpireTime(reqVO.getExpireDays())); tenantMapper.insert(tenant); // 初始化租户数据 initTenantData(tenant.getId(), tenantPackage); return tenant.getId(); } @Override @TenantIgnore public void updateTenant(TenantUpdateReqVO reqVO) { // 校验租户存在 validateTenantExists(reqVO.getId()); // 更新租户 TenantDO updateObj = TenantConvert.INSTANCE.convert(reqVO); tenantMapper.updateById(updateObj); } @Override @TenantIgnore public void deleteTenant(Long tenantId) { // 校验租户存在 validateTenantExists(tenantId); // 删除租户(软删除) tenantMapper.deleteById(tenantId); // 清理租户相关数据 cleanTenantData(tenantId); } /** * 初始化租户数据 */ private void initTenantData(Long tenantId, TenantPackageDO tenantPackage) { // 创建租户管理员账号 createTenantAdmin(tenantId); // 初始化租户菜单权限 initTenantMenus(tenantId, tenantPackage.getMenuIds()); // 初始化租户角色 initTenantRoles(tenantId); // 初始化其他基础数据 initTenantBasicData(tenantId); } } ``` ### 租户套餐管理 ```java @Service public class TenantPackageServiceImpl implements TenantPackageService { @Override @TenantIgnore // 套餐管理是系统级功能 public Long createTenantPackage(TenantPackageCreateReqVO reqVO) { // 校验菜单编号合法性 validateMenuIds(reqVO.getMenuIds()); // 创建租户套餐 TenantPackageDO tenantPackage = TenantPackageConvert.INSTANCE.convert(reqVO); tenantPackageMapper.insert(tenantPackage); return tenantPackage.getId(); } @Override @TenantIgnore public void updateTenantPackage(TenantPackageUpdateReqVO reqVO) { // 校验套餐存在 validateTenantPackageExists(reqVO.getId()); // 校验菜单编号 validateMenuIds(reqVO.getMenuIds()); // 更新套餐 TenantPackageDO updateObj = TenantPackageConvert.INSTANCE.convert(reqVO); tenantPackageMapper.updateById(updateObj); // 同步更新使用该套餐的租户权限 syncTenantMenuPermissions(reqVO.getId(), reqVO.getMenuIds()); } /** * 同步租户菜单权限 */ private void syncTenantMenuPermissions(Long packageId, List menuIds) { // 查找使用该套餐的租户 List tenants = tenantMapper.selectListByPackageId(packageId); for (TenantDO tenant : tenants) { // 更新租户菜单权限 updateTenantMenuPermissions(tenant.getId(), menuIds); } } } ``` ## 租户数据初始化 ### 租户初始化服务 ```java @Service public class TenantInitService { /** * 初始化租户基础数据 */ @TenantIgnore public void initTenantData(Long tenantId, Long packageId) { // 切换到目标租户上下文 TenantContextHolder.setTenantId(tenantId); try { // 1. 创建默认角色 Long adminRoleId = createDefaultAdminRole(tenantId); Long userRoleId = createDefaultUserRole(tenantId); // 2. 创建默认部门 Long rootDeptId = createDefaultDepartment(tenantId); // 3. 创建租户管理员 Long adminUserId = createTenantAdmin(tenantId, adminRoleId, rootDeptId); // 4. 初始化系统配置 initSystemConfigs(tenantId); // 5. 初始化字典数据 initDictData(tenantId); } finally { TenantContextHolder.clear(); } } private Long createDefaultAdminRole(Long tenantId) { RoleCreateReqVO roleReq = new RoleCreateReqVO(); roleReq.setName("租户管理员"); roleReq.setCode("tenant_admin"); roleReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); roleReq.setType(RoleTypeEnum.CUSTOM.getType()); roleReq.setRemark("租户默认管理员角色"); return roleService.createRole(roleReq, RoleTypeEnum.CUSTOM); } private Long createTenantAdmin(Long tenantId, Long roleId, Long deptId) { UserCreateReqVO userReq = new UserCreateReqVO(); userReq.setUsername("admin"); userReq.setNickname("管理员"); userReq.setPassword("admin123"); userReq.setStatus(CommonStatusEnum.ENABLE.getStatus()); userReq.setDeptId(deptId); userReq.setRoleIds(Collections.singleton(roleId)); return userService.createUser(userReq); } } ``` ## 租户权限控制 ### 菜单权限隔离 ```java @Service public class TenantMenuService { /** * 获取租户可用菜单 */ public List getTenantMenus(Long tenantId) { // 获取租户套餐 TenantDO tenant = tenantService.getTenant(tenantId); TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(tenant.getPackageId()); // 解析套餐菜单编号 List menuIds = StrUtils.splitToLong(tenantPackage.getMenuIds(), ','); // 查询菜单列表 return menuMapper.selectListByIds(menuIds); } /** * 校验用户菜单权限 */ public boolean hasMenuPermission(Long userId, Long menuId) { // 获取用户租户 UserDO user = userService.getUser(userId); Long tenantId = user.getTenantId(); // 获取租户可用菜单 List tenantMenus = getTenantMenus(tenantId); // 检查菜单是否在租户套餐中 boolean inTenantPackage = tenantMenus.stream() .anyMatch(menu -> menu.getId().equals(menuId)); if (!inTenantPackage) { return false; } // 检查用户角色权限 return permissionService.hasMenuPermission(userId, menuId); } } ``` ### 数据权限隔离 ```java @Service public class TenantDataPermissionService { /** * 校验数据归属权限 */ public void validateDataPermission(Long dataId, String dataType) { Long currentTenantId = TenantContextHolder.getTenantId(); if (currentTenantId == null) { throw new ServiceException("未设置租户上下文"); } // 查询数据所属租户 Long dataTenantId = getDataTenantId(dataId, dataType); if (!Objects.equals(currentTenantId, dataTenantId)) { throw new ServiceException("无权访问其他租户数据"); } } /** * 过滤租户数据 */ public List filterTenantData(List dataList, Function tenantIdExtractor) { Long currentTenantId = TenantContextHolder.getTenantId(); if (currentTenantId == null) { return dataList; } return dataList.stream() .filter(data -> Objects.equals(currentTenantId, tenantIdExtractor.apply(data))) .collect(Collectors.toList()); } } ``` ## 租户计费和限制 ### 租户使用量统计 ```java @Service public class TenantUsageService { /** * 统计租户用户数量 */ public long countTenantUsers(Long tenantId) { return userMapper.selectCount(new LambdaQueryWrapper() .eq(UserDO::getTenantId, tenantId) .eq(UserDO::getDeleted, false)); } /** * 统计租户存储使用量 */ public long calculateTenantStorage(Long tenantId) { return fileMapper.selectStorageByTenantId(tenantId); } /** * 检查租户账号数量限制 */ public void checkAccountLimit(Long tenantId) { TenantDO tenant = tenantService.getTenant(tenantId); long currentUserCount = countTenantUsers(tenantId); if (currentUserCount >= tenant.getAccountCount()) { throw new ServiceException("账号数量已达上限"); } } /** * 检查租户有效期 */ public void checkTenantExpire(Long tenantId) { TenantDO tenant = tenantService.getTenant(tenantId); if (tenant.getExpireTime().before(new Date())) { throw new ServiceException("租户已过期"); } } } ``` ### 租户限流控制 ```java @Component public class TenantRateLimitService { @Resource private RedisTemplate redisTemplate; /** * 检查 API 调用频率限制 */ public boolean checkApiRateLimit(Long tenantId, String apiPath) { String key = "tenant:rate_limit:" + tenantId + ":" + apiPath; // 获取租户套餐限制 TenantPackageDO tenantPackage = getTenantPackage(tenantId); int rateLimit = tenantPackage.getApiRateLimit(); // 使用滑动窗口限流 return checkRateLimit(key, rateLimit, Duration.ofMinutes(1)); } /** * 检查存储空间限制 */ public void checkStorageLimit(Long tenantId, long fileSize) { long currentStorage = tenantUsageService.calculateTenantStorage(tenantId); TenantPackageDO tenantPackage = getTenantPackage(tenantId); if (currentStorage + fileSize > tenantPackage.getStorageLimit()) { throw new ServiceException("存储空间不足"); } } } ``` ## 租户域名绑定 ### 域名解析服务 ```java @Service public class TenantDomainService { /** * 绑定租户域名 */ @TenantIgnore public void bindDomain(Long tenantId, String domain) { // 校验域名格式 validateDomainFormat(domain); // 校验域名未被使用 validateDomainNotUsed(domain); // 更新租户域名 TenantDO updateObj = new TenantDO(); updateObj.setId(tenantId); updateObj.setWebsite(domain); tenantMapper.updateById(updateObj); // 清理域名缓存 clearDomainCache(domain); } /** * 根据域名获取租户ID */ public Long getTenantIdByDomain(String domain) { String cacheKey = "tenant:domain:" + domain; // 先从缓存获取 Long tenantId = (Long) redisTemplate.opsForValue().get(cacheKey); if (tenantId != null) { return tenantId; } // 从数据库查询 TenantDO tenant = tenantMapper.selectOneByWebsite(domain); if (tenant != null) { tenantId = tenant.getId(); // 缓存结果 redisTemplate.opsForValue().set(cacheKey, tenantId, Duration.ofHours(1)); } return tenantId; } } ``` ## 租户数据迁移 ### 租户数据导出 ```java @Service public class TenantDataExportService { /** * 导出租户数据 */ public TenantDataExportVO exportTenantData(Long tenantId) { TenantDataExportVO exportData = new TenantDataExportVO(); // 设置租户上下文 TenantContextHolder.setTenantId(tenantId); try { // 导出用户数据 exportData.setUsers(userService.getUserList(null)); // 导出角色数据 exportData.setRoles(roleService.getRoleList()); // 导出部门数据 exportData.setDepts(deptService.getDeptList(null)); // 导出菜单数据 exportData.setMenus(menuService.getMenuList()); // 导出其他业务数据 exportBusinessData(exportData, tenantId); } finally { TenantContextHolder.clear(); } return exportData; } /** * 导入租户数据 */ public void importTenantData(Long tenantId, TenantDataExportVO importData) { TenantContextHolder.setTenantId(tenantId); try { // 导入用户数据 importUsers(importData.getUsers()); // 导入角色数据 importRoles(importData.getRoles()); // 导入部门数据 importDepts(importData.getDepts()); // 导入菜单数据 importMenus(importData.getMenus()); } finally { TenantContextHolder.clear(); } } } ``` ## 最佳实践 ### 性能优化 1. **租户缓存**: 缓存租户基础信息和套餐信息 2. **索引优化**: 为 tenant_id 字段添加复合索引 3. **连接池**: 考虑为大租户使用独立连接池 4. **数据分片**: 超大租户可考虑数据分片 ### 安全考虑 1. **数据隔离**: 确保租户数据完全隔离 2. **权限校验**: 多层权限校验防止越权访问 3. **审计日志**: 记录跨租户操作的审计日志 4. **数据备份**: 定期备份租户重要数据 ### 监控告警 1. **租户状态**: 监控租户是否正常运行 2. **使用量**: 监控租户资源使用情况 3. **性能指标**: 监控租户相关 API 性能 4. **异常访问**: 检测异常的跨租户访问