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——我们不仅实现了技术升级,更在构建数字世界的信任基石。

文档更新时间: 2025-07-13 07:30   作者:admin