1. 程式人生 > >hibernate4效能之併發和鎖機制

hibernate4效能之併發和鎖機制

資料庫事務的定義
資料庫事務(Database Transaction),是指作為單個邏輯工作單元執行的一系列操作。一個邏輯工作單元要成為事務,必須滿足所謂的ACID(原子性、一致性、隔離性和永續性)屬性。
● 原子性(atomic),事務必須是原子工作單元;對於其資料修改,要麼全都執行,要麼全都不執行
● 一致性(consistent),事務在完成時,必須使所有的資料都保持一致狀態。
● 隔離性(insulation),由併發事務所作的修改必須與任何其它併發事務所作的修改隔離。
● 永續性(Duration),事務完成之後,它對於系統的影響是永久性的。

資料庫事務併發可能帶來的問題
如果沒有鎖定且多個使用者同時訪問一個數據庫,則當他們的事務同時使用相同的資料時可能會發生問題。由於併發操作帶來的資料不一致性包括:丟失資料修改、讀”髒”資料(髒讀)、不可重複讀、產生幽靈資料:

假設資料庫中有如下一條記錄:

第一類丟失更新(lost update):在完全未隔離事務的情況下,兩個事物更新同一條資料資源,某一事物異常終止,回滾造成第一個完成的更新也同時丟失。

在T1時刻開啟了事務1,T2時刻開啟了事務2,在T3時刻事務1從資料庫中取出了id="402881e535194b8f0135194b91310001"的資料,T4時刻事務2取出了同一條資料,T5時刻事務1將age欄位值更新為30,T6時刻事務2更新age為35並提交了資料,但是T7事務1回滾了事務age最後的值依然為20,事務2的更新丟失了,這種情況就叫做"第一類丟失更新(lost update)"。
髒讀(dirty read):如果第二個事務查詢到第一個事務還未提交的更新資料,形成髒讀。因為第一個事務你還不知道是否提交,所以資料不一定是正確的。

在T1時刻開啟了事務1,T2時刻開啟了事務2,在T3時刻事務1從資料庫中取出了id="402881e535194b8f0135194b91310001"的資料,在T5時刻事務1將age的值更新為30,但是事務還未提交,T6時刻事務2讀取同一條記錄,獲得age的值為30,但是事務1還未提交,若在T7時刻事務1回滾了事務2的資料就是錯誤的資料(髒資料),這種情況叫做" 髒讀(dirty read)"。
虛讀(phantom read):一個事務執行兩次查詢,第二次結果集包含第一次中沒有或者某些行已被刪除,造成兩次結果不一致,只是另一個事務在這兩次查詢中間插入或者刪除了資料造成的。

在T1時刻開啟了事務1,T2時刻開啟了事務2,T3時刻事務1從資料庫中查詢所有記錄,記錄總共有一條,T4時刻事務2向資料庫中插入一條記錄,T6時刻事務2提交事務。T7事務1再次查詢資料資料時,記錄變成兩條了。這種情況是"虛讀(phantom read)"。
不可重複讀(unrepeated read)
:一個事務兩次讀取同一行資料,結果得到不同狀態結果,如中間正好另一個事務更新了該資料,兩次結果相異,不可信任。

在T1時刻開啟了事務1,T2時刻開啟了事務2,在T3時刻事務1從資料庫中取出了id="402881e535194b8f0135194b91310001"的資料,此時age=20,T4時刻事務2查詢同一條資料,T5事務2更新資料age=30,T6時刻事務2提交事務,T7事務1查詢同一條資料,發現數據與第一次不一致。這種情況就是"不可重複讀(unrepeated read)"。
第二類丟失更新(second lost updates):是不可重複讀的特殊情況,如果兩個事務都讀取同一行,然後兩個都進行寫操作,並提交,第一個事務所做的改變就會丟失。

在T1時刻開啟了事務1,T2時刻開啟了事務2,T3時刻事務1更新資料age=25,T5時刻事務2更新資料age=30,T6時刻提交事務,T7時刻事務2提交事務,把事務1的更新覆蓋了。這種情況就是"第二類丟失更新(second lost updates)"。

Hibernate事務隔離級別:(不同資料庫對應預設的級別不一樣)
為了解決資料庫事務併發執行時的各種問題資料庫系統提供四種事務隔離級別,在Hibernate的配置檔案中可以顯示的配置資料庫事務隔離級別。每一個隔離級別用一個整數表示:
Serializable 序列化(8)二進位制值0001
Repeatable Read 可重複讀(4)二進位制值0010 MySql預設隔離級別
Read Commited 可讀已提交(2)二進位制值0100 Oracle預設級別
Read Uncommited 可讀未提交(1)二進位制值1000
在hibernate.cfg.xml中使用hibernate.connection.isolation引數配置資料庫事務隔離級別。

每一個隔離級別可以解決的問題:
隔離級別 第一類丟失更新 髒讀 幻讀 不可重複讀 第二類丟失更新
序列化 不可能 不可能 不可能 不可能 不可能
可重複讀 不可能 不可能 可能 不可能 不可能
可讀已提交 不可能 不可能 可能 可能 可能
可讀未提交 不可能 可能 可能 可能 可能


Hibernate對資料的鎖機制:
Hibernate可以利用Query或者Criteria的setLockMode()方法來設定要鎖定的表或列(Row)及其鎖定模式:
LockMode.NONE:無鎖機制;在事務結束時,所有的物件都切換到該模式上來。與session相關聯的物件通過呼叫update()或者saveOrUpdate()脫離該模式
LockMode.WRITE:當更新或者插入一行記錄的時候,鎖定級別自動設定為LockMode.WRITE
LockMode.READ:當Hibernate在“可重複讀”或者是“序列化”資料庫隔離級別下讀取資料的時候,鎖定模式自動設定為LockMode.READ。這種模式也可以通過使用者顯式指定進行設定。
LockMode.UPGRADE:利用資料庫的for update子句加鎖
LockMode.UPGRADE_NOWAIT:利用oracle的特定實現for update nowait子句實現

使用悲觀鎖解決事務併發問題:
悲觀鎖,正如其名,他總是悲觀的認為要操作的資料會有併發訪問。因此,在整個資料處理過程中,將資料處於鎖定狀態。悲觀鎖的實現,往往依靠資料庫提供的鎖機制(也只有資料庫層提供的鎖機制才能真正保證資料訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改資料)。
一個典型的依賴資料庫的悲觀鎖呼叫:select * from account where name=”Erica” for update這條sql語句鎖定了account表中所有符合檢索條件(name=”Erica”)的記錄。本次事務提交之前(事務提交時會釋放事務過程中的鎖),外界無法修改這些記錄。悲觀鎖,也是基於資料庫的鎖機制實現。
在Hibernate使用悲觀鎖十分容易,但實際應用中悲觀鎖是很少被使用的,因為它大大限制了併發性:

T1,T2時刻取款事務和轉賬事務分別開啟,T3事務查詢ACCOUNTS表的資料並用悲觀鎖鎖定,T4轉賬事務也要查詢同一條資料,資料庫發現該記錄已經被前一個事務使用悲觀鎖鎖定了,然後讓轉賬事務等待直到取款事務提交。T6時刻取款事務提交,T7時刻轉賬事務獲取資料。

悲觀鎖用法參考下面程式碼例項:
Java程式碼  收藏程式碼
  1. Transaction tx=session.beginTransaction();  
  2. //取得持久化User物件,並使用LockMode.UPGRADE模式鎖定物件
  3. User user=(User)session.get(User.class,1,LockMode.UPGRADE);  
  4. user.setName(“newName”); //更改物件屬性,注意並不需要使用session.save(user)
  5. tx.commit();  
  6. String hqlStr="from TUser user where user.name='Erica'";  
  7. Query query=session.createQuery(hqlStr);    
  8. query.setLockMode("user",LockModel.UPGRADE);    

這樣的話,Hibernate會使用select …… for update語句載入User類,並且鎖住了這個物件在資料庫中的列,直到事務完成(commit()以後)。

使用樂觀鎖解決事務併發問題
相對悲觀鎖而言,樂觀鎖機制採取了更加寬鬆的加鎖機制。悲觀鎖大多數情況下依靠資料庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是資料庫效能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於資料版本(Version)記錄機制實現。何謂資料版本?即為資料增加一個版本標識,在基於資料庫表的版本解決方案中,一般是通過為資料庫表增加一個"version"欄位來實現。
  樂觀鎖的工作原理:讀取出資料時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交資料的版本資料與資料庫表對應記錄的當前版本資訊進行比對,如果提交的資料版本號大於資料庫表當前版本號,則予以更新,否則認為是過期資料。
Hibernate為樂觀鎖提供了3種實現:
●基於version
●基於timestamp

●為遺留專案新增新增樂觀鎖

例如:

一般是通過為資料庫表增加一個 “version” 欄位來實現(我只知道Oracle是這樣,其他的沒試過)
個人感覺火車站賣票系統就是樂觀鎖,查票時查到了髒資料(就是另外一些事務還未提交的資料),看到顯示屏上有少量剩餘票,但點進去去買又買不到。樂觀鎖原理其實就是給每一條資料加上時間概念的標識,表明我這條資料是什麼時候的,可以理解為是相對時間,就是你所說的建表的時候新增version,然後我去取這條資料,比如version這個時候是0015了,那麼我操作完這條資料,要update回去時,又去查一遍這個version值,發現如果還是0015,那麼我就放心了,我就可以把version變成0016然後提交,因為在我操作這個過程中沒人動他,如果比0015大,那說明有人動過他了,我就選擇不操作這條資料了。

還有一種方式是直接用時間欄位表示,也就是時間戳,就是我提交這條資料時,之前拿到資料時的那個時間值一定要等於我要提交之前這個時刻點的時間值,如果小於當前資料的這個時間戳,那麼肯定是有事務在我之前提交了。

其實樂觀鎖是解決高併發效能的辦法。悲觀鎖更安全,但是併發效能太差,特別是有長事務的時候,併發事務會排很長的隊


總結
資料庫事務應該儘可能的短
這樣能降低資料庫中的鎖爭用。資料庫長事務會阻止你的應用程式擴充套件到高的併發負載。因此,假若在使用者思考期間讓資料庫事務開著,直到整個工作單元完成才關閉這個事務,這絕不是一個好的設計。
這就引出一個問題:一個操作單元,也就是一個事務單元的範圍應該是多大?
一個操作一個?一個請求一個?一個應用一個?
反模式:session-per-operation
在單個執行緒中,不要因為一次簡單的資料庫呼叫,就開啟和關閉一次Session!資料庫事務也是如此。也就是說應該禁止自動事務提交(auto-commit)。
session-per-request
最常用的模式是每個請求一個會話。在這種模式下,來自客戶端的請求被髮送到伺服器端,即 Hibernate 持久化層執行的地方,一個新的 Hibernate Session 被開啟,並且執行這個操作單元中所有的資料庫操作。一旦操作完成(同時對客戶端的響應也準備就緒),session 被同步,然後關閉。會話和請求之間的關係是一對一的關係。
Hibernate內建了對“當前session(current session)”的管理,用於簡化此模式。你要做的一切就是在伺服器端要處理請求的時候,開啟事務,在響應傳送給客戶之前結束事務,通常使用Servelt Filter來完成。
針對這種模式,Spring提供了對Hibernate事務的管理,提供了“一請求一事務”的Filter來利用Http請求與響應來控制session和事務的生命週期。

<filter>
      <filter-name>HibernateOpenSessionInViewFilter</filter-name>
      <filter-class>
            org.springframework.orm.hibernate3.support.OpenSessionInViewFilter
      </filter-class>
</filter>