1. 引言
多租户 GEO 优化系统在服务多个业务方时,需要同时解决两个核心问题:一是租户之间的权限隔离,确保每个租户只能访问自己的数据和功能;二是数据分离,避免租户数据互相污染。本文从源码架构角度,分析权限隔离与数据分离的具体实现方案。

2. 整体架构设计
系统采用分层架构,从上到下依次为接入层、服务层、数据访问层和存储层。权限隔离贯穿接入层和服务层,数据分离则主要在数据访问层和存储层落地。
text
接入层(API Gateway / 租户上下文解析)
↓
服务层(业务服务 / 权限校验拦截器)
↓
数据访问层(多租户数据路由 / 数据源路由)
↓
存储层(共享库共享表 / 共享库独立表 / 独立库)
3. 权限隔离实现
3.1 租户上下文传递
租户标识通过请求头或 Token 解析后,写入线程上下文,供后续业务逻辑和拦截器使用。
java
public class TenantContext {
private static final ThreadLocal<String> TENANT_ID = new ThreadLocal<>();
public static void setTenantId(String tenantId) {
TENANT_ID.set(tenantId);
}
public static String getTenantId() {
return TENANT_ID.get();
}
public static void clear() {
TENANT_ID.remove();
}
}
3.2 拦截器校验
在服务层入口统一校验租户权限,未携带合法租户标识的请求直接拒绝。
java
public class TenantAuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String tenantId = request.getHeader("X-Tenant-Id");
if (tenantId == null || tenantId.isEmpty()) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
return false;
}
TenantContext.setTenantId(tenantId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
TenantContext.clear();
}
}
3.3 接口级权限控制
对于敏感操作,在方法级别通过注解或配置进一步校验租户角色权限。
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TenantPermission {
String[] roles() default {};
}
4. 数据分离实现
4.1 共享库共享表
所有租户共用同一张表,通过 tenant_id 字段区分数据归属。该方案成本最低,适合租户数据量小、隔离要求不高的场景。
sql
CREATE TABLE geo_optimization_task (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id VARCHAR(64) NOT NULL,
task_name VARCHAR(128) NOT NULL,
geo_config TEXT,
status TINYINT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
KEY idx_tenant_id (tenant_id)
);
4.2 数据访问层自动拼接租户条件
通过 MyBatis 拦截器或 JPA 过滤器,在 SQL 执行前自动追加 tenant_id 条件,避免业务代码重复编写。
java
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class TenantSqlInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
String tenantId = TenantContext.getTenantId();
if (tenantId != null && !sql.contains("tenant_id")) {
String newSql = injectTenantCondition(sql, tenantId);
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, newSql);
}
return invocation.proceed();
}
}
4.3 共享库独立表
每个租户拥有独立的表,表名带租户前缀,数据物理隔离,但共享同一个数据库实例。适合租户数据量较大、需要独立备份的场景。
java
public class TenantTableRouter {
public String resolveTableName(String baseTableName, String tenantId) {
return tenantId + "_" + baseTableName;
}
}
4.4 独立数据库
每个租户独占一个数据库,隔离级别最高,适合对数据安全要求严格的租户。通过动态数据源路由实现。
java
public class TenantDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return TenantContext.getTenantId();
}
}
5. 缓存与文件存储隔离
Redis 缓存键统一加上租户前缀,文件存储路径按租户目录隔离,防止跨租户数据泄露。
java
public class TenantCacheKeyBuilder {
public static String build(String key) {
return TenantContext.getTenantId() + ":" + key;
}
}
6. 源码目录结构
text
geo-optimization-system/
├── gateway/ # 接入层,租户上下文解析
├── common/ # 公共模块,租户上下文、拦截器
├── service/ # 业务服务
│ ├── task/ # 优化任务服务
│ └── permission/ # 权限校验服务
├── dal/ # 数据访问层
│ ├── interceptor/ # SQL 租户条件注入
│ └── router/ # 数据源与表路由
└── storage/ # 存储层,缓存与文件隔离
7. 总结
多租户 GEO 优化系统的权限隔离与数据分离,核心在于租户上下文的统一传递、拦截器的集中校验,以及数据访问层的自动路由。实际项目中应根据租户规模、数据敏感度和成本要求,灵活选择共享表、独立表或独立库方案,并在缓存和文件存储层面同步落实隔离策略。