https://www.toutiao.com/article/7523443175755645503
SpringBoot+MyBatisPlus动态数据源实战,亿级数据下的隔离战争
2025-07-12 08:00·Java架构成长之路
引子:一次数据泄露引发的千万级赔偿
2023年某金融SaaS平台重大事故报告
客户A登录系统后,在“我的账户”页面看到客户B的敏感交易记录根因链分析:
共享数据库表未隔离租户数据 → SQL缺少tenant_id过滤
动态数据源切换逻辑漏洞 → ThreadLocal污染
审计日志缺失 → 无法追踪操作轨迹
后果:赔偿客户B 2300万 + 金融牌照吊销风险。
本章技术纵深:
使用 Arthas监控SQL执行 还原数据泄露现场。
线程上下文快照 分析ThreadLocal污染过程。
数据血缘图谱 展示跨租户数据流动路径。
第一卷 多租户架构三大模式:生死抉择
1.1 模式对比:从共享到隔离
1.2 金融级选型决策树
是否需最高隔离? → 是 → 独立数据库
→ 否 → 是否有审计要求? → 是 → 独立表空间
→ 否 → 共享表
1.3 血泪教训表
方案
致命缺陷
真实案例
共享表
误操作泄露数据
某医疗SaaS泄露患者病历
独立表
分表上限瓶颈
电商平台5万分表后崩溃
独立数据库
连接池耗尽
银行系统宕机2小时
第二卷 动态数据源核武器:SpringBoot整合实战
2.1 核心架构设计
2.2 动态路由源码解析
public class TenantDataSourceRouter extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 从当前线程上下文获取租户ID
return TenantContextHolder.getTenantId();
}
// 连接池初始化
@Override
public void afterPropertiesSet() {
Map<Object, DataSource> targetDataSources = new HashMap<>();
// 预加载所有租户数据源
tenantService.listAll().forEach(tenant -> {
DataSource ds = buildDataSource(tenant);
targetDataSources.put(tenant.getId(), ds);
});
setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
}
2.3 连接池调优参数矩阵
参数
默认值
生产推荐值
原理说明
maxActive
8
50
最大活跃连接数
minIdle
0
10
最小空闲连接防突发
maxWait
-1(无限等)
3000ms
避免线程阻塞
timeBetweenEviction
60s
30s
空闲连接检测频率
validationQuery
null
SELECT 1
连接有效性校验
第三卷 MyBatisPlus多租户插件:深度魔改
3.1 基础拦截器方案
@Interceptor
public class TenantInterceptor implements InnerInterceptor {
@Override
public void beforeQuery(Executor executor, MappedStatement ms,
Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) {
// 自动追加租户过滤条件
String sql = boundSql.getSql();
String newSql = sql + " AND tenant_id = " + currentTenantId();
// 反射修改BoundSQL
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, newSql);
}
}
3.2 致命缺陷:越权漏洞
攻击场景:
-- 原始查询
SELECT * FROM orders WHERE id=100
-- 恶意注入
SELECT * FROM orders WHERE id=100 OR 1=1
-- 插件处理后
SELECT * FROM orders WHERE id=100 OR 1=1 AND tenant_id=123
-- 逻辑等价于:(id=100) OR (1=1 AND tenant_id=123)
解决方案:SQL重写引擎
public String rewriteWithTenant(String originSql) {
// 使用JSqlParser解析AST
Statement stmt = CCJSqlParserUtil.parse(originSql);
if (stmt instanceof Select) {
Select select = (Select) stmt;
PlainSelect ps = (PlainSelect) select.getSelectBody();
// 在WHERE条件中注入租户过滤
Expression where = ps.getWhere();
Expression tenantFilter = new EqualsTo(
new Column("tenant_id"),
new LongValue(currentTenantId())
);
if (where == null) {
ps.setWhere(tenantFilter);
} else {
ps.setWhere(new AndExpression(where, tenantFilter));
}
}
return stmt.toString();
}
3.3 多租户+分页+权限三合一插件
public class TripleComboInterceptor extends PaginationInterceptor {
@Override
public void beforeQuery(...) {
super.beforeQuery(...); // 处理分页
addTenantCondition(boundSql); // 租户隔离
addDataPermission(boundSql); // 数据权限
}
private void addDataPermission(BoundSql boundSql) {
// 根据用户角色动态追加条件
if (!user.hasRole("ADMIN")) {
String condition = " AND dept_id IN (" + user.getDeptScope() + ")";
appendCondition(boundSql, condition);
}
}
}
第四卷 数据隔离防线:安全加固实战
4.1 租户ID注入器(解决@Async上下文丢失)
public class TenantIdPropagator {
// 线程池装饰器
public static ExecutorService wrap(ExecutorService executor) {
return new DelegatingExecutorService(executor) {
@Override
public <T> Future<T> submit(Callable<T> task) {
// 捕获当前租户ID
String tenantId = TenantContextHolder.getTenantId();
return super.submit(() -> {
try {
TenantContextHolder.setTenantId(tenantId);
return task.call();
} finally {
TenantContextHolder.clear();
}
});
}
};
}
}
// 使用示例
@Bean
public Executor asyncExecutor() {
return TenantIdPropagator.wrap(
Executors.newVirtualThreadPerTaskExecutor()
);
}
4.2 动态表名策略
public class TenantTableNameParser implements ITableNameHandler {
@Override
public String dynamicTableName(String sql, String tableName) {
// 订单表按租户分表: orders_tenant123
if (tableName.equals("orders")) {
return tableName + "_" + TenantContextHolder.getTenantId();
}
return tableName;
}
}
// MyBatisPlus配置
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
interceptor.addInnerInterceptor(new DynamicTableNameInnerInterceptor(
new TenantTableNameParser()
));
return interceptor;
}
4.3 行级锁越权防护
-- 危险操作:可能修改其他租户数据
UPDATE account SET balance=0 WHERE id=100;
-- 加固方案:强制绑定租户ID
UPDATE account
SET balance=0
WHERE id=100
AND tenant_id=123 -- 自动追加
AND version=old_version; -- 乐观锁
第五卷 千万级生产实战:某银行系统优化实录
5.1 原始架构痛点
问题
发生频率
影响范围
数据泄露
2次/月
高额赔偿
连接池耗尽
3次/周
服务瘫痪
分表数量上限
1次/季度
无法开户
5.2 优化实施路线
5.3 性能核爆成果
指标
优化前
优化后
提升幅度
数据泄露事件
24次/年
0次
100%
最大租户数
5,000
无上限
∞
连接池创建耗时
120ms/个
8ms/个
93%↓
分表查询延迟
340ms
52ms
85%↓
系统扩容时间
4小时
12分钟
95%↓
第六卷 未来战场:云原生多租户架构
6.1 Kubernetes多租户方案
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: tenant-bank-a
spec:
owners:
- name: alice
kind: User
storageClasses:
allowed: ["glusterfs"]
allowedRegex: ".*fast.*"
ingressClasses:
allowed: ["nginx"]
allowedRegex: ".*"
6.2 基于Vitess的分库分表
-- 按租户ID分片
CREATE TABLE account (
id BIGINT,
tenant_id VARCHAR(120),
balance DECIMAL(18,2)
) ENGINE=InnoDB
COMMENT '/*! SHARD_KEY(tenant_id) */';
6.3 数据隔离AI守卫
class DataLeakDetector:
def detect(self, sql_execution_log):
# 使用NLP识别异常查询
model = load_model("tenant_leak_detect.model")
features = extract_features(sql_execution_log)
if model.predict(features) == "LEAK":
# 实时阻断并告警
block_request()
alert_security_team()
终章:多租户架构的哲学之思
当租户数量从100增长到10万,当查询延迟从300ms降至30ms——我们不仅实现了技术升级,更在构建数字世界的信任基石。