TAOCARTS 知识

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

2026-06-26 系统功能介绍

一、问题背景

SaaS模式的跨境电商独立站需要为成百上千个店铺提供服务,每个店铺的数据必须严格隔离,这是多租户架构要解决的核心问题。

在Taocarts跨境电商独立站系统的早期版本中,所有店铺数据混在同一套表里,通过shop_id字段区分。随着店铺数量增长到数千家,问题开始暴露:某次慢查询因为没有带shop_id条件,把全量数据扫了一遍,数据库CPU飙升到90%,影响了所有店铺的正常访问。那次事故让我深刻意识到,多租户的隔离不能只靠“约定”,必须靠“架构”。

二、三种数据隔离方案对比

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

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

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

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

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

三、租户上下文的传递与穿透

共享表方案的核心问题是:每次数据库查询都必须带上租户ID作为过滤条件,否则就会发生数据泄露。Taocarts使用ThreadLocal在请求链路中传递租户上下文:

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都手动拼接租户ID条件,Taocarts使用MyBatis拦截器自动注入:

java

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

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

public class TenantSqlInterceptor 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();

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

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

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

field.setAccessible(true);

field.set(boundSql, newSql);

}

return invocation.proceed();

}

}

五、动态数据源切换

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

java

public class DynamicDataSource extends AbstractRoutingDataSource {

@Override

protected Object determineCurrentLookupKey() {

String tenantId = TenantContext.getTenantId();

return TenantDataSourceRegistry.getDataSourceKey(tenantId);

}

}

六、踩坑与经验

在实践中,有几个容易被忽略的问题:

第一,定时任务的租户隔离。定时任务没有请求上下文,需要在执行时主动遍历所有租户,为每个租户单独初始化上下文。

第二,异步线程的租户传递。使用@Async时,子线程默认无法继承父线程的ThreadLocal。需要自定义TaskDecorator,在任务执行前复制租户ID。

第三,批量操作的租户校验。批量更新时必须确保所有数据都属于同一个租户,否则可能跨租户修改数据。

七、总结

多租户架构是SaaS系统的基石。Taocarts通过混合数据隔离策略和租户上下文的自动穿透,让业务代码完全不需要关心租户隔离细节。这套方案已在生产环境稳定支撑数千个店铺同时运行,租户间数据零泄露。