前言
本文主要記錄下spring是如何支持事物的,以及在Spring結合mybatis時,可以怎麼簡單的實現數據庫的事物功能,下面話不多說了,來一起看看詳細的介紹吧。
case1:兩張表的的事物支持情況
首先準備兩張表,一個user表,一個story表,結構如下
CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用戶名', `pwd` varchar(26) NOT NULL DEFAULT '' COMMENT '密碼', `isDeleted` tinyint(1) NOT NULL DEFAULT '0', `created` varchar(13) NOT NULL DEFAULT '0', `updated` varchar(13) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `name` (`name`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE `story` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `userId` int(20) unsigned NOT NULL DEFAULT '0' COMMENT '作者的userID', `name` varchar(20) NOT NULL DEFAULT '' COMMENT '作者名', `title` varchar(26) NOT NULL DEFAULT '' COMMENT '密碼', `story` text COMMENT '故事內容', `isDeleted` tinyint(1) NOT NULL DEFAULT '0', `created` varchar(13) NOT NULL DEFAULT '0', `updated` varchar(13) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `userId` (`userId`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
我們的事物場景在於用戶修改name時,要求兩張表的name都需要一起修改,不允許出現不一致的情況
case2:單表的事物支持
轉賬,一個用戶減錢,另一個用戶加錢
CREATE TABLE `money` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用戶名', `money` int(26) NOT NULL DEFAULT '0' COMMENT '錢', `isDeleted` tinyint(1) NOT NULL DEFAULT '0', `created` varchar(13) NOT NULL DEFAULT '0', `updated` varchar(13) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `name` (`name`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
相比上面那個case,這個更加簡單了,下面的實例則主要根據這個進行說明,至於case1,則留待擴展裡面進行
首先是實現對應的dao和entity
@Datapublic class MoneyEntity implements Serializable { private static final long serialVersionUID = -7074788842783160025L; private int id; private String name; private int money; private int isDeleted; private int created; private int updated;}public interface MoneyDao { MoneyEntity queryMoney(@Param("id") int userId); // 加錢,負數時表示減錢int incrementMoney(@Param("id") int userId, @Param("addMoney") int addMoney);}對應的mapper文件為
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.git.hui.demo.mybatis.mapper.MoneyDao"> <sql id="moneyEntity"> id, `name`, `money`, `isDeleted`, `created`, `updated` </sql> <select id="queryMoney" resultType="com.git.hui.demo.mybatis.entity.MoneyEntity"> select <include refid="moneyEntity"/> from money where id=#{id} </select> <update id="incrementMoney"> update money set money=money + #{addMoney} where id=#{id} </update></mapper>對應的mybatis連接數據源的相關配置
<bean> <property name="locations"> <value>classpath*:jdbc.properties</value> </property></bean><bean id="dataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> <property name="filters" value="stat"/> <property name="maxActive" value="20"/> <property name="initialSize" value="1"/> <property name="maxWait" value="60000"/> <property name="minIdle" value="1"/> <property name="timeBetweenEvictionRunsMillis" value="60000"/> <property name="minEvictableIdleTimeMillis" value="300000"/> <property name="validationQuery" value="SELECT 'x'"/> <property name="testWhileIdle" value="true"/> <property name="testOnBorrow" value="false"/> <property name="testOnReturn" value="false"/> <property name="poolPreparedStatements" value="true"/> <property name="maxPoolPreparedStatementPerConnectionSize" value="50"/></bean><bean id="sqlSessionFactory"> <property name="dataSource" ref="dataSource"/> <!-- 指定mapper文件--> <property name="mapperLocations" value="classpath*:mapper/*.xml"/></bean><!-- 指定掃描dao --><bean> <property name="basePackage" value="com.git.hui.demo.mybatis"/></bean>通過網上查詢,Spring事物管理總共有四種方式,下面逐一進行演示,每種方式是怎麼玩的,然後看實際項目中應該如何抉擇
編程式事物管理,既通過TransactionTemplate來實現多個db操作的事物管理
a. 實現
那麼,我們的轉賬case可以如下實現
@Repositorypublic class CodeDemo1 { @Autowired private MoneyDao moneyDao; @Autowired private TransactionTemplate transactionTemplate; /** * 轉賬* * @param inUserId * @param outUserId * @param payMoney * @param status 0 表示正常轉賬, 1 表示內部拋出一個異常, 2 表示新開一個線程,修改inUserId的錢+200, 3 表示新開一個線程,修改outUserId的錢+ 200 */ public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) { transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) { MoneyEntity entity = moneyDao.queryMoney(outUserId); if (entity.getMoney() > payMoney) { // 可以轉賬// 先減錢moneyDao.incrementMoney(outUserId, -payMoney); testCase(inUserId, outUserId, status); // 再加錢moneyDao.incrementMoney(inUserId, payMoney); System.out.println("轉賬完成! now: " + System.currentTimeMillis()); } } }); } // 下面都是測試用例相關private void testCase(final int inUserId, final int outUserId, final int status) { if (status == 1) { throw new IllegalArgumentException("轉賬異常!!!"); } else if(status == 2) { addMoney(inUserId); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } else if (status == 3) { addMoney(outUserId); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void addMoney(final int userId) { System.out.printf("內部加錢: " + System.currentTimeMillis()); new Thread(new Runnable() { public void run() { moneyDao.incrementMoney(userId, 200); System.out.println(" sub modify success! now: " + System.currentTimeMillis()); } }).start(); }}主要看上面的transfor方法,內部通過transactionTemplate 來實現事物的封裝,內部有三個db操作,一個查詢,兩個更新,具體分析後面說明
上面的代碼比較簡單了,唯一需要關注的就是transactionTemplate這個bean如何定義的,xml文件中與前面重複的就不貼了,直接貼上關鍵代碼, 一個是根據DataSource創建的TransactionManager,一個則是根據TransactionManager創建的TransactionTemplate
<!--編程式事物--><bean id="transactionManager"> <property name="dataSource" ref="dataSource"/></bean><bean id="transactionTemplate"> <property name="transactionManager" ref="transactionManager"/></bean>
b. 測試用例
正常演示情況, 演示沒有任何異常,不考慮並發的情況
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource1.xml"})public class CodeDemo1Test { @Autowired private CodeDemo1 codeDemo1; @Autowired private MoneyDao moneyDao; @Test public void testTransfor() { System.out.println("---------before----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); codeDemo1.transfor(1, 2, 10, 0); System.out.println("---------after----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); }}輸出如下,兩個賬號的錢都沒有問題
---------before----------
id: 1 money = 10000
id: 2 money = 50000
轉賬完成! now: 1526130394266
---------after----------
id: 1 money = 10010
id: 2 money = 49990
轉賬過程中出現異常,特別是轉賬方錢已扣,收款方還沒收到錢時,也就是case中的status為1的場景
// 內部拋異常的情況@Testpublic void testTransforException() { System.out.println("---------before----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); try { codeDemo1.transfor(1, 2, 10, 1); } catch (Exception e) { e.printStackTrace(); } System.out.println("---------after----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());}對此,我們希望把轉賬方的錢還回去, 輸出如下,發現兩個的錢都沒有變化
---------before----------
id: 1 money = 10010
id: 2 money = 49990
---------after----------
id: 1 money = 10010
java.lang.IllegalArgumentException: 轉賬異常!!!
... // 省略異常信息
id: 2 money = 49990
當status為2,表示在轉賬人錢已扣,收款人錢沒收到之間,又有人給收款人轉了200,此時根據mysql的鎖機制,另外人的轉賬應該是立馬到的(因為收款人賬號沒有被鎖住),且金額不應該有問題
輸出結果如下:
---------before----------
id: 1 money = 10010
id: 2 money = 49990
## 右邊是註釋: 轉賬過程中,另外存錢立馬到賬,沒有被鎖住內部加錢: 1526130827480
sub modify success! now: 1526130827500
## 存錢結束轉賬完成! now: 1526130830488
---------after----------
id: 1 money = 10220
id: 2 money = 49980
當status為3, 表示在轉賬人錢已扣,收款人錢沒收到之間,又有人給轉賬人轉了200,這時因為轉賬人的記錄以及被加了寫鎖,因此只能等待轉賬的事物提交之後,才有可能+200成功,當然最終的金額也得一致
輸出結果如下
---------before----------
id: 1 money = 10220
id: 2 money = 49980
## 右邊是註釋:內部存錢了,但沒有馬上成功
## 直到轉賬完成後,才立馬存成功,注意兩個時間戳內部加錢: 1526131101046
轉賬完成! now: 1526131104051
sub modify success! now: 1526131104053
---------after----------
id: 1 money = 10230
id: 2 money = 50170
c. 小結
至此,編程式事物已經實例演示ok,從上面的過程,給人的感覺就和直接寫事物相關的sql一樣,
start transaction;
-- 這中間就是TransactionTemplate#execute 方法內部的邏輯
-- 也就是需要事物管理的一組sqlcommit;
接下來的三個就是聲明式事物管理,這種用得也比較少,因為需要每個事物管理類,添加一個TransactionProxyFactoryBean
a. 實現
除了將TransactionTemplate 幹掉,並將內部的sql邏輯移除之外,對比前面的,發現基本上沒有太多差別
public class FactoryBeanDemo2 { @Autowired private MoneyDao moneyDao; /** * 轉賬* * @param inUserId * @param outUserId * @param payMoney * @param status 0 表示正常轉賬, 1 表示內部拋出一個異常, 2 表示新開一個線程,修改inUserId的錢+200, 3 表示新開一個線程,修改outUserId的錢+ 200 */ public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) { MoneyEntity entity = moneyDao.queryMoney(outUserId); if (entity.getMoney() > payMoney) { // 可以轉賬// 先減錢moneyDao.incrementMoney(outUserId, -payMoney); testCase(inUserId, outUserId, status); // 再加錢moneyDao.incrementMoney(inUserId, payMoney); System.out.println("轉賬完成! now: " + System.currentTimeMillis()); } } private void testCase(final int inUserId, final int outUserId, final int status) { if (status == 1) { throw new IllegalArgumentException("轉賬異常!!!"); } else if (status == 2) { addMoney(inUserId); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } else if (status == 3) { addMoney(outUserId); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } public void addMoney(final int userId) { System.out.println("內部加錢: " + System.currentTimeMillis()); new Thread(new Runnable() { public void run() { moneyDao.incrementMoney(userId, 200); System.out.println("sub modify success! now: " + System.currentTimeMillis()); } }).start(); }}重點來了,主要是需要配置一個TransactionProxyBeanFactory,我們知道BeanFactory就是我們自己來創建Bean的一種手段,相關的xml配置如下
<!--編程式事物--><bean id="transactionManager"> <property name="dataSource" ref="dataSource"/></bean><bean id="factoryBeanDemo2"/><!-- 配置業務層的代理--><bean id="factoryBeanDemoProxy"> <!-- 配置目標對象--> <property name="target" ref="factoryBeanDemo2" /> <!-- 注入事務管理器--> <property name="transactionManager" ref="transactionManager"/> <!-- 注入事務的屬性--> <property name="transactionAttributes"> <props> <!-- prop的格式: * PROPAGATION :事務的傳播行為* ISOTATION :事務的隔離級別* readOnly :只讀* -EXCEPTION :發生哪些異常回滾事務* +EXCEPTION :發生哪些異常不回滾事務--> <!-- 這個key對應的就是目標類中的方法--> <prop key="transfor">PROPAGATION_REQUIRED</prop> <!-- <prop key="transfer">PROPAGATION_REQUIRED,readOnly</prop> --> <!-- <prop key="transfer">PROPAGATION_REQUIRED,+java.lang.ArithmeticException</prop> --> </props> </property></bean>
通過上面的配置,大致可以了解到這個通過TransactionProxyFactoryBean就是創建了一個FactoryBeanDemo2的代理類,這個代理類內部封裝好事物相關的邏輯,可以看做是前面編程式的一種簡單通用抽象
b. 測試
測試代碼與前面基本相同,唯一的區別就是我們使用的應該是上面BeanFactory生成的Bean,而不是直接使用FactoryBeanDemo2
正常演示case:
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource2.xml"})public class FactoryBeanDemo1Test { @Resource(name = "factoryBeanDemoProxy") private FactoryBeanDemo2 factoryBeanDemo2; @Autowired private MoneyDao moneyDao; @Test public void testTransfor() { System.out.println("---------before----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); factoryBeanDemo2.transfor(1, 2, 10, 0); System.out.println("---------after----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); }}輸出
---------before----------
id: 1 money = 10000
id: 2 money = 50000
轉賬完成! now: 1526132058886
---------after----------
id: 1 money = 10010
id: 2 money = 49990
status為1,內部異常的情況下,我們希望錢也不會有問題
@Testpublic void testTransforException() { System.out.println("---------before----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); try { factoryBeanDemo2.transfor(1, 2, 10, 1); } catch (Exception e) { System.out.println(e.getMessage());; } System.out.println("---------after----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney());}輸出為
---------before----------
id: 1 money = 10010
id: 2 money = 49990
轉賬異常!!!
---------after----------
id: 1 money = 10010
id: 2 money = 49990
status為2 時,分析結果與上面應該相同,輸出如下
---------before----------
id: 1 money = 10010
id: 2 money = 49950
內部加錢: 1526133325376
sub modify success! now: 1526133325387
轉賬完成! now: 1526133328381
---------after----------
id: 1 money = 10220
id: 2 money = 49940
status為3時,輸出
---------before----------
id: 1 money = 10220
id: 2 money = 49940
內部加錢: 1526133373466
轉賬完成! now: 1526133376476
sub modify success! now: 1526133376480
---------after----------
id: 1 money = 10230
id: 2 money = 50130
c. 小結
TransactionProxyFactoryBean 的思路就是利用代理模式來實現事物管理,生成一個代理類,攔截目標方法,將一組sql的操作封裝到事物中進行;相比較於硬編碼,無侵入,而且支持靈活的配置方式
缺點也顯而易見,每個都要進行配置,比較繁瑣
Spring有兩大特點,IoC和AOP,對於事物這種情況而言,我們可不可以使用AOP來做呢?
對於需要開啟事物的方法,攔截掉,執行前開始事物,執行完畢之後提交事物,出現異常時回滾
這樣一看,感覺還是蠻有希望的,而下面兩種姿勢正是這麼玩的,因此需要加上aspect的依賴
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.7</version></dependency>
a. 實現
java類與第二種完全一致,變動的只有xml
<!-- 首先添加命名空間-->xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="... http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"<!--對應的事物通知和切面配置--><tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- propagation :事務傳播行為isolation :事務的隔離級別read-only :只讀rollback-for:發生哪些異常回滾no-rollback-for :發生哪些異常不回滾timeout :過期信息--> <tx:method name="transfor" propagation="REQUIRED"/> </tx:attributes></tx:advice><!-- 配置切面--><aop:config> <!-- 配置切入點--> <aop:pointcut expression="execution(* com.git.hui.demo.mybatis.repository.transaction.XmlDemo3.*(..))" id="pointcut1"/> <!-- 配置切面--> <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut1"/></aop:config>
觀察上面的配置,再想想第二種方式,思路都差不多了,但是這種方式明顯更加通用,通過切面和切點,可以減少大量的配置
b. 測試
@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration({"classpath*:spring/service.xml", "classpath*:test-datasource3.xml"})public class XmlBeanTest { @Autowired private XmlDemo3 xmlDemo; @Autowired private MoneyDao moneyDao; @Test public void testTransfor() { System.out.println("---------before----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); xmlDemo.transfor(1, 2, 10, 0); System.out.println("---------after----------"); System.out.println("id: 1 money = " + moneyDao.queryMoney(1).getMoney()); System.out.println("id: 2 money = " + moneyDao.queryMoney(2).getMoney()); }}這個測試起來,和一般的寫法就沒啥兩樣了,比第二種的FactoryBean的注入方式簡單點
正常輸出
---------before----------
id: 1 money = 10000
id: 2 money = 50000
轉賬完成! now: 1526135301273
---------after----------
id: 1 money = 10010
id: 2 money = 49990
status=1 出現異常時,輸出
---------before----------
id: 1 money = 10010
id: 2 money = 49990
轉賬異常!!!
---------after----------
id: 1 money = 10010
id: 2 money = 49990
status=2 轉賬過程中,又存錢的場景,輸出,與前面預期一致
---------before----------
id: 1 money = 10010
id: 2 money = 49990
內部加錢: 1526135438403
sub modify success! now: 1526135438421
轉賬完成! now: 1526135441410
---------after----------
id: 1 money = 10220
id: 2 money = 49980
status=3 的輸出,與前面預期一致
---------before----------
id: 1 money = 10220
id: 2 money = 49980
內部加錢: 1526135464341
轉賬完成! now: 1526135467349
sub modify success! now: 1526135467352
---------after----------
id: 1 money = 10230
id: 2 money = 50170
這個就是消滅xml,用註解來做的方式,就是將前面xml中的配置用@Transactional註解替換
a. 實現
@Repositorypublic class AnnoDemo4 { @Autowired private MoneyDao moneyDao; /** * 轉賬* * @param inUserId * @param outUserId * @param payMoney * @param status 0 表示正常轉賬, 1 表示內部拋出一個異常, 2 表示新開一個線程,修改inUserId的錢+200, 3 表示新開一個線程,修改outUserId的錢+ 200 * * * Transactional註解中的的屬性propagation :事務的傳播行為isolation :事務的隔離級別readOnly :只讀* rollbackFor :發生哪些異常回滾noRollbackFor :發生哪些異常不回滾* rollbackForClassName 根據異常類名回滾*/ @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, readOnly = false) public void transfor(final int inUserId, final int outUserId, final int payMoney, final int status) { MoneyEntity entity = moneyDao.queryMoney(outUserId); if (entity.getMoney() > payMoney) { // 可以轉賬// 先減錢moneyDao.incrementMoney(outUserId, -payMoney); testCase(inUserId, outUserId, status); // 再加錢moneyDao.incrementMoney(inUserId, payMoney); System.out.println("轉賬完成! now: " + System.currentTimeMillis()); } } private void testCase(final int inUserId, final int outUserId, final int status) { if (status == 1) { throw new IllegalArgumentException("轉賬異常!!!"); } else if (status == 2) { addMoney(inUserId); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } else if (status == 3) { addMoney(outUserId); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } } private void addMoney(final int userId) { System.out.println("內部加錢: " + System.currentTimeMillis()); new Thread(new Runnable() { public void run() { moneyDao.incrementMoney(userId, 200); System.out.println("sub modify success! now: " + System.currentTimeMillis()); } }).start(); }}因此需要在xml中配置,開啟事物註解
<!--編程式事物--><bean id="transactionManager"> <property name="dataSource" ref="dataSource"/></bean><tx:annotation-driven transaction-manager="transactionManager"/>
這樣一看,就更加清晰了,實際項目中,xml和註解方式也是用得最多的場景了
b. 測試case
和第三種測試case完全相同, 輸出結果也一樣,直接省略
上面說了Spring中四種使用事物的姿勢,其中硬編碼方式可能是最好理解的,就相當於將我們寫sql中,使用事物的方式直接翻譯成對應的java代碼了;而FactoryBean方式相當於特殊情況特殊對待,為每個事物來一個代理類來增強事物功能;後面的兩個則原理差不多都是利用事物通知(AOP)來實現,定義切點及相關信息
編程式:
transactionTemplate#execute方法內代理BeanFactory:
xml配置:
註解方式:
tx:annotation-driven transaction-manager="transactionManager"/>文件
Spring事務管理的四種方式
源碼
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。