Hibernate事務和並發控制
++YONG原創,轉載請注明
1. 事務介紹:
1.1. 事務的定義:
事務就是指作為單個邏輯工作單元執行的一組數據操作,這些操作要么必須全部成功,要么必須全部失敗,以保證數據的一致性和完整性。
1.2. 事務具有ACID屬性:
o
原子性(Atomic):事務由一個或多個行為綁在一起組成,好像是一個單獨的工作單元。原子性確保在事務中的所有操作要么都發生,要么都不發生。
o
一致性(Consistent):一旦一個事務結束了(不管成功與否),系統所處的狀態和它的業務規則是一致的。即數據應當不會被破壞。
o
隔離性(Isolated):事務應該允許多個用戶操作同一個數據,一個用戶的操作不會和其他用戶的操作相混淆。
o
持久性(Durable):一旦事務完成,事務的結果應該持久化。
事務的ACID特性是由關系數據庫管理系統(RDBMS)來實現的。
o 數據庫管理系統采用日志來保證事務的原子性、一致性和持久性。日志記錄了事務對數據庫所做的更新,如果某個事務在執行過程中發生錯誤,就可以根據日志,撤銷事務對數據庫已做的更新,使數據庫退回到執行事務前的初始狀態。
o 數據庫管理系統采用鎖機制來實現事務的隔離性。當多個事務同時更新數據庫相同的數據時,只允許持有鎖的事務能更新該數據,其他事務必須等待,直到前一個事務釋放了鎖,其他事務才有機會更新該數據。
2. 數據庫事務聲明:
數據庫系統的客戶程序只要向數據庫系統聲明了一個事務,數據庫系統就會自動保證事務的ACID特性。在JDBC API中,java.sql.Connection類代表一個數據庫連接。它提供了以下方法控制事務:
1.
setAutoCommit(Boolean autoCommit):設置是否自動提交事務。
2.
commit():提交事務。
3.
rollback():撤銷事務。
2.1. JDBC API聲明事務的示例代碼如下:
Connection = null;
PreparedStatement pstmt = null;
try{
con = DriverManager.getConnection(dbUrl, username, password);
//
設置手工提交事務模式
con.setAutoCommit(false);
pstmt = ……;
pstmt.executeUpdate();
//
提交事務
con.commit();
}catch(Exception e){
//
事務回滾
con.rollback();
…..
} finally{
…….
}
|
Hibernate
對
JDBC
進行了輕量級的對象封裝,
Hibernate
本身在設計時並不具備事務處理功能,平時所用的
Hibernate
的事務,只是將底層的
JDBCTransaction
或者
JTATransaction
進行了一下封裝,在外面套上
Transaction
和
Session
的外殼,其實底層都是通過委托底層的
JDBC
或
JTA
來實現事務的調度功能。
2.2. Hibernate中使用JDBC事務:
要在
Hibernate
中使用事務,可以配置
Hibernate
事務為
JDBCTransaction
或者
JTATransaction
,這兩種事務的生命周期不一樣,可以在
hibernate.cfg.xml
中指定使用的是哪一種事務。以下配置為使用
JDBC
事務。注:如果不進行配置,
Hibernate
也會默認使用
JDBC
事務。
<session-factory>
……
<property name="hibernate.transaction.factory_class">
org.hibernate.transaction.JDBCTransactionFactory
</property>
……
</session-factory>
|
Hibernate 使用JDBC transaction處理方式如下所示:
Transaction tx = null;
try {
tx = sess.beginTransaction();
// do some work
...
tx.commit();
}
catch (RuntimeException e) {
if (tx != null) tx.rollback();
throw e; // or display error message
}
finally {
sess.close();
}
|
2.3. Hibernate中使用JTA事務:
JTA(java Transaction API)
是事務服務的
JavaEE
解決方案。本質上,它是描述事務接口的
JavaEE
模型的一部分。
JTA
具有的
3
個接口:
UserTransaction
接口、
TransactionManager
接口和
Transaction
接口,這些接口共享公共的事務操作。
UserTransaction
能夠執行事務划分和基本的事務操作,
TransactionManager
能夠執行上下文管理。
在一個具有多個數據庫的系統中,可能一個程序將會調用幾個數據庫中的數據,需要一種分布事務,或者准備用
JTA
來管理
Session
的長事務,那么就需要使用
JTATransaction
。
在
hibernate.cfg.xml
中配置
JTA
事務管理:
<session-factory>
……
<property name="hibernate.transaction.factory_class">
org.hibernate.transaction.JTATransactionFactory
</property>
……
</session-factory>
|
下面是一個實際應用的
JTA
示例:
// BMT(bean管理事務) idiom with getCurrentSession()
try {
UserTransaction tx = (UserTransaction)new InitialContext()
.lookup("java:comp/UserTransaction");
tx.begin();
// Do some work on Session bound to transaction
factory.getCurrentSession().load(...);
factory.getCurrentSession().persist(...);
tx.commit();
}
catch (RuntimeException e) {
tx.rollback();
throw e; // or display error message
}
|
在CMT方式下,事務聲明是在session bean的部署描述符中,而不需要編程。 因此,代碼被簡化為:
// CMT idiom
Session sess = factory.getCurrentSession();
// do some work
...
|
3. 多個事務並發引起的問題:
3.1.
第一類丟失更新:撤消一個事務時,把其它事務已提交的更新的數據覆蓋了。
3.2.
臟讀:一個事務讀到另一個事務未提交的更新數據。
3.3.
幻讀:一個事務執行兩次查詢,但第二次查詢比第一次查詢多出了一些數據行。
3.4.
不可重復讀:一個事務兩次讀同一行數據,可是這兩次讀到的數據不一樣。
3.5.
第二類丟失更新:這是不可重復讀中的特例,一個事務覆蓋另一個事務已提交的更新數據。
4. 事務隔離級別:
為了解決多個事務並發會引發的問題。數據庫系統提供了四種事務隔離級別供用戶選擇。
o Serializable:串行化。隔離級別最高
o Repeatable Read:可重復讀。
o Read Committed:讀已提交數據。
o Read Uncommitted:讀未提交數據。隔離級別最差。
數據庫系統采用不同的鎖類型來實現以上四種隔離級別,具體的實現過程對用戶是透明的。用戶應該關心的是如何選擇合適的隔離級別。
對於多數應用程序,可以優先考慮把數據庫系統的隔離級別設為Read Committed,它能夠避免臟讀,而且具有較好的並發性能。
每個數據庫連接都有一個全局變量@@tx_isolation,表示當前的事務隔離級別。
JDBC
數據庫連接使用數據庫系統默認的隔離級別。在Hibernate的配置文件中可以顯示地設置隔離級別。每一種隔離級別對應着一個正整數。
Read Uncommitted: 1
Read Committed: 2
Repeatable Read: 4
Serializable: 8
在hibernate.cfg.xml中設置隔離級別如下:
<session-factory>
<!-- 設置JDBC的隔離級別 -->
<property name="hibernate.connection.isolation">2</property>
</session-factory>
|
設置之后,在開始一個事務之前,Hibernate將為從連接池中獲得的JDBC連接設置級別。需要注意的是,在受管理環境中,
如果
Hibernate
使用的數據庫連接來自於應用服務器提供的數據源,Hibernate
不會改變這些連接的事務隔離級別。在這種情況下,應該通過修改應用服務器的數據源配置來修改隔離級別。
5. 並發控制:
當數據庫系統采用Red Committed隔離級別時,會導致不可重復讀和第二類丟失更新的並發問題,在可能出現這種問題的場合。可以
在應用程序中采用悲觀鎖或樂觀鎖來避免這類問題。
5.1. 樂觀鎖(Optimistic Locking):
樂觀鎖假定當前事務操縱數據資源時,不會有其他事務同時訪問該數據資源,因此不作數據庫層次上的鎖定。為了維護正確的數據,樂觀鎖使用應用程序上的版本控制(由程序邏輯來實現的)來避免可能出現的並發問題。
唯一能夠同時保持高並發和高可伸縮性的方法就是使用帶版本化的樂觀並發控制。版本檢查使用版本號、 或者時間戳來檢測更新沖突(並且防止更新丟失)。
5.1.1.
使用版本檢查(<version>)
:
Hibernate中通過版本號檢查來實現后更新為主,這也是Hibernate推薦的方式。在數據庫表中加入一個version(版本)字段,在讀取數據時連同版本號一起讀取,並在更新數據時比較版本號與數據庫表中的版本號,如果等於數據庫表中的版本號則予以更新,並遞增版本號,如果小於數據庫表中的版本號就拋出異常。
使用<version>進行版本控制的步驟:
1)
在持久化類中定義一個代表版本號的屬性:
package org.qiujy.domain.versionchecking;
import java.util.Date;
public class Product implements java.io.Serializable{
private Long id ;
/** 版本號 */
private int version;
private String name; //產品名
private String description; //描述--簡介
private Double unitCost; //單價
private Date pubTime; //生產日期
public Product(){}
//以下為getter()和setter()方法
}
|
2)
在Product.hbm.xml文件中用<version>元素來建立Product類的version屬性與表中version字段的映射。
3)
Hibernate在其數據庫訪問引擎中內置了樂觀鎖定實現,默認也是選擇version方式作為Hibernate樂觀鎖定實現機制。所以,在配置文件及程序中可以不作其它設置。按往常一樣寫操作代碼。
package org.qiujy.domain.versionchecking;
import java.util.Date;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.qiujy.common.HibernateSessionFactory;
public class TestVersionChecking {
public static void main(String[] args) {
Product prod = new Product();
prod.setName("IBM thinkPad T60");
prod.setUnitCost(new Double(26000.00));
prod.setDescription("筆記本電腦");
prod.setPubTime(new Date());
//test start.......
Session session = HibernateSessionFactory.getSession();
Transaction tx =null;
try{
tx = session.beginTransaction();
session.save(prod);
tx.commit();
}catch(HibernateException e){
if(tx != null){
tx.rollback();
}
e.printStackTrace();
}finally{
HibernateSessionFactory.closeSession();
}
//進行更新 測試..
prod.setDescription("新款的");
Session session2 = HibernateSessionFactory.getSession();
Transaction tx2 =null;
try{
tx2 = session2.beginTransaction();
session2.update(prod);
tx2.commit();
}catch(HibernateException e){
if(tx2 != null){
tx2.rollback();
}
e.printStackTrace();
}finally{
HibernateSessionFactory.closeSession();
}
}
}
|
新增數據時產生的SQL是:
insert into products (version, name, description, unitCost, pubTime)
values(?, ?, ?, ?, ?)
|
程序無需為Product對象的version屬性顯示賦值,當持久化一個Product對象時,Hibernate會自動為它賦初始值為0。
更新數據時產生的SQL是:
update
products
set
version=?,
name=?,
description=?,
unitCost=?,
pubTime=?
where
id=?
and version=?
|
當Hibernate更新一個Product對象,會根據它的id和version屬性到相應的數據庫表中定位匹配的記錄,如果存在這條匹配的記錄,就更新記錄,並且把version字段的值加1。若找不到匹配的記錄,此時Hibernate會拋出StaleObjectStateException。
需要注意的是,由於樂觀鎖定是使用系統中的程序來控制,而不是使用數據庫中的鎖定機制,因而如果有人故意自行更新版本信息來超過檢查,則鎖定機制就無效。所以建議把持久化類中的version的get方法設置為private的。
5.1.2.
使用時間戳(<timestamp>)
:
跟版本檢查的用法相似。不再多說。
5.2. 悲觀鎖(Pessimistic Locking):
悲觀鎖假定當前事務操縱數據資源時,肯定還會有其他事務同時訪問該數據資源,為了避免當前事務的操作受到干擾,先鎖定資源。盡管悲觀鎖能夠防止丟失更新和不可重復讀這類並發問題,但是它影響並發性能,因此應該很謹慎地使用悲觀鎖。