悲觀鎖( Pessimistic Locking )
悲觀鎖,正如其名,他是對數據庫而言的,數據庫悲觀了,他感覺每一個對他操作的程序都有可能產生併發。它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處於鎖定狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。
一個典型的倚賴數據庫的悲觀鎖調用:
- select * from account wherename=”Erica” forupdate
select * from account wherename=”Erica” forupdate
這條 sql 語句鎖定了 account 表中所有符合檢索條件(name=”Erica”)的記錄。本次事務提交之前(事務提交時會釋放事務過程中的鎖),外界無法修改這些記錄。Hibernate 的悲觀鎖,也是基於數據庫的鎖機制實現。
下面的代碼實現了對查詢記錄的加鎖:
- String hqlStr ="from TUser as user where user.name=‘Erica‘";
- Query query = session.createQuery(hqlStr);
- query.setLockMode("user",LockMode.UPGRADE); // 加鎖
- List userList = query.list();// 執行查詢,獲取數據
String hqlStr ="from TUser as user where user.name=‘Erica‘";
Query query = session.createQuery(hqlStr);
query.setLockMode("user",LockMode.UPGRADE); // 加鎖
List userList = query.list();// 執行查詢,獲取數據
query.setLockMode 對查詢語句中,特定別名所對應的記錄進行加鎖(我們爲TUser 類指定了一個別名“user”),這裏也就是對返回的所有 user 記錄進行加鎖。
觀察運行期Hibernate 生成的 SQL 語句:
- select tuser0_.id as id, tuser0_.name as name,tuser0_.group_id
- as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex
- from t_user tuser0_ where (tuser0_.name=‘Erica‘ )for update
select tuser0_.id as id, tuser0_.name as name,tuser0_.group_id
as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex
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方法:
Get(Classclazz,Serializable id,LockMode lockMode)
Get(Classclazz,Serializable id,LockOptions lockOptions )
可以看到get方法第三個參數"lockMode"或"lockOptions",注意在Hibernate3.6以上的版本中"LockMode"已經不建議使用。方法的第三個參數就是用來設置悲觀鎖的,使用第三個參數之後,我們每次發送的SQL語句都會加上"for update"用於告訴數據庫鎖定相關數據。LockMode參數選擇UPGRADE選項,就會開啓悲觀鎖。
樂觀鎖(Optimistic Locking)
相對悲觀鎖而言,樂觀鎖機制採取了更加寬鬆的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨佔性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於數據版本(Version)記錄機制實現。何謂數據版本?即爲數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過爲數據庫表增加一個"version"字段來實現。
樂觀鎖的工作原理:讀取出數據時,將此版本號一同讀出,之後更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認爲是過期數據。
Hibernate爲樂觀鎖提供了3中實現:
1. 基於version
2. 基於timestamp
3. 爲遺留項目添加添加樂觀鎖
配置基於version的樂觀鎖:
- <?xml version="1.0" encoding="utf-8"?>
- <!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
- <hibernate-mapping>
- <classnameclassname="com.bzu.hibernate.pojos.People"table="people">
- <idnameidname="id"type="string">
- <columnnamecolumnname="id"></column>
- <generatorclassgeneratorclass="uuid"></generator>
- </id>
- <!--version標籤用於指定表示版本號的字段信息-->
- <versionnameversionname="version"column="version"type="integer"></version>
- <propertynamepropertyname="name"column="name"type="string"></property>
- </class>
- </hibernate-mapping>
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<classname="com.bzu.hibernate.pojos.People"table="people">
<idname="id"type="string">
<columnname="id"></column>
<generatorclass="uuid"></generator>
</id>
<!--version標籤用於指定表示版本號的字段信息-->
<versionname="version"column="version"type="integer"></version>
<propertyname="name"column="name"type="string"></property>
</class>
</hibernate-mapping>
注:不要忘記在實體類添加屬性version
配置基於timestamp的樂觀鎖:
- <?xml version="1.0" encoding="utf-8"?>
- <!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
- <hibernate-mapping>
- <classnameclassname="com.suxiaolei.hibernate.pojos.People"table="people">
- <id name="id"type="string">
- <column name="id"></column>
- <generator class="uuid"></generator>
- </id>
- <!--timestamp標籤用於指定表示版本號的字段信息-->
- <timestamp name="updateDate"column="updateDate"></timestamp>
- <propertynamepropertyname="name"column="name"type="string"></property>
- </class>
- </hibernate-mapping>
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<classname="com.suxiaolei.hibernate.pojos.People"table="people">
<id name="id"type="string">
<column name="id"></column>
<generator class="uuid"></generator>
</id>
<!--timestamp標籤用於指定表示版本號的字段信息-->
<timestamp name="updateDate"column="updateDate"></timestamp>
<propertyname="name"column="name"type="string"></property>
</class>
</hibernate-mapping>
下面我們就模擬多個session,基於version的來進行一下測試:
- /*
- * 模擬多個session操作student數據表
- */
- Sessionsession1=sessionFactory.openSession();
- Session session2=sessionFactory.openSession();
- Studentstu1=(Student)session1.createQuery("from Student s wheres.name='tom11'").uniqueResult();
- Studentstu2=(Student)session2.createQuery("from Student s wheres.name='tom11'").uniqueResult();
- //這時候,兩個版本號是相同的
- System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());
- Transactiontx1=session1.beginTransaction();
- stu1.setName("session1");
- tx1.commit();
- //這時候,兩個版本號是不同的,其中一個的版本號遞增了
- System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());
- Transactiontx2=session2.beginTransaction();
- stu2.setName("session2");
- tx2.commit();
/*
* 模擬多個session操作student數據表
*/
Sessionsession1=sessionFactory.openSession();
Session session2=sessionFactory.openSession();
Studentstu1=(Student)session1.createQuery("from Student s wheres.name='tom11'").uniqueResult();
Studentstu2=(Student)session2.createQuery("from Student s wheres.name='tom11'").uniqueResult();
//這時候,兩個版本號是相同的
System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());
Transactiontx1=session1.beginTransaction();
stu1.setName("session1");
tx1.commit();
//這時候,兩個版本號是不同的,其中一個的版本號遞增了
System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());
Transactiontx2=session2.beginTransaction();
stu2.setName("session2");
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更新過了,所以拋出了紅色的異常,我們可以在實際應用中處理這個異常,例如在處理中重新讀取數據庫中的數據,同時將目前的數據與數據庫中的數據展示出來,讓使用者有機會比較一下,或者設計程序自動讀取新的數據