Spring-04 聲明式事務 1、事務的定義 事務就是由一組邏輯上緊密關聯的多個工作單元(資料庫操作)而合併成一個整體,這些操作要麼都執行,要麼都不執行。 2、事務的特性:ACID 1)原子性A :原子即不可再分,表現:一個事務涉及的多個操作在業務邏輯上缺一不可,保證同一個事務中的操作要不都提交, ...
Spring-04 聲明式事務
1、事務的定義
事務就是由一組邏輯上緊密關聯的多個工作單元(資料庫操作)而合併成一個整體,這些操作要麼都執行,要麼都不執行。
2、事務的特性:ACID
1)原子性A :原子即不可再分,表現:一個事務涉及的多個操作在業務邏輯上缺一不可,保證同一個事務中的操作要不都提交,要不都不提交;
2)一致性C :數據的一致性,一個事務中,不管涉及到多少個操作,都必須保證數據提交的正確性(一致);即:如果在事務數據處理中,有一個或者幾個操作失敗,必須回退所有的數據操作,恢復到事務處理之前的統一狀態;
3)隔離性I :程式運行過程中,事務是併發執行的,要求每個事務之間都是隔離的,互不幹擾;
4)持久性D :事務處理結束,要將數據進行持久操作,即永久保存。
3、事務的分類:
1)編程式事務-使用jdbc原生的事務處理,可以將事務處理寫在業務邏輯代碼中,違背aop原則,不推薦;
2)聲明式事務-使用事務註解 @Transactional,可以聲明在方法上,也可以聲明在類上;
-
**優先順序**: * <mark>聲明在**類上**,會對**當前類內的所有方式生效**(所有方法都有事務處理);</mark> * <mark>聲明在**方法上**,只會**對當前方法生效**,當類上和方法上同時存在,**方法的優先順序高於類**(有些方法,對聲明式事務做特殊屬性配置);</mark>
4、事務的屬性:
4.1 事務的傳播行為:propagation屬性
事務的傳播行為:propagation 屬性指定;
當一個帶事務的方法被另一個帶事務的方法調用時(事務嵌套),當前事務如何處理:
-
propagation = Propagation.REQUIRED :
- 預設值,使用調用者的事務(全程就一個事務,如果有事務嵌套,以外部事務為主);
-
propagation = Propagation.REQUIRES_NEW :
- 將調用者的事務直接掛起,自己重開新的事務處理,結束提交事務,失敗回滾;(當事務嵌套時,內層事務,會重新開啟新事務的處理,不受外部事務的管理);
4.2 事務的隔離級別:isolation屬性
事務的隔離級別:isolation屬性指定隔離級別,只有InnoDB支持事務,所有這裡說的事務隔離級別指的是InnoDB下的事務隔離級別。
1、讀未提交 : 讀取其它事務未提交的數據,瞭解,基本不會使用;
2、讀已提交 : oracle的預設事務隔離級別,同一個事務處理中,只能讀取其它事務提交後的數據(也就是說事務提交之前對其餘事務不可見);
3、可重覆讀 : mysql預設事務隔離級別,同一個事務處理中,多次讀取同一數據是都是一樣的,不受其它事務影響;
4、串列化 : 可以避免上面所有併發問題,但是執行效率最低,數據一致性最高;
4.3 事務的指定回滾和不會滾
事務的指定回滾和不會滾:Spring在預設的情況下,是對所有的運行時異常會執行事務回滾
1、 rollbackFor : 指定回滾異常,只有產生了指定的異常類型,才會回滾事務;
2、 noRollbackFor : 指定不會滾異常,產生了指定的異常類型,也不會回滾事務;
4.4 事務的超時時長-瞭解
1、timeout,指定事務出現異常,沒有及時回滾,單位是秒,防止事務超時,占用資源;
4.5 事務的只讀-瞭解
1、readOnly=false,預設,可讀可寫‘;
2、readOnly=true,代表該事務處理,理論上只允許讀取,不能修改(只是通知spring,並不是一個強制選項)
目的就是:提示資料庫驅動程式和資料庫系統,這個事務並不包含更改數據的操作,那麼JDBC驅動程式和資料庫就有可能根據這種情況對該事務進行一些特定的優化,比方說不安排相應的資料庫鎖,以減輕事務對資料庫的壓力,畢竟事務也是要消耗資料庫的資源的。
但是你非要在“只讀事務”裡面修改數據,也並非不可以,只不過對於數據一致性的保護不像“讀寫事務”那樣保險而已。
5、 環境搭建
5.1主要 jar包
<spring.version>4.3.18.RELEASE</spring.version>
<mysql.version>5.1.47</mysql.version>
<c3p0.version>0.9.5.2</c3p0.version>
<!-- transaction begin -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<!--c3p0數據源 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>${c3p0.version}</version>
</dependency>
<!-- 最主要的是 spring-tx-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- transaction end -->
<!-- mysql begin -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- mysql end -->
5.2 配置文件
<?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:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
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">
<!-- 組件掃描-->
<context:component-scan base-package="com.kgc.spring"></context:component-scan>
<!-- spring框架讀取外部配置文件-->
<!-- 方式一-->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<!-- 指定配置文件的位置,classpath:是類路徑,只有spring可識別 -->
<property name="location" value="classpath:jdbc.properties"></property>
</bean>
<!-- c3p0 資料庫配置,可以管理資料庫連接,還可以自動重連 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${driver}"></property>
<property name="jdbcUrl" value="${url}"></property>
<property name="user" value="${username}"></property>
<property name="password" value="${password}"></property>
</bean>
<!-- Spring框架對JDBC進行封裝,我們使用JdbcTemplate可以方便實現對資料庫的增刪改查操作。 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 數據源事務管理器 -->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--
配置聲明式事務註解掃描,掃描所有添加的聲明式事務註解,交給事務管理器進行統一管理;
名稱空間是tx結尾,才可以生效;
transaction-manager屬性是指定當前自定義的事務管理器;
如果事務管理器的id值是transactionManager,可以省略此屬性的指定
-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>
6、測試
5.1 購買一輛車(沒有事務嵌套)
TES 購買一輛 AudiQ5;
模擬購買一輛車,主要流程:(1,2,3 整體是一個事務)
1、據買家購買汽車編號,獲取汽車詳情;
2、扣汽車的庫存
3、扣買家的餘額
5.1.2 主要業務代碼
5.1.2.1 扣用戶餘額業務
BuyerServiceImpl
如果買家餘額不足,直接返回;
@Service
public class BuyerServiceImpl implements BuyerService {
@Autowired
private BuyerDao buyerDao;
@Override
public void subBuyerMoneyByName(String buyerName, Car car) {
// 根據買家姓名,查詢買家詳情
Buyer buyer = buyerDao.selectBuyerByName(buyerName);
// 判斷買家餘額是否充足,如果不足,不能繼續扣減金額
if(buyer.getMoney() < car.getPrice()){
System.out.println(String.format("****** 買家:%s,餘額不足! ------", buyerName));
return; //直接return
}
// 餘額充足,執行扣減餘額
int row = buyerDao.updateBuyerMoneyById(buyer.getId(), car.getPrice());
System.out.println(String.format("****** 買家:%s,餘額扣減成功,影響行數:%s ------", buyerName, row));
}
}
5.1.2.2 扣庫存業務
CarsStockServiceImpl
如果庫存不足,直接返回;
@Service
public class CarsStockServiceImpl implements CarsStockService {
@Autowired
private CarsStockDao carsStockDao;
@Override
public void subCarsStockBuyId(Car car) {
//根據汽車編號,插敘汽車詳情
CarsStock carsStock = carsStockDao.selectCarsStockByCid(car.getId());
//判斷庫存是否充足,如果不足,不能購買
if(carsStock.getStock() <= 0){
System.out.println("汽車"+car.getName()+"庫存不足");
return; //直接return
}
//庫存足夠,執行庫存減少
int row = carsStockDao.updateCarStockById(car.getId());
System.out.println("------ 汽車"+car.getName()+"庫存扣減成功" + row +" ------");
}
}
5.1.2.3 用戶買車業務
根據 賣家名字,和汽車編號買車;
BuyCarServiceImpl
@Service("buyCarService") //方便從容器中獲取對象
public class BuyCarServiceImpl implements BuyCarService {
@Autowired
private CarDao carDao;
@Autowired
private CarsStockService carStockService;
@Autowired
private BuyerService buyerService;
//根據 賣家名字,和汽車編號買車
@Override
public void buyCar(String buyerName, Integer carId) {
System.out.println(String.format("------ 買家:%s,購買汽車編號:%s 開始 ------",buyerName,carId));
// 根據買家購買汽車編號,獲取汽車詳情
Car car = carDao.selectCarById(carId);
// 扣買家的餘額
buyerService.subBuyerMoneyByName(buyerName, car);
// 扣汽車的庫存
carStockService.subCarsStockBuyId(car);
System.out.println(String.format("------ 買家:%s,購買汽車編號:%s 結束 ------",buyerName,carId));
}
}
5.1.3 測試(沒有添加事務處理)
5.1.3.1 測試前的數據
- 汽車價格
- 用戶餘額
- 庫存
根據觀察,發現用戶TES的餘額不夠買AudiQ5;
5.1.3.2 測試
//沒有添加事務處理
@Test
public void testSpringUnUsedTx(){
//獲取買車的業務實現對象
BuyCarService buyCarService = context.getBean("buyCarService", BuyCarService.class);
//調用買車的業務方法
buyCarService.buyCar("TES",1);
}
運行結果:
5.1.3.3 測試後的數據
- 用戶餘額
- 庫存
5.1.4 測試 (加上@Transactional 註解添加事務處理)
5.1.4.1 方法上加上@Transactional 註解
@Transactional
public void buyCar(String buyerName, Integer carId) {
...
}
5.1.4.1 測試
恢復初始數據後測試;
5.1.5 測試 (增加異常拋出)
餘額不足,沒有異常直接return,不能觸發事務;
需要拋出自定義異常才會觸發事務處理;
5.1.5.1 自定義異常類
BuyCarException
public class BuyCarException extends RuntimeException {
//生成所有的構造方法
public BuyCarException() {
}
public BuyCarException(String message) {
super(message);
}
public BuyCarException(String message, Throwable cause) {
super(message, cause);
}
public BuyCarException(Throwable cause) {
super(cause);
}
public BuyCarException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
5.1.5.2 拋出異常
當餘額或庫存不足的時候,拋出自定義異常;
BuyerServiceImpl
@Service
public class BuyerServiceImpl implements BuyerService {
@Autowired
private BuyerDao buyerDao;
@Override
public void subBuyerMoneyByName(String buyerName, Car car) {
Buyer buyer = buyerDao.selectBuyerByName(buyerName);
if(buyer.getMoney() < car.getPrice()){
System.out.println(String.format("****** 買家:%s,餘額不足! ------", buyerName));
//return; //沒有異常直接return,不能觸發事務
//餘額不足拋出自定義異常
//*****餘額充足,執行扣減餘額*****
throw new BuyCarException(String.format("****** 買家:%s,餘額不足! ------", buyerName));
}
int row = buyerDao.updateBuyerMoneyById(buyer.getId(), car.getPrice());
System.out.println(String.format("****** 買家:%s,餘額扣減成功,影響行數:%s ------", buyerName, row));
}
}
CarsStockServiceImpl
@Service
public class CarsStockServiceImpl implements CarsStockService {
@Autowired
private CarsStockDao carsStockDao;
@Override
public void subCarsStockBuyId(Car car) {
CarsStock carsStock = carsStockDao.selectCarsStockByCid(car.getId());
if(carsStock.getStock() <= 0){
System.out.println("汽車"+car.getName()+"庫存不足");
//return; //沒有異常直接return,不能觸發事務
//*****庫存不足,執行扣減餘額*****
throw new BuyCarException("汽車"+car.getName()+"庫存不足");
}
int row = carsStockDao.updateCarStockById(car.getId());
System.out.println("------ 汽車"+car.getName()+"庫存扣減成功" + row +" ------");
}
}
5.1.5.3 測試
恢復初始數據後測試;
5.1.5.4 測試 (餘額充足)
5.1.5.4.1 測試前的數據
- 用戶餘額
- 庫存
5.1.5.4.2測試
5.1.5.4.3 測試後的數據
- 用戶餘額
- 庫存
5.2購買兩輛車(有事務嵌套) **對過程理解還有問題
JDG 購買一輛 AudiQ5 和一輛 BmwX3
模擬購物車一次購買兩輛車,主要流程:(1,2 整體式一個事務)
1、買第一輛車(1.1,1.2,1.3 整體是一個事務)
1.1 據買家購買汽車編號,獲取汽車詳情;
1.2扣汽車的庫存
1.3扣買家的餘額
2、買第二輛車(1.1,1.2,1.3 整體是一個事務)
1.1 據買家購買汽車編號,獲取汽車詳情;
1.2扣汽車的庫存
1.3扣買家的餘額
5.2.1 主要業務代碼
模擬購物車一次購買兩輛車;
多次調用購買一輛汽車業務;
BuyCarCartServiceImpl
@Service("BuyCarCartService" )
public class BuyCarCartServiceImpl implements BuyCarCartService {
@Autowired
private BuyCarService buyCarService;
@Override
@Transactional //購物車外層事務註解,buyCarService介面方法中也有事務註解
public void buyCarCart(String buyerName, List<Integer> carIds) {
//模擬購物車垢面多輛車,方便演示事務傳播行為,一輛一輛購買(單獨調用買車介面)
carIds.forEach(carId -> buyCarService.buyCar(buyerName,carId));
}
}
5.2.1 propagation = Propagation.REQUIRED
預設傳播特性,以外部事務為主;propagation = Propagation.REQUIRED 可以不寫;
5.2.1.1 測試前的數據
- 汽車價格
- 用戶餘額
- 庫存
5.2.1.2測試
//測試事務存在 事務嵌套 的傳播行為
//購物車結算
@Test
public void testSpring(){
BuyCarCartService buyCarCartService = context.getBean("BuyCarCartService", BuyCarCartService.class);
//調用購物車買車的業務方法
buyCarCartService.buyCarCart("JDG", Arrays.asList(1,2));
}
測試結果:
5.2.1.3測試後的數據
- 用戶餘額
- 庫存
5.2.1.4 總結
通過查看資料庫的數據發現,數據沒有改變,說明事務並事務已經回滾,也就是說預設傳播特性,以外部事務為主;
5.2.3 propagation = Propagation.REQUIRES_NEW
propagation = Propagation.REQUIRES_NEW的傳播特性,內部事務會自己重開新的事務處理;
5.2.3.1 內部事務註解添加屬性參數
buyCar方法
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void buyCar(String buyerName, Integer carId) {
...
}
5.2.3.2 測試
恢複數據再測試;
5.2.3.3測試後的數據
- 用戶餘額
- 庫存
5.2.3.4 總結
通過查看資料庫的數據發現,數據發生改變,說明內部事務重新開起新的事務處理,不受外部事務的管理;