709 lines
20 KiB
Plaintext
709 lines
20 KiB
Plaintext
# 多租户架构详解
|
||
|
||
## 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<Long> TENANT_ID = new ThreadLocal<>();
|
||
|
||
/**
|
||
* 是否忽略租户
|
||
*/
|
||
private static final ThreadLocal<Boolean> 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<Long> menuIds) {
|
||
// 查找使用该套餐的租户
|
||
List<TenantDO> 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<MenuDO> getTenantMenus(Long tenantId) {
|
||
// 获取租户套餐
|
||
TenantDO tenant = tenantService.getTenant(tenantId);
|
||
TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(tenant.getPackageId());
|
||
|
||
// 解析套餐菜单编号
|
||
List<Long> 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<MenuDO> 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 <T> List<T> filterTenantData(List<T> dataList, Function<T, Long> 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<UserDO>()
|
||
.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<String, Object> 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. **异常访问**: 检测异常的跨租户访问 |