1、Spring中事務管理的API
事務是指邏輯上要麼全部成功、要麼全部失敗的一組操作。例如用戶A給用戶B轉賬,則用戶A賬戶餘額減少、用戶B賬戶增加這兩個操作就是一組事務,必須全部成功或失敗撤回操作,不能出現A賬戶餘額減少,B增加失敗的情況。事務具有如下幾個特性:
- 原子性:事務的操作不可分割
- 一致性:賬戶A的減少和B的增加一起發生
- 隔離性:在多個事務操作時,彼此之間互不干擾
- 持久性:事務提交到數據庫之後永久的改變
spring提供了三個接口用於實現事務的管理,在進行事務管理時,spring首先會讀取TransactionDefinition中隔離、傳播、超時等事務定義信息,然後PlatformTransactionManager會根據這些信息進行事務管理,然後將產生的事務狀態保存在TransactionStatus中
PlatformTransactionManager
spring爲不同的持久層框架提供了相應的PlatformTransactionManager接口實現,spring JDBC和MyBatis對應DataSourceTransactionManager、Hibernate對應HibernateTransactionManager,還有JPA、Jdo、JTA等,不同的持久層使用不同的實現類
org.springframework.jdbc.datasource.DataSourceTransaction.Manager
|
Spring JDBC或iBatis逬行持久化數據時使用
|
org.springframework.orm.hibernate3.HibernateTransactionManager
|
Hibernate3.0版本進行持久化數據時使用
|
org.springframework.orm.jpa.JpaTransactionManager
|
JPA進行持久化時使用
|
org.springframework.jdo.JdoTransaction Manager
|
當持久化機制是Jdo時使用
|
org.springframework.transactionjta.JtaTransactionManager
|
JTA管理事務,在一個事務跨越多 個資源時必須使用
|
TransactionDefinition
事務定義信息接口中ISOLATION開頭的常量用於定義事務的隔離級別、PROPAGATION開頭定義事務傳播行爲、TIMEOUT_DEFAULT定義超時時間。
隔離級別用於解決多個事務提交時可能出現的髒讀、不可重複讀、
- 髒讀:一個事務讀取了另一個事務還未提交的數據
- 不可重複讀:一個事務讀取了另一個事務更新(update)前、後的數據,導致前後兩次讀取數據不一致
- 幻讀:一個事務讀取時,另一個事務進行插入(insert),導致讀取到了之前沒有的記錄
事務的隔離級別有如下四種類型,如果爲DEFAULT則使用後端數據庫默認的隔離級別,例如MySQL使用的是repeatable_read,Oracle數據庫使用的是read_committed級別
READ_UNCOMMITED | 允許你讀取還未提交的改變了的數據。可能導致髒、幻、不可重複讀 |
READ_COMMITTED | 允許在併發事努已經提交後讀取。可防止髒讀,但幻讀和不可重複讀仍可發生 |
REPEATABLE_READ | 對相同字段的多次讀取是一致的,除非數據被事務本身改變。可防止髒、不可重複讀,但幻讀仍可能發生。 |
SERIALIZABLE | 完全服從ACID的隔離級別,通過完全鎖定事務中涉及的數據表來確保不發生髒、幻、不可重複讀,但執行效率最低 |
事務的傳播行爲用於解決業務層方法互相調用時如何傳遞事務的問題。例如方法a和b中都用到了事務T,那麼a在調用b時是新建一個事務T還是使用b中的事務T呢?有如下七種傳播方式
PROPAGATION_REQUIRED | 支持當前事務,如果不存在就新建一個 |
PROPAGATION_SUPPORTS | 支持當前事務,如果不存在,就不使用事務 |
PROPAGATION_MANDATORY | 支持當前事務,如果不存在,拋出異常 |
PROPAGATION_REQUIRES_NEW | 如果有事務存在,掛起當前事勞,創建一個新的事務 |
PROPAGATION_NOT_SUPPORTED | 以非事務方式運行,如果有事務存在,掛起當前事務 |
PROPAGATION_NEVER | 以非事務方式運行,如果有事務存在,拋出異常 |
PROPAGATION_NESTED | 如果當前事務存在,則嵌套執行事務 |
TransactionStatus
用於記錄事務是否完成、是否產生保存點、是否可回滾等狀態。
2、編程式事務管理
在Spring中使用事務有兩種方式,第一種是通過手動編寫來實現一個事務。如下左圖所示爲一個用戶賬戶的數據表,通過事務管理實現根據id對賬戶的money進行轉賬,以實現一個賬戶money減少的同時另一個增加。
1、引入jar包。上面右圖所示爲項目的目錄結構,lib文件夾中包含了項目依賴的jar包,包括Spring基礎包commons-logging、spring-core、spring-beans、spring-context、spring-expression、spring-test,以及連接數據庫的spring-jdbc、mysql-connector,事務管理spring-tx,此外要用到c3p0對數據庫連接池進行管理,除了引入c3p0包之外還需要引入mchange-commons包纔可以使用。
2、配置數據源連接,在jdbc.properties文件中完成數據庫連接的配置
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test
jdbc.username=root
jdbc.password=1234
接着在配置文件spring-transaction.xml中引入properties並將屬性注入c3p0的配置中,完成dataSource的配置
<!-- 引入屬性文件jdbc.properties -->
<context:property-placeholder location="jdbc.properties"/>
<!-- 配置c3p0連接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driverClass}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
3、實現數據對象的DAO類,在AccountDao類中通過繼承spring的JdbcDaoSupport類,可以使用JdbcTemplate中的update()方法完成對數據庫的更新操作。由於JdbcDaoSupport需要DataSource獲取數據庫的連接,因此在AccountDao的Bean配置中通過屬性注入c3p0數據源dataSource
package com.transaction;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
public class AccountDao extends JdbcDaoSupport {
public void transferIn(int id,double money){
String sql="UPDATE customers SET money = money + ? WHERE id =?";
this.getJdbcTemplate().update(sql,money,id); //使用JdbDaoSupport類的方法操作數據庫
}
public void transferOut(int id,double money){
String sql="UPDATE customers SET money = money - ? WHERE id =?";
this.getJdbcTemplate().update(sql,money,id);
}
}
<!-- DAO類 -->
<bean class="com.transaction.AccountDao" id="accountDao">
<!-- SpringJdbc需要注入連接池DataSource作爲參數來初始化 -->
<property name="dataSource" ref="dataSource"/>
</bean>
4、實現事務操作Service類。在AccountService類中完成事務操作,spring編程式的事務管理通過Spring的TransactionTemplate類來實現,將一個事務操作放在其execute()方法內完成,execute()需要傳入一個TransactionCallback類作爲參數,這裏採用匿名內部類的方式實現。在內部類中doIntransaction()方法中執行事務操作,通過調用DAO層的方法完成具體數據庫操作
package com.transaction;
import org.springframework.transaction.support.TransactionTemplate;
public class AccountService {
private AccountDao accountDao;
private TransactionTemplate transactionTemplate;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
public void transfer(int inAccount, int outAccount, double money) {
//使用spring的TransactionTemplate進行事務操作
transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
accountDao.transferIn(inAccount, money);
int i = 1/0; //除數爲0會拋出異常
accountDao.transferOut(outAccount, money);
return null;
}
});
}
}
AccountService類中用到了accountDao和transactionTemplate兩個屬性,因此在配置Bean時需要諸如兩個bean對象。accountDao類是之前創建的,那麼transactionTemplate類哪裏來的呢?transactionTemplate是DataSourceTransactionManager創建的,之前提到它是PlatformTransactionManager接口用於spring jdbc的一個實現類。所以需要先創建一個transactionManager的bean,而要創建它還需要注入DataSource作爲屬性。
<!-- 事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 事務管理模板 -->
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager"/>
</bean>
<!-- 業務層類 -->
<bean id="accountService" class="com.transaction.AccountService">
<property name="accountDao" ref="accountDao"/>
<property name="transactionTemplate" ref="transactionTemplate"/>
</bean>
項目的數據流動如下:
最後測試AccountService類,調用accountService對象的transfer()方法從id爲2的向1轉賬50。由於在事務中進行了除以0的操作,事務執行失敗並且回滾,數據庫操作不生效
class AccountServiceTest {
@org.junit.jupiter.api.Test
void transfer() {
ApplicationContext appCtx=new ClassPathXmlApplicationContext("spring-transaction.xml");
AccountService accountService= appCtx.getBean("accountService",AccountService.class);
accountService.transfer(2,1,50);
}
}
3、聲明式事務管理
Spring基於聲明式的事務管理是利用AOP的思想,因此需要額外引入spring-aop和aopalliance兩個jar包。由於SpringAOP的使用有三種方式,所以聲明式事務管理也有三種:通過spring的TransactionProxy、使用<aop>標籤、使用註解的方式
基於TransactionProxy的方式
在accountService類的基礎上通過TransactionProxyBeanFactory產生代理類serviceProxy來對事務進行管理,由於是通過AOP引入的方式,所以原來的accountService類不需要增加transactionTemplate
public class AccountService {
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
public void transfer(int inAccount, int outAccount, double money) {
accountDao.transferIn(inAccount, money);
int i = 1/0; //除數爲0會拋出異常
accountDao.transferOut(outAccount, money);
}
}
xml文件中也不需要對TransactionTemplate進行配置,只需要保留TransactionManager,但是需要新配置代理類serviceProxy,代理配置時需要注入屬性--代理目標對象、事務管理器和事務屬性。在transactionAttributes中通過<prop>鍵值對標籤配置具體transfer()方法的事務屬性,其中PROPAGATION_開頭的是傳播行爲,ISOLATION_開頭的是隔離級別,+開頭的定義發生哪些異常後事務不回滾,-開頭定義發生哪些異常回滾。
<!-- 業務層代理 -->
<bean id="serviceProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<!-- 代理目標 -->
<property name="target" ref="accountService"/>
<!-- 注入事務管理器 -->
<property name="transactionManager" ref="transactionManager"/>
<!-- 配置事務屬性 -->
<property name="transactionAttributes">
<props>
<prop key="transfer">PROPAGATION_REQUIRED,ISOLATION_REPEATABLE_READ,+ArithmeticException</prop>
</props>
</property>
</bean>
在使用時通過serviceProxy獲取代理Bean,通過增強後的代理調用transfer()
@Test
void transfer() {
ApplicationContext appCtx=new ClassPathXmlApplicationContext("spring-transaction.xml");
AccountService accountService= appCtx.getBean("serviceProxy",AccountService.class);
accountService.transfer(1,2,50);
}
基於AspectJ的<aop>標籤
由於上面的方法需要爲每一個事務對象單獨配置一個代理類,較爲繁瑣,所以實際中不常用。使用AspectJ不產生代理類,而是直接織入到accountService中。由於需要使用aspectJ進行織入,所以需要引入aspectjweaver.jar包。此外在xml配置文件中使用到了<tx>和<aop>標籤,需要在<beans>聲明。然後通過<tx>標籤定義事務通知,其中的<tx:method>中可以對具體事務方法進行配置,包括隔離級別isolation、傳播屬性propagation、不回滾的異常。在<aop:config>標籤值對切面進行配置,包括切入點和通知。
<?xml version="1.0" encoding="UTF-8"?>
<!-- 聲明tx、aop等標籤 -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
......
<!-- 配置事務AOP的通知-->
<tx:advice id="serviceAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" no-rollback-for="ArithmeticException"/>
</tx:attributes>
</tx:advice>
<!-- 配置切面 -->
<aop:config>
<!-- 配置切入點 -->
<aop:pointcut id="transferPointcut" expression="execution(* com.transaction.AccountService.*(..))"/>
<!-- 配置通知 -->
<aop:advisor advice-ref="serviceAdvice" pointcut-ref="transferPointcut"/>
</aop:config>
</beans>
由於不產生代理類,可以直接使用accountService對象
@Test
void transfer() {
ApplicationContext appCtx=new ClassPathXmlApplicationContext("spring-transaction.xml");
AccountService accountService= appCtx.getBean("accountService",AccountService.class);
accountService.transfer(1,2,50);
}
基於註解的方式
通過註解來使用spring事務管理代碼更爲簡潔,只需要在xml配置文件中配置事務管理器並開啓事務註解即可
<!-- 事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 開啓基於註解的事務管理 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
接着在具體要使用到事務的類上添加@Transactional註解即可。在註解中可以指定隔離級別、傳播屬性、回滾/不回滾異常等屬性
package com.transaction;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED,
rollbackFor = java.lang.ArithmeticException.class)
public class AccountService {
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
public void transfer(int inAccount, int outAccount, double money) {
accountDao.transferIn(inAccount, money);
int i = 1/0; //除數爲0會拋出異常
accountDao.transferOut(outAccount, money);
}
}