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

相关推荐
侠客行03172 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪2 小时前
深入浅出LangChain4J
java·langchain·llm
灰子学技术4 小时前
go response.Body.close()导致连接异常处理
开发语言·后端·golang
老毛肚4 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎5 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
二十雨辰5 小时前
[python]-AI大模型
开发语言·人工智能·python
Yvonne爱编码5 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚5 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂5 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
pas1365 小时前
41-parse的实现原理&有限状态机
开发语言·前端·javascript