Hibernate 是最流行的 ORM 框架,用于在 Java 中與數(shù)據(jù)庫進行交互。在本文中,我們將探討使用批量選擇和更新的各種方法以及在 Java 中使用 Hibernate 框架時最有效的方法。
我嘗試了三種方式,分別如下:
- 使用 Hibernate 的 Query.list() 方法。
- 在 FORWARD_ONLY 滾動模式下使用 ScrollableResults。
- 在 StatelessSession 中使用具有 FORWARD_ONLY 滾動模式的 ScrollableResults。
為了確定哪一種為我們的用例提供最佳性能,我使用上面列出的三種方法進行了以下測試。
- 選擇并更新 1000 行。
下面我們將上述三種方式一一應(yīng)用到上述操作中,看看代碼和結(jié)果。
使用 Hibernate 的 Query.list() 方法
執(zhí)行的代碼:
java:
List rows;
Session session = getSession();
Transaction transaction = session.beginTransaction();
try {
Query query = session.createQuery("FROM PersonEntity WHERE id > :maxId ORDER BY id").setParameter("maxId",
MAX_ID_VALUE);
query.setMaxResults(1000);
rows = query.list();
int count = 0;
for (Object row : rows) {
PersonEntity personEntity = (PersonEntity) row;
personEntity.setName(randomAlphaNumeric(30));
session.saveOrUpdate(personEntity);
//Always flush and clear the session after updating 50(jdbc_batch_size specified in hibernate.properties) rows
if (++count % 50 == 0) {
session.flush();
session.clear();
}
}
} finally {
if (session != null && session.isOpen()) {
transaction.commit();
session.close();
}
}
測試結(jié)果:
- 耗時:360s到400s
- 堆模式:- 從 13m 逐漸增加到 51m(從 jconsole)。
在 FORWARD_ONLY 滾動模式下使用 ScrollableResults。
有了這個,我們預(yù)計它應(yīng)該比第一種方法消耗更少的內(nèi)存。讓我們看看下面的結(jié)果。
執(zhí)行的代碼:
java:
Session session = getSession();
Transaction transaction = session.beginTransaction();
ScrollableResults scrollableResults = session
.createQuery("FROM PersonEntity WHERE id > " + MAX_ID_VALUE + " ORDER BY id")
.setMaxResults(1000).scroll(ScrollMode.FORWARD_ONLY);
int count = 0;
try {
while (scrollableResults.next()) {
PersonEntity personEntity = (PersonEntity) scrollableResults.get(0);
personEntity.setName(randomAlphaNumeric(30));
session.saveOrUpdate(personEntity);
if (++count % 50 == 0) {
session.flush();
session.clear();
}
}
} finally {
if (session != null && session.isOpen()) {
transaction.commit();
session.close();
}
}
檢測結(jié)果:
- 花費的時間:185秒到200秒。
- 堆模式:- 從 13MB 逐漸增加到 41MB(使用 jconsole 測量相同)。
在 StatelessSession 中使用 ScrollableResults 和 FORWARD_ONLY 滾動模式
無狀態(tài)會話不實現(xiàn)一級緩存,也不與任何二級緩存交互,也不實現(xiàn)事務(wù)性后寫或自動臟檢查,也不執(zhí)行級聯(lián)到關(guān)聯(lián)實例的操作。無狀態(tài)會話會忽略集合。通過無狀態(tài)會話執(zhí)行的操作繞過 Hibernate 的事件模型和攔截器。
在批量更新的情況下始終建議使用這種類型的會話,因為在這些類型的用例中我們確實不需要這些休眠功能的開銷。
執(zhí)行的代碼:
java:
StatelessSession session = getStatelessSession();
Transaction transaction = session.beginTransaction();
ScrollableResults scrollableResults = session
.createQuery("FROM PersonEntity WHERE id > " + MAX_ID_VALUE + " ORDER BY id")
.setMaxResults(TRANSACTION_BATCH_SIZE).scroll(ScrollMode.FORWARD_ONLY);
try {
while (scrollableResults.next()) {
PersonEntity personEntity = (PersonEntity) scrollableResults.get(0);
personEntity.setName(randomAlphaNumeric(20));
session.update(personEntity);
}
} finally {
if (session != null && session.isOpen()) {
transaction.commit();
session.close();
}
}
測試結(jié)果:
- 花費的時間:185 秒到 200 秒。
- 堆模式:- 從 13MB 逐漸增加到 39MB。
我還對 2000 行進行了相同的測試,得到的結(jié)果如下:
結(jié)果:
- 使用 list():- 花費的時間:大約 750 秒,堆模式:從 13MB 逐漸增加到 74 MB。
- 使用 ScrollableResultSet:花費的時間:大約 380s,堆模式:從 13MB 逐漸增加到 46MB
- 使用 Stateless:花費的時間:大約 380 秒,堆模式:從 13MB 逐漸增加到 43MB
上述所有方法的攔截器問題
ScrollableResults 和 Stateless ScrollableResults 提供幾乎相同的性能,這比 Query.list() 好得多。但是上述所有方法仍然存在一個問題。加鎖,以上所有方法都在同一個事務(wù)中選擇和更新數(shù)據(jù),這意味著只要事務(wù)正在運行,已經(jīng)執(zhí)行更新的行將被鎖定,任何其他操作都必須等待事務(wù)完成結(jié)束。
一個辦法
為了解決上述問題,我們應(yīng)該在這里做兩件事:
- 我們需要在不同的事務(wù)中選擇和更新數(shù)據(jù)。
- 并且,這些類型的更新應(yīng)該分批進行
因此,我再次執(zhí)行了與上述相同的測試,但這次更新是在一個不同的事務(wù)中執(zhí)行的,該事務(wù)以 50 個為一組提交。
注意:在 Scrollable 和 Stateless 的情況下,我們還需要一個不同的會話,因為我們需要原始會話和事務(wù)來滾動結(jié)果。
使用批處理的結(jié)果
- 使用list():花費的時間:大約400s,堆模式:從13MB逐漸增加到61MB。
- 使用 ScrollableResultSet:花費的時間:大約 380 秒,堆模式:從 13MB 逐漸增加到 51MB。
- 使用無狀態(tài):花費的時間:大約 190 秒,堆模式:從 13MB 逐漸增加到 44MB。
觀察:ScrollableResults 的這個時間性能下降到幾乎等于 Query.list(),但 Stateless 的性能幾乎保持不變。
總結(jié)與結(jié)論
從以上所有的實驗來看,在我們需要做批量選擇和更新的情況下,從內(nèi)存消耗和時間來看,最好的方法如下:
- 在無狀態(tài)會話中使用 ScrollableResults。
- 以 20 到 50 的批次(批處理)在不同的事務(wù)中執(zhí)行選擇和更新(注意:批次大小可以視具體情況而定)。
最佳方法的示例代碼
java:
StatelessSession session = getStatelessSession();
Transaction transaction = session.beginTransaction();
ScrollableResults scrollableResults = session
.createQuery("FROM PersonEntity WHERE id > " + MAX_ID_VALUE + " ORDER BY id")
.setMaxResults(TRANSACTION_BATCH_SIZE).scroll(ScrollMode.FORWARD_ONLY);
int count = 0;
try {
StatelessSession updateSession = getStatelessSession();
Transaction updateTransaction = updateSession.beginTransaction();
while (scrollableResults.next()) {
PersonEntity personEntity = (PersonEntity) scrollableResults.get(0);
personEntity.setName(randomAlphaNumeric(5));
updateSession.update(personEntity);
if (++count % 50 == 0) {
updateTransaction.commit();
updateTransaction = updateSession.beginTransaction();
}
}
updateSession.close();
} finally {
if (session != null && session.isOpen()) {
transaction.commit();
session.close();
}
}
使用 spring 等 java 框架,這段代碼可以更小,比如不需要處理會話關(guān)閉等。 上面的代碼是使用 hibernate 用純 java 編寫的。