一.事務簡介
在學些spring的事務管理之前,我們先來看看數據庫的事務!
事務(transaction):是一系列對系統中數據進行訪問與更新的操作組成的一個程序執行邏輯單元。
下面是事務管理的思維導圖:詳情可以查看文章,https://www.jianshu.com/p/aa35c8703d61
下面我們進入主題,開始學習spring的事務管理。
Spring採用AOP機制完成事務控制,可以實現在不修改原有組件的代碼情況下實現事務控制功能。
二.Spring對事務管理的支持
sping提供了兩種事務管理方式:編程式事務管理和聲明式事務管理。
1.編程式事務
場景:
現在有用戶表user(id,username,pwd、name、gender);
用戶的記錄表record(record_id,user_id,remark,status),其中status字段類型爲int。
現在要刪除某個一個用戶,同時修改這個用戶記錄的status爲廢棄。
通過編碼方式實現事務管理。
Spring實現編程式事務要依賴兩大類:PlatformTransactionManager類和TransactionTemplate類。我們在之前搭好的springMVC項目的基礎上來做,前面的文章基於Spring MVC的前後端分離開發和Spring中的AOP任意一個的基礎上都行。
(1).PlatformTransactionManager類
a.配置數據源事務管理
在spring容器配置文件applicationContext.xml,配置數據源事務管理,完整代碼如下:
<?xml version="1.0" encoding="UTF-8"?>
<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:util="http://www.springframework.org/schema/util" xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 開啓組件掃描 -->
<context:component-scan base-package="com.cdd" />
<!-- 配置要用到其他bean,包括handlerMapping,handlerAdapter等bean,支持@RequestMapping,@RequestBody等註解 -->
<mvc:annotation-driven/>
<!-- 配置數據庫連接池 -->
<bean id="dbcp" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/jsd15077db?useUnicode=true&characterEncoding=utf-8"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 註冊jdbcTemplate的Bean -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dbcp"></property>
</bean>
<!-- 註冊組件bean -->
<bean id="printLog" class="com.cdd.aspect.PrintLog"></bean>
<!-- aop配置 -->
<aop:config>
<!-- 配置切面組件 -->
<aop:aspect ref="printLog">
<!-- 在進入com.cdd.controller包下組件之前(切點),先調用切面組件的callTime方法 -->
<aop:before method="callTime" pointcut="within(com.cdd.controller.*)"/>
</aop:aspect>
</aop:config>
<!-- 開啓aop註解支持,@Aspect,@通知標記 -->
<aop:aspectj-autoproxy />
<!-- 配置數據源事務管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dbcp"></property>
</bean>
</beans>
其他的配置在前面兩篇文章介紹時已經寫好了,我們這次要配置的如下:
b.在com.cdd.dao包下,新建一個dao組件,名稱是 TransactionDao,代碼如下:
package com.cdd.dao;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
@Repository
public class TransactionDao {
@Resource
private PlatformTransactionManager ptm;
@Resource
private DataSource dataSource;
@Resource
private JdbcTemplate jdbcTemplate;
private final static String USER_SQL = "DELETE FROM user WHERE id=4";
private final static String RECORD_SQL = "UPDATE record SET status='hello' WHERE user_id=4";
public void testTransaction1(){
// 定義事務
DefaultTransactionDefinition dtd = new DefaultTransactionDefinition();
//設置隔離機制
dtd.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
//設置傳播行爲
dtd.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
//獲取事務狀態
TransactionStatus status = ptm.getTransaction(dtd);
try{
//DML
int i = jdbcTemplate.update(USER_SQL);
int j = jdbcTemplate.update(RECORD_SQL);
System.out.println(i+","+j);
ptm.commit(status);
System.out.println("沒問題,提交了");
}catch(Exception e){
ptm.rollback(status);
System.out.println("發生異常回滾了");
e.printStackTrace();
}
}
}
我們可以看到在上面代碼中,RECORD_SQL語句中把status字段更新爲“hello”字符串,這個字段類型爲int,所以執行時,肯定會發生異常。
c.測試
我們在com.cdd.test包下編寫測試類TestTransaction類,代碼如下:
package com.cdd.test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.cdd.dao.TransactionDao;
public class TestTransaction {
public static void main(String[] args){
String conf = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(conf);
TransactionDao dao = ac.getBean("transactionDao",TransactionDao.class);
dao.testTransaction1();
}
}
d.查看事務控制是否成功
查看數據庫user表和record表如下:
發現這兩條記錄都還在,說明事物控制成功了,接下來看控制檯:
的確發生了異常,回滾了,也打印出來 “發生異常回滾了” 這句話。
(2).TransactionTemplate類
a.spring容器中的配置上面已經完成了,現在在TransactionDao類中添加testTransaction2方法,代碼如下:
package com.cdd.dao;
import javax.annotation.Resource;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
@Repository
public class TransactionDao {
@Resource
private PlatformTransactionManager ptm;
@Resource
private DataSource dataSource;
@Resource
private JdbcTemplate jdbcTemplate;
private final static String USER_SQL = "DELETE FROM user WHERE id=4";
private final static String RECORD_SQL = "UPDATE record SET status='hello' WHERE user_id=4";
//測試PlatformTransactionManager類實現事務控制
public void testTransaction1(){
// 定義事務
DefaultTransactionDefinition dtd = new DefaultTransactionDefinition();
//設置隔離機制
dtd.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
//設置傳播行爲
dtd.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
//獲取事務狀態
TransactionStatus status = ptm.getTransaction(dtd);
try{
//DML
int i = jdbcTemplate.update(USER_SQL);
int j = jdbcTemplate.update(RECORD_SQL);
System.out.println(i+","+j);
ptm.commit(status);
System.out.println("沒問題,提交了");
}catch(Exception e){
ptm.rollback(status);
System.out.println("發生異常回滾了");
e.printStackTrace();
}
}
//測試TransactionTemplate類實現事務控制
public void testTransaction2(){
//實例化事務目標類
TransactionTemplate tt = new TransactionTemplate(ptm);
//設置隔離機制
tt.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
//設置傳播行爲
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
//重寫事務回調方法
TransactionCallbackWithoutResult tcw = new TransactionCallbackWithoutResult(){
@Override
protected void doInTransactionWithoutResult(TransactionStatus arg0) {
//DML
int i = jdbcTemplate.update(USER_SQL);
int j = jdbcTemplate.update(RECORD_SQL);
}
};
tt.execute(tcw);
}
}
就是這部分:
b.測試
在testTransaction類用調用testTransaction2()方法,代碼如下:
package com.cdd.test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.cdd.dao.TransactionDao;
public class TestTransaction {
public static void main(String[] args){
String conf = "applicationContext.xml";
ApplicationContext ac = new ClassPathXmlApplicationContext(conf);
TransactionDao dao = ac.getBean("transactionDao",TransactionDao.class);
// //測試PlatformTransactionManager類實現事務控制
// dao.testTransaction1();
//測試TransactionTemplate類實現事務控制
dao.testTransaction2();
}
}
c.查看是否成功
先看數據庫,這兩條記錄還在
再看控制檯:
發生異常後,的確回滾了,事務控制成功。在編程式事務管理中,這種方式推薦使用。
2.聲明式事務
場景:
我們要用聲明式事務的方式,給com.cdd.dao包下的UserDao組件的checkLogin()方法配置事務。
(1).xml方式配置
a.在spring容器進行aop配置
這裏的配置是接着上面的聲明式配置的,spring容器配置文件全部代碼如下:
<?xml version="1.0" encoding="UTF-8"?>
<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:util="http://www.springframework.org/schema/util" xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 開啓組件掃描 -->
<context:component-scan base-package="com.cdd" />
<!-- 配置要用到其他bean,包括handlerMapping,handlerAdapter等bean,支持@RequestMapping,@RequestBody等註解 -->
<mvc:annotation-driven/>
<!-- 配置數據庫連接池 -->
<bean id="dbcp" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/jsd15077db?useUnicode=true&characterEncoding=utf-8"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 註冊jdbcTemplate的Bean -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dbcp"></property>
</bean>
<!-- 註冊組件bean -->
<bean id="printLog" class="com.cdd.aspect.PrintLog"></bean>
<!-- aop配置 -->
<aop:config>
<!-- 配置切面組件 -->
<aop:aspect ref="printLog">
<!-- 在進入com.cdd.controller包下組件之前(切點),先調用切面組件的callTime方法 -->
<aop:before method="callTime" pointcut="within(com.cdd.controller.*)"/>
</aop:aspect>
</aop:config>
<!-- 開啓aop註解支持,@Aspect,@通知標記 -->
<aop:aspectj-autoproxy />
<!-- 配置數據源事務管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dbcp"></property>
</bean>
<!-- 配置切面aspect -->
<tx:advice id="txAdvice">
<tx:attributes>
<!-- 配置需要事務管理的方式(給單個方法配置) -->
<tx:method name="checkLogin"/>
<!-- 給以test開頭的方法配置事務 -->
<!-- <tx:method name="check*"/> -->
<!-- 給所有方法配置事務 -->
<!-- <tx:method name="*"/> -->
</tx:attributes>
</tx:advice>
<!-- 配置aop -->
<aop:config>
<aop:pointcut expression="within(com.cdd.dao.UserDao)" id="target"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="target"/>
</aop:config>
</beans>
其他配置是前面已經配好的,在這個步驟我們要配置是下面這部分:
這個就是給com.cdd.dao包UserDao組件的checkLogin()方法配置事務
(2).註解方式配置
a.開啓事務註解@Transacntional
我們先註釋掉剛纔xml形式配置的事務管理,再用聲明式事務方式配置事務管理,spring容器配置文件全部代碼如下:
<?xml version="1.0" encoding="UTF-8"?>
<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:util="http://www.springframework.org/schema/util" xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.2.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 開啓組件掃描 -->
<context:component-scan base-package="com.cdd" />
<!-- 配置要用到其他bean,包括handlerMapping,handlerAdapter等bean,支持@RequestMapping,@RequestBody等註解 -->
<mvc:annotation-driven/>
<!-- 配置數據庫連接池 -->
<bean id="dbcp" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/jsd15077db?useUnicode=true&characterEncoding=utf-8"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 註冊jdbcTemplate的Bean -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dbcp"></property>
</bean>
<!-- 註冊組件bean -->
<bean id="printLog" class="com.cdd.aspect.PrintLog"></bean>
<!-- aop配置 -->
<aop:config>
<!-- 配置切面組件 -->
<aop:aspect ref="printLog">
<!-- 在進入com.cdd.controller包下組件之前(切點),先調用切面組件的callTime方法 -->
<aop:before method="callTime" pointcut="within(com.cdd.controller.*)"/>
</aop:aspect>
</aop:config>
<!-- 開啓aop註解支持,@Aspect,@通知標記 -->
<aop:aspectj-autoproxy />
<!-- 配置數據源事務管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dbcp"></property>
</bean>
<!-- 配置切面aspect -->
<tx:advice id="txAdvice">
<tx:attributes>
<!-- 配置需要事務管理的方式(給單個方法配置) -->
<tx:method name="checkLogin"/>
<!-- 給以test開頭的方法配置事務 -->
<!-- <tx:method name="check*"/> -->
<!-- 給所有方法配置事務 -->
<!-- <tx:method name="*"/> -->
</tx:attributes>
</tx:advice>
<!-- 配置aop -->
<!-- <aop:config>
<aop:pointcut expression="within(com.cdd.dao.UserDao)" id="target"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="target"/>
</aop:config> -->
<!-- 開啓事務註解@Transactionl,當調用@Tranactional標註的組件或方法時,將事務管理功能切入進去 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
在這個步驟中,註釋的是:
在這個步驟中,我們添加的配置是:
b.使用@Transactional註解
給UserDao組件的checkLogin()方法打上@Transactional,即,對這個方法進行事務管理。如果給UserDao組件打上@Transactional註解,則給這個組件的所有方法進行事務管理,UserDao組件的代碼如下:
package com.cdd.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
@Repository //標註數據庫訪問組件
public class UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 驗證用戶登錄
* @param name 用戶登錄名稱
* @param pwd 密碼
* @return
*/
@Transactional
public Integer checkLogin(String name, String pwd){
String sql = "select count(*) from user where username=? and pwd=?";
Object[] args = {name,pwd};
Integer isExist = jdbcTemplate.queryForObject(sql, Integer.class, args);
return isExist;
}
}
聲明式事務是不是簡單多了,我們推薦使用註解方法配置。就是現在說的這種方式。
三.spring對事務管理的控制
1.控制事務可讀可寫性
spring分爲可讀寫事務和只讀事務。
默認爲可讀寫,一般只涉及查詢操作建議用只讀事務,用註解@Transactional(readOnly=ture) 只可讀。
2.控制事務是否回滾
Spring遇到RuntimeExceptioin異常,會事務回滾;遇到非運行時異常不會回滾。
要在非異常時回滾,可用下面的註解方式(指定針對具體的非運行時異常回滾):@Transactional(rollbackFor=IOException.class) //遇到IOException時回滾
相反:
@Transactional(noRollbackFor=IOException.class) //遇到IOException時不回滾
建議:自定義異常繼承運行時異常RuntimeException,這樣默認情況下,自定義的異常發生時會回滾
例如:public MyException extends RuntimeException{ }
3.控制事務傳播類型
spring的事務傳播共有7種類型,如下表所示:
事務類型 | 含義 |
PROPAGATION_REQUIRED | 如果當前沒有事務,就創建一個新事務,如果當前存在事務,就加入該事務,該設置是最常用的設置。 |
PROPAGATION_SUPPORTS | 支持當前事務,如果當前存在事務,就加入該事務,如果當前不存在事務,就以非事務執行。‘ |
PROPAGATION_MANDATORY | 支持當前事務,如果當前存在事務,就加入該事務,如果當前不存在事務,就拋出異常。 |
PROPAGATION_REQUIRES_NEW | 創建新事務,無論當前存不存在事務,都創建新事務。 |
PROPAGATION_NOT_SUPPORTED | 以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。 |
PROPAGATION_NEVER | 以非事務方式執行,如果當前存在事務,則拋出異常。 |
PROPAGATION_NESTED | 如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與PROPAGATION_REQUIRED類似的操作。 |
4.控制事務隔離級別
(1).爲什麼要有事務隔離級別。
在一個項目中,如果事務T1和事務T2併發進行,同時操作一個數據。可能會發生哪些情況:
髒讀(Dirty read):事務T1更新了數據,但是沒提交。然後,事務T2讀取了讀取事務T1更新後的數據。接着,事務T1發生異常,數據回滾了。那麼事務T2讀到的數據就是無效的,就是髒數據。
不可重複讀(Nonrepeatable read):事務T2讀取了一次數據,之後,事務T1更新了這個數據,事務T2再次讀取這個數據時,這時發現兩次讀取到的數據不一樣。這就是不可重複讀。
幻讀(Phantom reads):事務T2在兩次讀取數據之間,事務T1向這個數據中insert了幾條記錄。事務T2發現第一次讀的數據比第二次多幾條,這就是幻讀。和不可重複讀是一種情況,只不過看的角度不一樣。
那麼爲了避免這些問題的發生,我們不使用事務機制了?爲了保證數據的一致性和完整性是事務我們一定要用的。還有什麼辦法避免這些問題,我們可以將這些事務徹底隔離,一個事務徹底執行完,提交或回滾後,再繼續下一個事務。這樣可以避免上面的問題,但是這樣做的話又引發了新問題,效率低下,性能降低。那麼,我們既要使用事務,又要保證性能,這時候,能控制事務重要性(不被別人更改可能性)的事務隔離級別就順勢而生了。
下表是事務的隔離級別:
隔離級別 | 含義 |
---|---|
ISOLATION_DEFAULT | 使用後端數據庫默認的隔離級別。 |
ISOLATION_READ_UNCOMMITTED | 允許讀取尚未提交的更改。可能導致髒讀、幻影讀或不可重複讀。 |
ISOLATION_READ_COMMITTED | 允許從已經提交的併發事務讀取。可防止髒讀,但幻影讀和不可重複讀仍可能會發生。 |
ISOLATION_REPEATABLE_READ | 對相同字段的多次讀取的結果是一致的,除非數據被當前事務本身改變。可防止髒讀和不可重複讀,但幻影讀仍可能發生。 |
ISOLATION_SERIALIZABLE | 完全服從ACID的隔離級別,確保不發生髒讀、不可重複讀和幻影讀。這在所有隔離級別中也是最慢的,因爲它通常是通過完全鎖定當前事務所涉及的數據表來完成的。 |
四.參考資料
https://www.jianshu.com/p/aa35c8703d61
https://blog.csdn.net/hsgao_water/article/details/52860380
https://www.cnblogs.com/zhishan/p/3195219.html