常常用到老搞混,從網上摘了點記錄下來。java
// 業務方法須要在一個事物中運行,若是方法運行時,已經存在一個事物中,spring
// 那麼加入該事物,不然爲本身建立一個新事物。sql
@Transactional(propagation = Propagation.REQUIRED)數據庫
public void test1() {緩存
}服務器
// 聲明方法不須要事務,若是方法沒有關聯到一個事務,容器不會爲它開啓事物。session
// 若是方法在一個事物中被調用,該事物會被掛起,在方法調用結束後,原先的併發
// 事物便會恢復執行。app
@Transactional(propagation = Propagation.NOT_SUPPORTED)框架
public void test2() {
}
// 代表無論是否存在事物,業務方法總會爲本身發起一個新事物。
// 若是方法已經運行在一個事物中,則原有事物會被掛起,
// 新的事物會被建立,直到方法執行結束,新事物纔算結束,
// 原先的事務纔會被恢復執行。
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void test3() {
}
// 該屬性指定業務方法只能在一個已經存在的事物中執行,
// 業務方法不能發起本身的事物,若是業務方法在沒有事物的環境
// 下調用,容器就會拋出異常。
@Transactional(propagation = Propagation.MANDATORY)
public void test4() {
}
// 這個事物屬性代表,若是業務方法在某個事務範圍內被調用,則方法成爲該事物的一部分,
// 若是業務方法在事務範圍外被調用,則方法在沒有事物的環境下執行。
@Transactional(propagation = Propagation.SUPPORTS)
public void test5() {
}
// 指定業務方法絕對不能在事物範圍內執行。若是業務方法在某個事物中執行,
// 容器會拋出異常,只有業務方法沒有關聯到任何事物,才能正常執行。
@Transactional(propagation = Propagation.NEVER)
public void test6() {
}
// 若是一個活動的事物存在,則運行在一個嵌套的事物中,若是沒有活動事物,
// 則按REQUIRED屬性執行,它使用了一個單獨的事物,這個事物擁有多個回滾的保存點,
// 內部事務的回滾不會對外事物形成影響,它只對DataSourceTransactionManager
// 事務管理器起效。
@Transactional(propagation = Propagation.NESTED)
public void test7() {
}
@Transactional(isolation = Isolation.DEFAULT)
public void test8() {
}
// 讀已提交數據(會出現不可重複讀和幻讀)
@Transactional(isolation = Isolation.READ_COMMITTED)
public void test9() {
}
// 讀未提交數據(會出現髒讀、不可重複讀和幻讀)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void test10() {
}
// 可重複讀(會出現幻讀)
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void test11() {
}
// 串行化
@Transactional(isolation = Isolation.SERIALIZABLE)
public void test12() {
}
// 拋出Exception異常時,記錄回滾
@Transactional(rollbackFor = Exception.class)
public void test13() throws Exception {
}
// 拋出Exception異常時,記錄不回滾
@Transactional(noRollbackFor = Exception.class)
public void test14() throws Exception {
}
髒讀:一個事物讀取到另外一個事物未提交的更新數據。
不可重複讀:在同一事物中,屢次讀取同一數據返回的結果有所不一樣。換句話說就是,後續讀取能夠讀到另外一事務已提交的更新數據。
可重複讀:在同一事物中屢次讀取數據時,可以保證所讀數據同樣,也就是,後續讀取不能讀到另外一事務已提交的更新數據。
幻讀:一個事務讀取到另外一事務提交的insert數據。
Property | Type | Description |
---|---|---|
value | String | Optional qualifier specifying the transaction manager to be used. |
propagation | enum: Propagation | Optional propagation setting. |
isolation | enum: Isolation | Optional isolation level. |
readOnly | boolean | Read/write vs. read-only transaction |
timeout | int (in seconds granularity) | Transaction timeout. |
rollbackFor | Array of Class objects, which must be derived from Throwable. | Optional array of exception classes thatmust cause rollback. |
rollbackForClassname | Array of class names. Classes must be derived from Throwable. | Optional array of names of exception classes that must cause rollback. |
noRollbackFor | Array of Class objects, which must be derived from Throwable. | Optional array of exception classes thatmust not cause rollback. |
noRollbackForClassname | Array of String class names, which must be derived from Throwable. | Optional array of names of exception classes that must not cause rollback. |
在service類前加上@Transactional,聲明這個service全部方法須要事務管理。每個業務方法開始時都會打開一個事務。
Spring默認狀況下會對運行期例外(RunTimeException)進行事務回滾。這個例外是unchecked
若是遇到checked意外就不回滾。
如何改變默認規則:
1 讓checked例外也回滾:在整個方法前加上 @Transactional(rollbackFor=Exception.class)
2 讓unchecked例外不回滾: @Transactional(notRollbackFor=RunTimeException.class)
3 不須要事務管理的(只查詢的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED)
注意: 若是異常被try{}catch{}了,事務就不回滾了,若是想讓事務回滾必須再往外拋try{}catch{throw Exception}。
spring——@Transactional事務無論理jdbc,因此要本身把jdbc事務回滾。
@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true),這樣就作成一個只讀事務,能夠提升效率。
各類屬性的意義:
REQUIRED:業務方法須要在一個容器裏運行。若是方法運行時,已經處在一個事務中,那麼加入到這個事務,不然本身新建一個新的事務。
NOT_SUPPORTED:聲明方法不須要事務。若是方法沒有關聯到一個事務,容器不會爲他開啓事務,若是方法在一個事務中被調用,該事務會被掛起,調用結束後,原先的事務會恢復執行。
REQUIRESNEW:無論是否存在事務,該方法總彙爲本身發起一個新的事務。若是方法已經運行在一個事務中,則原有事務掛起,新的事務被建立。
MANDATORY:該方法只能在一個已經存在的事務中執行,業務方法不能發起本身的事務。若是在沒有事務的環境下被調用,容器拋出例外。
SUPPORTS:該方法在某個事務範圍內被調用,則方法成爲該事務的一部分。若是方法在該事務範圍外被調用,該方法就在沒有事務的環境下執行。
NEVER:該方法絕對不能在事務範圍內執行。若是在就拋例外。只有該方法沒有關聯到任何事務,才正常執行。
NESTED:若是一個活動的事務存在,則運行在一個嵌套的事務中。若是沒有活動事務,則按REQUIRED屬性執行。它使用了一個 單獨的事務,這個事務 擁有多個能夠回滾的保存點。內部事務的回滾不會對外部事務形成影響。它只對 DataSourceTransactionManager事務管理器起效。
事務陷阱-1
清單 1. 使用 JDBC 的簡單數據庫插入
view plaincopy to clipboardprint?
@Stateless
public class TradingServiceImpl implements TradingService {
@Resource SessionContext ctx;
@Resource(mappedName="java:jdbc/tradingDS") DataSource ds;
public long insertTrade(TradeData trade) throws Exception {
Connection dbConnection = ds.getConnection();
try {
Statement sql = dbConnection.createStatement();
String stmt =
"INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE, STATE)"
+ "VALUES ("
+ trade.getAcct() + "','"
+ trade.getAction() + "','"
+ trade.getSymbol() + "',"
+ trade.getShares() + ","
+ trade.getPrice() + ",'"
+ trade.getState() + "')";
sql.executeUpdate(stmt, Statement.RETURN_GENERATED_KEYS);
ResultSet rs = sql.getGeneratedKeys();
if (rs.next()) {
return rs.getBigDecimal(1).longValue();
} else {
throw new Exception("Trade Order Insert Failed");
}
} finally {
if (dbConnection != null) dbConnection.close();
}
}
}
@Stateless
public class TradingServiceImpl implements TradingService {
@Resource SessionContext ctx;
@Resource(mappedName="java:jdbc/tradingDS") DataSource ds;
public long insertTrade(TradeData trade) throws Exception {
Connection dbConnection = ds.getConnection();
try {
Statement sql = dbConnection.createStatement();
String stmt =
"INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE, STATE)"
+ "VALUES ("
+ trade.getAcct() + "','"
+ trade.getAction() + "','"
+ trade.getSymbol() + "',"
+ trade.getShares() + ","
+ trade.getPrice() + ",'"
+ trade.getState() + "')";
sql.executeUpdate(stmt, Statement.RETURN_GENERATED_KEYS);
ResultSet rs = sql.getGeneratedKeys();
if (rs.next()) {
return rs.getBigDecimal(1).longValue();
} else {
throw new Exception("Trade Order Insert Failed");
}
} finally {
if (dbConnection != null) dbConnection.close();
}
}
}
清單 1 中的 JDBC 代碼沒有包含任何事務邏輯,它只是在數據庫中保存 TRADE 表中的交易訂單。在本例中,數據庫處理事務邏輯。
在 LUW 中,這是一個不錯的單個數據庫維護操做。可是若是須要在向數據庫插入交易訂單的同時更新賬戶餘款呢?如清單 2 所示:
清單 2. 在同一方法中執行屢次表更新
view plaincopy to clipboardprint?
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
在本例中,insertTrade() 和 updateAcct() 方法使用不帶事務的標準 JDBC 代碼。insertTrade() 方 法結束後,數據庫保存(並提交了)交易訂單。若是 updateAcct() 方法因爲任意緣由失敗,交易訂單仍然會在 placeTrade() 方法 結束時保存在 TRADE 表內,這會致使數據庫出現不一致的數據。若是 placeTrade() 方法使用了事務,這兩個活動都會包含在一 個 LUW 中,若是賬戶更新失敗,交易訂單就會回滾。
事務陷阱-2
隨着 Java 持久性框架的不斷普及,如 Hibernate、TopLink 和 Java 持久 性 API(Java Persistence API,JPA),咱們不多再會去編寫簡單的 JDBC 代碼。更常見的狀況是,咱們使用更新的對象關係 映射(ORM)框架來減輕工做,即用幾個簡單的方法調用替換全部麻煩的 JDBC 代碼。例如,要插入 清單 1 中 JDBC 代碼示例的交易訂單,使 用帶有 JPA 的 Spring Framework,就能夠將 TradeData 對象映射到 TRADE 表,並用清單 3 中的 JPA 代碼 替換全部 JDBC 代碼:
清單 3. 使用 JPA 的簡單插入
view plaincopy to clipboardprint?
public class TradingServiceImpl {
@PersistenceContext(unitName="trading") EntityManager em;
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}
public class TradingServiceImpl {
@PersistenceContext(unitName="trading") EntityManager em;
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}
注意,清單 3 在 EntityManager 上調用了 persist() 方法來插入交易訂單。很簡單,是吧?其實否則。這段代碼不會像預 期那樣向 TRADE 表插入交易訂單,也不會拋出異常。它只是返回一個值 0 做爲交易訂單的鍵,而不會更改數據庫。這是事務處理的主要陷阱之一:基 於 ORM 的框架須要一個事務來觸發對象緩存與數據庫之間的同步。這經過一個事務提交完成,其中會生成 SQL 代碼,數據庫會執行須要的操做(即插 入、更新、刪除)。沒有事務,就不會觸發 ORM 去生成 SQL 代碼和保存更改,所以只會終止方法 — 沒有異常,沒有更新。若是使用基 於 ORM 的框架,就必須利用事務。您再也不依賴數據庫來管理鏈接和提交工做。
這些簡單的示例應該清楚地說明,爲了維護數據完整性和一致性,必須使用事務。不過對於在 Java 平臺中實現事務的複雜性和陷阱而言,這些示例只是涉及了冰山一角。
Spring Framework @Transactional 註釋陷阱-3
清單 4. 使用 @Transactional 註釋
view plaincopy to clipboardprint?
public class TradingServiceImpl {
@PersistenceContext(unitName="trading") EntityManager em;
@Transactional
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}
public class TradingServiceImpl {
@PersistenceContext(unitName="trading") EntityManager em;
@Transactional
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}
如今從新測試代碼,您發現上述方法仍然不能工做。問題在於您必須告訴 Spring Framework,您正在對事務管理應用註釋。除非您進行充 分的單元測試,不然有時候很難發現這個陷阱。這一般只會致使開發人員在 Spring 配置文件中簡單地添加事務邏輯,而不會使用註釋。
要在 Spring 中使用 @Transactional 註釋,必須在 Spring 配置文件中添加如下代碼行:
view plaincopy to clipboardprint?
<tx:annotation-driven transaction-manager="transactionManager"/>
<tx:annotation-driven transaction-manager="transactionManager"/>
transaction-manager 屬性保存一個對在 Spring 配置文件中定義的事務管理器 bean 的引用。這段代碼告 訴 Spring 在應用事務攔截器時使用 @Transaction 註釋。若是沒有它,就會忽略 @Transactional 註釋,致使代碼不會 使用任何事務。
讓基本的 @Transactional 註釋在 清單 4 的代碼中工做僅僅是開始。注意,清單 4 使用 @Transactional 註釋 時沒有指定任何額外的註釋參數。我發現許多開發人員在使用 @Transactional 註釋時並無花時間理解它的做用。例如,像我同樣在清 單 4 中單獨使用 @Transactional 註釋時,事務傳播模式被設置成什麼呢?只讀標誌被設置成什麼呢?事務隔離級別的設置是怎樣的?更重要 的是,事務應什麼時候回滾工做?理解如何使用這個註釋對於 確保在應用程序中得到合適的事務支持級別很是重要。回答我剛纔提出的問題:在單獨使用不帶任何參數 的 @Transactional 註釋時,傳播模式要設置爲 REQUIRED,只讀標誌設置爲 false,事務隔離級別設置 爲 READ_COMMITTED,並且事務不會針對受控異常(checked exception)回滾。
@Transactional 只讀標誌陷阱
我在工做中常常碰到的一個常見陷阱是 Spring @Transactional 註釋中的只讀標誌沒有獲得恰當使用。這裏有一個快速測試方法: 在使用標準 JDBC 代碼得到 Java 持久性時,若是隻讀標誌設置爲 true,傳播模式設置爲 SUPPORTS,清單 5 中 的 @Transactional 註釋的做用是什麼呢?
清單 5. 將只讀標誌與 SUPPORTS 傳播模式結合使用 — JDBC
view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public long insertTrade(TradeData trade) throws Exception {
//JDBC Code...
}
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public long insertTrade(TradeData trade) throws Exception {
//JDBC Code...
}
當執行清單 5 中的 insertTrade() 方法時,猜一猜會獲得下面哪種結果:
拋出一個只讀鏈接異常
正確插入交易訂單並提交數據
什麼也不作,由於傳播級別被設置爲 SUPPORTS
是哪個呢?正確答案是 B。交易訂單會被正確地插入到數據庫中,即便只讀標誌被設置爲 true,且事務傳播模式被設置爲 SUPPORTS。但 這是如何作到的呢?因爲傳播模式被設置爲 SUPPORTS,因此不會啓動任何事物,所以該方法有效地利用了一個本地(數據庫)事務。只讀標誌只在事務啓 動時應用。在本例中,由於沒有啓動任何事 務,因此只讀標誌被忽略。
Spring Framework @Transactional 註釋陷阱-4
清單 6 中的 @Transactional 註釋在設置了只讀標誌且傳播模式被設置爲 REQUIRED 時,它的做用是什麼呢?
清單 6. 將只讀標誌與 REQUIRED 傳播模式結合使用 — JDBC
view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
//JDBC code...
}
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
//JDBC code...
}
執行清單 6 中的 insertTrade() 方法會獲得下面哪種結果呢:
拋出一個只讀鏈接異常
正確插入交易訂單並提交數據
什麼也不作,由於只讀標誌被設置爲 true
根據前面的解釋,這個問題應該很好回答。正確的答案是 A。會拋出一個異常,表示您正在試圖對一個只讀鏈接執行更新。由於啓動了一個事務(REQUIRED),因此鏈接被設置爲只讀。毫無疑問,在試圖執行 SQL 語句時,您會獲得一個異常,告訴您該鏈接是一個只讀鏈接。
關於只讀標誌很奇怪的一點是:要使用它,必須啓動一個事務。若是隻是讀取數據,須要事務嗎?答案是根本不須要。啓動一個事務來執行只讀操做會增長 處 理線程的開銷,並會致使數據庫發生共享讀取鎖定(具體取決於使用的數據庫類型和設置的隔離級別)。總的來講,在獲取基於 JDBC 的 Java 持 久性時,使用只讀標誌有點毫無心義,並會啓動沒必要要的事務而增長額外的開銷。
使用基於 ORM 的框架會怎樣呢?按照上面的測試,若是在結合使用 JPA 和 Hibernate 時調用 insertTrade() 方法,清單 7 中的 @Transactional 註釋會獲得什麼結果?
清單 7. 將只讀標誌與 REQUIRED 傳播模式結合使用 — JPA
view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
清單 7 中的 insertTrade() 方法會獲得下面哪種結果:
拋出一個只讀鏈接異常
正確插入交易訂單並提交數據
什麼也不作,由於 readOnly 標誌被設置爲 true
正確的答案是 B。交易訂單會被準確無誤地插入數據庫中。請注意,上一示例代表,在使用 REQUIRED 傳播模式時,會拋出一個只讀鏈接異常。 使用 JDBC 時是這樣。使用基於 ORM 的框架時,只讀標誌只是對數據庫的一個提示,而且一條基於 ORM 框架的指令(本例中 是 Hibernate)將對象緩存的 flush 模式設置爲 NEVER,表示在這個工做單元中,該對象緩存不該與數據庫同步。不 過,REQUIRED 傳播模式會覆蓋全部這些內容,容許事務啓動並工做,就好像沒有設置只讀標誌同樣。
這令我想到了另外一個我常常碰到的主要陷阱。閱讀了前面的全部內容後,您認爲若是隻對 @Transactional 註釋設置只讀標誌,清單 8 中的代碼會獲得什麼結果呢?
清單 8. 使用只讀標誌 — JPA
view plaincopy to clipboardprint?
@Transactional(readOnly = true)
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}
@Transactional(readOnly = true)
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}
清單 8 中的 getTrade() 方法會執行如下哪種操做?
啓動一個事務,獲取交易訂單,而後提交事務
獲取交易訂單,但不啓動事務
正確的答案是 A。一個事務會被啓動並提交。不要忘了,@Transactional 註釋的默認傳播模式是 REQUIRED。這意味着事務會在 沒必要要的狀況下啓動。根據使用的數據庫,這會引發沒必要要的共享鎖,可能會使數據庫中出現死鎖的狀況。此外,啓動和中止 事務將消耗沒必要要的處理時間和資 源。總的來講,在使用基於 ORM 的框架時,只讀標誌基本上毫無用處,在大多數狀況下會被忽略。但若是您堅持使用它,請記得將傳播模式設置 爲 SUPPORTS(如清單 9 所示),這樣就不會啓動事務:
清單 9. 使用只讀標誌和 SUPPORTS 傳播模式進行選擇操做
view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}
@Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}
另外,在執行讀取操做時,避免使用 @Transactional 註釋,如清單 10 所示:
清單 10. 刪除 @Transactional 註釋進行選擇操做
view plaincopy to clipboardprint?
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}
public TradeData getTrade(long tradeId) throws Exception {
return em.find(TradeData.class, tradeId);
}
REQUIRES_NEW 事務屬性陷阱
無論是使用 Spring Framework,仍是使用 EJB,使用 REQUIRES_NEW 事務屬性都會獲得很差的結果並致使數據損壞和 不一致。REQUIRES_NEW 事務屬性老是會在啓動方法時啓動一個新的事務。許多開發人員都錯誤地使用 REQUIRES_NEW 屬性,認爲它是 確保事務啓動的正確方法。
Spring Framework @Transactional 註釋陷阱-5
清單 11. 使用 REQUIRES_NEW 事務屬性
view plaincopy to clipboardprint?
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {...}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void updateAcct(TradeData trade) throws Exception {...}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {...}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void updateAcct(TradeData trade) throws Exception {...}
注意,清單 11 中的兩個方法都是公共方法,這意味着它們能夠單獨調用。當使用 REQUIRES_NEW 屬性的幾個方法經過服務間通訊或編排 在同一邏輯工做單元內調用時,該屬性就會出現問題。例如,假設在清單 11 中,您能夠獨立於一些用例中的任何其餘方法來調 用 updateAcct() 方法,但也有在 insertTrade() 方法中調用 updateAcct() 方法的狀況。如今若是調 用 updateAcct() 方法後拋出異常,交易訂單就會回滾,但賬戶更新將會提交給數據庫,如清單 12 所示:
清單 12. 使用 REQUIRES_NEW 事務屬性的屢次更新
view plaincopy to clipboardprint?
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
updateAcct(trade);
//exception occurs here! Trade rolled back but account update is not!
...
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
updateAcct(trade);
//exception occurs here! Trade rolled back but account update is not!
...
}
之因此會發生這種狀況是由於 updateAcct() 方法中啓動了一個新事務,因此在 updateAcct() 方法結束後,事務將被提交。 使用 REQUIRES_NEW 事務屬性時,若是存在現有事務上下文,當前的事務會被掛起並啓動一個新事務。方法結束後,新的事務被提交,原來的事務繼 續執行。
因爲這種行爲,只有在被調用方法中的數據庫操做須要保存到數據庫中,而無論覆蓋事務的結果如什麼時候,才應該使用 REQUIRES_NEW 事務屬 性。好比,假設嘗試的全部股票交易都必須被記錄在一個審計數據庫中。出於驗證錯誤、資金不足或其餘緣由,無論交易是否失敗,這條信息都須要被持久 化。如 果沒有對審計方法使用 REQUIRES_NEW 屬性,審計記錄就會連同嘗試執行的交易一塊兒回滾。使用 REQUIRES_NEW 屬性能夠確保無論初 始事務的結果如何,審計數據都會被保存。這裏要注意的一點是,要始終使用 MANDATORY 或 REQUIRED 屬性,而不 是 REQUIRES_NEW,除非您有足夠的理由來使用它,相似審計示例中的那些理由。
事務回滾陷阱
我將最多見的事務陷阱留到最後來說。遺憾的是,我在生產代碼中屢次遇到這個錯誤。我首先從 Spring Framework 開始,而後介紹 EJB 3。
到目前爲止,您研究的代碼相似清單 13 所示:
清單 13. 沒有回滾支持
view plaincopy to clipboardprint?
@Transactional(propagation=Propagation.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
@Transactional(propagation=Propagation.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
假設賬戶中沒有足夠的資金來購買須要的股票,或者尚未準備購買或出售股票,並拋出了一個受控異常(例 如 FundsNotAvailableException),那麼交易訂單會保存在數據庫中嗎?仍是整個邏輯工做單元將執行回滾?答案出乎意料:根據受 控異 常(無論是在 Spring Framework 中仍是在 EJB 中),事務會提交它還未提交的全部工做。使用清單 13,這意味着,若是在執 行 updateAcct() 方法期間拋出受控異常,就會保存交易訂單,但不會更新賬戶來反映交易狀況。
這多是在使用事務時出現的主要數據完整性和一致性問題了。運行時異常(即非受控異常)自動強制執行整個邏輯工做單元的回滾,但受控異常不會。所以,清單 13 中的代碼從事務角度來講毫無用處;儘管看上去它使用事務來維護原子性和一致性,但事實上並無。
儘管這種行爲看起來很奇怪,但這樣作自有它的道理。首先,不是全部受控異常都是很差的;它們可用於事件通知或根據某些條件重定向處理。但更重要 的 是,應用程序代碼會對某些類型的受控異常採起糾正操做,從而使事務所有完成。例如,考慮下面一種場景:您正在爲在線書籍零售商編寫代碼。要完成圖書的 訂 單,您須要將電子郵件形式的確認函做爲訂單處理的一部分發送。若是電子郵件服務器關閉,您將發送某種形式的 SMTP 受控異常,表示郵件沒法發送。 若是受控異常引發自動回滾,整個圖書訂單就會因爲電子郵件服務器的關閉所有回滾。經過禁止自動回滾受控異常,您能夠捕獲該異 常並執行某種糾正操做(如向 掛起隊列發送消息),而後提交剩餘的訂單。
Spring Framework @Transactional 註釋陷阱-6
使用 Declarative 事務模式時,必須指定容器或框架應該如何處理受控異常。在 Spring Framework 中,經過 @Transactional 註釋中的 rollbackFor 參數進行指定,如清單 14 所示:
清單 14. 添加事務回滾支持 — Spring
view plaincopy to clipboardprint?
@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
@Transactional(propagation=Propagation.REQUIRED, rollbackFor=Exception.class)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
注意,@Transactional 註釋中使用了 rollbackFor 參數。這個參數接受一個單一異常類或一組異常類,您也可使 用 rollbackForClassName 參數將異常的名稱指定爲 Java String 類型。還可使用此屬性的相反形式 (noRollbackFor)指定除某些異常之外的全部異常應該強制回滾。一般大多數開發人員指定 Exception.class 做爲值,表示該方 法中的全部異常應該強制回滾。
在回滾事務這一點上,EJB 的工做方式與 Spring Framework 稍微有點不一樣。EJB 3.0 規範中 的 @TransactionAttribute 註釋不包含指定回滾行爲的指令。必須使 用 SessionContext.setRollbackOnly() 方法將事務標記爲執行回滾,如清單 15 所示:
清單 15. 添加事務回滾支持 — EJB
view plaincopy to clipboardprint?
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
sessionCtx.setRollbackOnly();
throw up;
}
}
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
sessionCtx.setRollbackOnly();
throw up;
}
}
調用 setRollbackOnly() 方法後,就不能改變主意了;唯一可能的結果是在啓動事務的方法完成後回滾事務。本系列後續文章中描述的事務策略將介紹什麼時候、何處使用回滾指令,以及什麼時候使用 REQUIRED 與 MANDATORY 事務屬性。
Isolation Level(事務隔離等級)
一、Serializable:最嚴格的級別,事務串行執行,資源消耗最大;
二、REPEATABLE READ:保證了一個事務不會修改已經由另外一個事務讀取但未提交(回滾)的數據。避免了「髒讀取」和「不可重複讀取」的狀況,可是帶來了更多的性能損失。
三、READ COMMITTED:大多數主流數據庫的默認事務等級,保證了一個事務不會讀到另外一個並行事務已修改但未提交的數據,避免了「髒讀取」。該級別適用於大多數系統。
四、Read Uncommitted:保證了讀取過程當中不會讀取到非法數據。隔離級別在於處理多事務的併發問題。
咱們知道並行能夠提升數據庫的吞吐量和效率,可是並非全部的併發事務均可以併發運行。
咱們首先說併發中可能發生的3中不討人喜歡的事情
1: Dirty reads--讀髒數據。也就是說,好比事務A的未提交(還依然緩存)的數據被事務B讀走,若是事務A失敗回滾,會致使事務B所讀取的的數據是錯誤的。
2: non-repeatable reads--數據不可重複讀。好比事務A中兩處讀取數據-total-的值。在第一讀的時候,total是 100,而後事務B就把total的數據改爲 200,事務A再讀一次,結果就發現,total居然就變成200了,形成事務A數據混亂。
3: phantom reads--幻象讀數據,這個和non-repeatable reads類似,也是同一個事務中屢次讀不一致的問題。但 是non-repeatable reads的不一致是由於他所要取的數據集被改變了(好比total的數據),可是phantom reads所要讀的 數據的不一致卻不是他所要讀的數據集改變,而是他的條件數據集改變。好比 Select account.id where account.name="ppgogo*",第一次讀去了6個符合條件的id,第二次讀取的時候, 因爲事務b把一個賬號的名字由"dd"改 成"ppgogo1",結果取出來了7個數據。
Dirty reads non-repeatable reads phantom reads
Serializable 不會 不會 不會
REPEATABLE READ 不會 不會 會
READ COMMITTED 不會 會 會
Read Uncommitted 會 會 會
readOnly