Hibernate 性能之隔離機制
1. 前言
事務有 4 大特性,其隔離性尤其重要,沒有良好的隔離性就相當于你可以隨意出入鄰居家。不能保證數據的完整性。
每一種隔離機制都有自己使用的真實場景。
本節課探討一下 Hibernate 中是如何進行隔離設置的。通過本節課程的學習,你將了解到:
- Hibernate 中如何設置隔離級別;
- 悲觀鎖和樂觀鎖的比較。
2. Hibernate 中的隔離機制
如前面課程所述,隔離機制能保證事務之間的良好秩序,但是,太嚴格的隔離機制會讓事務之間產生時間上的等待或延遲,也就是說并發性弱。
太松散的隔離機制,雖然可以增加并發性,但可能會產生事務之間的數據臟讀等一系列不希望出現的事情。
有時,純粹地依靠 JDBC 提供的 4 種隔離機制很難做到隔離的優雅性,所以,一般采用 讀取已提交 或者 更低的事務隔離級別,再配合各種并發訪問控制策略來達到并發事務控制的目的。
Hibernate 中如何設置隔離機制?
這個問題很簡單,你要做的就是在 Hibernate 主配置文件中添加如下信息:
<property name="connection.isolation">2</property>
這里的 2 是什么意思?
是這樣的,Hibernate 使用 1 、2 、4 、8 這幾個數字分別代表 4 種隔離機制。
- 8 - Serializable 串行化;
- 4 - Repeatable Read 可重復讀;
- 2 - Read Commited 可讀已提交;
- 1 - Read Uncommited 可讀未提交。
使用數字有幾個好處,畢竟不用記那么一長串字符串,最主要的是,這幾個數字可以換算成 二進制中的 0001、0010、0100、1000。可以直接通過二進制位運算的方式進行權限控制。
設置就是這么簡單,但是,這還不夠。
剛說過, 最好再配合并發控制策略。
那么, Hibernate 提供了怎樣的 策略,告訴你,有 2 種 “鎖” 機制:
- 樂觀鎖;
- 悲觀鎖。
你是喜歡先苦后甜還是先甜后苦了,我喜歡先苦后甜。好吧,先講解什么是悲觀鎖。
3. 悲觀鎖 Pessimistic Locking
悲觀地認為并發的事務時時會發生,總是擔心隔離機制不能很好的保證事務之間的安全性。
- 基本思想就是當一個事務讀取某一條記錄后,就會把這條記錄鎖住,如果其它的事務要想更新,必須等以前的事務提交或者回滾解除鎖;
- 悲觀鎖的實現,一般依靠數據庫提供的鎖機制。
只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在系統中實現了加鎖機制,也無法保證外部系統不會修改數據。
SQL 的 select 語句中有一個 for 語法關鍵字:
SELECT * from student where stuName='Hibernate' for UPDATE
其作用就是鎖定這條記錄,當前事務沒有結束之前,其它的事務不能在這條記錄上進行數據更新操作。
Hiberntae 實現悲觀鎖
前面我們使用過 Session 的 get()方法,大家還記得是怎么用的嗎?
stu = (Student) session.get(Student.class, new Integer(1));
其實這個方法還可以傳遞第三個參數,好吧,先看一下方法的原型:
public Object get(Class clazz, Serializable id, LockOptions lockOptions);
LockOptions 類本質是對 LockMode 枚舉類型的高級封裝,提供了幾種鎖的使用:
- 無鎖的機制,Transaction 結束時,切換到此模式;
hibernate 內部使用。
public static final LockOptions NONE = new LockOptions(LockMode.NONE);
- 查詢的時候,Hibernate 自動獲取鎖;
hibernate 內部使用。
public static final LockOptions READ = new LockOptions(LockMode.READ);
- 利用數據庫的 for update 子句加鎖(Select * from 表 for update),通過此選項實現悲觀鎖。
public static final LockOptions UPGRADE = new LockOptions(LockMode.UPGRADE);
悲觀鎖在實際生產環境中使用頻率并不高,限制了并發的發生率,降低了程序的響應速度。
編寫一個簡單的測試實例:
- 第一個事務,查詢加鎖,使用 Thread.sleep()模擬事務操作時長;
模擬時間不要太長,如果長時間不釋放鎖,其它等待事務會拋出等待超時異常。
stu = (Student) session.get(Student.class, new Integer(1), LockOptions.UPGRADE);
Thread.sleep(30000);
transaction.commit();
System.out.println("-----------第一個事務結束-----------");
執行此實例,查看控制臺輸出信息,查詢語句上添加了 for update,在模擬時長內事務沒有結束。
Hibernate:
select
student0_.stuId as stuId1_1_0_,
student0_.classRoomId as classRoo5_1_0_,
student0_.stuName as stuName2_1_0_,
student0_.stuPassword as stuPassw3_1_0_,
student0_.stuSex as stuSex4_1_0_
from
Student student0_
where
student0_.stuId=? for update
- 第二個事務,進行查詢、更新操作,此事務并不能馬上更新成功,只有等待第一個事務結束后才能成功。
stu = (Student) session.get(Student.class, new Integer(1));
System.out.println("-------------更新-------------");
stu.setStuName("Hibernate 01");
transaction.commit();
System.out.println("--------------更新成功-----------");
悲觀鎖的實現很簡單,也很好理解,無非就是我用時你不能用的問題。
4. 樂觀鎖
樂觀是一種積極的解決問題的態度。
所謂樂觀鎖認為系統中的事務并發更新不會很頻繁,即使沖突了也沒事,大不了重新再來一次。
- 基本思想:
每次提交一個事務更新時,查看要修改的數據從上次讀取以后有沒有被其它事務修改過,如果修改過,那么更新就會失敗。
- 實現方案:
在實體中增加一個版本控制字段,每次事務更新后就將版本 (Version) 字段的值加 1。
Tips: 樂觀鎖本質就是版本控制管理的實現,記錄的每一次更新操作都會以版本遞增的方式進行記錄。
一個事務在更新之前,先獲取記錄的當前版本號,更新時,如果版本還是最新的則可以更新,否則說明有事務比你先更新,則需要放棄?;蛘咧匦虏樵兊阶钚掳姹拘畔⒑笤俑隆?/p>
所以,在樂觀鎖的實現中,沖突是常態。
- 實現過程:
在學生實體類中添加新屬性,用來記錄每次更新的版本號。
public class Student implements Serializable {
//省略……
@Version
private Long version;
//省略……
}
stu = (Student) session.get(Student.class, new Integer(1));
System.out.println("當前版本號:"+stu.getVersion);
//模擬延遲,如果在這個時間內有其它事務進行了更新操作,此事務的更新不會成功
Thread.sleep(30000);
stu.setStuName("Hibernate");
transaction.commit();
好了,悲觀也好,樂觀也好,只是一種解決問題的態度。對于這兩種態度,咱們要總結一下。
樂觀鎖:
優勢:性能好,并發性高。
缺點:用戶體驗不好,可能會出現高高興興去更新,卻告知已經有人捷足先登了。
悲觀鎖:
優勢:鎖住記錄為我所用,沒修改完成之前,其他事務只能瞪眼瞧著,時間雖然延遲,至少心里有底。
缺點:并發性不好,性能不高。
Hibernate 的其它性能優化:
-
隨時使用 Session.clear()及時清除 Session 緩存區的內容;
-
1+N 問題 ( 一條 SQL 語句能解決的問題用了很多條 SQL 語句來實現) ;
-
使用 Criteria 查詢可以解決這個問題;
-
Lazy 加載:需要時,使用 get() 方法發出 SQL 語句。
-
使用類似于 from Student s left join s.classRoom c 的關聯查詢語句。
-
-
緩存使用:在對象更新、刪除、添加相對于查詢要少得多時, 二級緩存的應用將不怕 n+1 問題,因為即使第一次查詢很慢,之后直接緩存命中也是很快的,剛好又利用了 n+1。
5. 小結
性能優化顯然是一個不輕松、但又絕對不能忽視的話題。本節課程和大家講解了在 Hibernate 是如何處理事務隔離的,在隔離機制的基礎上,結合樂觀鎖或悲觀鎖更好地解決這個問題。
希望我們以樂觀的態度看待我們的生活、學習以及未來!