下载工作台
Spring Cloud 微服务

多租户与数据隔离

试读上半部分 · 解锁后可读全文

第 14 章 · 多租户与数据隔离

本章目标:理解 SaaS 多租户(Multi-Tenancy) 业务场景;在 svc-spring-demo 实现 共享库共享表 + tenant_id 行级隔离(教学主路径);了解 独立 Schema独立数据库 两种强隔离方案;在 Gateway / Feign / JPA 全链路传递 X-Tenant-Id;掌握 租户上下文越权防护缓存 key 隔离;讨论微服务拆分下租户一致性。

学时建议:5~6 小时(含 2 小时 tenant 过滤器与 JPA 联调)

前置spring-cloud-web ch01~ch12spring-boot-web ch05 JPA;Gateway 过滤器(ch03)。


14.1 什么是多租户

租户(Tenant):使用同一套 svc-spring-demo 部署的不同客户组织,如「贤紫旗舰店」「虚构商户 B」。

                    svc-spring-demo(单部署)
                              │
        ┌─────────────────────┼─────────────────────┐
        ▼                     ▼                     ▼
   Tenant A              Tenant B              Tenant C
   tenant_id=1           tenant_id=2           tenant_id=3
   商品/订单隔离          商品/订单隔离          商品/订单隔离
需求说明
数据隔离A 不能看到 B 的订单
配置隔离各租户 Logo、费率可不同(选修)
计费按租户统计 API 调用量
合规部分行业要求物理隔离

14.2 三种隔离模型

模型结构隔离性成本适用
共享表每表加 tenant_id逻辑隔离MVP、租户量大
独立 Schema同实例不同 schema中型 SaaS
独立库每租户一 DB金融、政企

本章主练 共享表;ch12 毕业项目可扩展 tenant_id 列。

共享表(本章):
product_db.products (id, tenant_id, name, ...)

独立 Schema:
product_db.tenant_1.products
product_db.tenant_2.products

独立库:
product_db_tenant_1 / product_db_tenant_2

14.3 租户识别与 Gateway 入口

租户 ID 来源(教学优先级):

  1. JWT Claim tenantId(与 ch10 结合,推荐)
  2. 子域名 tenant-a.api.example.com
  3. Header X-Tenant-Id(仅内网服务间,外部须经 Gateway 校验)

Gateway 过滤器:

@Component
public class TenantResolveFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String tenantId = resolveFromJwt(exchange); // 或 subdomain
        if (tenantId == null) {
            exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
            return exchange.getResponse().setComplete();
        }
        // mutate 生成的是新对象,必须用 mutate 后的 exchange 继续过滤器链,否则 Header 不生效
        ServerWebExchange mutated = exchange.mutate()
            .request(r -> r.header("X-Tenant-Id", tenantId))
            .build();
        return chain.filter(mutated);
    }
    @Override public int getOrder() { return -90; }
}

安全:禁止客户端随意伪造 X-Tenant-Id;必须以 auth-svc 签发的 tenantId 为准。


14.4 TenantContext 线程传递

public final class TenantContext {
    private static final ThreadLocal<String> TENANT = new ThreadLocal<>();

    public static void set(String tenantId) { TENANT.set(tenantId); }
    public static String get() { return TENANT.get(); }
    public static void clear() { TENANT.remove(); }
}

Servlet 过滤器:

@Component
public class TenantFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        String tenantId = req.getHeader("X-Tenant-Id");
        if (tenantId == null || tenantId.isBlank()) {
            res.sendError(HttpStatus.BAD_REQUEST.value(), "Missing tenant");
            return;
        }
        try {
            TenantContext.set(tenantId);
            chain.doFilter(req, res);
        } finally {
            TenantContext.clear();
        }
    }
}

Feign 拦截器继续透传 X-Tenant-Id;BFF 异步线程池需 TTL 包装(TransmittableThreadLocal 或 Micrometer Context)。


14.5 JPA 行级隔离

14.5.1 实体基类

@MappedSuperclass
@EntityListeners(TenantEntityListener.class)
public abstract class TenantAwareEntity {
    @Column(name = "tenant_id", nullable = false, updatable = false)
    private String tenantId;

    @PrePersist
    void fillTenant() {
        if (tenantId == null) {
            tenantId = TenantContext.get();
        }
    }
    // getter/setter
}
@Entity
@Table(name = "products", indexes = @Index(columnList = "tenant_id"))
public class Product extends TenantAwareEntity {
    @Id @GeneratedValue private Long id;
    private String name;
    // ...
}

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。