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 方案)
对于使用独立 Schema 的租户,需要动态切换数据库连接。我们使用 Spring 的 AbstractRoutingDataSource 实现。
java
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String tenantId = TenantContext.getTenantId();
// 根据租户ID返回对应的数据源Key
return TenantDataSourceRegistry.getDataSourceKey(tenantId);
}
}
// 配置数据源
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
Map targetDataSources = new HashMap<>();
targetDataSources.put("tenant_a", createDataSource("db_tenant_a"));
targetDataSources.put("tenant_b", createDataSource("db_tenant_b"));
// 默认数据源(共享表)
DynamicDataSource ds = new DynamicDataSource();
ds.setDefaultTargetDataSource(defaultDataSource());
ds.setTargetDataSources(targetDataSources);
return ds;
}
}
五、总结
多租户架构是 SaaS 系统的基石。Taocarts 通过混合数据隔离策略,既控制了成本,又满足了不同规模租户的需求。租户上下文的自动穿透和动态数据源切换,让业务代码无需关心租户隔离细节,大幅提升了开发效率。生产环境中,这套方案已稳定支撑数千个店铺同时运行。
第3篇:跨境电商独立站多语言多货币架构:Laravel + 翻译表 + Redis 实时汇率
全球化的跨境电商独立站必须支持多语言和多货币,否则海外用户会因为看不懂价格、付不了款而流失。但多语言意味着商品标题、描述需要存储多个版本;多货币意味着价格需要实时换算且汇率波动会影响已下单金额。本文以 Taocarts 系统的实现为例,讲解多语言多货币的数据库设计、缓存策略以及前端展示的完整代码。
一、多语言数据库设计:主表 + 翻译表
将语言相关的字段(标题、描述)抽离到独立的翻译表,主表只存储不依赖语言的字段(价格、重量、SKU 等)。
sql
-- 商品主表
CREATE TABLE products (
id bigint PRIMARY KEY AUTO_INCREMENT,
sku varchar(64) NOT NULL,
price decimal(10,2) NOT NULL COMMENT '基准货币价格(CNY)',
weight decimal(8,2) NOT NULL,
created_at datetime NOT NULL
);
-- 翻译表
CREATE TABLE product_translations (
id bigint PRIMARY KEY AUTO_INCREMENT,
product_id bigint NOT NULL,
locale char(5) NOT NULL COMMENT '语言代码: zh_CN, en_US, ja_JP',
field varchar(32) NOT NULL COMMENT '字段名: title, description',
value text NOT NULL,
UNIQUE KEY uk_product_locale_field (product_id, locale, field)
);
二、多语言查询的 Eloquent 实现(Laravel)
php
// Product 模型
class Product extends Model
{
public function translations()
{
return $this->hasMany(ProductTranslation::class);
}
public function getTitleAttribute($value)
{
$locale = app()->getLocale();
$translation = $this->translations
->where('locale', $locale)
->where('field', 'title')
->first();
return $translation ? $translation->value : $value;
}
}
// 全局 Scope 预加载翻译,避免 N+1 查询
class LocaleScope implements ScopeInterface
{
public function apply(Builder $builder, Model $model)
{
$locale = app()->getLocale();
$builder->with(['translations' => function ($query) use ($locale) {
$query->where('locale', $locale);
}]);
}
}
三、实时汇率缓存与自动更新
使用 Redis 缓存汇率,每天从第三方 API 拉取 3 次。
php
class ExchangeRateService
{
const CACHE_KEY = 'exchange_rates';
const CACHE_TTL = 28800; // 8小时
public function getRate($currency)
{
return Cache::remember(self::CACHE_KEY, self::CACHE_TTL, function () {
$response = Http::get('https://api.exchangerate-api.com/v4/latest/CNY');
return $response->json('rates');
})[$currency] ?? 1;
}
}
四、下单时的汇率锁定
用户下单时的汇率必须锁定,后续汇率波动不应影响已下订单的金额。
php
class OrderController
{
public function store(Request $request)
{
$order = new Order();
$order->user_id = auth()->id();
$order->total_cny = $this->calculateTotalCNY($request->items);
$order->currency = $request->currency;
$order->exchange_rate = app(ExchangeRateService::class)->getRate($request->currency);
$order->total_foreign = $order->total_cny * $order->exchange_rate;
$order->save();
}
}
五、前端价格动态换算(Vue.js)
vue
{ { convertedPrice }} { { currency }}
EUR
JPY
六、多语言 SEO 优化(hreflang 标签)
在页面头部自动生成 hreflang 标签,告诉搜索引擎不同语言版本的对应关系。
php
@foreach($supportedLocales as $locale)
@endforeach
七、总结
多语言多货币是跨境独立站的标配能力。Taocarts 系统通过“主表+翻译表”的数据库设计、Redis 缓存的实时汇率、下单时的汇率锁定机制,以及前端的动态换算,为用户提供了无缝的本地化购物体验。这套方案已在生产环境服务来自 14 个语言区、10 余种货币的用户,转化率提升超过 30%。