您好,登錄后才能下訂單哦!
這篇文章主要介紹“Spring事務管理怎么正確使用”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“Spring事務管理怎么正確使用”文章能幫助大家解決問題。
事務(Transaction)是訪問數據庫的一個操作序列,這些操作要么都做,要么都不做,是一個不可分割的工作單元。通過事務,數據庫能將邏輯相關的一組操作綁定在一起,以便保持數據的完整性。
事務有4個重要特性,簡稱 ACID。
A:Automicity,原子性,即事務中的所有操作要么全部執行,要么全部不執行。
C:Consistency,一致性,事務執行的結果必須是使數據庫從一個一致狀態變到另一個一致狀態。
I:Isolation,隔離性,即一個事務的執行不能被另一個事務影響。
D:Durabillity,持久性,即事務提交后將被永久保存。
在Java EE開發中,事務原本屬于 Dao 層中的范疇,但一般情況下需要將事務提升到業務層(Service層),以便能夠使用事務的特性來管理具體的業務。
Spring 的事務管理,主要用到兩個事務相關的接口。
事務管理器接口 PlatformTransactionManager 主要用于完成事務的提交、回滾,及獲取事務的狀態信息。PlatformTransactionManager 接口有兩個常用的實現類:
DataSourceTransactionManager實現類:使用JDBC或MyBatis進行數據持久化時使用。
HibernateTransactionManager實現類:使用Hibernate進行數據持久化時使用。
關于Spring的事務提交與回滾方式,默認是:發生運行時異常時回滾,發生受檢查異常時提交, 也就是說程序拋出runtime異常的時候才會進行回滾,其他異常不回滾。
事務定義接口 TransactionDefinition 中定義了事務描述相關的三類常量:事務隔離級別常量、事務傳播行為常量、事務默認超時時限常量,及對它們的操作。
【1】事務隔離級別常量
在應用程序中,多個事務并發運行,操作相同的數據,可能會引起臟讀,不可重復讀,幻讀等問題 。
臟讀(Dirty reads):第一個事務訪問并改寫了數據,尚未提交事務,這時第二個事務進來了,讀取了剛剛改寫的數據,如果這時第一個事務回滾了,這樣第二個事務讀取到的數據就是無效的“臟數據”。
不可重復讀(Nonrepeatable read):第一個事務在其生命周期內多次查詢同一個數據,在兩次查詢之間,第二個事務訪問并改寫了該數據,導致第一個事務兩次查詢同一個數據得到的結果不一樣。
幻讀(Phantom read)——幻讀與不可重復讀類似。它發生在第一個事務在生命周期進行了兩次按同一查詢條件查詢數據,第一次按該查詢條件讀取了幾行數據,這時第二個事務進來了,插入或刪除了一些數據時,然后第一個事務再次按同一條件查詢,發現多了一些原本不存在的記錄或者原有的記錄不見了。
為了解決并發問題,TransactionDefinition 接口定義了5個事務隔離常量如下:
DEFAULT:采用數據庫 默認 的事務隔離級別。不同數據庫不一樣,MySql的默認為 REPEATABLE_READ(可重復讀);Oracle默認為READ_COMMITTED(讀已提交)。
READ_UNCOMMITTED:讀未提交。允許另外一個事務讀取到當前事務未提交的數據,隔離級別最低,未解決任何并發問題,會產生臟讀,不可重復讀和幻像讀。
READ_COMMITTED:讀已提交,被一個事務修改的數據提交后才能被另外一個事務讀取,另外一個事務不能讀取該事務未提交的數據。解決臟讀,但還存在不可重復讀與幻讀。
REPEATABLE_READ:可重復讀。解決了臟讀、不可重復讀,但還存在幻讀。
SERIALIZABLE:串行化。按時間順序一一執行多個事務,不存在并發問題,最可靠,但性能與效率最低。
【2】事務傳播行為常量
事務傳播行為是指處于不同事務中的方法在相互調用時,執行期間事務的維護情況。例如,當一個事務方法B調用另一個事務方法A時,應當明確規定事務如何傳播,比方可以規定A方法繼續在B方法的現有事務中運行,也可以規定A方法開啟一個新事務,在新事務中運行,現有事務先掛起,等A方法的新事務執行完畢后再恢復。TransactionDefinition 接口一共定義了 七種 傳播行為常量說明如下。
PROPAGATION_ REQUIRED:指定的方法必須在事務內執行。若當前存在事務,就加入到當前事務中;若當前沒有事務,則創建一個新事務。這種傳播行為是最常見的選擇,也是 Spring 默認的事務傳播行為。如該傳播行為加在actionB ()方法上,該方法將被actionA ()調用,若actionA ()方法在執行時就是在事務內的,則actionB ()方法的執行也加入到該事務內執行。若actionA ()方法沒有在事務內執行,則actionB ()方法會創建一個事務,并在其中執行。
PROPAGATION_ SUPPORTS:指定的方法支持當前事務,但若當前沒有事務,也可以以非事務方式執行。
PROPAGATION_ MANDATORY:指定的方法必須在當前事務內執行,若當前沒有事務,則直接拋出異常。
PROPAGATION_ REQUIRES_NEW:總是新建一個事務,若當前存在事務,就將當前事務掛起,直到新事務執行完畢。
PROPAGATION_ NOT_SUPPORTED:指定的方法不能在事務環境中執行,若當前存在事務,就將當前事務掛起。
PROPAGATION_ NEVER:指定的方法不能在事務環境下執行,若當前存在事務,就直接拋出異常。
PROPAGATION_ NESTED:指定的方法必須在事務內執行。若當前存在事務,則在嵌套事務內執行;若當前沒有事務,則創建一個新事務。
【3】默認事務超時時限
常量 TIMEOUT_DEFAULT 定義了事務底層默認的超時時限,及不支持事務超時時限設置的none值。該值一般使用默認值即可。
Spring 支持編程式事務和聲明式事務。
編程式事務 直接在主業務代碼中精確定義事務的邊界,事務以硬編碼的方式嵌入到了主業務代碼里面,好處是能提供更加詳細的事務管理,但由于編程式事務主業務與事務代碼混在一起,不易分離,耦合度高,不利于維護與重用。
聲明式事務 則基于 AOP 方式,能將主業務操作與事務規則進行解耦。能在不影響業務代碼的具體實現情況下實現事務管理。所以比較常用的是聲明式事務。聲明式事務又有兩種具體的實現方式:基于XML配置文件的方式 和 基于注解的方式。
項目案例: 模擬支付寶轉賬,張三、李四原本各有賬戶余額 2000 元,張三轉賬 500 元給李四,但轉賬過程中出現了異常。
實現步驟:
【1】在 MySQL 中創建數據庫表,代碼如下:
create table alipay( aliname varchar (60), amount double );
【2】在 dao 層創建 IAccountDao 接口,代碼如下
package com.hh.dao; public interface AlipayDao { public void transfer(String fromA,String toB,int amount); }
【3】創建 IAccountDao 接口的實現類 IAccountDaoImpl,代碼如下:
package com.hh.dao; import org.springframework.jdbc.core.JdbcTemplate; public class AlipayDaoImpl implements AlipayDao{ JdbcTemplate jdbcTemplate; public JdbcTemplate getJdbcTemplate() { return jdbcTemplate; } public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public void transfer(String fromA, String toB, int amount) { jdbcTemplate.update("update alipay set amount=amount-? where aliname=?",amount,fromA); Integer.parseInt("a"); jdbcTemplate.update("update alipay set amount=amount+? where aliname=?",amount,toB); } }
這個 transfer 方法主要實現兩個操作:
操作一:轉出操作,張三的賬戶錢減少;
操作二:轉入操作,里斯的賬戶錢增加;
但兩個操作中間模擬出現了差錯(異常),這將導致張三的錢少了,而李四的錢卻沒有增加。
【4】添加 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:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 配置數據源 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName"> <value>com.mysql.jdbc.Driver</value> </property> <property name="url"> <value>jdbc:mysql://localhost:3306/usersdb </value> </property> <property name="username"> <value>root</value> </property> <property name="password"> <value>root</value> </property> </bean> <!-- 配置jdbcTemplate模板 注入dataSource --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource" /> </bean> <!-- 配置DAO,注入jdbcTemplate屬性值 --> <bean id="userDao" class="com.hh.dao.UserDaoImpl"> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean> <!-- 配置DAO,注入jdbcTemplate屬性值 --> <bean id="alipayDao" class="com.hh.dao.AlipayDaoImpl"> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean> </beans>
【5】添加測試類 TestAlipay
package com.hh.test; import java.util.List; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.hh.dao.AlipayDao; public class TestAlipay { public static void main(String[] args) { ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml"); AlipayDao alipayDao=(AlipayDao) context.getBean("alipayDao"); alipayDao.transfer("張三", "李四", 500); } }
下面進行事務管理方面的改進,目標是把類 AlipayDaoImpl 里的整個 transfer( ) 方法作為事務管理,這樣 transfer( ) 里的所有操作(包括轉出/轉入操作)都納入同一個事務,從而使 transfer( ) 里的所有操作要么一起成功,要么一起失敗。這里利用了 Spring 的事務管理機制進行處理。
項目案例: 模擬支付寶轉賬,張三、李四原本各有賬戶余額 2000 元,張三轉賬 500 元給李四,但轉賬過程中間出現異常,導致數據不一致 ,現應用 Spring 的事務管理,配置 XML,避免不一致的情況。
實現步驟:
【1】修改 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:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://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 http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 配置數據源 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName"> <value>com.mysql.jdbc.Driver</value> </property> <property name="url"> <value>jdbc:mysql://localhost:3306/usersdb </value> </property> <property name="username"> <value>root</value> </property> <property name="password"> <value>root</value> </property> </bean> <!-- 配置jdbcTemplate模板 注入dataSource --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource" /> </bean> <!-- 配置DAO,注入jdbcTemplate屬性值 --> <bean id="alipayDao" class="com.lifeng.dao.AlipayDaoImpl"> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean> <!-- 定義事務管理器 --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!-- 編寫事務通知 --> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" /> <!-- <tx:method name="save*" propagation="REQUIRED" /> <tx:method name="add*" propagation="REQUIRED" /> <tx:method name="insert*" propagation="REQUIRED" /> <tx:method name="delete*" propagation="REQUIRED" /> <tx:method name="update*" propagation="REQUIRED" /> <tx:method name="search*" propagation="SUPPORTS" read-only="true"/> <tx:method name="select*" propagation="SUPPORTS" read-only="true"/> <tx:method name="find*" propagation="SUPPORTS" read-only="true"/> <tx:method name="get*" propagation="SUPPORTS" read-only="true"/> --> </tx:attributes> </tx:advice> <!-- 編寫AOP,讓spring自動將事務切入到目標切點 --> <aop:config> <!-- 定義切入點 --> <aop:pointcut id="txPointcut" expression="execution(* com.lifeng.dao.*.*(..))" /> <!-- 將事務通知與切入點組合 --> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut" /> </aop:config> </beans>
這里可以把事務功能理解為切面,通過aop配置實現事務(切面)自動切入到切入點(目標方法),從而將目標方法(切入點)納入事務管理,而目標方法本身可以不用管事務,專心做自已的主業務功能就行了
【2】其它程序代碼不變,運行測試。
測試時盡管轉賬中間出現了異常,但是張三、李四的錢都沒變化,保持了一致性,這樣就達到了目的,證明了 transfer 方法中的兩個操作都納入了同一個事務。發生異常時,事務回滾,保證了數據的一致性。
上面配置中:
<tx:method name="*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" />
表示匹配的切點方法都進行事務管理,這里*表示匹配所有切點方法,propagation="REQUIRED"表示匹配的切點方法必須在事務內執行,isolation="DEFAULT"表示事務隔離級別默認,對于MySQL數據庫,隔離級別為REPEATABLE_READ(可重復讀)。read-only="false"表示非只讀。
這個配置粒度太大,所有方法都同一種事務管理模式,要想不同的方法實現不一樣的事務管理,還得細化配置。項目中常見的細化配置如下面代碼所示。
<!-- 編寫通知 --> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="save*" propagation="REQUIRED" /> <tx:method name="add*" propagation="REQUIRED" /> <tx:method name="insert*" propagation="REQUIRED" /> <tx:method name="delete*" propagation="REQUIRED" /> <tx:method name="update*" propagation="REQUIRED" /> <tx:method name="search*" propagation="SUPPORTS" read-only="true"/> <tx:method name="select*" propagation="SUPPORTS" read-only="true"/> <tx:method name="find*" propagation="SUPPORTS" read-only="true"/> <tx:method name="get*" propagation="SUPPORTS" read-only="true"/> </tx:attributes> </tx:advice>
這樣,不同的方法匹配不同的事務管理模式。
<tx:method name=“save*” propagation=“REQUIRED” />表示凡是以save開頭的切點方法必須在事務內執行,其他增刪改都一樣的意思,查的話就不同<tx:method name=“select*” propagation=“SUPPORTS” read-only=“true”/>表示心是以select開頭的切點方法支持當前事務,但若當前沒有事務,也可以以非事務方式執行,read-only="true"表示只讀,其他幾個類似。
上面是利用XML配置文件實現事務管理的辦法,下面來學習用注解實現事務管理。
使用@Transactional注解在類或方法上,即可實現事務管理。 @Transactional注解的屬性有下面這些(可選):
propagation:用于設置事務傳播的屬性,該屬性類型為propagation枚舉,默認值為Propagation.REQUIRED。
isolation:用于設置事務的隔離級別,該屬性類型為Isolation枚舉,默認值為Isolation.DEFAULT。
readOnly:用于設置該方法對數據庫的操作是否是只讀的,該屬性為boolean,默認值false。
timeout:用于設置本操作與數據庫連接的超時時限。單位為秒,類型為int,默認值為-1,即沒有時限。
rollbackFor:指定需要回滾的異常類,類型為 Class[],默認值為空數組。當然,若只有一個異常類時,可以不使用數組。
rollbackForClassName:指定需要回滾的異常類類名,類型為 String[],默認值為空數組。當然,若只有一個異常類時,可以不使用數組。
noRollbackFor:指定不需要回滾的異常類。類型為Class[],默認值為空數組。當然,若只有一個異常類時,可以不使用數組。
noRollbackForClassName:指定不需要回滾的異常類類名。類型為String[],默認值為空數組。當然,若只有一個異常類時,可以不使用數組。
需要注意的是,@Transactional 若用在方法上,只能用于public方法上。對于其他非 public方法,如果加上了注解 @Transactional,雖然 Spring 不會報錯,但不會將指定事務織入到該方法中。因為Spring會忽略掉所有非public方法上的 @Transaction 注解。若 @Transaction 注解在類上,則表示該類上所有的方法均將在執行時織入事務。
項目案例: 模擬支付寶轉賬,張三李四原本各有賬戶余額2000元,張三轉賬500元給李四,但轉賬過程中間出現異常,應用spring的事務管理,使用注解,避免不一致的情況。
實現步驟:
【1】修改 Spring 配置文件如下:
<!-- 定義事務管理器 --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!-- 開啟事務注解驅動 --> <tx:annotation-driven transaction-manager="txManager"/></beans>
可以發現,配置文件比之前簡化了很多,事務方面,只需定義好事務管理器,再開啟事務注解驅動就行了。其他的交給注解來解決。
【2】利用 @Transactional 注解修改轉賬方法。
@Transactional 既可以用來修飾類,也可以修飾方法,如果修飾類,則表示事務的設置對整個類的所有方法都起作用,如果修飾在方法上,則只對該方法起作用,關鍵代碼如下。
public class AlipayDaoImpl implements AlipayDao{ @Override @Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,readOnly=false) public void transfer(String fromA, String toB, int amount) { jdbcTemplate.update("update alipay set amount=amount-? where aliname=?",amount,fromA); Integer.parseInt("a"); jdbcTemplate.update("update alipay set amount=amount+? where aliname=?",amount,toB); }
將transfer方法注解為事務。
【3】運行測試,發現數據庫同樣沒改變,所以注解事務起到作用了。
上面的案例是在DAO層實現事務管理,相對簡單一些,但實際上開發時需要在業務層實現事務管理,而不是在DAO層,為此,項目修改如下,特別要注意在業務層的事務管理實現。
項目案例: 模擬支付寶轉賬,張三李四原本各有賬戶余額2000元,張三轉賬500元給李四,但轉賬過程中間出現異常,在業務層應用spring的事務管理,配置xml,避免不一致的情況。
實現步驟:
【1】修改DAO層,轉出轉入分拆成兩個方法。
@Override public void tranferFrom(String fromA,int amount){ jdbcTemplate.update("update alipay set amount=amount-? where aliname=?",amount,fromA); } @Override public void tranferTo(String toB,int amount){ jdbcTemplate.update("update alipay set amount=amount+? where aliname=?",amount,toB); }
【2】新建包com.hh.service,創建業務層AlipayService.java類,代碼如下:
public class AlipayService { private AlipayDao alipayDao; public void transfer(String fromA, String toB, int amount) { alipayDao.tranferFrom(fromA, amount); Integer.parseInt("a"); alipayDao.tranferTo(toB, amount); } }
上述代碼相當于把有異常問題的 transfer( ) 方法遷移到業務層中來。
【3】修改配置文件。關鍵配置如下:
<!-- 定義事務管理器 --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!-- 編寫事務通知 --> <tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" /> </tx:attributes> </tx:advice> <!-- 編寫AOP,讓spring自動將事務切入到目標切點 --> <aop:config> <!-- 定義切入點 --> <aop:pointcut id="txPointcut" expression="execution(* com.lifeng.service.*.*(..))" /> <!-- 將事務通知與切入點組合 --> <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut" /> </aop:config>
【4】修改測試類,代碼如下:
public class TestAlipay { public static void main(String[] args) { ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml"); AlipayService alipayService=(AlipayService) context.getBean("alipayService"); alipayService.transfer("張三", "李四", 500); } }
測試結束,數據庫的數據保持不變,證明事務管理成功。
關于“Spring事務管理怎么正確使用”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。