轉載:https://www.ibm.com/developerworks/cn/java/j-lo-dataMultitenant/index.html#N1010D
在上一篇“淺析多租戶在 Java 平台和某些 PaaS 上的實現”中我們談到了應用層面的多租戶架構,涉及到 PaaS、JVM、OS 等,與之相應的是數據層也有多租戶的支持。
數據層的多租戶綜述
多租戶(Multi Tenancy/Tenant)是一種軟件架構,其定義是:
在一台服務器上運行單個應用實例,它為多個租戶提供服務。
在SaaS實施過程中,有一個顯著的考量點,就是如何對應用數據進行設計,以支持多租戶,而這種設計的思路,是要在數據的共享、安全隔離和性能間取得平衡。
傳統的應用,僅僅服務於單個租戶,數據庫多部署在企業內部網絡環境,對於數據擁有者來說,這些數據是自己“私有”的,它符合自己所定義的全部安全標准。而在雲計算時代,隨着應用本身被放到雲端,導致數據層也經常被公開化,但租戶對數據安全性的要求,並不因之下降。同時,多租戶應用在租戶數量增多的情況下,會比單租戶應用面臨更多的性能壓力。本文即對這個主題進行探討:多租戶在數據層的框架如何在共享、安全與性能間進行取舍,同時了解一下市面上一些常見的數據廠商怎樣實現這部分內容。
常見的三種模式
在 MSDN 的這篇文章 Multi-Tenant Data Architecture 中,系統的總結了數據層的三種多租戶架構:
- 獨立數據庫
- 共享數據庫、獨立 Schema
- 共享數據庫、共享 Schema、共享數據表
獨立數據庫是一個租戶獨享一個數據庫實例,它提供了最強的分離度,租戶的數據彼此物理不可見,備份與恢復都很靈活;共享數據庫、獨立 Schema 將每個租戶關聯到同一個數據庫的不同 Schema,租戶間數據彼此邏輯不可見,上層應用程序的實現和獨立數據庫一樣簡單,但備份恢復稍顯復雜; 最后一種模式則是租戶數據在數據表級別實現共享,它提供了最低的成本,但引入了額外的編程復雜性(程序的數據訪問需要用 tenantId 來區分不同租戶),備份與恢復也更復雜。這三種模式的特點可以用一張圖來概括:
圖 1. 三種部署模式的異同

上圖所總結的是一般性的結論,而在常規場景下需要綜合考慮才能決定那種方式是合適的。例如,在占用成本上,認為獨立數據庫會高,共享模式較低。但如果考慮到大租戶潛在的數據擴展需求,有時也許會有相反的成本耗用結論。
而多租戶采用的選擇,主要是成本原因,對於多數場景而言,共享度越高,軟硬件資源的利用效率更好,成本也更低。但同時也要解決好租戶資源共享和隔離帶來的安全與性能、擴展性等問題。畢竟,也有客戶無法滿意於將數據與其他租戶放在共享資源中。
目前市面上各類數據廠商在多租戶的支持上,大抵都是遵循上文所述的這幾類模式,或者混合了幾種策略,這部分內容將在下面介紹。
JPA Provider
JSR 338 定義了 JPA 規范 2.1,但如我們已經了解到的,Oracle 把多租戶的多數特性推遲到了 Java EE 8 中。盡管這些曾經在 JavaOne 大會中有過演示,但無論是在 JPA 2.0(JSR 317)還是 2.1 規范中,都依然沒有明文提及多租戶。不過這並不妨礙一些 JPA provider 在這部分領域的實現,Hibernate 和 EclipseLink 已提供了全部或部分的多租戶數據層的解決方案。
Hibernate 是當今最為流行的開源的對象關系映射(ORM)實現,並能很好地和 Spring 等框架集成,目前 Hibernate 支持多租戶的獨立數據庫和獨立 Schema 模式。EclipseLink 也是企業級數據持久層JPA標准的參考實現,對最新 JPA2.1 完整支持,在目前 JPA 標准尚未引入多租戶概念之際,已對多租戶支持完好,其前身是誕生已久、功能豐富的對象關系映射工具 Oracle TopLink。因此本文采用 Hibernate 和 EclipseLink 對多租戶數據層進行分析。
Hibernate
Hibernate 是一個開放源代碼的對象/關系映射框架和查詢服務。它對 JDBC 進行了輕量級的對象封裝,負責從 Java 類映射到數據庫表,並從 Java 數據類型映射到 SQL 數據類型。在 4.0 版本 Hibenate 開始支持多租戶架構——對不同租戶使用獨立數據庫或獨立 Sechma,並計划在 5.0 中支持共享數據表模式。
在 Hibernate 4.0 中的多租戶模式有三種,通過 hibernate.multiTenancy 屬性有下面幾種配置:
- NONE:非多租戶,為默認值。
- SCHEMA:一個租戶一個 Schema。
- DATABASE:一個租戶一個 database。
- DISCRIMINATOR:租戶共享數據表。計划在 Hibernate5 中實現。
模式1:獨立數據庫
如果是獨立數據庫,每個租戶的數據保存在物理上獨立的數據庫實例。JDBC 連接將指向具體的每個數據庫,一個租戶對應一個數據庫實例。在 Hibernate 中,這種模式可以通過實現 MultiTenantConnectionProvider 接口或繼承 AbstractMultiTenantConnectionProvider 類等方式來實現。三種模式中它的共享性最低,因此本文重點討論以下兩種模式。
模式 2:共享數據庫,獨立 Schema
對於共享數據庫,獨立 Schema。所有的租戶共享一個數據庫實例,但是他們擁有獨立的 Schema 或 Catalog,本文將以多租戶酒店管理系統為案例說明 Hibernate 對多租戶的支持和用使用方法。
圖 2. guest 表結構

這是酒店客戶信息表,我們僅以此表對這種模式進行說明,使用相同的表結構在 MySQL 中創建 DATABASE hotel_1 和 hotel_2。基於 Schema 的多租戶模式,需要在 Hibernate 配置文件 Hibernate.cfg.xml 中設置 hibernate.multiTenancy 等相關屬性。
清單 1. 配置文件 Hibernate.cfg.xml
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<
session-factory
>
<
property
name
=
"connection.url"
>
jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8
</
property
>
<
property
name
=
"connection.username"
>root</
property
>
<
property
name
=
"connection.password"
></
property
>
<
property
name
=
"connection.driver_class"
>com.mysql.jdbc.Driver</
property
>
<
property
name
=
"dialect"
>org.hibernate.dialect.MySQLInnoDBDialect</
property
>
<
property
name
=
"hibernate.connection.autocommit"
>false</
property
>
<
property
name
=
"hibernate.cache.use_second_level_cache"
>false</
property
>
<
property
name
=
"show_sql"
>false</
property
>
<
property
name
=
"hibernate.multiTenancy"
>SCHEMA</
property
>
<
property
name
=
"hibernate.tenant_identifier_resolver"
>
hotel.dao.hibernate.TenantIdResolver
</
property
>
<
property
name
=
"hibernate.multi_tenant_connection_provider"
>
hotel.dao.hibernate.SchemaBasedMultiTenantConnectionProvider
</
property
>
<
mapping
class
=
"hotel.model.Guest"
/>
</
session-factory
>
|
<hibernate.tenant_identifier_resolver> 屬性規定了一個合約,以使 Hibernate 能夠解析出應用當前的 tenantId,該類必須實現 CurrentTenantIdentifierResolver 接口,通常我們可以從登錄信息中獲得 tenatId。
清單 2. 獲取當前 tenantId
|
1
2
3
4
5
|
public class TenantIdResolver implements CurrentTenantIdentifierResolver {
public String resolveCurrentTenantIdentifier() {
return Login.getTenantId();
}
}
|
< hibernate.multi_tenant_connection_provider> 屬性指定了 ConnectionProvider,即 Hibernate 需要知道如何以租戶特有的方式獲取數據連接,SchemaBasedMultiTenantConnectionProvider 類實現了MultiTenantConnectionProvider 接口,根據 tenantIdentifier 獲得相應的連接。在實際應用中,可結合使用 JNDI DataSource 技術獲取連接以提高性能。
清單 3. 以租戶特有的方式獲取數據庫連接
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class SchemaBasedMultiTenantConnectionProvider
implements MultiTenantConnectionProvider, Stoppable,
Configurable, ServiceRegistryAwareService {
private final DriverManagerConnectionProviderImpl connectionProvider
= new DriverManagerConnectionProviderImpl();
@Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
final Connection connection = connectionProvider.getConnection();
connection.createStatement().execute("USE " + tenantIdentifier);
return connection;
}
@Override
public void releaseConnection(String tenantIdentifier, Connection connection)
throws SQLException {
connection.createStatement().execute("USE test");
connectionProvider.closeConnection(connection);
}
……
}
|
與表 guest 對應的 POJO 類 Guest,其中主要是一些 getter 和 setter方法。
清單 4. POJO 類 Guest
|
1
2
3
4
5
6
7
8
9
10
11
12
|
@Table(name = "guest")
public class Guest {
private Integer id;
private String name;
private String telephone;
private String address;
private String email;
//getters and setters
……
}
|
我們使用 ServiceSchemaBasedMain.java 來進行測試,並假設了一些數據以方便演示,如當有不同租戶的管理員登錄后分別進行添加客戶的操作。
清單 5. 測試類 ServiceSchemaBasedMain
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
public class ServiceSchemaBasedMain {
public static void main(String[] args) {
Session session = null;
Guest guest =null;
List<
Guest
> list = null;
Transaction tx = null;
System.out.println("======== 租戶 hotel_1 ========");
Login.setTenantId("hotel_1");
session = sessionFactory.openSession();
tx = session.beginTransaction();
guest = new Guest();
guest.setName("張三");
guest.setTelephone("56785678");
guest.setAddress("上海市張揚路88號");
guest.setEmail("zhangsan@gmail.com");
session.saveOrUpdate(guest);
list = session.createCriteria(Guest.class).list();
for (Guest gue : list) {
System.out.println(gue.toString());
}
tx.commit();
session.close();
System.out.println("======== 租戶 hotel_2 ========");
Login.setTenantId("hotel_2");
session = sessionFactory.openSession();
tx = session.beginTransaction();
guest = new Guest();
guest.setName("李四");
guest.setTelephone("23452345");
guest.setAddress("上海市南京路100號");
guest.setEmail("lisi@gmail.com");
session.saveOrUpdate(guest);
list = session.createCriteria(Guest.class).list();
for (Guest gue : list) {
System.out.println(gue.toString());
}
tx.commit();
session.close();
}
}
|
清單 6. 運行程序 ServiceSchemaBasedMain 的輸出
|
1
2
3
4
5
6
7
8
|
======== 租戶 hotel_1 ========
Guest [id=1, name=Victor, telephone=56008888, address=上海科苑路399號, email=vic@gmail.com]
Guest [id=2, name=Jacky, telephone=66668822, address=上海金科路28號, email=jacky@sina.com]
Guest [id=3, name=張三, telephone=56785678, address=上海市張揚路88號, email=zhangsan@gmail.com]
======== 租戶 hotel_2 ========
Guest [id=1, name=Anton, telephone=33355566, address=上海南京路8號, email=anton@gmail.com]
Guest [id=2, name=Gus, telephone=33355566, address=北京大道3號, email=gus@yahoo.com]
Guest [id=3, name=李四, telephone=23452345, address=上海市南京路100號, email=lisi@gmail.com]
|
模式3:共享數據庫、共享 Schema、共享數據表
在這種情況下,所有租戶共享數據表存放數據,不同租戶的數據通過 tenant_id 鑒別器來區分。但目前的 Hibernate 4 還不支持這個多租戶鑒別器策略,要在 5.0 才支持。但我們是否有可選的替代方案呢?答案是使用 Hibernate Filter.
為了區分多個租戶,我在 Schema 的每個數據表需要添加一個字段 tenant_id 以判定數據是屬於哪個租戶的。
圖 3. 共享 Schema、共享數據表案例 E-R 圖

根據上圖在 MySQL 中創建 DATABASE hotel。
我們在 OR-Mapping 配置文件中使用了 Filter,以便在進行數據查詢時,會根據 tenant_id 自動查詢出該租戶所擁有的數據。
清單 7. 對象關系映射文件 Room.hbm.xml
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<
hibernate-mapping
default-lazy
=
"true"
package
=
"hotel.model"
>
<
class
name
=
"Room"
table
=
"room"
>
<
id
name
=
"id"
column
=
"id"
type
=
"int"
unsaved-value
=
"0"
>
<
generator
class
=
"native"
/>
</
id
>
<
property
name
=
"serialNumber"
column
=
"serial_number"
/>
<
property
name
=
"position"
column
=
"position"
/>
<
property
name
=
"status"
column
=
"status"
/>
<
many-to-one
name
=
"tenant"
class
=
"Tenant"
column
=
"tenant_id"
access
=
"field"
not-null
=
"true"
lazy
=
"false"
/>
<
many-to-one
name
=
"category"
class
=
"Category"
column
=
"category_id"
access
=
"field"
not-null
=
"true"
lazy
=
"false"
/>
<
filter
name
=
"tenantFilter"
condition
=
"tenant_id = :tenantFilterParam"
/>
</
class
>
<
filter-def
name
=
"tenantFilter"
>
<
filter-param
name
=
"tenantFilterParam"
type
=
"string"
/>
</
filter-def
>
</
hibernate-mapping
>
|
接下來我們在 HibernateUtil 類中通過 ThreadLocal 存放和獲取 Hibernate Session,並將用戶登錄信息中的 tenantId 設置為 tenantFilterParam 的參數值。
清單 8. 獲取 Hibernate Session 的工具類 HibernateUtil
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class HibernateUtil {
public static final ThreadLocal<
Session
> session = new ThreadLocal<
Session
>();
public static Session currentSession() throws HibernateException {
Session s = session.get();
if (s == null) {
s = sessionFactory.openSession();
String tenantId = LoginContext.getTenantId();
s.enableFilter("tenantFilter").setParameter("tenantFilterParam", tenantId);
session.set(s);
}
return s;
}
}
|
不過 Filter 只是有助於我們讀取數據時顯示地忽略掉 tenantId,但在進行數據插入的時候,我們還是不得不顯式設置相應 tenantId 才能進行持久化。這種狀況只能在 Hibernate5 版本中得到根本改變。
清單 9. 運行程序 HotelServiceMain 輸出
|
1
2
3
4
5
6
7
|
======當前可用房間列表======
Room [ID=1, 房間編號=R1011, 位置=南樓, 床數=2, 狀態=Free, 租戶=新亞酒店]
Room [ID=7, 房間編號=R7011, 位置=南樓, 床數=2, 狀態=Free, 租戶=新亞酒店]
======預訂后======
RentHistory [ID=1, 開始時間=2013-10-09 12:42:15.0, 結束時間=2013-10-12 12:42:15.0, Room ID=1, 房間編號=R1011, 房間狀態=Booked, 金額=0.0元, 客人=Victor]
======結賬后======
RentHistory [ID=1, 開始時間=2013-10-09 12:42:15.0, 結束時間=2013-10-12 12:42:15.0, Room ID=1, 房間編號=R1011, 房間狀態=Free, 金額=300.0元, 客人=Victor]
|
多租戶下的 Hibernate 緩存
基於獨立 Schema 模式的多租戶實現,其數據表無需額外的 tenant_id。通過 ConnectionProvider 來取得所需的 JDBC 連接,對其來說一級緩存(Session 級別的緩存)是安全的可用的,一級緩存對事物級別的數據進行緩存,一旦事物結束,緩存也即失效。但是該模式下的二級緩存是不安全的,因為多個 Schema 的數據庫的主鍵可能會是同一個值,這樣就使得 Hibernate 無法正常使用二級緩存來存放對象。例如:在 hotel_1 的 guest 表中有個 id 為 1 的數據,同時在 hotel_2 的 guest 表中也有一個 id 為 1 的數據。通常我會根據 id 來覆蓋類的 hashCode() 方法,這樣如果使用二級緩存,就無法區別 hotel_1 的 guest 和 hote_2 的 guest。
在共享數據表的模式下的緩存, 可以同時使用 Hibernate的一級緩存和二級緩存, 因為在共享的數據表中,主鍵是唯一的,數據表中的每條記錄屬於對應的租戶,在二級緩存中的對象也具有唯一性。Hibernate 分別為 EhCache、OSCache、SwarmCache 和 JBossCache 等緩存插件提供了內置的 CacheProvider 實現,讀者可以根據需要選擇合理的緩存,修改 Hibernate 配置文件設置並啟用它,以提高多租戶應用的性能。
EclipseLink
EclipseLink 是 Eclipse 基金會管理下的開源持久層服務項目,為 Java 開發人員與各種數據服務(比如:數據庫、web services、對象XML映射(OXM)、企業信息系統(EIS)等)交互提供了一個可擴展框架,目前支持的持久層標准中包括:
- Java Persistence API (JPA)
- Java Architecture for XML Binding (JAXB)
- Java Connector Architecture (JCA)
- Service Data Objects (SDO)
EclipseLink 前身是 Oracle TopLink, 2007年 Oracle 將后者絕大部分捐獻給了 Eclipse 基金會,次年 EclipseLink 被 Sun 挑選成為 JPA 2.0 的參考實現。
注: 目前 EclipseLink2.5 完全支持 2013 年發布的 JPA2.1(JSR 338) 。
在完整實現 JPA 標准之外,針對 SaaS 環境,在多租戶的隔離方面 EclipseLink 提供了很好的支持以及靈活地解決方案。
應用程序隔離
- 隔離的容器/應用服務器
- 共享容器/應用服務器的應用程序隔離
- 同一應用程序內的共享緩存但隔離的 entity manager factory
- 共享的 entity manager factory 但每隔離的 entity manager
數據隔離
- 隔離的數據庫
- 隔離的Schema/表空間
- 隔離的表
- 共享表但隔離的行
- 查詢過濾
- Oracle Virtual Private Database (VPD)
對於多租戶數據源隔離主要有以下方案
- Single-Table Multi-tenancy,依靠租戶區分列(tenant discriminator columns)來隔離表的行,實現多租戶共享表。
- Table-Per-Tenant Multi-tenancy,依靠表的租戶區分(table tenant discriminator)來隔離表,實現一租戶一個表,大體類似於上文的共享數據庫獨立Schema模式。
- Virtual Private Database(VPD ) Multi-tenancy,依靠 Oracle VPD 自身的安全訪問策略(基於動態SQL where子句特性),實現多租戶共享表。
本節重點介紹多租戶在 EclipseLink 中的共享數據表和一租戶一個表的實現方法,並也以酒店多租戶應用的例子展現共享數據表方案的具體實踐。
EclipseLink Annotation @Multitenant
與 @Entity 或 @MappedSuperclass 一起使用,表明它們在一個應用程序中被多租戶共享, 如清單 10。
清單10. @Multitenant
|
1
2
3
4
5
6
|
@Entity
@Table(name="room")
@Multitenant
...
publicclass Room {
}
|
表 1. Multitenant 包含兩個屬性
共享數據表(SINGLE_TABLE)
- Metadata配置
依靠租戶區分列修飾符 @TenantDiscriminatorColumn 實現。
清單11. @TenantDiscriminatorColumn
|
1
2
3
4
5
6
|
@Entity
@Table(name="hotel_guest")
@Multitenant(SINGLE_TABLE)
@TenantDiscriminatorColumn(name="tenant_id", contextProperty="tenant.id")
publicclass HotelGuest {
}
|
或者在EclipseLink描述文件orm.xml定義對象與表映射時進行限制,兩者是等價的。
清單12. orm.xml
|
1
2
3
4
5
6
7
|
<
entity
class
=
"mtsample.hotel.model.HotelGuest"
>
<
multitenant
>
<
tenant-discriminator-column
name
=
"tenant_id"
context-property
=
"tenant.id"
/>
</
multitenant
>
<
table
name
=
"HotelGuest"
/>
...
</
entity
>
|
- 屬性配置
租戶區分列定義好后,在運行時環境需要配置具體屬性值,以確定當前操作環境所屬的租戶。
三種方式的屬性配置,按優先生效順序排序如下
- EntityManager(EM)
- EntityManagerFactory(EMF)
- Application context (when in a Java EE container)
例如 EntityManagerFactory 可以間接通過在 persistence.xml 中配置持久化單元(Persistence Unit)或直接傳屬性參數給初始化時 EntityManagerFactory。
清單 13. 配置 persistence.xml
|
1
2
3
4
5
6
7
|
<
persistence-unit
name
=
"multi-tenant"
>
...
<
properties
>
<
property
name
=
"tenant_id"
value
=
"開發部"
/>
...
</
properties
>
</
persistence-unit
>
|
或者
清單 14. 初始化 EntityManagerFactory
|
1
2
3
4
|
HashMap properties = new HashMap();
properties.put("tenant_id", "人力資源部");
...
EntityManager em = Persistence.createEntityManagerFactory("multi-tenant", properties).createEntityManager();
|
按共享粒度可以作如下區分,
- EntityManagerFactory 級別
用戶需要通過 eclipselink.session-name 提供獨立的會話名,確保每個租戶占有獨立的會話和緩存。
清單 15. 為 EntityManagerFactory 配置會話名
|
1
2
3
4
5
|
HashMap properties = new HashMap();
properties.put("tenant_id", "開發部");
properties.put("eclipselink.session-name", "multi-tenant-dev");
...
EntityManager em = Persistence.createEntityManagerFactory("multi-tenant", properties).createEntityManager();
|
- 共享的 EntityManagerFactory 級別
EntityManagerFactory 的默認模式, 此級別缺省配置為獨立二級緩存(L2 cache), 即每個 mutlitenant 實體緩存設置為 ISOLATED,用戶也可設置 eclipselink.multitenant.tenants-share-cache 屬性為真以共享,此時多租戶 Entity 緩存設置為 PROTECTED。
這種級別下,一個活動的 EntityManager 不能更換 tenantId。
- EntityManager 級別
這種級別下,共享 session,共享 L2 cache, 用戶需要自己設置緩存策略,以設置哪些租戶信息是不能在二級緩存共享的。
清單 16. 設置緩存
|
1
2
3
4
5
6
7
8
9
|
HashMap tenantProperties = new HashMap();
tenantProperties.put("tenant_id", "人力資源部");
HashMap cacheProperties = new HashMap();
cacheProperties.put("eclipselink.cache.shared.Employee", "false");
cacheProperties.put("eclipselink.cache.size.Address", "10");
cacheProperties.put("eclipselink.cache.type.Contract", "NONE");
...
EntityManager em = Persistence.createEntityManagerFactory("multi-tenant", cacheProperties).createEntityManager(tenantProperties);
|
同樣,一個活動的EntityManager不能更換tenant ID。
幾點說明:
- 每個表的區分列可以有任意多個,使用修飾符 TenantDiscriminatorColumns。
清單 17. 多個分區列
|
1
2
3
4
|
@TenantDiscriminatorColumns({
@TenantDiscriminatorColumn(name="tenant_id", contextProperty="tenant.id"),
@TenantDiscriminatorColumn(name = "guest_id", contextProperty="guest.id")
})
|
- 租戶區分列的名字和對應的上下文屬性名可以取任意值,由應用程序開發者設定。
- 生成的 Schema 可以也可以不包含租戶區分列,如 tenant_id 或 guest_id。
- 租戶區分列可以映射到實體對象也可以不。
注意:當映射的時候,實體對象相應的屬性必須標記為只讀(insertable=false, updatable=false),這種限制使得區分列不能作為實體表的 identifier。
TenantDiscriminatorColumn被以下 EntityManager 的操作和查詢支持:
persist,find,refresh,named queries,update all,delete all 。
一租戶一表(TABLE_PER_TENANT )
這種多租戶類型使每個租戶的數據可以占據專屬它自己的一個或多個表,多租戶間的這些表可以共享相同 Schema 也可使用不同的,前者使用前綴(prefix)或后綴(suffix)命名模式的表的租戶區分符,后者使用租戶專屬的 Schema 名來定義表的租戶區分符。
- Metadata配置
依靠數據表的租戶區分修飾符 @TenantTableDiscriminator 實現
清單 18.
|
1
2
3
4
5
6
|
@Entity
@Table(name=“CAR”)
@Multitenant(TABLE_PER_TENANT)
@TenantTableDiscriminator(type=SCHEMA, contextProperty="eclipselink-tenant.id")
public class Car{
}
|
或
清單 19.
|
1
2
3
4
5
6
|
<
entity
class
=
"Car"
>
<
multitenant
type
=
"TABLE_PER_TENANT"
>
<
tenant-table-discriminator
type
=
"SCHEMA"
context-property
=
"eclipselink.tenant-id"
/>
</
multitenant
>
<
table
name
=
"CAR"
>
</
entity
>
|
如前所述,TenantTableDiscriminatorType有 3 種類型:SCHEMA、SUFFIX 和 PREFIX。
- 屬性配置
與另外兩種多租戶類型一樣,默認情況下,多租戶共享EMF,如不想共享 EMF,可以通過配置 PersistenceUnitProperties.MULTITENANT_SHARED_EMF 以及 PersistenceUnitProperties.SESSION_NAME 實現。
清單 20.
|
1
2
3
4
5
6
7
8
9
10
11
12
|
// Shared EMF
EntityManager em = createEntityManager(MULTI_TENANT_PU);
em.getTransaction().begin();
em.setProperty(EntityManagerProperties.MULTITENANT_PROPERTY_DEFAULT, "RLT");
// Non shared EMF
HashMap properties = new HashMap();
properties.put(PersistenceUnitProperties.MULTITENANT_SHARED_EMF, "false");
properties.put(PersistenceUnitProperties.SESSION_NAME, "non-shared-emf-for-rlt");
properties.put(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, "RLT");
...
EntityManager em = Persistence.createEntityManagerFactory("multi-tenant-pu", properties).createEntityManager();
|
或在 persistence.xml 配置屬性。
酒店多租戶應用實例(EclipseLink 共享(單)表)
數據庫 Schema 和測試數據與上文 Hibernate 實現相同,關於對象關系映射(OR mapping)的配置均采用 JPA 和 EclipseLink 定義的 Java Annotation 描述。
關於幾個基本操作
- 添加一個對象實例, 利用EntityManager.persist()
清單 21. 添加
|
1
2
3
4
5
6
7
8
|
public <
T
> void save(T t) {
em.persist(t);
}
public <
T
> void saveBulk(List<
T
> bulk) {
for(T t:bulk){
em.persist(t);
}
}
|
- 更新一個對象實例, 利用EntityManager.merge()
清單 22. 更新
|
1
2
3
|
public <
T
> void update(T t){
em.merge(t);
}
|
- 查詢, 利用EntityManager的NamedQuery,
清單 23. 多條件多結果查詢
|
1
2
3
4
5
6
7
8
9
10
11
12
|
protected <
T
> List<
T
> queryResultList(String queryName, Class<
T
> clazz, String[] argNames, Object[] argValues){
TypedQuery<
T
> query = em.createNamedQuery(queryName, clazz);
if(argNames != null && argValues != null){
if(argNames.length != argValues.length){
return null;
}
for(int i = 0; i < argNames.length; i++){
query.setParameter(argNames[i], argValues[i]);
}
}
return query.getResultList();
}
|
若用 JPQL 實現則示例如下:
清單 24. JPQL NamedQuery 定義
|
1
2
3
4
5
6
7
8
9
10
11
12
|
@Entity
@Table(name = "rent_history")
@Multitenant
@TenantDiscriminatorColumn(name="tenant_id", contextProperty="tenant.id")
@NamedQueries({
@NamedQuery(
name="find_renthistory_by_hotel_guest_name",
query="select h from RentHistory h, HotelGuest g where h.hotelGuestId=g.id and g.name=:hotelGuestName order by h.createTime DESC"
),
})
public class RentHistory implements Serializable {
}
|
部分測試數據如下(MySQL):
hotel_admin

hotel_guest

room

運行附件 MT_Test_Hotels.zip 中的測試代碼(請參照 readme)來看看多租戶的一些典型場景。
清單 25. 運行測試代碼
|
1
2
|
java -classpath build/classes/;D:/workspace/eclipselink/jlib/jpa/javax.persistence_2.1.0
.v201304241213.jar;D:/workspace/eclipselink/jlib/eclipselink.jar;D:/workspace/mysql/mysql-connector-java-5.1.26/mysql-connector-java-5.1.26-bin.jar mtsample.hotel.test.TestHotelAdmin
|
能得到輸出片段如下:
清單 26. 輸出
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
*****登錄*****
用戶名:letian
密碼:
letian 已登錄.
...
...
*****查房*****
1.獲取可用房間
Room [id=3, categoryId=3, createTime=2013-09-22 23:13:11.0, position=南樓, serialNumber=R2011, status=Free]
Room [id=4, categoryId=4, createTime=2013-09-24 09:07:02.0, position=北樓, serialNumber=R2012, status=Free]
...
...
*****入住*****
請輸入房間號:R2012
請輸入房客編號:4
請輸入入住時間(yyyy.MM.dd H:mm):2014.02.14 14:02
請輸入退房時間(yyyy.MM.dd H:mm):2014.02.17 11:00
...
...
*****查房*****
1.獲取可用房間
Room [id=3, categoryId=3, createTime=2013-09-26 13:02:49.0, position=南樓, serialNumber=R2011, status=Free]
...
...
*****退房*****
請輸入房間號:R2012
請輸入退房時間(yyyy.MM.dd H:mm):2014.02.15 5:20
...
...
1.獲取所有入住歷史信息
RentHistory [id=3, amount=260.0, createTime=2013-09-26 13:07:48.0, endTime=Sat Feb 15 05:20:00 CST 2014, hotelGuestId=4, roomId=4, startTime=Fri Feb 14 14:02:00
CST 2014, ]
RentHistory [id=1, amount=1040.0, createTime=2013-09-17 13:28:00.0, endTime=Fri Sep 20 11:28:00 CST 2013, hotelGuestId=3, roomId=3, startTime=Wed Sep 18 13:28:00 CST 2013, ]
...
...
*****查房*****
1.獲取可用房間
Room [id=3, categoryId=3, createTime=2013-09-26 13:02:49.0, position=南樓, serialNumber=R2011, status=Free]
Room [id=4, categoryId=4, createTime=2013-09-26 13:07:48.0, position=北樓, serialNumber=R2012, status=Free]
|
通過共享表的測試數據以及運行結果可以看到,對於多個不同的租戶(hotel_admin),在添加、查找、更新操作沒有顯示聲明租戶標識的情況下,EntityManager 可以根據自身的租戶屬性配置
實現租戶分離。在本實例,EntityManager 初始化時利用到 hotel_admin 登錄后的會話上下文進行租戶判斷,這里不再贅述。
注:上文中提及的全部源碼都可以在附件中找到。
其它方面的考慮
數據備份
獨立數據庫和獨立Sechma的模式,為每個租戶備份數據比較容易,因為他們存放在不同的數據表中,只需對整個數據庫或整個Schema進行備份。
在共享數據表的模式下,可以將所有租戶的數據一起備份,但是若要為某一個租戶或按租戶分開進行數據備份,就會比較麻煩。通常需要另外寫sql腳本根據tenant_id來取得對應的數據然后再備份,但是要按租戶來導入的話依然比較麻煩,所以必要時還是需要備份所有並為以后導入方便。
性能
獨立數據庫:性能高,但價格也高,需要占用資源多,不能共享,性價比低。
共享數據庫,獨立 Schema:性能中等,但價格合適,部分共享,性價比中等。
共享數據庫,共享 Schema,共享數據表:性能中等(可利用 Cache 可以提高性能),但價格便宜,完全共享,性價比高。如果在某些表中有大量的數據,可能會對所有租戶產生性能影響。
對於共享數據庫的情況下,如果因為太多的最終用戶同時訪問數據庫而導致應用程序性能問題,可以考慮數據表分區等數據庫端的優化方案。
經濟考慮
為了支持多租戶應用,共享模式的應用程序往往比使用獨立數據庫模式的應用程序相對復雜,因為開發一個共享的架構,導致在應用設計上得花較大的努力,因而初始成本會較高。然而,共享模式的應用在運營成本上往往要低一些,每個租戶所花的費用也會比較低。
結束語
多租戶數據層方案的選擇是一個綜合的考量過程,包括成本、數據隔離與保護、維護、容災、性能等。但無論怎樣選擇,OR-Mapping 框架對多租戶的支持將極大的解放開發人員的工作,從而可以更多專注於應用邏輯。最后我們以一個 Hibernate 和 EclipseLink 的比較來結束本文。
表 2. Hibernate 與 EclipeLink 對多租戶支持的比較
下載資源
- 代碼示例 (Multi-tenancy-attachment.zip | 272k)
