簡介
- 大多數時候,開發者極少關注事務管理從而導致大量代碼需要重新開發,或是實現事務的時候沒有注意事務究竟是如何實現的以及在這些場景中需要關注的維度。
- 事務管理的一個重要方面是定義正確的事務邊界,例如事務何時開始,什麼時候應該結束,什麼時候應該在數據庫中提交數據,什麼時候應該回滾(在出現異常的時候)。
- 對於開發人員而言,最重要的是瞭解如何在應用程序中更好的實現事務管理。所以現在讓我們用不同的方式探索事務。
管理事務的方法
事務可以用以下方式管理:
1. 以編程方式,如下所示
EntityManagerFactory factory = Persistence.createEntityManagerFactory("PERSISTENCE_UNIT_NAME"); EntityManager entityManager = entityManagerFactory.createEntityManager(); Transaction transaction = entityManager.getTransaction() try { transaction.begin(); someBusinessCode(); transaction.commit(); } catch(Exception ex) { transaction.rollback(); throw ex; }
優點:
- 代碼中事務的邊界很清晰
缺點:
- 重複的代碼,容易出錯
- 任何錯誤都會產生很大的影響
- 需要編寫大量樣板文件,如果要從此方法調用另一個方法,則還需要在那段代碼中進行管理。
2. 使用Spring管理事務
Spring支持兩類事務管理
- 編程式事務管理:這意味着必須在編程的幫助下管理事務。這提供了極大的靈活性,但很難維護。
- 聲明式事務管理:意味着您將事務管理與業務代碼分開。只能使用註釋或基於XML的配置來管理事務。
強烈建議使用聲明式事務。如果想知道其原因,請閱讀下面的內容,否則,可以直接跳轉到聲明式事務管理實現的部分。
現在,讓我們細緻的分析每一種事務管理方法。
編程式事務管理
Spring Framework提供了兩種編程式事務管理方法。 a. 使用TransactionTemplate (Spring推薦這種實現): Context Xml file:
<!-- Initialization for data source --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/TEST"/> <property name="username" value="root"/> <property name="password" value="password"/> </bean> <!-- Initialization for TransactionManager --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!-- Definition for ServiceImpl bean --> <bean id="serviceImpl" class="com.service.ServiceImpl"> <constructor-arg ref="transactionManager"/> </bean>
Service類:
public class ServiceImpl implements Service { private final TransactionTemplate transactionTemplate; // 使用構造器注入來使用PlatfromTransactionManager public ServiceImpl(PlatformTransactionManager transactionManager) { this.transactionTemplate = new TransactionTemplate(transactionManager); } public Object someServiceMethod() { return transactionTemplate.execute(new TransactionCallback() { //這段代碼在事務上下文中執行 public Object doInTransaction(TransactionStatus status) { updateOperation1(); return resultOfUpdateOperation2(); } }); }}
如果沒有返回值,就使用TransactionCallbackWithoutResult
匿名類。
transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus status) { updateOperation1(); updateOperation2(); } });
-
TransactionTemplate
類的實例是線程安全的,這些實例不包含任何會話狀態。 - 然而
TransactionTemplate
實例確實會維持配置信息狀態,所以即使多個類共享同一個TransactionTemplate
實例,如果一個類需要使用另一種配置的TransactionTemplate
(比如不同的隔離級別),那麼就需要配置兩個不同的實例。
b. 直接使用PlatformTransactionManager
實現
<!-- Initialization for data source --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/TEST"/> <property name="username" value="root"/> <property name="password" value="password"/> </bean> <!-- Initialization for TransactionManager --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean>
public class ServiceImpl implements Service { private PlatformTransactionManager transactionManager; public void setTransactionManager( PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } DefaultTransactionDefinition def = new DefaultTransactionDefinition(); // explicitly setting the transaction name is something that can only be done programmatically def.setName("SomeTxName"); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = txManager.getTransaction(def); try { // execute your business logic here } catch (Exception ex) { txManager.rollback(status); throw ex; } txManager.commit(status); }
在進入聲明式事務管理之前,讓我們看看如何選擇事務管理方式:
- 只有在少量事務操作時,編程式事務管理更佳合適。
- 只能通過編程式事務管理設置事務的名稱
- 當希望顯示管理事務時,應當使用編程式事務管理
- 另一方面,如果您的應用程序具有大量事務操作,則聲明式事務管理是值得的。
- 聲明式事務管理使事務代碼也業務代碼分離,並且配置難度不大。
聲明式事務管理(幾乎用於所有web應用場景)
第一步:在spring應用程序上下文xml文件中定義事務管理器。
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"/>
第二步:通過在spring應用程序上下文XML文件中添加以下條目,打開對事務註釋的支持。
<tx:annotation-driven transaction-manager="txManager"/>
或是在配置類中添加@EnableTransactionManagement
@Configuration @EnableTransactionManagement public class AppConfig { ... }
Spring建議只使用@Transactional來註解具體類(以及具體類的方法),而不是接口。
原因是如果在接口上註解,並且使用基於類的代理(proxy-target-class="true")或是aop(mode="aspectj"),那麼事務註解將無法被識別。
第三步:將註解添加在類(或是類的方法)或是接口(或是接口的方法上)
<tx:annotation-driven proxy-target-class="true">
默認配置爲proxy-target-class="false"
-
@Transactional
註解可以放在接口,接口方法,類或是類方法上 - 如果你希望被註解在方法上的事務和類的事務配置不同,如隔離級別或傳播級別,那麼就在方法上覆蓋類的配置
- 在代理模式中,只有通過代理進入的“外部”方法調用纔會被截獲。這意味着“自我調用”,即目標對象中調用目標對象的其他方法的方法,即使被調用的方法用@Transactional標記,也不會在運行時觸發事務。
現在讓我們瞭解一下@Transactional
的屬性:
@Transactional (isolation=Isolation.READ_COMMITTED)
- 默認值爲
Isolation.DEFAULT
- 大多數場景下,使用默認值即可
- 需要在事務開始之前配置。因爲一旦事務開始,就無法進行配置
READ_COMMITTED 防止髒讀;會發生不可重複的讀取和幻讀。
READ_UNCOMMITTED 會出現髒讀,不可重複讀和幻讀。即可以看到事務尚未提交的數據
REPEATABLE_READ 可重複讀。會出現幻讀
序列化 防止髒讀,幻讀和不可重複讀
@Transactional(timeout=60)
默認爲底層事務系統的默認超時。
當事務超過該時間沒有響應時,則會對底層系統發出回滾請求
@Transactional(propagation=Propagation.REQUIRED)
默認的傳播級別爲Required
。其它的選項如REQUIRES_NEW, MANDATORY, SUPPORTS, NOT_SUPPORTED, NEVER, 和NESTED
REQUIRED 表示如果當前沒有活躍的事務上下文,目標方法將無法運行。如果在調用此方法之前已經啓動了事務管理,那麼它將在相同的事務中繼續,或者在調用此方法時將立即開始新的事務。
REQUIRES_NEW 表示每次調用目標方法時都必須啓動新的事務。如果已有事務,它將暫停。
MANDATORY 表示目標方法需要運行中的事務。如果沒有事務,它將拋出異常。
SUPPORTS 無論是否有事務上下文,目標方法可以執行。如果當前有事務上下文,它將在同一個上下文中運行。如果沒有,它仍將執行。這個選項適合獲取數據的方法。
NOT_SUPPORTED 目標方法無需傳播事務上下文。
NEVER 如果在事務上下文中執行目標方法,則拋出異常
@Transactional (rollbackFor=Exception.class)
- 默認爲
rollbackFor=RunTimeException.class
- 在spring中,這意味着只要事務上下文中拋出
RuntimeException
,事務就會回滾。 - 可用於顯示聲明在某個異常出現時回滾
@Transactional (noRollbackFor=IllegalStateException.class)
如果該異常出現時,則不進行回滾
最後,也是最重要的一個問題,@Transactional
註解究竟應該放在哪一層?Service層還是Dao層?
- Service層是最合適的。服務層應該包含邏輯上進入事務的用戶交互的詳細級用例行爲。
- 在一些CRUD應用中,Service層的業務代碼並不複雜,和Dao層的代碼差不多。在這種場景下可以放置在DAO層
- 如果在DAO層設置事務,而又有多個Service調用了DAO層的方法,那麼將很難管理
- 假如你的Service層是使用Hibernate在獲取對象,而且你還使用懶加載獲取集合。那麼你需要在Service層開啓事務,否則會拋出
LazyInitializationException