宣告式事務
1.事務分類
- 編程式事務
Connection connection = JdbcUtils.getConnection();
try{
//1.先設定事務不要提交
connection.setAutoCommit(false);
//2.進行業務 crud
//3.提交事務
connection.commit();
}catch(Exception e){
//4.出現例外,回滾
connection.rollback();
}
- 宣告式事務(后面以一個購買商品的系統為例)
2.宣告式事務-使用實體
2.1需求說明
-
需求說明 - 用戶購買商品
去處理用戶購買商品的業務邏輯:當一個用戶去購買商品,應該包含三個步驟:
- 通過商品 id 獲取價格
- 購買商品(某人購買商品,修改用戶余額)
- 修改庫存量
這里一共涉及到三張表:用戶表、商品表、商品存量表,顯然,應該使用事務處理,
2.2解決方案分析
方案一:使用傳統的編程式事務來處理,將代碼寫到一起
(缺點是:代碼冗余,效率低,不利于拓展;優點是簡單,好理解)
//例如:
Connection connection = JdbcUtils.getConnection();
try{
//1.先設定事務不要提交
connection.setAutoCommit(false);
//2.進行業務 crud
//多個表的修改,添加,洗掉
//select form 商品表 => 獲取價格
//修改用戶余額 update...
//修改商品庫存量 update...
//3.提交事務
connection.commit();
}catch(Exception e){
//4.出現例外,回滾
connection.rollback();
}
方案二:使用 Spring 的宣告式事務來處理,可以將上面三個子步驟分別寫成一個方法,然后統一管理,
(這是Spring的優越性所在,開發中使用很多,優點是無代碼冗余,效率高,拓展方便,缺點是理解較困難)底層使用AOP(動態代理+動態系結+反射+注解)
2.3宣告式事務使用-代碼實作
- 創建表
-- 演示宣告式事務創建的表
-- 用戶表
CREATE TABLE `user_account`(
user_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(32) NOT NULL DEFAULT '',
money DOUBLE NOT NULL DEFAULT 0.0
)CHARSET=utf8;
INSERT INTO `user_account` VALUES(NULL,'張三', 1000);
INSERT INTO `user_account` VALUES(NULL,'李四', 2000);
-- 商品表
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
)CHARSET=utf8 ;
INSERT INTO `goods` VALUES(NULL,'小風扇', 10.00);
INSERT INTO `goods` VALUES(NULL,'小臺燈', 12.00);
INSERT INTO `goods` VALUES(NULL,'可口可樂', 3.00);
-- 商品存量表
CREATE TABLE `goods_amount`(
goods_id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
goods_num INT UNSIGNED DEFAULT 0
)CHARSET=utf8 ;
INSERT INTO `goods_amount` VALUES(1,200);
INSERT INTO `goods_amount` VALUES(2,20);
INSERT INTO `goods_amount` VALUES(3,15);

- 創建GoodsDao
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);
}
}
- 配置容器檔案
因為使用了注解 @Resource 的方式自動裝配 JdbcTemplate 物件,這里需要配置該物件,
<!--配置要掃描的包-->
<context:component-scan base-package="com.li.tx"/>
<!--引入外部的屬性檔案-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--配置資料源物件-DataSource-->
<bean id="dataSource">
<!--給資料源物件配置屬性值-->
<property name="user" value="https://www.cnblogs.com/liyuelian/p/${jdbc.user}"/>
<property name="password" value="https://www.cnblogs.com/liyuelian/p/${jdbc.pwd}"/>
<property name="driverClass" value="https://www.cnblogs.com/liyuelian/p/${jdbc.driver}"/>
<property name="jdbcUrl" value="https://www.cnblogs.com/liyuelian/p/${jdbc.url}"/>
</bean>
<!--配置JdbcTemplate物件-->
<bean id="jdbcTemplate">
<!--給JdbcTemplate物件配置DataSource屬性-->
<property name="dataSource" ref="dataSource"/>
</bean>
- 創建GoodsService,撰寫方法,驗證不使用事務就會出現資料不一致現象
package com.li.tx.service;
import com.li.tx.dao.GoodsDao;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* @author 李
* @version 1.0
*/
@Service //將GoodsService物件注入到容器中
public class GoodsService {
@Resource
private GoodsDao goodsDao;
/**
* 撰寫一個方法,完成用戶購買商品的業務
*
* @param userId 用戶 id
* @param goodsId 商品 id
* @param amount 購買的商品數量
*/
public void buyGoods(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("用戶購買成功...");
}
}
- 新增添掃描的包
<context:component-scan base-package="com.li.tx.service"/>
- 為了測驗,故意在Dao的sql陳述句中添加錯誤符號
測驗:
@Test
public void buyGoodsTest() {
ApplicationContext ioc =
new ClassPathXmlApplicationContext("tx.xml");
GoodsService goodsService = ioc.getBean(GoodsService.class);
goodsService.buyGoods(1,1,10);
}
測驗結果:出現例外
原始表資訊:

當前表資訊:


可以看到用戶表的余額減少了,但是商品庫存表的庫存沒有改變,這就產生了資料不一致問題,因此要使用事務,
- 改進GoodsService的業務方法,使用宣告式事務:
/**
* 1.使用注解 @Transactional 可以進行宣告式事務控制
* 2.該注解會將標識方法中,對資料庫的操作 作為一個事務來管理
* 3.@Transactional 底層是使用的仍然是AOP機制
* 4.底層是使用動態代理物件來呼叫 buyGoodsByTx()方法
* 5.在執行 buyGoodsByTx()方法前,先呼叫事務管理器的 doBegin()方法,再呼叫目標方法
* 如果執行沒有發生例外,就呼叫事務管理器 doCommit()方法,否則呼叫 doRollback()方法
* @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("用戶購買成功...");
}
- 之前的基礎上,在容器檔案中配置事務管理器,并啟用基于注解的宣告式事務管理功能
<!--配置事務管理器-物件
1.DataSourceTransactionManager 這個物件是進行事務管理的
2.一定要配置資料源屬性,即指定該事務管理器 是對哪個資料源進行事務控制
-->
<bean
id="transactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置:啟用基于注解的宣告式事務管理功能-->
<tx:annotation-driven transaction-manager="transactionManager"/>
注意:這里的 annotation-driven 標簽要選擇以tx結尾的
- 再次測驗
@Test
public void buyGoodsTestByTx() {
ApplicationContext ioc =
new ClassPathXmlApplicationContext("tx.xml");
GoodsService goodsService = ioc.getBean(GoodsService.class);
goodsService.buyGoodsByTx(1,1,10);
}
測驗結果:可以看到仍然出現例外(因為之前在sql陳述句中故意添加了錯誤字符)
測驗前資料:

測驗后資料:

表資料在測驗前后資料一致,這說明事務控制起作用了,在出現例外時進行了回滾,因此資料沒有被改變,
2.4宣告式事務機制-Debug
在整個宣告式事務中,DataSourceTransactionManager類尤為重要,
我們可以看到在 DataSourceTransactionManager 的原始碼中,有一個 DataSource 屬性,即資料源物件,因為連接是在 DataSource 中獲取,而事務管理器通過連接才能進行事務管理,
此外,DataSourceTransactionManager 還有很多重要的方法:doBegin(),doCommit(),doRollback()等,
debug-1-例外情況
-
以 2.3 的代碼為例,在doBegin方法旁打上斷點,
-
debug測驗方法:buyGoodsTestByTx()
@Test public void buyGoodsTestByTx() { ApplicationContext ioc = new ClassPathXmlApplicationContext("tx.xml"); GoodsService goodsService = ioc.getBean(GoodsService.class); goodsService.buyGoodsByTx(1,1,10); } -
游標首先跳轉到doBegin方法,點擊Step Over,當運行到下面的代碼時,可以看到
con.getAutoCommit()的值為true,即此時事務默認自動提交:
-
繼續點擊Step Over,當運行了
con.setAutoCommit(false);后,可以看到con.getAutoCommit()的值變成了false,此時事務不再進行自動提交:
-
在GoodsService的方法旁添加第二個斷點,點擊 Resume Program

-
游標跳轉到第二個斷點處,說明程式是先執行了doBegin()方法,再執行的bugGoodsByTx()方法,
-
在事務管理器的doRollback方法中打上第三個斷點
-
繼續點擊step Over,當bugGoodsByTx()方法執行到
goodsDao.updateAmount(goodsId, amount);時,游標跳轉到了第三個斷點處!最終在該方法中,執行了con.rollback(),進行回滾,
debug-2-正常的流程
修改之前的sql陳述句,將其變回正確的SQL,在事務管理器的doCommit方法中添加斷點,然后點擊debug,
游標仍然先進入到doBegin方法中,將自動事務提交修改為false后,又調轉到目標方法,這次執行完目標方法后,游標跳轉到了doCommit()方法中,在沒有出現例外的情況下,執行了事務提交,
總結:
在執行目標方法 buyGoodsByTx() 前,先呼叫事務管理器的 doBegin() 方法,再呼叫目標方法,如果執行沒有發生例外,就呼叫事務管理器 doCommit()方法,否則呼叫 doRollback()方法,
3.事務的傳播機制
事務的傳播機制說明:
-
當有多個事務處理并存時,如何控制?
-
比如用戶去購買兩次商品(使用不同的方法),每個方法都是一個事務,那么如何控制呢?
也就是說,某個方法本身是一個事務,然后該方法中又呼叫了其他一些方法,這些方法也是被@Transactional 修飾的,同樣是事務,
-
問題在于:里層方法的事務是被外層方法事務管理?還是它本身作為一個獨立的事務呢?這就涉及到事務的傳播機制問題,
3.1事務傳播機制種類
事務傳播的屬性 / 種類:
| 傳播屬性 | 說明 |
|---|---|
| REQUIRED | (默認)如果有事務在運行,當前的方法就在這個事務內運行,否則,就啟動一個新的事務,并且在自己的事務內運行 |
| REQUIRES_NEW | 當前的方法必須啟動新事務,并在它自己的事務內運行,如果有事務正在運行,應該將它掛起 |
| SUPPORTS | 如果有事務在運行,當前的方法就在這個事務內運行,否則它可以不運行在事務中 |
| NOT_SUPPORTED | 當前的方法不應該運行在事務中,如果有運行的事務,將它掛起 |
| MANDATORY | 當前的方法必須運行在事務內部,如果沒有正在運行的事務,就拋出例外 |
| NEVER | 當前的方法不應該運行在事務中,如果有運行的事務,就拋出例外 |
| NESTED | 如果有事務在運行,當前的方法就應該在這個事務的嵌套事務內運行,否則,就啟動一個新的事務,并在它自己的事務內運行 |
常用的就是前面兩種:(1)REQUIRED,(2)REQUIRES_NEWREQUIRES_NEW
其他的不常用
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/542676.html
標籤:Java
