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

相关推荐
过期动态21 小时前
【LeetCode 热题 100】接雨水
java·数据结构·算法·leetcode·职场和发展
bug和崩溃我都要21 小时前
Qt 封装 libmpv 全功能视频播放器开发指南
开发语言·qt·音视频
郝学胜-神的一滴21 小时前
Qt 高级开发 018:复刻经典登录界面布局与窗口美化全解析
开发语言·c++·qt·程序人生·用户界面
郝亚军21 小时前
IEEE 754 单精度浮点的SEM表示
开发语言·c++·算法
zhangjw3421 小时前
第15篇:Java多线程零基础入门,进程线程、线程创建方式、线程生命周期、线程安全彻底吃透
java·开发语言·面试
蝈理塘(/_\)大怨种21 小时前
类和对象 (上)
java·开发语言
小新1101 天前
qt creator 将qInfo的输出日志写入日志文档,方便查看
开发语言·qt
我材不敲代码1 天前
Python 函数核心:位置参数与关键字参数详解
java·前端·python
hssfscv1 天前
QT的学习记录1
开发语言·qt·学习
SunnyDays10111 天前
Python操作Excel批注:从基础添加到高级自定义的完整指南
开发语言·python·excel