基于 Java 21 ScopedValue 的多租户动态数据源完整实践

基于 Java 21 ScopedValue 的多租户动态数据源完整实践

关键词:Java 21、ScopedValue、多租户、SaaS、动态数据源、Spring Boot 3

本文给出一套可直接用于生产的 SaaS 多租户核心基础设施实现,覆盖:

  • 域名 → 租户解析
  • ScopedValue 承载租户上下文
  • 动态数据源注册
  • RoutingDataSource 路由
  • Filter 级生命周期管理

目标:彻底摆脱 ThreadLocal,构建 Java 21 时代的"正确解"


一、总体架构设计

复制代码
HTTP Request
   ↓
DomainScopeFilter
   ↓
TenantResolver (domain → TenantContext)
   ↓
DataSourceRegistry.ensure()
   ↓
ScopedValue<TenantContext>
   ↓
DynamicRoutingDataSource
   ↓
Spring Transaction / JPA / MyBatis

核心原则:

数据源不是全局状态,而是请求作用域状态


二、核心模型定义

1. TenantContext

java 复制代码
public record TenantContext(
        String tenantId,
        String domain,
        String datasourceKey
) {}

2. TenantScope(ScopedValue 定义)

java 复制代码
public final class TenantScope {

    public static final ScopedValue<TenantContext> TENANT =
            ScopedValue.newInstance();

    public static TenantContext current() {
        return TENANT.get();
    }

    private TenantScope() {}
}

三、TenantResolver(域名 → 租户)

1. 接口定义

java 复制代码
public interface TenantResolver {
    TenantContext resolve(String domain);
}

2. 默认实现(带缓存)

java 复制代码
@Component
public class DefaultTenantResolver implements TenantResolver {

    @Resource
    private DatabaseExtApi databaseExtApi;

    private final ConcurrentMap<String, TenantContext> cache =
            new ConcurrentHashMap<>();

    @Override
    public TenantContext resolve(String domain) {
        String key = normalize(domain);
        return cache.computeIfAbsent(key, this::load);
    }

    private TenantContext load(String domain) {
        ResponseObject<DatabaseSimple> res =
                databaseExtApi.selectByDomain(domain);

        if (res == null || res.getCode() != 200 || res.getData() == null) {
            throw new SysException("无效应用: " + domain);
        }

        DatabaseSimple db = res.getData();

        return new TenantContext(
                db.getTenantId(),
                domain,
                "ds_" + db.getTenantId()
        );
    }

    private String normalize(String domain) {
        return domain == null ? "" : domain.toLowerCase(Locale.ROOT);
    }
}

四、DynamicRoutingDataSource(ScopedValue 版)

java 复制代码
public class DynamicRoutingDataSource
        extends AbstractRoutingDataSource {

    private final Map<Object, Object> targets = new ConcurrentHashMap<>();

    @Override
    protected Object determineCurrentLookupKey() {
        if (!TenantScope.TENANT.isBound()) {
            throw new IllegalStateException("TenantContext not bound");
        }
        return TenantScope.current().datasourceKey();
    }

    public void addTargetDataSource(String key, DataSource ds) {
        targets.put(key, ds);
        super.setTargetDataSources(targets);
        super.afterPropertiesSet();
    }
}

五、DataSourceRegistry(动态注册)

java 复制代码
@Component
public class DataSourceRegistry {

    private final ConcurrentMap<String, DataSource> registry =
            new ConcurrentHashMap<>();

    private final DynamicRoutingDataSource routingDataSource;

    public DataSourceRegistry(DynamicRoutingDataSource routingDataSource) {
        this.routingDataSource = routingDataSource;
    }

    public void ensure(TenantContext ctx) {
        registry.computeIfAbsent(ctx.datasourceKey(), k -> create(ctx));
    }

    private DataSource create(TenantContext ctx) {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:mysql://localhost:3306/" + ctx.tenantId());
        ds.setUsername("root");
        ds.setPassword("123456");
        routingDataSource.addTargetDataSource(ctx.datasourceKey(), ds);
        return ds;
    }
}

六、DomainScopeFilter(生命周期绑定)

java 复制代码
public class DomainScopeFilter implements Filter {

    @Resource
    private TenantResolver tenantResolver;

    @Resource
    private DataSourceRegistry registry;

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        String domain = request.getServerName();

        try {
            TenantContext ctx = tenantResolver.resolve(domain);
            registry.ensure(ctx);

            ScopedValue.where(TenantScope.TENANT, ctx)
                    .run(() -> {
                        try {
                            chain.doFilter(req, res);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    });
        } catch (SysException e) {
            response.setStatus(403);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"msg\":\"" + e.getMessage() + "\"}");
        }
    }
}

七、Spring Boot 配置

java 复制代码
@Configuration
public class DataSourceConfig {

    @Bean
    @Primary
    public DataSource dataSource() {
        return new DynamicRoutingDataSource();
    }
}
java 复制代码
@Bean
public FilterRegistrationBean<DomainScopeFilter> domainFilter() {
    FilterRegistrationBean<DomainScopeFilter> bean = new FilterRegistrationBean<>();
    bean.setFilter(new DomainScopeFilter());
    bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return bean;
}

八、关键架构结论

  1. 不再使用 ThreadLocal
  2. 租户上下文是 ScopedValue,而不是全局变量
  3. RoutingDataSource 只做路由,不做管理
  4. 动态数据源创建必须幂等
  5. 事务开始前必须绑定 ScopedValue

九、总结

ScopedValue + DynamicRoutingDataSource 是 Java 21 时代多租户 SaaS 的"终局架构"

这套实现具备:

  • 高并发安全性
  • 清晰的职责边界
  • 对虚拟线程天然友好
  • 可长期演进的基础设施结构

建议直接作为 SaaS 内核模板使用。

相关推荐
2501_944521592 小时前
Flutter for OpenHarmony 微动漫App实战:图片加载实现
android·开发语言·前端·javascript·flutter·php
2301_780669862 小时前
线程安全、线程同步(三种加锁方式)、线程池(两种创建线程池方式、线程池处理Runnable任务、线程池处理Callable任务)、并发/并行
java
liuc03172 小时前
Java项目关于不同key的读取
java·开发语言
yaoxin5211232 小时前
296. Java Stream API - 二元操作符与“单位元“
java·服务器·windows
Zach_yuan2 小时前
面向对象封装线程:用 C++ 封装 pthread
开发语言·c++·算法
罗伯特_十三2 小时前
Spring AI ChatModel 使用记录
java·人工智能·spring
毕设源码-朱学姐2 小时前
【开题答辩全过程】以 基于SpringBoot的律师事务所管理系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
菜宾2 小时前
java-seata基础教学
java·开发语言·adb
梦6502 小时前
JavaScript 循环
开发语言·javascript·ecmascript