基于 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 内核模板使用。

相关推荐
PHP实战开发录6 小时前
PHP接口大整数为什么变了
开发语言·php
临床数据科学和人工智能兴趣组9 小时前
在R语言中, 使用 as.factor() 函数转换数值型变量
开发语言·r语言
andyweike12 小时前
php笔记
开发语言·笔记·php
EatFan12 小时前
Spring Boot 4 落地观察:从 yudao-cloud、matecloud、JPower 看国产脚手架的升级路线与迁移清单
java·spring boot·后端·spring cloud·微服务·后端开发·jdk 21
Maiko Star13 小时前
* LangChain 提示词模板详解:ChatPromptTemplate 的使用与高级特性
java·人工智能·langchain
Circ.13 小时前
Python 调试 Litellm 本地大模型接口:模型列表与对话接口实操踩坑记录
开发语言·python
现任明教教主~15 小时前
Thinkphp站群蜘蛛池SaaS系统YanyvSEO含多用户/积分/六大引擎计费/易支付对接
java·开发语言·spring
Zhou14113615 小时前
SpringSecurity_02_授权与高级功能
开发语言·python
一条破秋裤16 小时前
Linux 线程创建:pthread_create 与基本回收
java·linux·运维
布吉岛的石头16 小时前
Java 程序员第 49 阶段5:BERT 预训练目标 MLM+NSP 的工程含义
java·人工智能·深度学习·bert·transformer