Spring + mybatis + mysql 使用事務的幾種姿勢
主要記錄下spring是如何支援事務的,以及在Spring結合mybatis時,可以怎麼簡單的實現資料庫的事務功能
原文檢視地址:一灰灰Blog
I. 前提
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
@Data
public 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 class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>classpath*:jdbc.properties</value>
</property>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" 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" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 指定mapper檔案 -->
<property name="mapperLocations" value="classpath*:mapper/*.xml"/>
</bean>
<!-- 指定掃描dao -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.git.hui.demo.mybatis"/>
</bean>
複製程式碼
II. 例項演示
通過網上查詢,Spring事務管理總共有四種方式,下面逐一進行演示,每種方式是怎麼玩的,然後看實際專案中應該如何抉擇
1. 硬編碼方式
程式設計式事務管理,既通過TransactionTemplate來實現多個db操作的事務管理
a. 實現
那麼,我們的轉賬case可以如下實現
@Repository
public 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" 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>
複製程式碼
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的場景
// 內部拋異常的情況
@Test
public 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 方法內部的邏輯
-- 也就是需要事務管理的一組sql
commit;
複製程式碼
2. 基於TransactionProxyFactoryBean
方式
接下來的三個就是宣告式事務管理,這種用得也比較少,因為需要每個事務管理類,新增一個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" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="factoryBeanDemo2" class="com.git.hui.demo.mybatis.repository.transaction.FactoryBeanDemo2"/>
<!-- 配置業務層的代理 -->
<bean id="factoryBeanDemoProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<!-- 配置目標物件 -->
<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,內部異常的情況下,我們希望錢也不會有問題
@Test
public 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的操作封裝到事務中進行;相比較於硬編碼,無侵入,而且支援靈活的配置方式
缺點也顯而易見,每個都要進行配置,比較繁瑣
3. xml使用方式
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
複製程式碼
4. 註解方式
這個就是消滅xml,用註解來做的方式,就是將前面xml中的配置用 @Transactional註解替換
a. 實現
@Repository
public 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" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
複製程式碼
這樣一看,就更加清晰了,實際專案中,xml和註解方式也是用得最多的場景了
b. 測試case
和第三種測試case完全相同, 輸出結果也一樣,直接省略
III. 小結
上面說了Spring中四種使用事務的姿勢,其中硬編碼方式可能是最好理解的,就相當於將我們寫sql中,使用事務的方式直接翻譯成對應的java程式碼了;而FactoryBean方式相當於特殊情況特殊對待,為每個事務來一個代理類來增強事務功能;後面的兩個則原理差不多都是利用事務通知(AOP)來實現,定義切點及相關資訊
程式設計式:
- 注入
TransactionTemplate
- 將利用事務的邏輯封裝到
transactionTemplate#execute
方法內
代理BeanFactory:
- 利用
TransactionProxyFactoryBean
為事務相關類生成代理 - 使用方通過FactoryBean獲取代理類,作為使用的Bean
xml配置:
- 利用 tx標籤 + aop方式來實現
<tx:advice>
標籤定義事務通知,內部可有較多的配置資訊<aop:config>
配置切點,切面
註解方式:
- 在開啟事務的方法or類上新增
@Transactional
註解即可 - 開啟事務註解
<tx:annotation-driven transaction-manager="transactionManager"/>
IV. 其他
1. 參考
文件
原始碼
- 專案原始碼:study-demo
- 主要檢視包路徑: 事務demo
- 測試相關程式碼: 測試demo
2. 個人部落格: 一灰灰Blog
基於hexo + github pages搭建的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
3. 宣告
盡信書則不如,已上內容,純屬一家之言,因本人能力一般,見識有限,如發現bug或者有更好的建議,隨時歡迎批評指正
- 微博地址: 小灰灰Blog
- QQ: 一灰灰/3302797840