TAOCARTS 知识

跨境电商独立站多租户架构设计:数据隔离与租户上下文穿透-阿里云开发者社区

2026-06-26 系统功能介绍

一、问题背景

SaaS 模式的跨境电商独立站需要为多个店铺提供服务,每个店铺的数据必须严格隔离,同时又要共用一套代码和基础设施。如何设计一套安全、高效、可扩展的多租户架构?本文以 Taocarts 跨境电商独立站系统为例,详细讲解数据隔离方案选型、租户上下文传递以及动态数据源切换的实现。

二、数据隔离方案对比

多租户数据隔离通常有三种经典方案:

独立数据库:每个租户拥有独立的数据库实例,隔离级别最高,但成本也最高,适合大租户。

独立 Schema:同一数据库实例,每个租户拥有独立的 Schema,隔离级别较高,成本适中。

共享表(租户ID区分) :所有租户共用同一套表,通过 tenant_id 字段区分,成本最低,但隔离级别相对较弱。

Taocarts 采用混合策略:免费版租户使用共享表方案降低入门门槛;付费版租户分配独立 Schema 提供更好的性能和隔离;企业级租户使用独立数据库满足高安全要求。

三、租户上下文传递(ThreadLocal)

在共享表方案中,每次数据库查询都需要带上租户 ID 条件。首先在请求入口处解析租户标识,并在整个请求链路中传递。

java

// 租户上下文持有者

public class TenantContext {

private static final ThreadLocal currentTenant = new ThreadLocal<>();

public static void setTenantId(String tenantId) {

currentTenant.set(tenantId);

}

public static String getTenantId() {

return currentTenant.get();

}

public static void clear() {

currentTenant.remove();

}

}

通过拦截器从请求头或子域名中解析租户 ID:

java

@Component

public class TenantInterceptor implements HandlerInterceptor {

@Override

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

// 从子域名解析租户ID,例如: shop123.taocarts.com

String host = request.getServerName();

String tenantId = extractTenantFromHost(host);

TenantContext.setTenantId(tenantId);

return true;

}

@Override

public void afterCompletion(HttpServletRequest request, HttpServletResponse response,

Object handler, Exception ex) {

TenantContext.clear(); // 请求结束后必须清理

}

}

四、MyBatis 拦截器自动注入租户 ID

为了避免每个 SQL 都手动拼接 tenant_id 条件,使用 MyBatis 拦截器自动注入。

java

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare",

args = {Connection.class, Integer.class})})

public class TenantInterceptor implements Interceptor {

@Override

public Object intercept(Invocation invocation) throws Throwable {

String tenantId = TenantContext.getTenantId();

if (StringUtils.isBlank(tenantId)) {

return invocation.proceed();

}

StatementHandler handler = (StatementHandler) invocation.getTarget();

BoundSql boundSql = handler.getBoundSql();

String sql = boundSql.getSql();

// 如果 SQL 中不包含 tenant_id 条件,则自动添加

if (sql.toLowerCase().contains("where") && !sql.contains("tenant_id")) {

String newSql = sql.replaceFirst("(?i)where", "where tenant_id = '" + tenantId + "' and ");

// 通过反射修改 BoundSql

Field field = boundSql.getClass().getDeclaredField("sql");

field.setAccessible(true);

field.set(boundSql, newSql);

}

return invocation.proceed();

}

}

五、动态数据源切换

对于使用独立 Schema 的租户,需要动态切换数据库连接。使用 Spring 的 AbstractRoutingDataSource 实现:

java

public class DynamicDataSource extends AbstractRoutingDataSource {

@Override

protected Object determineCurrentLookupKey() {

String tenantId = TenantContext.getTenantId();

return TenantDataSourceRegistry.getDataSourceKey(tenantId);

}

}

六、总结

Taocarts 通过混合数据隔离策略,在成本、性能和隔离性之间取得了平衡。租户上下文的自动穿透让业务代码无需关心租户隔离细节,大幅提升了开发效率。这套方案已在生产环境稳定支撑数千个店铺同时运行。