多租戶架構中,相同表結構需路由至不同 Schema(如租戶 A 使用 schema_a,租戶 B 使用 schema_b)。本文對比三種 Hibernate 動態切換 Schema 方案,分析適用場景與優劣勢,助力架構選型。
單一應用實例,根據租戶標識(公司代碼、區域等)動態路由數據庫操作至對應 Schema。表結構完全一致,邏輯代碼復用,僅 Schema 名稱不同。
移除映射文件中硬編碼的 schema 屬性,改用 Hibernate 全局配置 hibernate.default_schema,運行時動態修改並重建 SessionFactory。
<!-- 移除 schema 屬性 -->
<class name="com.example.Poi" table="POI">
<id name="poiId" type="java.lang.Long">
<column name="POI_ID" precision="10" scale="0" />
<generator class="increment" />
</id>
<property name="cnName" type="java.lang.String">
<column name="CN_NAME" length="1000" />
</property>
</class>
public static SessionFactory rebuildSessionFactoryForSchema(String newSchema) {
try {
Properties props = configuration.getProperties();
props.put("hibernate.default_schema", newSchema);
// 必須重建 SessionFactory 生效
return configuration.buildSessionFactory();
} catch (Exception e) {
log.error("Schema 切換失敗: {}", newSchema, e);
return null;
}
}
SessionFactory 為全局單例,並發請求切換 Schema 會相互覆蓋SessionFactory 導致元數據重解析、連接池重建為每個 Schema 定義獨立映射,通過 entity-name 或 id 區分,運行時根據租戶選擇對應映射。
<hibernate-mapping>
<class name="com.example.Poi" table="POI" schema="P_BEIJING" entity-name="Poi_Beijing">...</class>
<class name="com.example.Poi" table="POI" schema="P_TIANJIN" entity-name="Poi_Tianjin">...</class>
</hibernate-mapping>
// 通過 entity-name 獲取特定 Schema 的 Session 操作
Session session = sessionFactory.openSession();
session.save("Poi_" + tenantCode, poiObject); // tenantCode 如 "Beijing"
session.flush();
| 優點 | 缺點 |
|---|---|
| 實現簡單,無需修改底層架構 | 映射文件冗餘,維護成本隨租戶數線性增長 |
| 線程安全,每請求獨立選擇映射 | 新增租戶需修改映射文件並重啟應用 |
| 實體類耦合多個 entity-name,代碼擴散 |
實現 AbstractRoutingDataSource 子類,內部維護 Map<String, DataSource>,根據 ThreadLocal 租戶上下文動態路由獲取連接。
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> tenantContext = new ThreadLocal<>();
@Override
protected Object determineCurrentLookupKey() {
return tenantContext.get(); // 返回當前租戶標識
}
public static void setTenant(String tenantCode) {
tenantContext.set(tenantCode);
}
public static void clear() {
tenantContext.remove();
}
}
<bean id="dataSource" class="com.example.TenantRoutingDataSource">
<property name="targetDataSources">
<map>
<entry key="BEIJING" value-ref="beijingDataSource" />
<entry key="TIANJIN" value-ref="tianjinDataSource" />
</map>
</property>
<property name="defaultTargetDataSource" ref="defaultDataSource" />
</bean>
<!-- 各租戶獨立 DataSource 配置 -->
<bean id="beijingDataSource" class="com.zaxxer.hikari.HikariDataSource">
<property name="jdbcUrl" value="jdbc:oracle:thin:@host:1521:BEIJING" />
<property name="username" value="schema_beijing" />
<property name="password" value="***" />
</bean>
<!-- 其他租戶類似 -->
public class TenantInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String tenant = resolveTenant(request); // 從 Header、Token、域名等解析
TenantRoutingDataSource.setTenant(tenant);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
TenantRoutingDataSource.clear(); // 必須清理,防止線程池污染
}
}
ThreadLocal 隔離請求上下文| 維度 | 方案一:全局重建 | 方案二:多映射 | 方案三:路由 DataSource |
|---|---|---|---|
| 線程安全 | ❌ 極差 | ✅ 良好 | ✅ 優秀 |
| 性能影響 | ❌ 重建開銷大 | ✅ 無額外開銷 | ✅ 僅路由查找 |
| 維護成本 | 低(單配置) | 高(映射冗餘) | 中(DataSource 管理) |
| 動態擴展 | 需重啟 | 需重啟 | 支援熱插拔 |
| 推薦場景 | 單機測試/批處理 | 租戶固定、數量少 | 生產級多租戶 SaaS |
tenant.app.com)、JWT Token Claim、HTTP Header、用戶關聯表@IgnoreTenant 註解或專用 EntityManager方案三雖實現複雜度較高,但為生產級多租戶系統提供了穩健、可擴展的基礎架構,是長遠技術債務最低的選擇。