宣告式事務-02
3.事務的傳播機制
事務的傳播機制說明:
-
當有多個事務處理并存時,如何控制?
-
比如用戶去購買兩次商品(使用不同的方法),每個方法都是一個事務,那么如何控制呢?
也就是說,某個方法本身是一個事務,然后該方法中又呼叫了其他一些方法,這些方法也是被@Transactional 修飾的,同樣是事務,
-
問題在于:里層方法的事務是被外層方法事務管理?還是它本身作為一個獨立的事務呢?這就涉及到事務的傳播機制問題,
3.1事務傳播機制種類
- 事務傳播的屬性 / 種類:
| 傳播屬性 | 說明 |
|---|---|
| REQUIRED | (默認)如果有事務在運行,當前的方法就在這個事務內運行,否則,就啟動一個新的事務,并且在自己的事務內運行 |
| REQUIRES_NEW | 當前的方法必須啟動新事務,并在它自己的事務內運行,如果有事務正在運行,應該將它掛起 |
| SUPPORTS | 如果有事務在運行,當前的方法就在這個事務內運行,否則它可以不運行在事務中 |
| NOT_SUPPORTED | 當前的方法不應該運行在事務中,如果有運行的事務,將它掛起 |
| MANDATORY | 當前的方法必須運行在事務內部,如果沒有正在運行的事務,就拋出例外 |
| NEVER | 當前的方法不應該運行在事務中,如果有運行的事務,就拋出例外 |
| NESTED | 如果有事務在運行,當前的方法就應該在這個事務的嵌套事務內運行,否則,就啟動一個新的事務,并在它自己的事務內運行 |
常用的只有前面兩種:(1)REQUIRED,(2)REQUIRES_NEWREQUIRES_NEW
- 事務傳播的屬性/種類機制分析
重點分析 REQUIRED 和 REQUIRES_NEW 兩種事務傳播屬性,其他知道即可,
如下,有一個multiTxTest()方法,該方法中又有f1(),f2() 方法,所有方法都分別開啟了宣告式事務,
@Transactional
public void multiTxTest() {
f1(); //含事務
f2(); //含事務
}
-
如果f1(),f2() 的傳播屬性都是 REQUIRED,那么它們實際上是被Tx()的事務統一管理的,所有方法是一個整體,只要有一個方法的事務錯誤,那么兩個方法都不會執行成功,
-
如果f1(),f2() 的傳播屬性都是 REQUIRES_NEW,那么f1(),f2()實際上是獨立的事務,不會受到Tx()事務的影響,如果f1()錯誤,不會影響到f2(),反之亦然,
3.2應用實體
需求說明:
- 用戶要去購買兩次商品(使用不同的方法),每個方法都是一個事務,那么如何控制呢?
- 看一個具體的案例(用 required 和 requires_new 測驗)
代碼實作
1.GoodsDao.java
分別有6個方法:queryPriceById,queryPriceById2,updateBalance,updateBalance2,updateAmount,updateAmount2,
package com.li.tx.dao;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
/**
* @author 李
* @version 1.0
*/
@Repository //將GoodsDao物件 注入到 spring 容器
public class GoodsDao {
@Resource
private JdbcTemplate jdbcTemplate;
/**
* 根據商品id,查詢對應的商品價格
* @param id
* @return
*/
public Float queryPriceById(Integer id) {
String sql = "select price from goods where goods_id = ?";
Float price = jdbcTemplate.queryForObject(sql, Float.class, id);
return price;
}
/**
* 修改用戶余額 [減少用戶余額]
* @param user_id
* @param money
*/
public void updateBalance(Integer user_id, Float money) {
String sql = "update user_account set money=money-? where user_id=? ";
jdbcTemplate.update(sql, money, user_id);
}
/**
* 修改商品庫存量
* @param goods_id
* @param amount
*/
public void updateAmount(Integer goods_id, int amount) {
String sql = "update goods_amount set goods_num=goods_num-? where goods_id=? ";
jdbcTemplate.update(sql, amount, goods_id);
}
//和queryPriceById的操作是一樣的
public Float queryPriceById2(Integer id) {
String sql = "select price from goods where goods_id = ?";
Float price = jdbcTemplate.queryForObject(sql, Float.class, id);
return price;
}
//和updateBalance的操作是一樣的
public void updateBalance2(Integer user_id, Float money) {
String sql = "update user_account set money=money-? where user_id=? ";
jdbcTemplate.update(sql, money, user_id);
}
//和updateAmount的操作是一樣的
public void updateAmount2(Integer goods_id, int amount) {
String sql = "update goods_amount set goods_num=goods_num-? where goods_id=? ";
jdbcTemplate.update(sql, amount, goods_id);
}
}
2.GoodsService.java,分別有兩個方法buyGoodsByTx,buyGoodsByTx02
package com.li.tx.service;
import com.li.tx.dao.GoodsDao;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.beans.Transient;
/**
* @author 李
* @version 1.0
*/
@Service //將GoodsService物件注入到容器中
public class GoodsService {
@Resource
private GoodsDao goodsDao;
/**
* 進行商品購買的方法
* @param userId
* @param goodsId
* @param amount
*/
@Transactional
public void buyGoodsByTx(int userId, int goodsId, int amount) {
//輸出購買的相關資訊
System.out.println("用戶購買資訊 userId=" + userId
+ " goodsId=" + goodsId + " 購買數量=" + amount);
//1.得到商品價格
Float price = goodsDao.queryPriceById(goodsId);
//2.減少用戶余額
goodsDao.updateBalance(userId, price * amount);
//3.減少商品庫存量
goodsDao.updateAmount(goodsId, amount);
System.out.println("用戶購買成功...");
}
/**
* 進行商品購買的方法02,呼叫的是GoodsDao的2后綴的方法
* @param userId
* @param goodsId
* @param amount
*/
@Transactional
public void buyGoodsByTx02(int userId, int goodsId, int amount) {
//輸出購買的相關資訊
System.out.println("用戶購買資訊 userId=" + userId
+ " goodsId=" + goodsId + " 購買數量=" + amount);
//1.得到商品價格
Float price = goodsDao.queryPriceById2(goodsId);
//2.減少用戶余額
goodsDao.updateBalance2(userId, price * amount);
//3.減少商品庫存量
goodsDao.updateAmount2(goodsId, amount);
System.out.println("用戶購買成功...");
}
}
3.MultiplyService.java
package com.li.tx.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
/**
* @author 李
* @version 1.0
*/
@Service
public class MultiplyService {
@Resource
private GoodsService goodsService;
/**
* 說明
* 1.multiBuyGoodsByTx() 方法中,有兩次購商品的操作
* 2.buyGoodsByTx 和 buyGoodsByTx02 都是宣告式事務
* 3.并且buyGoodsByTx 和 buyGoodsByTx02使用的傳播屬性為默認的 REQUIRED,
* 即會當做一個整體事務來處理
*/
@Transactional
public void multiBuyGoodsByTx() {
goodsService.buyGoodsByTx(1, 1, 1);
goodsService.buyGoodsByTx02(1, 1, 1);
}
}
4.測驗
//測驗事務的傳播機制
@Test
public void multiBuyGoodsByTx(){
ApplicationContext ioc =
new ClassPathXmlApplicationContext("tx.xml");
MultiplyService multiplyService = ioc.getBean(MultiplyService.class);
multiplyService.multiBuyGoodsByTx();
}
測驗結果:購買成功
測驗前資料:
表結構詳見上一篇

測驗后資料:

5.在GoodsDao的updateAmount2()方法中添加錯誤字符,使其不能成功執行:
因為 buyGoodsByTx() 和buyGoodsByTx02() 的事務傳播屬性都是required,且都在multiBuyGoodsByTx()方法內部,因此它們被視為一個整體,當 buyGoodsByTx02() 執行出現錯誤,兩個方法將會一起回滾,
執行4.的測驗代碼,測驗結果:出現例外,
測驗后資料:

仍然是之前的資料,說明兩個方法一起進行了事務回滾,
6.將GoodsService 的 buyGoodsByTx() / buyGoodsByTx02() 方法的事務傳播屬性改為REQUIRES_NEW,
這時兩個方法的事務是獨立的,buyGoodsByTx02() 失敗不會造成 buyGoodsByTx() 的回滾,
7.再執行4.測驗方法,結果如下:仍然出現例外
但是只有 buyGoodsByTx() 方法操作改變了資料,
測驗前資料:

測驗后資料:

說明只有 buyGoodsByTx02() 方法進行了回滾,
4.事務的隔離機制
4.1事務隔離級別說明
MySQL 隔離級別定義了事務與事務之間的隔離程度
| MySQL隔離級別(4種) | 臟讀 | 不可重復讀 | 幻讀 | 加鎖讀 |
|---|---|---|---|---|
| 讀未提交(Read uncommitted) | v | v | v | 不加鎖 |
| 讀已提交(Read committed) | x | v | v | 不加鎖 |
| 可重復讀(Repeatable read) | x | x | x | 不加鎖 |
| 可串行化(Serializable) | x | x | x | 加鎖 |
關于可重復讀會不會發生幻讀問題:
SQL92標準有,mysql資料庫改進了,解決了這個級別的幻讀問題,
- 事務隔離級別說明
-
Spring宣告式事務的默認隔離級別,就是 mysql 資料庫默認的隔離級別,一般為 REPREATABLE_READ
查看原始碼可知:Use the default isolation level of the underlying datastore. All other levels correspond to the JDBC isolation levels.
-
查看資料庫的隔離級別
SELECT @@global.tx_isolation
4.2事務隔離級別的設定和測驗
整體思路如下:
在開啟了宣告式事務的某方法中,查詢兩次資料,在第一次查詢后,先在控制臺中修改該資料(在終端中默認為自動提交),方法再進行第二次的查詢,查看兩次查詢的資料是否相同,通過這樣的方法來模擬兩個客戶端,測驗宣告式事務的隔離級別,
1.修改GoodsService.java,先測驗默認隔離級別,增加方法 buyGoodsByTxISOLATION()
/**
* 在默認下,宣告式事務使用的隔離界別為 可重復讀-Repeatable read
*/
@Transactional
public void buyGoodsByTxISOLATION() {
//查詢兩次商品的價格
Float price = goodsDao.queryPriceById(1);
System.out.println("第一次查詢的價格=" + price);
Float price2 = goodsDao.queryPriceById(1);
System.out.println("第二次查詢的價格=" + price2);
}
并在方法如下位置打上斷點
2.測驗方法
//測驗宣告式事務的隔離級別
@Test
public void buyGoodsByTxISOLATIONTest() {
ApplicationContext ioc =
new ClassPathXmlApplicationContext("tx.xml");
GoodsService goodsService = ioc.getBean(GoodsService.class);
goodsService.buyGoodsByTxISOLATION();
}
3.點擊debug,當游標跳轉到斷點時,可以看到第一次查詢的 price=10
4.這時我們在控制臺修改該資料為 15
5.然后點擊Step Over,發現第二次查詢的價格仍然為 10
這說明Spring的宣告是事務的默認隔離級別為 可重復讀,
6.將方法buyGoodsByTxISOLATION() 的事務隔離級別改為 讀已提交
讀已提交表示只要是提交的資料,在當前事務中都可以讀取到最新資料
同時和之前一樣打上斷點,
7.測驗方法不變,點擊debug,游標跳轉到斷點時,可以看到第一次查詢時 price=15
8.此時在控制臺將該資料改為 20
9.點擊Step Over,可以看到第二次查詢的資料已經變成了 20
說明當前事務的隔離級別為 讀已提交,
4.3事務的超時回滾
- 基本介紹
- 如果一個事務執行的時間超過某個時間限制,就讓該事務回滾,
- 可以通過設定事務超時回滾來實作
- 基本語法
例子:超時回滾代碼實作
1.GoodsService 中增加方法 buyGoodsByTxTimeout(),并設定事務超時時間為2s,為了模擬超時效果,在方法中休眠4s,
/**
* 1.timeout = 2,表示該方法如果執行時間超過了兩秒,就進行回滾
* 2.如果沒有設定 timeout,則默認該值為 -1,表示使用默認超時時間,
* 一般為連接的資料庫的默認超時時間
*/
@Transactional(timeout = 2)
public void buyGoodsByTxTimeout(int userId, int goodsId, int amount){
//輸出購買的相關資訊
System.out.println("用戶購買資訊 userId=" + userId
+ " goodsId=" + goodsId + " 購買數量=" + amount);
//1.得到商品價格
Float price = goodsDao.queryPriceById2(goodsId);
//2.減少用戶余額
goodsDao.updateBalance2(userId, price * amount);
//模擬超時
System.out.println("==========超時開始4s=========");
try {
Thread.sleep(4000);//休眠4s
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("==========超時結束4s=========");
//3.減少商品庫存量
goodsDao.updateAmount2(goodsId, amount);
System.out.println("用戶購買成功...");
}
2.測驗方法
//測驗超時 timeout 屬性
@Test
public void buyGoodsByTxTimeoutTest() {
ApplicationContext ioc =
new ClassPathXmlApplicationContext("tx.xml");
GoodsService goodsService = ioc.getBean(GoodsService.class);
goodsService.buyGoodsByTxTimeout(1, 1, 1);
}
測驗結果:出現例外,顯示事務超時,
測驗前資料:

測驗后資料:

資料沒有進行改變,說明事務超時,并進行了回滾,
5.練習
要求:模擬一個用戶,進行銀行轉賬,購買淘寶商品的業務,資料表,dao層,service層自己設定,要求保證資料一致性,
- seller [賣家表]
- buyer [買家表]
- goods [商品表[有庫存量屬性]]
- taoBao [taoBao表,提取入賬成交額的 10%]
- 要求簡單實作,使用宣告式事務完成
- 要求創建新的spring容器檔案 shopping_ioc.xml,完成測驗
實作
1.創建表格,并插入初始資料
-- buyer表
CREATE TABLE `buyer`(
buyer_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
buyer_name VARCHAR(32) NOT NULL DEFAULT '',
buyer_money DOUBLE NOT NULL DEFAULT 0.0
)CHARSET=utf8;
INSERT INTO `buyer` VALUES(NULL,'張三', 1000);
INSERT INTO `buyer` VALUES(NULL,'李四', 2000);
-- seller表
CREATE TABLE `seller`(
seller_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
seller_name VARCHAR(32) NOT NULL DEFAULT '',
seller_money DOUBLE NOT NULL DEFAULT 0.0
)CHARSET=utf8 ;
INSERT INTO `seller` VALUES(NULL,'賣家1', 0);
INSERT INTO `seller` VALUES(NULL,'賣家2', 0);
-- goods表
CREATE TABLE `goods`(
goods_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
goods_name VARCHAR(32) NOT NULL DEFAULT '',
price DOUBLE NOT NULL DEFAULT 0.0,
seller_id INT UNSIGNED,
goods_num INT UNSIGNED DEFAULT 0
)CHARSET=utf8 ;
INSERT INTO `goods` VALUES(NULL,'小風扇', 10.00, 1, 100);
INSERT INTO `goods` VALUES(NULL,'小臺燈', 12.00, 1, 100);
INSERT INTO `goods` VALUES(NULL,'可口可樂', 3.00, 2, 100);
-- taoBao表
CREATE TABLE `taoBao`(
taoBao_money DOUBLE NOT NULL DEFAULT 0.0
)CHARSET=utf8 ;
INSERT INTO `taoBao` VALUES(0);


2.ShopDao
package com.li.tx.hw.dao;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
/**
* @author 李
* @version 1.0
*/
@Repository
public class ShopDao {
@Resource
private JdbcTemplate jdbcTemplate;
//通過商品id,查詢商品價格
public Double queryGoodsPrice(int goodsId) {
String sql = "SELECT price FROM goods WHERE goods_id=?";
return jdbcTemplate.queryForObject(sql, Double.class, goodsId);
}
//通過商品id,查詢商品所屬的賣家id
public Integer queryGoodsOwner(int goodsId) {
String sql = "SELECT seller_id FROM goods WHERE goods_id=?";
return jdbcTemplate.queryForObject(sql, Integer.class, goodsId);
}
//通過商品id,修改商品庫存量
public void updateGoodsNum(int goodsId, int shopNum) {
String sql = "UPDATE goods SET goods_num=goods_num-? WHERE goods_id=?";
jdbcTemplate.update(sql, shopNum, goodsId);
}
//通過買家id,修改買家余額
public void updateBuyerMoney(Integer buyerId, Double money) {
String sql = "UPDATE buyer SET buyer_money=buyer_money-? WHERE buyer_id=?";
jdbcTemplate.update(sql, money, buyerId);
}
//通過賣家id,修改賣家余額
public void updateSellerMoney(Integer sellerId, Double money) {
String sql = "UPDATE seller SET seller_money=seller_money+? WHERE seller_id=?";
jdbcTemplate.update(sql, money, sellerId);
}
//修改 taoBao余額
public void updateTaobaoMoney(Double money) {
String sql = "UPDATE taoBao SET taoBao_money=taoBao_money+?";
jdbcTemplate.update(sql, money);
}
}
3.ShopService
package com.li.tx.hw.service;
import com.li.tx.hw.dao.ShopDao;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
/**
* @author 李
* @version 1.0
*/
@Service
public class ShopService {
@Resource
private ShopDao shopDao;
@Transactional
public void shopping(int buyerId, int goodsId, int goodsNum) {
System.out.println("用戶購買資訊 buyerId=" + buyerId
+ " goodsId=" + goodsId + " 購買數量=" + goodsNum);
//查詢商品價格
Double goodsPrice = shopDao.queryGoodsPrice(goodsId);
System.out.println("商品價格=" + goodsPrice);
//查詢商品賣家
Integer sellerId = shopDao.queryGoodsOwner(goodsId);
System.out.println("商品所屬賣家=" + sellerId);
//減少商品庫存量
shopDao.updateGoodsNum(goodsId, goodsNum);
System.out.println("商品庫存-" + goodsNum);
//修改買家余額
shopDao.updateBuyerMoney(buyerId, goodsPrice * goodsNum);
System.out.println("買家余額-" + goodsPrice * goodsNum);
//將成交額的 90% 轉入賣家余額
shopDao.updateSellerMoney(sellerId, goodsPrice * goodsNum * 0.9);
System.out.println("賣家余額+" + goodsPrice * goodsNum * 0.9);
//將成交額的 10% 轉入taoBao余額
shopDao.updateTaobaoMoney(goodsPrice * goodsNum * 0.1);
System.out.println("taoBao余額+" + goodsPrice * goodsNum * 0.1);
System.out.println("購買成功...");
}
}
4.配置容器檔案
<!--配置要掃描的包-->
<context:component-scan base-package="com.li.tx.hw"/>
<!--引入外部的屬性檔案-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--配置資料源物件-->
<bean id="dataSources">
<property name="user" value="https://www.cnblogs.com/liyuelian/archive/2023/02/01/${jdbc.user}"/>
<property name="password" value="https://www.cnblogs.com/liyuelian/archive/2023/02/01/${jdbc.pwd}"/>
<property name="driverClass" value="https://www.cnblogs.com/liyuelian/archive/2023/02/01/${jdbc.driver}"/>
<property name="jdbcUrl" value="https://www.cnblogs.com/liyuelian/archive/2023/02/01/${jdbc.url}"/>
</bean>
<bean id="jdbcTemplate">
<property name="dataSource" ref="dataSources"/>
</bean>
<!--配置事務管理器物件
1.DataSourceTransactionManager 這個物件是進行事務管理的
2.一定要配置資料源屬性,即指定該事務管理器 是對哪個資料源進行事務控制
-->
<bean
id="dataSourceTransactionManager">
<property name="dataSource" ref="dataSources"/>
</bean>
<!--配置:啟用基于注解的宣告式事務管理功能-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
5.jdbc.properties
jdbc.user=root
jdbc.pwd=123456
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring
6.測驗
@Test
public void shoppingTest() {
ApplicationContext ioc =
new ClassPathXmlApplicationContext("shopping_ioc.xml");
ShopService shopService = ioc.getBean(ShopService.class);
shopService.shopping(1, 1, 10);
}
測驗結果:
測驗后的資料:


7.測驗資料一致性:
修改sql,使其無法執行:
測驗結果:出現例外,
查看資料庫表,資料沒有改變,說明事務進行了回滾,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/542776.html
標籤:其他
