hibernate的悲觀鎖與樂觀鎖實現

實際情況:多個事物同時更新某一條數據,第一個事物讀取完之後掛起,第二個事務查詢並做了修改,第一個事物提交,這個時候會覆蓋掉第二個事物提交的東西

另外,減少整個對象save,save單獨字段是更好的方式


悲觀鎖(Pessimistic Lock), 顧名思義,就是很悲觀,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。


樂觀鎖(Optimistic Lock), 顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量,像數據庫如果提供類似於write_condition機制的其實都是提供的樂觀鎖。


兩種鎖各有優缺點,不可認爲一種好於另一種,像樂觀鎖適用於寫比較少的情況下,即衝突真的很少發生的時候,這樣可以省去了鎖的開銷,加大了系統的整個吞吐量。但如果經常產生衝突,上層應用會不斷的進行retry,這樣反倒是降低了性能,所以這種情況下用悲觀鎖就比較合適。





Hibernate支持樂觀鎖。當多個事務同時對數據庫表中的同一條數據操作時,如果沒有加鎖機制的話,就會產生髒數據(duty data)。Hibernate有2種機制可以解決這個問題:樂觀鎖和悲觀鎖。這裏我們只討論樂觀鎖。 

     Hibernate樂觀鎖,能自動檢測多個事務對同一條數據進行的操作,並根據先勝原則,提交第一個事務,其他的事務提交時則拋出org.hibernate.StaleObjectStateException異常。 
    Hibernate樂觀鎖是怎麼做到的呢? 
    我們先從Hibernate樂觀鎖的實現說起。要實現Hibenate樂觀鎖,我們首先要在數據庫表裏增加一個版本控制字段,字段名隨意,比如就叫version,對應hibernate類型只能爲 long,integer,short,timestamp,calendar,也就是隻能爲數字或timestamp類型。然後在hibernate mapping裏作如下類似定義: 

    <version name="version" 
        column="VERSION" 
        type="integer"/> 

告訴Hibernate version作爲版本控制用,交由它管理。 
當然在entity class裏也需要給version加上定義,定義的方法跟其他字段完全一樣。 
private Integer version; 
… 
// setVersion() && getVersion(Integer) 

Hibernate樂觀鎖的的使用: 
Session session1 = sessionFactory.openSession(); 
Session session2 = sessionFactory.openSession(); 
MyEntity et1 = session1.load(MyEntity.class, id); 
MyEntity et2 = session2.load(MyEntity.class, id); 
//這裏 et1, et2爲同一條數據 
Transaction tx1 = session1.beginTransaction(); 
//事務1開始 
et1.setName(“Entity1”); 
//事務1中對該數據修改 
tx1.commit(); 
session1.close(); 
//事務1提交 
Transaction tx2 = session2.beginTransaction(); 
//事務2開始 
et2.setName(“Entity2”); 
//事務2中對該數據修改 
tx2.commit(); 
session2.close(); 
//事務2提交 

在事務2提交時,因爲它提交的數據比事務1提交後的數據舊,所以hibernate會拋出一個org.hibernate.StaleObjectStateException異常。 
回到前面的問題,Hibernate怎麼知道事務2提交的數據比事務1提交後的數據舊呢? 
因爲MyEntity有個version版本控制字段。 

回頭看看上面的源代碼中的: 
MyEntity et1 = session1.load(MyEntity.class, id); 
MyEntity et2 = session2.load(MyEntity.class, id); 
這裏,et1.version==et2.version,比如此時version=1, 
當事務1提交後,該數據的版本控制字段version=version+1=2,而事務2提交時version=1<2所以Hibernate認爲事務2提交的數據爲過時數據,拋出異常。 
這就是Hibernate樂觀鎖的原理機制。 
我們已經知道了Hibernate樂觀鎖是根據version的值來判斷數據是否過時,也就是說,在向數據庫update某數據時,必須保證該entity 裏的version字段被正確地設置爲update之前的值,否則hibernate樂觀鎖機制將無法根據version作出正確的判斷。 

在我們的WEB應用中,尤其應該注意這個問題。



鎖( locking,這個概念在我們學習多線程的時候曾經接觸過,其實這裏的鎖和多線程裏面處理併發的鎖是一個道理,都是暴力的把資源歸爲自己所有。這裏我們用到鎖的目的就是通過一些機制來保證一些數據在某個操作過程中不會被外界修改,這樣的機制,在這裏,也就是所謂的,即給我們選定的目標數據上鎖,使其無法被其他程序修改。Hibernate支持兩種鎖機制:即通常所說的悲觀鎖(Pessimistic Locking 樂觀鎖(Optimistic Locking 
悲觀鎖( Pessimistic Locking
悲觀鎖,正如其名,他是對數據庫而言的,數據庫悲觀了,他感覺每一個對他操作的程序都有可能產生併發。它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處於鎖定狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。
一個典型的倚賴數據庫的悲觀鎖調用:

  1. select * from account wherename=”Erica” forupdate  
[sql] view plain copy
  1. select * from account wherename=”Erica” forupdate  
這條 sql語句鎖定了 account表中所有符合檢索條件(name=Erica)的記錄。本次事務提交之前(事務提交時會釋放事務過程中的鎖),外界無法修改這些記錄。Hibernate的悲觀鎖,也是基於數據庫的鎖機制實現。
下面的代碼實現了對查詢記錄的加鎖:
[java] view plaincopy
  1. String hqlStr ="from TUser as user where user.name=‘Erica‘";  
  2. Query query = session.createQuery(hqlStr);  
  3. query.setLockMode("user",LockMode.UPGRADE); // 加鎖  
  4. List userList = query.list();// 執行查詢,獲取數據  
[java] view plain copy
  1. String hqlStr ="from TUser as user where user.name=‘Erica‘";  
  2. Query query = session.createQuery(hqlStr);  
  3. query.setLockMode("user",LockMode.UPGRADE); // 加鎖  
  4. List userList = query.list();// 執行查詢,獲取數據  
query.setLockMode對查詢語句中,特定別名所對應的記錄進行加鎖(我們爲TUser類指定了一個別名user),這裏也就是對返回的所有 user 記錄進行加鎖。
觀察運行期Hibernate生成的 SQL語句:
  1. select tuser0_.id as id, tuser0_.name as name,tuser0_.group_id  
  2. as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex  
  3. from t_user tuser0_ where (tuser0_.name=‘Erica‘ )for update  
[sql] view plain copy
  1. select tuser0_.id as id, tuser0_.name as name,tuser0_.group_id  
  2. as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex  
  3. from t_user tuser0_ where (tuser0_.name=‘Erica‘ )for update  
這裏Hibernate通過使用數據庫的 for update子句實現了悲觀鎖機制。

hibernate的加鎖模式有:
LockMode.NONE 
無鎖機制。
LockMode.WRITE Hibernate Insert Update記錄的時候會自動獲取。
LockMode.READ Hibernate在讀取記錄的時候會自動獲取。

以上這三種鎖機制一般由 Hibernate內部使用,如Hibernate爲了保證 Update過程中對象不會被外界修改,會在 save 方法實現中自動爲目標對象加上WRITE鎖。
LockMode.UPGRADE 利用數據庫 for update 子句加鎖。
LockMode. UPGRADE_NOWAIT  Oracle的特定實現,利用 oracle for update nowait子句實現加鎖。
上面這兩種鎖機制是我們在應用層較爲常用的,加鎖一般通過以下方法實現:
Criteria.setLockMode
Query.setLockMode
Session.lock

注意,只有在查詢開始之前(也就是 Hiberate生成 SQL之前)設定加鎖,纔會真正通過數據庫的鎖機制進行加鎖處理,否則,數據已經通過不包含 for update子句的 Select SQL加載進來,所謂數據庫加鎖也就無從談起。

Hibernate使用悲觀鎖十分容易,但實際應用中悲觀鎖是很少被使用的,因爲它大大限制了併發性,並且利用數據庫底層來維護鎖,這樣大大降低了應用程序的效率。

下面我們來看一下hibernateAPI中提供的兩個get方法:

GetClassclazzSerializable idLockMode lockMode

GetClassclazzSerializable idLockOptions lockOptions 

可以看到get方法第三個參數"lockMode""lockOptions",注意在Hibernate3.6以上的版本中"LockMode"已經不建議使用。方法的第三個參數就是用來設置悲觀鎖的,使用第三個參數之後,我們每次發送的SQL語句都會加上"for update"用於告訴數據庫鎖定相關數據。LockMode參數選擇UPGRADE選項,就會開啓悲觀鎖。

樂觀鎖(Optimistic Locking

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

Hibernate爲樂觀鎖提供了3中實現:

1.基於version

2.基於timestamp

3.爲遺留項目添加添加樂觀鎖

配置基於version的樂觀鎖:

[html] view plaincopy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">  
  3.   
  4. <hibernate-mapping>  
  5.     <classnameclassname="com.bzu.hibernate.pojos.People"table="people">  
  6.         <idnameidname="id"type="string">  
  7.             <columnnamecolumnname="id"></column>  
  8.             <generatorclassgeneratorclass="uuid"></generator>  
  9.         </id>  
  10.         
  11.         <!--version標籤用於指定表示版本號的字段信息-->  
  12.         <versionnameversionname="version"column="version"type="integer"></version>  
  13.   
  14.         <propertynamepropertyname="name"column="name"type="string"></property>  
  15.         
  16.     </class>  
  17. </hibernate-mapping>  
[html] view plain copy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">  
  3.   
  4. <hibernate-mapping>  
  5.     <classnameclassname="com.bzu.hibernate.pojos.People"table="people">  
  6.         <idnameidname="id"type="string">  
  7.             <columnnamecolumnname="id"></column>  
  8.             <generatorclassgeneratorclass="uuid"></generator>  
  9.         </id>  
  10.         
  11.         <!--version標籤用於指定表示版本號的字段信息-->  
  12.         <versionnameversionname="version"column="version"type="integer"></version>  
  13.   
  14.         <propertynamepropertyname="name"column="name"type="string"></property>  
  15.         
  16.     </class>  
  17. </hibernate-mapping>  

注:不要忘記在實體類添加屬性version

配置基於timestamp的樂觀鎖:

[html] view plaincopy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">  
  3.   
  4. <hibernate-mapping>  
  5.     <classnameclassname="com.suxiaolei.hibernate.pojos.People"table="people">  
  6.         <id name="id"type="string">  
  7.             <column name="id"></column>  
  8.             <generator class="uuid"></generator>  
  9.         </id>  
  10.         
  11.         <!--timestamp標籤用於指定表示版本號的字段信息-->  
  12.         <timestamp name="updateDate"column="updateDate"></timestamp>  
  13.   
  14.         <propertynamepropertyname="name"column="name"type="string"></property>  
  15.   
  16.   
  17.     </class>  
  18. </hibernate-mapping>  
[html] view plain copy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">  
  3.   
  4. <hibernate-mapping>  
  5.     <classnameclassname="com.suxiaolei.hibernate.pojos.People"table="people">  
  6.         <id name="id"type="string">  
  7.             <column name="id"></column>  
  8.             <generator class="uuid"></generator>  
  9.         </id>  
  10.         
  11.         <!--timestamp標籤用於指定表示版本號的字段信息-->  
  12.         <timestamp name="updateDate"column="updateDate"></timestamp>  
  13.   
  14.         <propertynamepropertyname="name"column="name"type="string"></property>  
  15.   
  16.   
  17.     </class>  
  18. </hibernate-mapping>  

下面我們就模擬多個session,基於version的來進行一下測試

[java] view plaincopy
  1. /* 
  2.          * 模擬多個session操作student數據表 
  3.          */  
  4.          
  5.         Sessionsession1=sessionFactory.openSession();  
  6.         Session session2=sessionFactory.openSession();  
  7.         Studentstu1=(Student)session1.createQuery("from Student s wheres.name='tom11'").uniqueResult();  
  8.         Studentstu2=(Student)session2.createQuery("from Student s wheres.name='tom11'").uniqueResult();  
  9.          
  10.         //這時候,兩個版本號是相同的   
  11.        System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());  
  12.          
  13.         Transactiontx1=session1.beginTransaction();  
  14.        stu1.setName("session1");  
  15.         tx1.commit();  
  16.         //這時候,兩個版本號是不同的,其中一個的版本號遞增了   
  17.        System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());  
  18.          
  19.         Transactiontx2=session2.beginTransaction();  
  20.        stu2.setName("session2");  
  21.         tx2.commit();  
[java] view plain copy
  1. /* 
  2.          * 模擬多個session操作student數據表 
  3.          */  
  4.          
  5.         Sessionsession1=sessionFactory.openSession();  
  6.         Session session2=sessionFactory.openSession();  
  7.         Studentstu1=(Student)session1.createQuery("from Student s wheres.name='tom11'").uniqueResult();  
  8.         Studentstu2=(Student)session2.createQuery("from Student s wheres.name='tom11'").uniqueResult();  
  9.          
  10.         //這時候,兩個版本號是相同的  
  11.        System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());  
  12.          
  13.         Transactiontx1=session1.beginTransaction();  
  14.        stu1.setName("session1");  
  15.         tx1.commit();  
  16.         //這時候,兩個版本號是不同的,其中一個的版本號遞增了  
  17.        System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());  
  18.          
  19.         Transactiontx2=session2.beginTransaction();  
  20.        stu2.setName("session2");  
  21.         tx2.commit();  

運行結果:

Hibernate: insert into studentVersion (ver, name,id) values (?, ?, ?)
Hibernate: select student0_.id as id0_, student0_.ver as ver0_, student0_.nameas name0_ from studentVersion student0_ where student0_.name='tom11'
Hibernate: select student0_.id as id0_, student0_.ver as ver0_, student0_.nameas name0_ from studentVersion student0_ where student0_.name='tom11'
v1=0--v2=0
Hibernate: update studentVersion set ver=?, name=? where id=? and ver=?
v1=1--v2=0
Hibernate: update studentVersion set ver=?, name=? where id=? and ver=?
Exception in thread "main" org.hibernate.StaleObjectStateException:Row was updated or deleted by another transaction (or unsaved-value mapping wasincorrect): [Version.Student#4028818316cd6b460116cd6b50830001]

 

     可以看到,第二個用戶session2修改數據時候,記錄的版本號已經被session1更新過了,所以拋出了紅色的異常,我們可以在實際應用中處理這個異常,例如在處理中重新讀取數據庫中的數據,同時將目前的數據與數據庫中的數據展示出來,讓使用者有機會比較一下,或者設計程序自動讀取新的數據


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章