事務有四個特性:ACID
- 原子性(Atomicity):事務是一個原子操作,由一系列動作組成。事務的原子性確保動作要么全部完成,要么完全不起作用。
- 一致性(Consistency):一旦事務完成(不管成功還是失敗),系統必須確保它所建模的業務處于一致的狀態,而不會是部分完成部分失敗。在現實中的數據不應該被破壞。
- 隔離性(Isolation):可能有許多事務會同時處理相同的數據,因此每個事務都應該與其他事務隔離開來,防止數據損壞。
- 持久性(Durability):一旦事務完成,無論發生什么系統錯誤,它的結果都不應該受到影響,這樣就能從任何系統崩潰中恢復過來。通常情況下,事務的結果被寫到持久化存儲器中。
spring事務類圖
Spring 事務屬性分析
在 Spring 中,事務是通過 TransactionDefinition 接口來定義的。該接口包含與事務屬性有關的方法。
public interface TransactionDefinition{
int getIsolationLevel();
int getPropagationBehavior();
int getTimeout();
boolean isReadOnly();
}
事務隔離級別
隔離級別是指若干個并發的事務之間的隔離程度。TransactionDefinition 接口中定義了五個表示隔離級別的常量:
- TransactionDefinition.ISOLATION_DEFAULT:這是默認值,表示使用底層數據庫的默認隔離級別。對大部分數據庫而言,通常這值就是TransactionDefinition.ISOLATION_READ_COMMITTED。
- 讀未提交:TransactionDefinition.ISOLATION_READ_UNCOMMITTED:該隔離級別表示一個事務可以讀取另一個事務修改但還沒有提交的數據。該級別不能防止臟讀和不可重復讀,因此很少使用該隔離級別。
- TransactionDefinition.ISOLATION_READ_COMMITTED:該隔離級別表示一個事務只能讀取另一個事務已經提交的數據。該級別可以防止臟讀,這也是大多數情況下的推薦值。
- TransactionDefinition.ISOLATION_REPEATABLE_READ:該隔離級別表示一個事務在整個過程中可以多次重復執行某個查詢,并且每次返回的記錄都相同。即使在多次查詢之間有新增的數據滿足該查詢,這些新增的記錄也會被忽略。該級別可以防止臟讀和不可重復讀。
- TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止臟讀、不可重復讀以及幻讀。但是這將嚴重影響程序的性能。通常情況下也不會用到該級別。
臟讀、不可重復讀、幻讀
臟讀 :所謂的臟讀,其實就是讀到了別的事務回滾前的臟數據。比如事務B執行過程中修改了數據X,在未提交前,事務A讀取了X,而事務B卻回滾了,這樣事務A就形成了臟讀。
不可重復讀 :不可重復讀字面含義已經很明了了,比如事務A首先讀取了一條數據,然后執行邏輯的時候,事務B將這條數據改變了,然后事務A再次讀取的時候,發現數據不匹配了,就是所謂的不可重復讀了。
幻讀 :事務A首先根據條件索引得到10條數據,然后事務B增加了數據庫一條數據,導致也符合事務A當時的搜索條件,這樣事務A再次搜索發現有11條數據了,就產生了幻讀。
事務傳播行為
事務傳播行為(函數調用,如何處理事務)
所謂事務的傳播行為是指,如果在開始當前事務之前,一個事務上下文已經存在,此時有若干選項可以指定一個事務性方法的執行行為。在TransactionDefinition定義中包括了如下幾個表示傳播行為的常量:
//事務屬性 PROPAGATION_REQUIRED
methodA{
……
methodB();
……
}
//事務屬性 PROPAGATION_REQUIRED
methodB{
……
}
- 1.TransactionDefinition.PROPAGATION_REQUIRED(默認的spring事務傳播級別):如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。這個級別通常能滿足處理大多數的業務場景。
main{
Connection con = null;
try{
con = getConnection();
methodA(); //單獨調用MethodA時,在MethodA內又會調用MethodB
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
closeCon();
}
}
- 2.TransactionDefinition.PROPAGATION_REQUIRES_NEW:從字面即可知道,new,每次都要一個新事務,該傳播級別的特點是,每次都會新建一個事務,并且如果當前有事務,則將上下文中的事務掛起,執行當前新建事務完成以后,上下文事務恢復再執行。
main(){
TransactionManager tm = null;
try{
//獲得一個JTA事務管理器
tm = getTransactionManager();
tm.begin();//開啟一個新的事務
Transaction ts1 = tm.getTransaction();
doSomeThing();
tm.suspend();//掛起當前事務
try{
tm.begin();//重新開啟第二個事務
Transaction ts2 = tm.getTransaction();
methodB();
ts2.commit();//提交第二個事務
} Catch(RunTimeException ex) {
ts2.rollback();//回滾第二個事務
} finally {
//釋放資源
}
//methodB執行完后,恢復第一個事務
tm.resume(ts1);
doSomeThing();
ts1.commit();//提交第一個事務
} catch(RunTimeException ex) {
ts1.rollback();//回滾第一個事務
} finally {
//釋放資源
}
}
- 3.TransactionDefinition.PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。所以說,并非所有的包在transactionTemplate.execute中的代碼都會有事務支持。這個通常是用來處理那些并非原子性的非核心業務邏輯操作。應用場景較少。
- 4.TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式運行,如果當前存在事務,則把當前事務掛起。
- 5.TransactionDefinition.PROPAGATION_NEVER:要求上下文中不能存在事務,一旦有事務,就拋出runtime異常,強制停止執行!這個級別上輩子跟事務有仇。
- 6.TransactionDefinition.PROPAGATION_MANDATORY:該級別的事務要求上下文中必須要存在事務,否則就會拋出異常!配置該方式的傳播級別是有效的控制上下文調用代碼遺漏添加事務控制的保證手段。比如一段代碼不能單獨被調用執行,但是一旦被調用,就必須有事務包含的情況,就可以使用這個傳播級別。
- 7.TransactionDefinition.PROPAGATION_NESTED:如果當前存在事務,則創建一個事務作為當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價于TransactionDefinition.PROPAGATION_REQUIRED。該傳播級別特征是,如果上下文中存在事務,則嵌套事務執行,如果不存在事務,則新建事務。
那么什么是嵌套事務呢?
嵌套是子事務套在父事務中執行,子事務是父事務的一部分,在進入子事務之前,父事務建立一個回滾點,叫save point,然后執行子事務,這個子事務的執行也算是父事務的一部分,然后子事務執行結束,父事務繼續執行。重點就在于那個save point。看幾個問題就明了了:
如果子事務回滾,會發生什么?
父事務會回滾到進入子事務前建立的save point,然后嘗試其他的事務或者其他的業務邏輯,父事務之前的操作不會受到影響,更不會自動回滾。
如果父事務回滾,會發生什么?
父事務回滾,子事務也會跟著回滾!為什么呢,因為父事務結束之前,子事務是不會提交的,我們說子事務是父事務的一部分,正是這個道理。那么:
事務的提交,是什么情況?
是父事務先提交,然后子事務提交,還是子事務先提交,父事務再提交?答案是第二種情況,還是那句話,子事務是父事務的一部分,由父事務統一提交。
現在你再體會一下這個”嵌套“,是不是有那么點意思?
main(){
Connection con = null;
Savepoint savepoint = null;
try{
con = getConnection();
con.setAutoCommit(false);
doSomeThingA();
savepoint = con2.setSavepoint();
try{
methodB();
} catch(RuntimeException ex) {
con.rollback(savepoint);
} finally {
//釋放資源
}
doSomeThingB();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
//釋放資源
}
}
事務常用的兩個屬性:readonly和timeout
- readonly:設置事務為只讀以提升性能。
- timeout:設置事務的超時時間,一般用于防止大事務的發生。還是那句話,事務要盡可能的小!
事務的回滾規則
通常情況下,如果在事務中拋出了未檢查異常(繼承自 RuntimeException 的異常),則默認將回滾事務。如果沒有拋出任何異常,或者拋出了已檢查異常,則仍然提交事務。這通常也是大多數開發者希望的處理方式,也是 EJB 中的默認處理方式。但是,我們可以根據需要人為控制事務在拋出某些未檢查異常時任然提交事務,或者在拋出某些已檢查異常時回滾事務。
Spring 事務管理 API 分析
- TransactionDefinition:
它用于定義一個事務。它包含了事務的靜態屬性,比如:事務傳播行為、超時時間等等。Spring 為我們提供了一個默認的實現類:DefaultTransactionDefinition,該類適用于大多數情況。如果該類不能滿足需求,可以通過實現 TransactionDefinition 接口來實現自己的事務定義。 - PlatformTransactionManager:
用于執行具體的事務操作.
根據底層所使用的不同的持久化 API 或框架,PlatformTransactionManager 的主要實現類大致如下:- DataSourceTransactionManager:適用于使用JDBC和iBatis進行數據持久化操作的情況。
- HibernateTransactionManager:適用于使用Hibernate進行數據持久化操作的情況。
- JpaTransactionManager:適用于使用JPA進行數據持久化操作的情況。
另外還有JtaTransactionManager 、JdoTransactionManager、JmsTransactionManager等等。
- TransactionStatus
返回的TransactionStatus 對象可能代表一個新的或已經存在的事務(如果在當前調用堆棧有一個符合條件的事務)。TransactionStatus 接口提供了一個簡單的控制事務執行和查詢事務狀態的方法。
Spring支持的事務管理類型
編程式事務管理:
這意味你通過編程的方式管理事務,給你帶來極大的靈活性,但是難維護。
用過 Hibernate 的人都知道,我們需要在代碼中顯式調用beginTransaction()、commit()、rollback()等事務管理相關的方法,這就是編程式事務管理。通過 Spring 提供的事務管理 API,我們可以在代碼中靈活控制事務的執行。在底層,Spring 仍然將事務操作委托給底層的持久化框架來執行。
- 基于底層 API 的編程式事務管理
根據PlatformTransactionManager、TransactionDefinition 和 TransactionStatus 三個核心接口,我們完全可以通過編程的方式來進行事務管理。
public class BankServiceImpl implements BankService {
private BankDao bankDao;
private TransactionDefinition txDefinition;
private PlatformTransactionManager txManager;
public boolean transfer(Long fromId,Long toId,double amount) {
TransactionStatus txStatus = txManager.getTransaction(txDefinition);
boolean result = false;
try {
result = bankDao.transfer(fromId,toId,amount);
txManager.commit(txStatus);
} catch(Exception e) {
result = false;
txManager.rollback(txStatus);
System.out.println("Transfer Error!");
}
return result;
}
}
//配置
<?xml version="1.0" encoding="utf-8"?>
<bean id="bankService" class="footmark.spring.core.tx.programmatic.origin.BankServiceImpl">
<property name="bankDao" ref="bankDao"/>
<property name="txManager" ref="transactionManager"/>
<property name="txDefinition">
<bean class="org.springframework.transaction.support.DefaultTransactionDefinition">
<property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
</bean>
</property>
</bean>
- 基于 TransactionTemplate 的編程式事務管理
Spring 在數據訪問層非常常見的模板回調模式。
public class BankServiceImpl implements BankService {
private BankDao bankDao;
private TransactionTemplate transactionTemplate;
public boolean transfer(final Long fromId,final Long toId,final double amount) {
return (Boolean) transactionTemplate.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
Object result;
try {
result = bankDao.transfer(fromId,toId,amount);
} catch(Exception e) {
status.setRollbackOnly();
result = false;
System.out.println("Transfer Error!");
}
return result;
}
});
}
}
//配置
<bean id="bankService"
class="footmark.spring.core.tx.programmatic.template.BankServiceImpl">
<property name="bankDao" ref="bankDao"/>
<property name="transactionTemplate" ref="transactionTemplate"/>
</bean>
聲明式事務管理:
這意味著你可以將業務代碼和事務管理分離,你只需用注解和XML配置來管理事務。
Spring 的聲明式事務管理在底層是建立在 AOP 的基礎之上的。其本質是對方法前后進行攔截,然后在目標方法開始之前創建或者加入一個事務,在執行完目標方法之后根據執行情況提交或者回滾事務。
聲明事務最細粒度只能作用到方法級別,無法做到像編程式事務那樣可以作用到代碼塊級別。
但其簡單,這樣使得純業務代碼不被污染,極大方便后期的代碼維護。
- TransactionInterceptor 攔截器
<beans>
<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="transfer">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
<bean id="bankServiceTarget" class="footmark.spring.core.tx.declare.origin.BankServiceImpl">
<property name="bankDao" ref="bankDao"/>
</bean>
<bean id="bankService" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="bankServiceTarget"/>
<property name="interceptorNames">
<list>
<idref bean="transactionInterceptor"/>
</list>
</property>
</bean>
</beans>
首先,我們配置了一個 TransactionInterceptor 來定義相關的事務規則,他有兩個主要的屬性:一個是 transactionManager,用來指定一個事務管理器,并將具體事務相關的操作委托給它;另一個是 Properties 類型的 transactionAttributes 屬性,它主要用來定義事務規則,該屬性的每一個鍵值對中,鍵指定的是方法名,方法名可以使用通配符,而值就表示相應方法的所應用的事務屬性。
指定事務屬性的取值有較復雜的規則,這在 Spring 中算得上是一件讓人頭疼的事。具體的書寫規則如下:
傳播行為 [,隔離級別] [,只讀屬性] [,超時屬性] [不影響提交的異常] [,導致回滾的異常]
<property name="*Service">
PROPAGATION_REQUIRED,ISOLATION_READ_COMMITTED,TIMEOUT_20,+AbcException,+DefException,-HijException
</property>
配置好了 TransactionInterceptor,我們還需要配置一個 ProxyFactoryBean 來組裝 target 和advice。這也是典型的 Spring AOP 的做法。通過 ProxyFactoryBean 生成的代理類就是織入了事務管理邏輯后的目標類。至此,聲明式事務管理就算是實現了。我們沒有對業務代碼進行任何操作,所有設置均在配置文件中完成,這就是聲明式事務的最大優點。
- TransactionProxyFactoryBean 代理
用于將TransactionInterceptor 和 ProxyFactoryBean 的配置合二為一
<beans>
<bean id="bankServiceTarget" class="footmark.spring.core.tx.declare.classic.BankServiceImpl">
<property name="bankDao" ref="bankDao"/>
</bean>
<bean id="bankService" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<property name="target" ref="bankServiceTarget"/>
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttributes">
<props>
<prop key="transfer">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
</beans>
- 基于 <tx> 命名空間的聲明式事務管理
前面兩種聲明式事務配置方式奠定了 Spring 聲明式事務管理的基石。在此基礎上,Spring 2.x 引入了 <tx> 命名空間,結合使用 <aop> 命名空間,帶給開發人員配置聲明式事務的全新體驗,配置變得更加簡單和靈活。另外,得益于 <aop> 命名空間的切點表達式支持,聲明式事務也變得更加強大。
<beans>
<bean id="bankService" class="footmark.spring.core.tx.declare.namespace.BankServiceImpl">
<property name="bankDao" ref="bankDao"/>
</bean>
<tx:advice id="bankAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="transfer" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="bankPointcut" expression="execution(* *.transfer(..))"/>
<aop:advisor advice-ref="bankAdvice" pointcut-ref="bankPointcut"/>
</aop:config>
</beans>
由于使用了切點表達式,我們就不需要針對每一個業務類創建一個代理對象了。
- 基于 @Transactional 的聲明式事務管理
除了基于命名空間的事務配置方式,Spring 2.x 還引入了基于 Annotation 的方式,具體主要涉及@Transactional 標注。@Transactional 可以作用于接口、接口方法、類以及類方法上。當作用于類上時,該類的所有 public 方法將都具有該類型的事務屬性,同時,我們也可以在方法級別使用該標注來覆蓋類級別的定義。
@Transactional(propagation = Propagation.REQUIRED)
public boolean transfer(Long fromId, Long toId, double amount) {
return bankDao.transfer(fromId, toId, amount);
}
Spring 使用 BeanPostProcessor 來處理 Bean 中的標注,因此我們需要在配置文件中作如下聲明來激活該后處理 Bean
<tx:annotation-driven transaction-manager="transactionManager"/>
雖然 @Transactional 注解可以作用于接口、接口方法、類以及類方法上,但是 Spring 小組建議不要在接口或者接口方法上使用該注解,因為這只有在使用基于接口的代理時它才會生效。另外, @Transactional 注解應該只被應用到 public 方法上,這是由 Spring AOP 的本質決定的。如果你在 protected、private 或者默認可見性的方法上使用 @Transactional 注解,這將被忽略,也不會拋出任何異常。
基于 <tx> 命名空間和基于 @Transactional 的事務聲明方式各有優缺點。基于 <tx> 的方式,其優點是與切點表達式結合,功能強大。利用切點表達式,一個配置可以匹配多個方法,而基于 @Transactional 的方式必須在每一個需要使用事務的方法或者類上用 @Transactional 標注,盡管可能大多數事務的規則是一致的,但是對 @Transactional 而言,也無法重用,必須逐個指定。另一方面,基于 @Transactional 的方式使用起來非常簡單明了,沒有學習成本。開發人員可以根據需要,任選其中一種使用,甚至也可以根據需要混合使用這兩種方式。
Spring框架的事務管理有哪些優點
- 不同的事務API 提供一個統一的編程模式。
- 為編程式事務管理提供了一套簡單的API
- 支持聲明式事務管理。
- 和Spring各種數據訪問抽象層很好得集成
你更傾向用那種事務管理類型?
- 選擇聲明式事務管理,因為它對應用代碼無侵入。
- 聲明式事務管理在可維護性上要優于編程式事務管理,雖然比編程式事務管理(這種方式允許你通過代碼控制事務)少了一點靈活性。
一個聲明式事務的實例
數據庫表
book(isbn, book_name, price)
account(username, balance)
book_stock(isbn, stock)
XML配置
<import resource="applicationContext-db.xml" />
<context:component-scan
base-package="com.springinaction.transaction">
</context:component-scan>
<tx:annotation-driven transaction-manager="txManager"/>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
使用的類
//BookShopDao
public interface BookShopDao {
// 根據書號獲取書的單價
public int findBookPriceByIsbn(String isbn);
// 更新書的庫存,使書號對應的庫存-1
public void updateBookStock(String isbn);
// 更新用戶的賬戶余額:account的balance-price
public void updateUserAccount(String username, int price);
}
//BookShopDaoImpl
@Repository("bookShopDao")
public class BookShopDaoImpl implements BookShopDao {
@Autowired
private JdbcTemplate JdbcTemplate;
@Override
public int findBookPriceByIsbn(String isbn) {
String sql = "SELECT price FROM book WHERE isbn = ?";
return JdbcTemplate.queryForObject(sql, Integer.class, isbn);
}
@Override
public void updateBookStock(String isbn) {
//檢查書的庫存是否足夠,若不夠,則拋出異常
String sql2 = "SELECT stock FROM book_stock WHERE isbn = ?";
int stock = JdbcTemplate.queryForObject(sql2, Integer.class, isbn);
if (stock == 0) {
throw new BookStockException("庫存不足!");
}
String sql = "UPDATE book_stock SET stock = stock - 1 WHERE isbn = ?";
JdbcTemplate.update(sql, isbn);
}
@Override
public void updateUserAccount(String username, int price) {
//檢查余額是否不足,若不足,則拋出異常
String sql2 = "SELECT balance FROM account WHERE username = ?";
int balance = JdbcTemplate.queryForObject(sql2, Integer.class, username);
if (balance < price) {
throw new UserAccountException("余額不足!");
}
String sql = "UPDATE account SET balance = balance - ? WHERE username = ?";
JdbcTemplate.update(sql, price, username);
}
}
//BookShopService
public interface BookShopService {
public void purchase(String username, String isbn);
}
//BookShopServiceImpl
@Service("bookShopService")
public class BookShopServiceImpl implements BookShopService {
@Autowired
private BookShopDao bookShopDao;
/**
* 1.添加事務注解
* 使用propagation 指定事務的傳播行為,即當前的事務方法被另外一個事務方法調用時如何使用事務。
* 默認取值為REQUIRED,即使用調用方法的事務
* REQUIRES_NEW:使用自己的事務,調用的事務方法的事務被掛起。
*
* 2.使用isolation 指定事務的隔離級別,最常用的取值為READ_COMMITTED
* 3.默認情況下 Spring 的聲明式事務對所有的運行時異常進行回滾,也可以通過對應的屬性進行設置。通常情況下,默認值即可。
* 4.使用readOnly 指定事務是否為只讀。 表示這個事務只讀取數據但不更新數據,這樣可以幫助數據庫引擎優化事務。若真的是一個只讀取數據庫值得方法,應設置readOnly=true
* 5.使用timeOut 指定強制回滾之前事務可以占用的時間。
*/
@Transactional(propagation=Propagation.REQUIRES_NEW,
isolation=Isolation.READ_COMMITTED,
noRollbackFor={UserAccountException.class},
readOnly=true, timeout=3)
@Override
public void purchase(String username, String isbn) {
//1.獲取書的單價
int price = bookShopDao.findBookPriceByIsbn(isbn);
//2.更新書的庫存
bookShopDao.updateBookStock(isbn);
//3.更新用戶余額
bookShopDao.updateUserAccount(username, price);
}
}
//Cashier
public interface Cashier {
public void checkout(String username, List<String>isbns);
}
//CashierImpl:CashierImpl.checkout和bookShopService.purchase聯合測試了事務的傳播行為
@Service("cashier")
public class CashierImpl implements Cashier {
@Autowired
private BookShopService bookShopService;
@Transactional
@Override
public void checkout(String username, List<String> isbns) {
for(String isbn : isbns) {
bookShopService.purchase(username, isbn);
}
}
}
//BookStockException
public class BookStockException extends RuntimeException {
private static final long serialVersionUID = 1L;
public BookStockException() {
super();
// TODO Auto-generated constructor stub
}
public BookStockException(String arg0, Throwable arg1, boolean arg2,
boolean arg3) {
super(arg0, arg1, arg2, arg3);
// TODO Auto-generated constructor stub
}
public BookStockException(String arg0, Throwable arg1) {
super(arg0, arg1);
// TODO Auto-generated constructor stub
}
public BookStockException(String arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
public BookStockException(Throwable arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
}
//UserAccountException
public class UserAccountException extends RuntimeException {
private static final long serialVersionUID = 1L;
public UserAccountException() {
super();
// TODO Auto-generated constructor stub
}
public UserAccountException(String arg0, Throwable arg1, boolean arg2,
boolean arg3) {
super(arg0, arg1, arg2, arg3);
// TODO Auto-generated constructor stub
}
public UserAccountException(String arg0, Throwable arg1) {
super(arg0, arg1);
// TODO Auto-generated constructor stub
}
public UserAccountException(String arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
public UserAccountException(Throwable arg0) {
super(arg0);
// TODO Auto-generated constructor stub
}
}