前言

多租户(Multi-Tenant)是SaaS应用的核心架构模式,通过共享应用实例为多个租户提供服务,同时确保租户间的数据完全隔离。MyBatisPlus作为优秀的ORM框架,提供了强大的多租户支持能力。本文从架构设计到代码实现,系统梳理企业级多租户数据隔离的完整解决方案。

一、多租户架构设计

1.1 多租户数据隔离策略

1.1.1 数据库级隔离(Database per Tenant)

  • 优点:完全隔离,安全性最高,性能独立
  • 缺点:资源消耗大,维护成本高,扩展性差
  • 适用场景:对安全性要求极高的场景

1.1.2 Schema级隔离(Schema per Tenant)

  • 优点:逻辑隔离,资源相对独立,维护相对简单
  • 缺点:仍存在资源竞争,备份恢复复杂
  • 适用场景:中等规模的多租户应用

1.1.3 表级隔离(Table per Tenant)

  • 优点:共享数据库,资源利用率高,扩展性好
  • 缺点:需要租户ID字段,存在数据泄露风险
  • 适用场景:大规模SaaS应用的主流方案

1.1.4 行级隔离(Row per Tenant)

  • 优点:完全共享,资源利用率最高,维护最简单
  • 缺点:安全性依赖应用层,性能影响较大
  • 适用场景:对成本敏感的小型SaaS应用

1.2 多租户架构图

二、租户识别与上下文管理

2.1 租户识别策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public enum TenantIdentificationStrategy {
/**
* 基于域名识别
*/
DOMAIN_BASED("domain"),

/**
* 基于子域名识别
*/
SUBDOMAIN_BASED("subdomain"),

/**
* 基于请求头识别
*/
HEADER_BASED("header"),

/**
* 基于JWT Token识别
*/
JWT_BASED("jwt"),

/**
* 基于数据库查询识别
*/
DATABASE_BASED("database");

private final String type;

TenantIdentificationStrategy(String type) {
this.type = type;
}
}

2.2 租户上下文管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@Component
@Slf4j
public class TenantContext {

private static final ThreadLocal<TenantInfo> TENANT_HOLDER = new ThreadLocal<>();

/**
* 设置当前租户
*/
public static void setCurrentTenant(TenantInfo tenantInfo) {
TENANT_HOLDER.set(tenantInfo);
log.debug("设置当前租户: {}", tenantInfo.getTenantId());
}

/**
* 获取当前租户
*/
public static TenantInfo getCurrentTenant() {
TenantInfo tenantInfo = TENANT_HOLDER.get();
if (tenantInfo == null) {
throw new TenantNotFoundException("未找到当前租户信息");
}
return tenantInfo;
}

/**
* 获取当前租户ID
*/
public static String getCurrentTenantId() {
TenantInfo tenantInfo = getCurrentTenant();
return tenantInfo.getTenantId();
}

/**
* 清除当前租户
*/
public static void clear() {
TENANT_HOLDER.remove();
log.debug("清除当前租户信息");
}

/**
* 检查是否有租户信息
*/
public static boolean hasTenant() {
return TENANT_HOLDER.get() != null;
}
}

@Data
public class TenantInfo {
private String tenantId;
private String tenantName;
private String tenantCode;
private String databaseName;
private String schemaName;
private Map<String, Object> attributes;
private LocalDateTime createTime;
private LocalDateTime expireTime;

public TenantInfo(String tenantId) {
this.tenantId = tenantId;
this.attributes = new HashMap<>();
}

/**
* 检查租户是否有效
*/
public boolean isValid() {
if (expireTime == null) {
return true;
}
return LocalDateTime.now().isBefore(expireTime);
}

/**
* 获取属性值
*/
public Object getAttribute(String key) {
return attributes != null ? attributes.get(key) : null;
}

/**
* 设置属性值
*/
public void setAttribute(String key, Object value) {
if (attributes == null) {
attributes = new HashMap<>();
}
attributes.put(key, value);
}
}

2.3 租户识别服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
@Service
@Slf4j
public class TenantIdentificationService {

@Autowired
private TenantMapper tenantMapper;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

/**
* 基于域名识别租户
*/
public TenantInfo identifyByDomain(String domain) {
try {
// 1. 从缓存获取
String cacheKey = "tenant:domain:" + domain;
TenantInfo cachedTenant = (TenantInfo) redisTemplate.opsForValue().get(cacheKey);
if (cachedTenant != null && cachedTenant.isValid()) {
return cachedTenant;
}

// 2. 从数据库查询
Tenant tenant = tenantMapper.findByDomain(domain);
if (tenant == null) {
throw new TenantNotFoundException("未找到域名对应的租户: " + domain);
}

// 3. 构建租户信息
TenantInfo tenantInfo = buildTenantInfo(tenant);

// 4. 缓存结果
redisTemplate.opsForValue().set(cacheKey, tenantInfo, Duration.ofHours(1));

return tenantInfo;

} catch (Exception e) {
log.error("基于域名识别租户失败: domain={}", domain, e);
throw new TenantIdentificationException("租户识别失败", e);
}
}

/**
* 基于子域名识别租户
*/
public TenantInfo identifyBySubdomain(String subdomain) {
try {
// 1. 从缓存获取
String cacheKey = "tenant:subdomain:" + subdomain;
TenantInfo cachedTenant = (TenantInfo) redisTemplate.opsForValue().get(cacheKey);
if (cachedTenant != null && cachedTenant.isValid()) {
return cachedTenant;
}

// 2. 从数据库查询
Tenant tenant = tenantMapper.findBySubdomain(subdomain);
if (tenant == null) {
throw new TenantNotFoundException("未找到子域名对应的租户: " + subdomain);
}

// 3. 构建租户信息
TenantInfo tenantInfo = buildTenantInfo(tenant);

// 4. 缓存结果
redisTemplate.opsForValue().set(cacheKey, tenantInfo, Duration.ofHours(1));

return tenantInfo;

} catch (Exception e) {
log.error("基于子域名识别租户失败: subdomain={}", subdomain, e);
throw new TenantIdentificationException("租户识别失败", e);
}
}

/**
* 基于请求头识别租户
*/
public TenantInfo identifyByHeader(String tenantHeader) {
try {
if (StringUtils.isEmpty(tenantHeader)) {
throw new TenantNotFoundException("请求头中未找到租户信息");
}

// 1. 从缓存获取
String cacheKey = "tenant:header:" + tenantHeader;
TenantInfo cachedTenant = (TenantInfo) redisTemplate.opsForValue().get(cacheKey);
if (cachedTenant != null && cachedTenant.isValid()) {
return cachedTenant;
}

// 2. 从数据库查询
Tenant tenant = tenantMapper.findByTenantCode(tenantHeader);
if (tenant == null) {
throw new TenantNotFoundException("未找到租户代码对应的租户: " + tenantHeader);
}

// 3. 构建租户信息
TenantInfo tenantInfo = buildTenantInfo(tenant);

// 4. 缓存结果
redisTemplate.opsForValue().set(cacheKey, tenantInfo, Duration.ofHours(1));

return tenantInfo;

} catch (Exception e) {
log.error("基于请求头识别租户失败: tenantHeader={}", tenantHeader, e);
throw new TenantIdentificationException("租户识别失败", e);
}
}

/**
* 基于JWT Token识别租户
*/
public TenantInfo identifyByJwt(String token) {
try {
// 1. 解析JWT Token
Claims claims = parseJwtToken(token);
String tenantId = claims.get("tenantId", String.class);

if (StringUtils.isEmpty(tenantId)) {
throw new TenantNotFoundException("JWT Token中未找到租户信息");
}

// 2. 从缓存获取
String cacheKey = "tenant:jwt:" + tenantId;
TenantInfo cachedTenant = (TenantInfo) redisTemplate.opsForValue().get(cacheKey);
if (cachedTenant != null && cachedTenant.isValid()) {
return cachedTenant;
}

// 3. 从数据库查询
Tenant tenant = tenantMapper.findById(tenantId);
if (tenant == null) {
throw new TenantNotFoundException("未找到租户ID对应的租户: " + tenantId);
}

// 4. 构建租户信息
TenantInfo tenantInfo = buildTenantInfo(tenant);

// 5. 缓存结果
redisTemplate.opsForValue().set(cacheKey, tenantInfo, Duration.ofHours(1));

return tenantInfo;

} catch (Exception e) {
log.error("基于JWT Token识别租户失败: token={}", token, e);
throw new TenantIdentificationException("租户识别失败", e);
}
}

/**
* 构建租户信息
*/
private TenantInfo buildTenantInfo(Tenant tenant) {
TenantInfo tenantInfo = new TenantInfo(tenant.getId());
tenantInfo.setTenantName(tenant.getName());
tenantInfo.setTenantCode(tenant.getCode());
tenantInfo.setDatabaseName(tenant.getDatabaseName());
tenantInfo.setSchemaName(tenant.getSchemaName());
tenantInfo.setCreateTime(tenant.getCreateTime());
tenantInfo.setExpireTime(tenant.getExpireTime());

// 设置额外属性
tenantInfo.setAttribute("status", tenant.getStatus());
tenantInfo.setAttribute("plan", tenant.getPlan());
tenantInfo.setAttribute("features", tenant.getFeatures());

return tenantInfo;
}

/**
* 解析JWT Token
*/
private Claims parseJwtToken(String token) {
// JWT解析实现
// 这里使用简化的实现,实际项目中建议使用专业的JWT库
return new Claims() {
@Override
public String getSubject() {
return null;
}

@Override
public String get(String key, Class<String> clazz) {
// 简化的实现
return "tenant123";
}

// 其他方法实现...
};
}
}

三、MyBatisPlus多租户拦截器

3.1 多租户拦截器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
@Component
@Slf4j
public class TenantInterceptor implements Interceptor {

@Autowired
private TenantIdentificationService tenantIdentificationService;

/**
* 租户字段名
*/
private static final String TENANT_COLUMN = "tenant_id";

/**
* 需要忽略的表
*/
private static final Set<String> IGNORE_TABLES = Set.of(
"sys_tenant", "sys_user", "sys_role", "sys_permission"
);

@Override
public Object intercept(Invocation invocation) throws Throwable {
try {
// 1. 检查是否有租户信息
if (!TenantContext.hasTenant()) {
return invocation.proceed();
}

// 2. 获取当前租户信息
TenantInfo tenantInfo = TenantContext.getCurrentTenant();

// 3. 获取SQL语句
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];

// 4. 重写SQL
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
String originalSql = boundSql.getSql();

// 5. 检查是否需要添加租户条件
if (shouldAddTenantCondition(mappedStatement, originalSql)) {
String newSql = addTenantCondition(originalSql, tenantInfo.getTenantId());

// 6. 创建新的BoundSql
BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(),
newSql, boundSql.getParameterMappings(), parameter);

// 7. 复制参数
for (ParameterMapping mapping : boundSql.getParameterMappings()) {
String prop = mapping.getProperty();
if (boundSql.hasAdditionalParameter(prop)) {
newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
}
}

// 8. 创建新的MappedStatement
MappedStatement newMappedStatement = copyMappedStatement(mappedStatement, newBoundSql);
invocation.getArgs()[0] = newMappedStatement;
}

return invocation.proceed();

} catch (Exception e) {
log.error("多租户拦截器处理异常", e);
throw e;
}
}

/**
* 检查是否需要添加租户条件
*/
private boolean shouldAddTenantCondition(MappedStatement mappedStatement, String sql) {
// 1. 检查SQL类型
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
if (sqlCommandType == SqlCommandType.UNKNOWN) {
return false;
}

// 2. 检查表名
String tableName = extractTableName(sql);
if (tableName == null || IGNORE_TABLES.contains(tableName.toLowerCase())) {
return false;
}

// 3. 检查是否已包含租户条件
if (sql.toLowerCase().contains(TENANT_COLUMN)) {
return false;
}

return true;
}

/**
* 添加租户条件
*/
private String addTenantCondition(String sql, String tenantId) {
// 1. 解析SQL
SQLStatement sqlStatement = SQLUtils.parseSingleStatement(sql, JdbcConstants.MYSQL);

if (sqlStatement instanceof SQLSelectStatement) {
return addSelectTenantCondition(sql, tenantId);
} else if (sqlStatement instanceof SQLUpdateStatement) {
return addUpdateTenantCondition(sql, tenantId);
} else if (sqlStatement instanceof SQLDeleteStatement) {
return addDeleteTenantCondition(sql, tenantId);
} else if (sqlStatement instanceof SQLInsertStatement) {
return addInsertTenantCondition(sql, tenantId);
}

return sql;
}

/**
* 添加SELECT租户条件
*/
private String addSelectTenantCondition(String sql, String tenantId) {
// 1. 查找WHERE子句
int whereIndex = sql.toLowerCase().indexOf(" where ");
if (whereIndex > 0) {
// 已有WHERE子句,添加AND条件
return sql.substring(0, whereIndex + 7) +
TENANT_COLUMN + " = '" + tenantId + "' AND " +
sql.substring(whereIndex + 7);
} else {
// 没有WHERE子句,添加WHERE条件
int orderIndex = sql.toLowerCase().indexOf(" order by ");
int groupIndex = sql.toLowerCase().indexOf(" group by ");
int havingIndex = sql.toLowerCase().indexOf(" having ");

int insertIndex = sql.length();
if (orderIndex > 0) insertIndex = Math.min(insertIndex, orderIndex);
if (groupIndex > 0) insertIndex = Math.min(insertIndex, groupIndex);
if (havingIndex > 0) insertIndex = Math.min(insertIndex, havingIndex);

return sql.substring(0, insertIndex) +
" WHERE " + TENANT_COLUMN + " = '" + tenantId + "'" +
sql.substring(insertIndex);
}
}

/**
* 添加UPDATE租户条件
*/
private String addUpdateTenantCondition(String sql, String tenantId) {
// 1. 查找WHERE子句
int whereIndex = sql.toLowerCase().indexOf(" where ");
if (whereIndex > 0) {
// 已有WHERE子句,添加AND条件
return sql.substring(0, whereIndex + 7) +
TENANT_COLUMN + " = '" + tenantId + "' AND " +
sql.substring(whereIndex + 7);
} else {
// 没有WHERE子句,添加WHERE条件
return sql + " WHERE " + TENANT_COLUMN + " = '" + tenantId + "'";
}
}

/**
* 添加DELETE租户条件
*/
private String addDeleteTenantCondition(String sql, String tenantId) {
// 1. 查找WHERE子句
int whereIndex = sql.toLowerCase().indexOf(" where ");
if (whereIndex > 0) {
// 已有WHERE子句,添加AND条件
return sql.substring(0, whereIndex + 7) +
TENANT_COLUMN + " = '" + tenantId + "' AND " +
sql.substring(whereIndex + 7);
} else {
// 没有WHERE子句,添加WHERE条件
return sql + " WHERE " + TENANT_COLUMN + " = '" + tenantId + "'";
}
}

/**
* 添加INSERT租户条件
*/
private String addInsertTenantCondition(String sql, String tenantId) {
// 1. 查找VALUES子句
int valuesIndex = sql.toLowerCase().indexOf(" values ");
if (valuesIndex > 0) {
// 在VALUES前添加租户字段
String beforeValues = sql.substring(0, valuesIndex);
String afterValues = sql.substring(valuesIndex);

// 添加租户字段到字段列表
beforeValues = beforeValues.replace(")", ", " + TENANT_COLUMN + ")");

// 添加租户值到值列表
afterValues = afterValues.replace(")", ", '" + tenantId + "')");

return beforeValues + afterValues;
}

return sql;
}

/**
* 提取表名
*/
private String extractTableName(String sql) {
try {
SQLStatement sqlStatement = SQLUtils.parseSingleStatement(sql, JdbcConstants.MYSQL);

if (sqlStatement instanceof SQLSelectStatement) {
SQLSelectStatement selectStatement = (SQLSelectStatement) sqlStatement;
SQLSelectQueryBlock queryBlock = selectStatement.getSelect().getQueryBlock();
if (queryBlock != null && queryBlock.getFrom() != null) {
return queryBlock.getFrom().toString();
}
} else if (sqlStatement instanceof SQLUpdateStatement) {
SQLUpdateStatement updateStatement = (SQLUpdateStatement) sqlStatement;
return updateStatement.getTableName().getSimpleName();
} else if (sqlStatement instanceof SQLDeleteStatement) {
SQLDeleteStatement deleteStatement = (SQLDeleteStatement) sqlStatement;
return deleteStatement.getTableName().getSimpleName();
} else if (sqlStatement instanceof SQLInsertStatement) {
SQLInsertStatement insertStatement = (SQLInsertStatement) sqlStatement;
return insertStatement.getTableName().getSimpleName();
}

} catch (Exception e) {
log.warn("提取表名失败: sql={}", sql, e);
}

return null;
}

/**
* 复制MappedStatement
*/
private MappedStatement copyMappedStatement(MappedStatement ms, BoundSql newBoundSql) {
MappedStatement.Builder builder = new MappedStatement.Builder(
ms.getConfiguration(), ms.getId(), ms.getSqlSource(), ms.getSqlCommandType());

builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
builder.keyProperty(ms.getKeyProperties());
builder.keyColumn(ms.getKeyColumns());
builder.databaseId(ms.getDatabaseId());
builder.lang(ms.getLang());
builder.resultOrdered(ms.isResultOrdered());
builder.resultSets(ms.getResultSets());
builder.resultMaps(ms.getResultMaps());
builder.resultSetType(ms.getResultSetType());
builder.flushCacheRequired(ms.isFlushCacheRequired());
builder.useCache(ms.isUseCache());
builder.cache(ms.getCache());
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.resultSetType(ms.getResultSetType());
builder.statementType(ms.getStatementType());

return builder.build();
}

@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
}

@Override
public void setProperties(Properties properties) {
// 设置属性
}
}

3.2 多租户配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Configuration
@EnableTransactionManagement
public class MyBatisPlusConfig {

@Autowired
private TenantInterceptor tenantInterceptor;

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

// 添加多租户拦截器
interceptor.addInnerInterceptor(tenantInterceptor);

// 添加分页拦截器
PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor();
paginationInterceptor.setDbType(DbType.MYSQL);
paginationInterceptor.setMaxLimit(1000L);
interceptor.addInnerInterceptor(paginationInterceptor);

// 添加乐观锁拦截器
OptimisticLockerInnerInterceptor optimisticLockerInterceptor = new OptimisticLockerInnerInterceptor();
interceptor.addInnerInterceptor(optimisticLockerInterceptor);

return interceptor;
}

@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();

// 设置数据库字段策略
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
dbConfig.setLogicDeleteField("deleted");
dbConfig.setLogicDeleteValue("1");
dbConfig.setLogicNotDeleteValue("0");
dbConfig.setIdType(IdType.AUTO);
globalConfig.setDbConfig(dbConfig);

// 设置租户字段
globalConfig.setTenantLineHandler(new TenantLineHandler() {
@Override
public Expression getTenantId() {
return new StringValue(TenantContext.getCurrentTenantId());
}

@Override
public String getTenantIdColumn() {
return "tenant_id";
}

@Override
public boolean ignoreTable(String tableName) {
return IGNORE_TABLES.contains(tableName.toLowerCase());
}
});

return globalConfig;
}
}

四、租户数据管理

4.1 租户实体设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@Entity
@Table(name = "sys_tenant")
@Data
public class Tenant {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "tenant_id", unique = true, nullable = false)
private String tenantId;

@Column(name = "tenant_name", nullable = false)
private String name;

@Column(name = "tenant_code", unique = true, nullable = false)
private String code;

@Column(name = "domain")
private String domain;

@Column(name = "subdomain")
private String subdomain;

@Column(name = "database_name")
private String databaseName;

@Column(name = "schema_name")
private String schemaName;

@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private TenantStatus status;

@Enumerated(EnumType.STRING)
@Column(name = "plan", nullable = false)
private TenantPlan plan;

@Column(name = "features", columnDefinition = "TEXT")
private String features;

@Column(name = "max_users")
private Integer maxUsers;

@Column(name = "max_storage")
private Long maxStorage;

@Column(name = "expire_time")
private LocalDateTime expireTime;

@Column(name = "create_time", nullable = false)
private LocalDateTime createTime;

@Column(name = "update_time", nullable = false)
private LocalDateTime updateTime;

@Column(name = "deleted", nullable = false)
private Boolean deleted = false;
}

public enum TenantStatus {
ACTIVE("激活"),
INACTIVE("未激活"),
SUSPENDED("暂停"),
EXPIRED("过期");

private final String description;

TenantStatus(String description) {
this.description = description;
}
}

public enum TenantPlan {
FREE("免费版"),
BASIC("基础版"),
PROFESSIONAL("专业版"),
ENTERPRISE("企业版");

private final String description;

TenantPlan(String description) {
this.description = description;
}
}

4.2 租户服务实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
@Service
@Transactional
@Slf4j
public class TenantService {

@Autowired
private TenantMapper tenantMapper;

@Autowired
private TenantIdentificationService tenantIdentificationService;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

/**
* 创建租户
*/
public Tenant createTenant(CreateTenantRequest request) {
try {
// 1. 参数校验
validateCreateTenantRequest(request);

// 2. 检查租户代码是否已存在
if (tenantMapper.existsByCode(request.getCode())) {
throw new BusinessException("租户代码已存在: " + request.getCode());
}

// 3. 创建租户
Tenant tenant = new Tenant();
tenant.setTenantId(generateTenantId());
tenant.setName(request.getName());
tenant.setCode(request.getCode());
tenant.setDomain(request.getDomain());
tenant.setSubdomain(request.getSubdomain());
tenant.setDatabaseName(request.getDatabaseName());
tenant.setSchemaName(request.getSchemaName());
tenant.setStatus(TenantStatus.ACTIVE);
tenant.setPlan(request.getPlan());
tenant.setFeatures(request.getFeatures());
tenant.setMaxUsers(request.getMaxUsers());
tenant.setMaxStorage(request.getMaxStorage());
tenant.setExpireTime(request.getExpireTime());
tenant.setCreateTime(LocalDateTime.now());
tenant.setUpdateTime(LocalDateTime.now());

// 4. 保存租户
tenantMapper.insert(tenant);

// 5. 创建租户数据库
createTenantDatabase(tenant);

// 6. 初始化租户数据
initializeTenantData(tenant);

// 7. 缓存租户信息
cacheTenantInfo(tenant);

log.info("创建租户成功: tenantId={}, name={}", tenant.getTenantId(), tenant.getName());

return tenant;

} catch (Exception e) {
log.error("创建租户失败: request={}", request, e);
throw new BusinessException("创建租户失败: " + e.getMessage());
}
}

/**
* 更新租户
*/
public Tenant updateTenant(String tenantId, UpdateTenantRequest request) {
try {
// 1. 查找租户
Tenant tenant = tenantMapper.findByTenantId(tenantId);
if (tenant == null) {
throw new BusinessException("租户不存在: " + tenantId);
}

// 2. 更新租户信息
if (request.getName() != null) {
tenant.setName(request.getName());
}
if (request.getDomain() != null) {
tenant.setDomain(request.getDomain());
}
if (request.getSubdomain() != null) {
tenant.setSubdomain(request.getSubdomain());
}
if (request.getStatus() != null) {
tenant.setStatus(request.getStatus());
}
if (request.getPlan() != null) {
tenant.setPlan(request.getPlan());
}
if (request.getFeatures() != null) {
tenant.setFeatures(request.getFeatures());
}
if (request.getMaxUsers() != null) {
tenant.setMaxUsers(request.getMaxUsers());
}
if (request.getMaxStorage() != null) {
tenant.setMaxStorage(request.getMaxStorage());
}
if (request.getExpireTime() != null) {
tenant.setExpireTime(request.getExpireTime());
}

tenant.setUpdateTime(LocalDateTime.now());

// 3. 保存更新
tenantMapper.updateById(tenant);

// 4. 更新缓存
cacheTenantInfo(tenant);

log.info("更新租户成功: tenantId={}", tenantId);

return tenant;

} catch (Exception e) {
log.error("更新租户失败: tenantId={}, request={}", tenantId, request, e);
throw new BusinessException("更新租户失败: " + e.getMessage());
}
}

/**
* 删除租户
*/
public void deleteTenant(String tenantId) {
try {
// 1. 查找租户
Tenant tenant = tenantMapper.findByTenantId(tenantId);
if (tenant == null) {
throw new BusinessException("租户不存在: " + tenantId);
}

// 2. 检查租户状态
if (tenant.getStatus() == TenantStatus.ACTIVE) {
throw new BusinessException("激活状态的租户不能删除");
}

// 3. 软删除租户
tenant.setDeleted(true);
tenant.setUpdateTime(LocalDateTime.now());
tenantMapper.updateById(tenant);

// 4. 清除缓存
clearTenantCache(tenantId);

// 5. 删除租户数据
deleteTenantData(tenant);

log.info("删除租户成功: tenantId={}", tenantId);

} catch (Exception e) {
log.error("删除租户失败: tenantId={}", tenantId, e);
throw new BusinessException("删除租户失败: " + e.getMessage());
}
}

/**
* 获取租户信息
*/
public Tenant getTenant(String tenantId) {
try {
// 1. 从缓存获取
String cacheKey = "tenant:info:" + tenantId;
Tenant cachedTenant = (Tenant) redisTemplate.opsForValue().get(cacheKey);
if (cachedTenant != null) {
return cachedTenant;
}

// 2. 从数据库查询
Tenant tenant = tenantMapper.findByTenantId(tenantId);
if (tenant == null) {
throw new BusinessException("租户不存在: " + tenantId);
}

// 3. 缓存结果
redisTemplate.opsForValue().set(cacheKey, tenant, Duration.ofHours(1));

return tenant;

} catch (Exception e) {
log.error("获取租户信息失败: tenantId={}", tenantId, e);
throw new BusinessException("获取租户信息失败: " + e.getMessage());
}
}

/**
* 缓存租户信息
*/
private void cacheTenantInfo(Tenant tenant) {
try {
String cacheKey = "tenant:info:" + tenant.getTenantId();
redisTemplate.opsForValue().set(cacheKey, tenant, Duration.ofHours(1));

// 缓存域名映射
if (tenant.getDomain() != null) {
String domainKey = "tenant:domain:" + tenant.getDomain();
redisTemplate.opsForValue().set(domainKey, tenant, Duration.ofHours(1));
}

// 缓存子域名映射
if (tenant.getSubdomain() != null) {
String subdomainKey = "tenant:subdomain:" + tenant.getSubdomain();
redisTemplate.opsForValue().set(subdomainKey, tenant, Duration.ofHours(1));
}

// 缓存租户代码映射
String codeKey = "tenant:code:" + tenant.getCode();
redisTemplate.opsForValue().set(codeKey, tenant, Duration.ofHours(1));

} catch (Exception e) {
log.warn("缓存租户信息失败: tenantId={}", tenant.getTenantId(), e);
}
}

/**
* 清除租户缓存
*/
private void clearTenantCache(String tenantId) {
try {
// 获取租户信息
Tenant tenant = tenantMapper.findByTenantId(tenantId);
if (tenant == null) {
return;
}

// 清除各种缓存
redisTemplate.delete("tenant:info:" + tenantId);
redisTemplate.delete("tenant:code:" + tenant.getCode());

if (tenant.getDomain() != null) {
redisTemplate.delete("tenant:domain:" + tenant.getDomain());
}

if (tenant.getSubdomain() != null) {
redisTemplate.delete("tenant:subdomain:" + tenant.getSubdomain());
}

} catch (Exception e) {
log.warn("清除租户缓存失败: tenantId={}", tenantId, e);
}
}

/**
* 创建租户数据库
*/
private void createTenantDatabase(Tenant tenant) {
// 实现租户数据库创建逻辑
// 这里可以根据需要创建独立的数据库或Schema
}

/**
* 初始化租户数据
*/
private void initializeTenantData(Tenant tenant) {
// 实现租户数据初始化逻辑
// 例如:创建默认用户、角色、权限等
}

/**
* 删除租户数据
*/
private void deleteTenantData(Tenant tenant) {
// 实现租户数据删除逻辑
// 注意:这里应该是软删除,避免数据丢失
}

/**
* 生成租户ID
*/
private String generateTenantId() {
return "tenant_" + System.currentTimeMillis() + "_" + RandomUtils.nextInt(1000, 9999);
}

/**
* 校验创建租户请求
*/
private void validateCreateTenantRequest(CreateTenantRequest request) {
if (StringUtils.isEmpty(request.getName())) {
throw new BusinessException("租户名称不能为空");
}
if (StringUtils.isEmpty(request.getCode())) {
throw new BusinessException("租户代码不能为空");
}
if (request.getPlan() == null) {
throw new BusinessException("租户计划不能为空");
}
}
}

五、安全防护与权限控制

5.1 租户权限拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
@Component
@Slf4j
public class TenantSecurityInterceptor implements HandlerInterceptor {

@Autowired
private TenantIdentificationService tenantIdentificationService;

@Autowired
private TenantService tenantService;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
// 1. 识别租户
TenantInfo tenantInfo = identifyTenant(request);

// 2. 验证租户状态
validateTenantStatus(tenantInfo);

// 3. 设置租户上下文
TenantContext.setCurrentTenant(tenantInfo);

// 4. 记录访问日志
logTenantAccess(request, tenantInfo);

return true;

} catch (TenantNotFoundException e) {
log.warn("租户识别失败: {}", e.getMessage());
response.setStatus(HttpStatus.NOT_FOUND.value());
response.getWriter().write("{\"error\":\"租户不存在\"}");
return false;
} catch (TenantExpiredException e) {
log.warn("租户已过期: {}", e.getMessage());
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().write("{\"error\":\"租户已过期\"}");
return false;
} catch (TenantSuspendedException e) {
log.warn("租户已暂停: {}", e.getMessage());
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().write("{\"error\":\"租户已暂停\"}");
return false;
} catch (Exception e) {
log.error("租户安全拦截器异常", e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.getWriter().write("{\"error\":\"系统异常\"}");
return false;
}
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清除租户上下文
TenantContext.clear();
}

/**
* 识别租户
*/
private TenantInfo identifyTenant(HttpServletRequest request) {
// 1. 基于域名识别
String host = request.getHeader("Host");
if (host != null) {
try {
return tenantIdentificationService.identifyByDomain(host);
} catch (TenantNotFoundException e) {
// 继续尝试其他方式
}
}

// 2. 基于子域名识别
if (host != null && host.contains(".")) {
String subdomain = host.substring(0, host.indexOf("."));
try {
return tenantIdentificationService.identifyBySubdomain(subdomain);
} catch (TenantNotFoundException e) {
// 继续尝试其他方式
}
}

// 3. 基于请求头识别
String tenantHeader = request.getHeader("X-Tenant-ID");
if (tenantHeader != null) {
try {
return tenantIdentificationService.identifyByHeader(tenantHeader);
} catch (TenantNotFoundException e) {
// 继续尝试其他方式
}
}

// 4. 基于JWT Token识别
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer ")) {
String token = authorization.substring(7);
try {
return tenantIdentificationService.identifyByJwt(token);
} catch (TenantNotFoundException e) {
// 继续尝试其他方式
}
}

throw new TenantNotFoundException("无法识别租户");
}

/**
* 验证租户状态
*/
private void validateTenantStatus(TenantInfo tenantInfo) {
// 1. 检查租户是否有效
if (!tenantInfo.isValid()) {
throw new TenantExpiredException("租户已过期: " + tenantInfo.getTenantId());
}

// 2. 检查租户状态
String status = (String) tenantInfo.getAttribute("status");
if ("SUSPENDED".equals(status)) {
throw new TenantSuspendedException("租户已暂停: " + tenantInfo.getTenantId());
}

if ("INACTIVE".equals(status)) {
throw new TenantSuspendedException("租户未激活: " + tenantInfo.getTenantId());
}
}

/**
* 记录租户访问日志
*/
private void logTenantAccess(HttpServletRequest request, TenantInfo tenantInfo) {
try {
TenantAccessLog accessLog = new TenantAccessLog();
accessLog.setTenantId(tenantInfo.getTenantId());
accessLog.setRequestUri(request.getRequestURI());
accessLog.setRequestMethod(request.getMethod());
accessLog.setClientIp(getClientIp(request));
accessLog.setUserAgent(request.getHeader("User-Agent"));
accessLog.setAccessTime(LocalDateTime.now());

// 异步记录访问日志
CompletableFuture.runAsync(() -> {
try {
// 记录到数据库或日志文件
log.info("租户访问: {}", accessLog);
} catch (Exception e) {
log.error("记录租户访问日志失败", e);
}
});

} catch (Exception e) {
log.warn("记录租户访问日志异常", e);
}
}

/**
* 获取客户端IP
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}

5.2 数据访问权限控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
@Component
@Slf4j
public class TenantDataAccessControl {

@Autowired
private TenantService tenantService;

/**
* 检查数据访问权限
*/
public void checkDataAccessPermission(String tenantId, String resourceType, String operation) {
try {
// 1. 获取租户信息
Tenant tenant = tenantService.getTenant(tenantId);

// 2. 检查租户状态
if (tenant.getStatus() != TenantStatus.ACTIVE) {
throw new AccessDeniedException("租户状态异常,无法访问数据");
}

// 3. 检查租户计划限制
checkPlanLimits(tenant, resourceType, operation);

// 4. 检查功能权限
checkFeaturePermissions(tenant, resourceType, operation);

} catch (Exception e) {
log.error("检查数据访问权限失败: tenantId={}, resourceType={}, operation={}",
tenantId, resourceType, operation, e);
throw new AccessDeniedException("数据访问权限检查失败: " + e.getMessage());
}
}

/**
* 检查计划限制
*/
private void checkPlanLimits(Tenant tenant, String resourceType, String operation) {
switch (tenant.getPlan()) {
case FREE:
checkFreePlanLimits(tenant, resourceType, operation);
break;
case BASIC:
checkBasicPlanLimits(tenant, resourceType, operation);
break;
case PROFESSIONAL:
checkProfessionalPlanLimits(tenant, resourceType, operation);
break;
case ENTERPRISE:
// 企业版无限制
break;
default:
throw new AccessDeniedException("未知的租户计划: " + tenant.getPlan());
}
}

/**
* 检查免费版限制
*/
private void checkFreePlanLimits(Tenant tenant, String resourceType, String operation) {
// 1. 检查用户数量限制
if ("user".equals(resourceType) && "create".equals(operation)) {
int currentUserCount = getCurrentUserCount(tenant.getTenantId());
if (currentUserCount >= 5) {
throw new AccessDeniedException("免费版用户数量限制为5个");
}
}

// 2. 检查存储空间限制
if ("storage".equals(resourceType)) {
long currentStorage = getCurrentStorageUsage(tenant.getTenantId());
if (currentStorage >= 1024 * 1024 * 100) { // 100MB
throw new AccessDeniedException("免费版存储空间限制为100MB");
}
}

// 3. 检查API调用限制
if ("api".equals(resourceType)) {
int apiCallCount = getTodayApiCallCount(tenant.getTenantId());
if (apiCallCount >= 1000) {
throw new AccessDeniedException("免费版每日API调用限制为1000次");
}
}
}

/**
* 检查基础版限制
*/
private void checkBasicPlanLimits(Tenant tenant, String resourceType, String operation) {
// 1. 检查用户数量限制
if ("user".equals(resourceType) && "create".equals(operation)) {
int currentUserCount = getCurrentUserCount(tenant.getTenantId());
if (currentUserCount >= 50) {
throw new AccessDeniedException("基础版用户数量限制为50个");
}
}

// 2. 检查存储空间限制
if ("storage".equals(resourceType)) {
long currentStorage = getCurrentStorageUsage(tenant.getTenantId());
if (currentStorage >= 1024 * 1024 * 1024) { // 1GB
throw new AccessDeniedException("基础版存储空间限制为1GB");
}
}
}

/**
* 检查专业版限制
*/
private void checkProfessionalPlanLimits(Tenant tenant, String resourceType, String operation) {
// 1. 检查用户数量限制
if ("user".equals(resourceType) && "create".equals(operation)) {
int currentUserCount = getCurrentUserCount(tenant.getTenantId());
if (currentUserCount >= 500) {
throw new AccessDeniedException("专业版用户数量限制为500个");
}
}

// 2. 检查存储空间限制
if ("storage".equals(resourceType)) {
long currentStorage = getCurrentStorageUsage(tenant.getTenantId());
if (currentStorage >= 1024 * 1024 * 1024 * 10) { // 10GB
throw new AccessDeniedException("专业版存储空间限制为10GB");
}
}
}

/**
* 检查功能权限
*/
private void checkFeaturePermissions(Tenant tenant, String resourceType, String operation) {
String features = tenant.getFeatures();
if (features == null) {
return;
}

// 解析功能列表
List<String> featureList = Arrays.asList(features.split(","));

// 检查特定功能权限
if ("advanced_analytics".equals(resourceType) && !featureList.contains("analytics")) {
throw new AccessDeniedException("租户未开通数据分析功能");
}

if ("api_integration".equals(resourceType) && !featureList.contains("api")) {
throw new AccessDeniedException("租户未开通API集成功能");
}

if ("custom_fields".equals(resourceType) && !featureList.contains("custom_fields")) {
throw new AccessDeniedException("租户未开通自定义字段功能");
}
}

/**
* 获取当前用户数量
*/
private int getCurrentUserCount(String tenantId) {
// 实现获取当前用户数量的逻辑
return 0;
}

/**
* 获取当前存储使用量
*/
private long getCurrentStorageUsage(String tenantId) {
// 实现获取当前存储使用量的逻辑
return 0;
}

/**
* 获取今日API调用次数
*/
private int getTodayApiCallCount(String tenantId) {
// 实现获取今日API调用次数的逻辑
return 0;
}
}

六、性能优化与监控

6.1 多租户性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@Component
@Slf4j
public class TenantPerformanceOptimizer {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private TenantService tenantService;

/**
* 租户数据缓存策略
*/
public void optimizeTenantDataCache(String tenantId) {
try {
// 1. 预热租户信息缓存
Tenant tenant = tenantService.getTenant(tenantId);
String cacheKey = "tenant:info:" + tenantId;
redisTemplate.opsForValue().set(cacheKey, tenant, Duration.ofHours(1));

// 2. 预热租户配置缓存
TenantConfig config = getTenantConfig(tenantId);
String configKey = "tenant:config:" + tenantId;
redisTemplate.opsForValue().set(configKey, config, Duration.ofHours(1));

// 3. 预热租户权限缓存
List<String> permissions = getTenantPermissions(tenantId);
String permissionKey = "tenant:permissions:" + tenantId;
redisTemplate.opsForValue().set(permissionKey, permissions, Duration.ofHours(1));

log.info("租户数据缓存优化完成: tenantId={}", tenantId);

} catch (Exception e) {
log.error("租户数据缓存优化失败: tenantId={}", tenantId, e);
}
}

/**
* SQL查询优化
*/
public String optimizeSqlForTenant(String sql, String tenantId) {
try {
// 1. 添加租户索引提示
if (sql.toLowerCase().contains("select")) {
sql = addTenantIndexHint(sql, tenantId);
}

// 2. 优化JOIN查询
if (sql.toLowerCase().contains("join")) {
sql = optimizeJoinQuery(sql, tenantId);
}

// 3. 添加查询缓存提示
sql = addQueryCacheHint(sql, tenantId);

return sql;

} catch (Exception e) {
log.error("SQL查询优化失败: sql={}, tenantId={}", sql, tenantId, e);
return sql;
}
}

/**
* 添加租户索引提示
*/
private String addTenantIndexHint(String sql, String tenantId) {
// 1. 查找FROM子句
int fromIndex = sql.toLowerCase().indexOf(" from ");
if (fromIndex > 0) {
String tableName = extractTableName(sql, fromIndex);
if (tableName != null) {
// 2. 添加索引提示
String indexHint = " USE INDEX (idx_" + tableName + "_tenant_id)";
sql = sql.substring(0, fromIndex + 6) + indexHint + sql.substring(fromIndex + 6);
}
}

return sql;
}

/**
* 优化JOIN查询
*/
private String optimizeJoinQuery(String sql, String tenantId) {
// 1. 确保JOIN条件包含租户ID
if (!sql.toLowerCase().contains("tenant_id")) {
// 添加租户ID条件到WHERE子句
int whereIndex = sql.toLowerCase().indexOf(" where ");
if (whereIndex > 0) {
sql = sql.substring(0, whereIndex + 7) +
"tenant_id = '" + tenantId + "' AND " +
sql.substring(whereIndex + 7);
} else {
sql += " WHERE tenant_id = '" + tenantId + "'";
}
}

return sql;
}

/**
* 添加查询缓存提示
*/
private String addQueryCacheHint(String sql, String tenantId) {
// 1. 检查是否适合缓存
if (isCacheableQuery(sql)) {
// 2. 添加缓存提示
sql = "/*+ USE_CACHE */ " + sql;
}

return sql;
}

/**
* 检查是否适合缓存
*/
private boolean isCacheableQuery(String sql) {
// 1. 只读查询
if (!sql.toLowerCase().contains("select")) {
return false;
}

// 2. 不包含动态函数
if (sql.toLowerCase().contains("now()") ||
sql.toLowerCase().contains("current_timestamp")) {
return false;
}

// 3. 不包含随机函数
if (sql.toLowerCase().contains("rand()") ||
sql.toLowerCase().contains("random()")) {
return false;
}

return true;
}

/**
* 提取表名
*/
private String extractTableName(String sql, int fromIndex) {
try {
String afterFrom = sql.substring(fromIndex + 6);
String[] parts = afterFrom.split("\\s+");
if (parts.length > 0) {
return parts[0].replaceAll("[`'\"]", "");
}
} catch (Exception e) {
log.warn("提取表名失败: sql={}", sql, e);
}

return null;
}

/**
* 获取租户配置
*/
private TenantConfig getTenantConfig(String tenantId) {
// 实现获取租户配置的逻辑
return new TenantConfig();
}

/**
* 获取租户权限
*/
private List<String> getTenantPermissions(String tenantId) {
// 实现获取租户权限的逻辑
return new ArrayList<>();
}
}

6.2 多租户监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
@Component
@Slf4j
public class TenantMonitoringService {

@Autowired
private MeterRegistry meterRegistry;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

/**
* 记录租户访问指标
*/
public void recordTenantAccess(String tenantId, String operation, long duration) {
try {
// 1. 记录访问次数
Counter.builder("tenant.access.count")
.tag("tenant_id", tenantId)
.tag("operation", operation)
.register(meterRegistry)
.increment();

// 2. 记录访问耗时
Timer.builder("tenant.access.duration")
.tag("tenant_id", tenantId)
.tag("operation", operation)
.register(meterRegistry)
.record(duration, TimeUnit.MILLISECONDS);

// 3. 记录到Redis
String key = "tenant:metrics:" + tenantId + ":" + LocalDate.now().toString();
redisTemplate.opsForHash().increment(key, operation + ":count", 1);
redisTemplate.opsForHash().increment(key, operation + ":duration", duration);
redisTemplate.expire(key, Duration.ofDays(7));

} catch (Exception e) {
log.error("记录租户访问指标失败: tenantId={}, operation={}", tenantId, operation, e);
}
}

/**
* 记录租户错误指标
*/
public void recordTenantError(String tenantId, String operation, String errorType) {
try {
// 1. 记录错误次数
Counter.builder("tenant.error.count")
.tag("tenant_id", tenantId)
.tag("operation", operation)
.tag("error_type", errorType)
.register(meterRegistry)
.increment();

// 2. 记录到Redis
String key = "tenant:errors:" + tenantId + ":" + LocalDate.now().toString();
redisTemplate.opsForHash().increment(key, operation + ":" + errorType, 1);
redisTemplate.expire(key, Duration.ofDays(7));

} catch (Exception e) {
log.error("记录租户错误指标失败: tenantId={}, operation={}", tenantId, operation, e);
}
}

/**
* 获取租户性能统计
*/
public TenantPerformanceStats getTenantPerformanceStats(String tenantId, LocalDate date) {
try {
TenantPerformanceStats stats = new TenantPerformanceStats();
stats.setTenantId(tenantId);
stats.setDate(date);

// 1. 获取访问统计
String metricsKey = "tenant:metrics:" + tenantId + ":" + date.toString();
Map<Object, Object> metrics = redisTemplate.opsForHash().entries(metricsKey);

// 2. 获取错误统计
String errorsKey = "tenant:errors:" + tenantId + ":" + date.toString();
Map<Object, Object> errors = redisTemplate.opsForHash().entries(errorsKey);

// 3. 计算统计指标
calculatePerformanceStats(stats, metrics, errors);

return stats;

} catch (Exception e) {
log.error("获取租户性能统计失败: tenantId={}, date={}", tenantId, date, e);
return new TenantPerformanceStats();
}
}

/**
* 计算性能统计
*/
private void calculatePerformanceStats(TenantPerformanceStats stats,
Map<Object, Object> metrics,
Map<Object, Object> errors) {
// 1. 计算总访问次数
int totalAccess = 0;
long totalDuration = 0;

for (Map.Entry<Object, Object> entry : metrics.entrySet()) {
String key = entry.getKey().toString();
if (key.endsWith(":count")) {
totalAccess += Integer.parseInt(entry.getValue().toString());
} else if (key.endsWith(":duration")) {
totalDuration += Long.parseLong(entry.getValue().toString());
}
}

stats.setTotalAccess(totalAccess);
stats.setTotalDuration(totalDuration);
stats.setAvgDuration(totalAccess > 0 ? totalDuration / totalAccess : 0);

// 2. 计算错误次数
int totalErrors = 0;
for (Object value : errors.values()) {
totalErrors += Integer.parseInt(value.toString());
}

stats.setTotalErrors(totalErrors);
stats.setErrorRate(totalAccess > 0 ? (double) totalErrors / totalAccess : 0);

// 3. 计算可用性
stats.setAvailability(totalAccess > 0 ? (double) (totalAccess - totalErrors) / totalAccess : 1.0);
}

/**
* 检查租户性能异常
*/
@Scheduled(fixedDelay = 300000) // 5分钟检查一次
public void checkTenantPerformanceAnomalies() {
try {
// 1. 获取所有活跃租户
List<String> activeTenants = getActiveTenants();

for (String tenantId : activeTenants) {
// 2. 检查今日性能
TenantPerformanceStats stats = getTenantPerformanceStats(tenantId, LocalDate.now());

// 3. 检查异常指标
if (stats.getErrorRate() > 0.1) {
log.warn("租户错误率过高: tenantId={}, errorRate={}", tenantId, stats.getErrorRate());
sendPerformanceAlert(tenantId, "ERROR_RATE_HIGH", stats.getErrorRate());
}

if (stats.getAvgDuration() > 5000) {
log.warn("租户响应时间过长: tenantId={}, avgDuration={}", tenantId, stats.getAvgDuration());
sendPerformanceAlert(tenantId, "RESPONSE_TIME_HIGH", stats.getAvgDuration());
}

if (stats.getAvailability() < 0.95) {
log.warn("租户可用性过低: tenantId={}, availability={}", tenantId, stats.getAvailability());
sendPerformanceAlert(tenantId, "AVAILABILITY_LOW", stats.getAvailability());
}
}

} catch (Exception e) {
log.error("检查租户性能异常失败", e);
}
}

/**
* 发送性能告警
*/
private void sendPerformanceAlert(String tenantId, String alertType, double value) {
try {
PerformanceAlert alert = new PerformanceAlert();
alert.setTenantId(tenantId);
alert.setAlertType(alertType);
alert.setValue(value);
alert.setTimestamp(LocalDateTime.now());

// 发送告警通知
// 这里可以集成邮件、短信、钉钉等通知方式

log.info("发送性能告警: {}", alert);

} catch (Exception e) {
log.error("发送性能告警失败: tenantId={}, alertType={}", tenantId, alertType, e);
}
}

/**
* 获取活跃租户
*/
private List<String> getActiveTenants() {
// 实现获取活跃租户的逻辑
return new ArrayList<>();
}
}

七、最佳实践总结

7.1 实施建议

  1. 渐进式实施:从核心业务表开始,逐步扩展到所有表
  2. 租户识别:选择适合的租户识别策略,确保准确性
  3. 性能优化:合理使用缓存,优化SQL查询
  4. 安全防护:建立完善的权限控制和安全检查

7.2 性能优化

  1. 索引优化:为租户ID字段建立合适的索引
  2. 缓存策略:使用Redis缓存租户信息和配置
  3. SQL优化:避免全表扫描,使用分页查询
  4. 连接池:合理配置数据库连接池

7.3 安全建议

  1. 数据隔离:确保租户间数据完全隔离
  2. 权限控制:建立细粒度的权限控制机制
  3. 审计日志:记录所有数据访问操作
  4. 异常监控:实时监控异常访问行为

八、总结

MyBatisPlus多租户数据隔离是SaaS应用的核心技术,通过合理的架构设计和实现方案,可以构建一个安全、高效、可扩展的多租户系统。

关键要点:

  1. 架构设计:选择合适的数据隔离策略
  2. 租户识别:建立可靠的租户识别机制
  3. 数据隔离:确保租户间数据完全隔离
  4. 性能优化:优化查询性能和缓存策略
  5. 安全防护:建立完善的权限控制体系

通过本文的实践指导,读者可以快速搭建企业级的多租户系统,为SaaS应用提供强有力的技术支撑。