對於mysql而言,關於事務的主要知識點可能幾種在隔離級別上;在Spring體系中,使用事務的時候,還有一個知識點事務的傳遞屬性同樣重要,本文將主要介紹7中傳遞屬性的使用場景
I. 配置
本文的case,將使用聲明式事務,首先我們創建一個SpringBoot項目,版本為2.2.1.RELEASE
,使用mysql作為目標數據庫,存儲引擎選擇Innodb
,事務隔離級別為RR
1. 項目配置
在項目pom.xml
文件中,加上spring-boot-starter-jdbc
,會注入一個DataSourceTransactionManager
的bean,提供了事務支持
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
2. 數據庫配置
進入spring配置文件application.properties
,設置一下db相關的信息
## DataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=
3. 數據庫
新建一個簡單的表結構,用於測試
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 '錢',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;
II. 使用說明
0. 准備
在正式開始之前,得先准備一些基礎數據
@Component
public class PropagationDemo {
@Autowired
private JdbcTemplate jdbcTemplate;
@PostConstruct
public void init() {
String sql = "replace into money (id, name, money) values (420, '初始化', 200)," + "(430, '初始化', 200)," +
"(440, '初始化', 200)," + "(450, '初始化', 200)," + "(460, '初始化', 200)," + "(470, '初始化', 200)," +
"(480, '初始化', 200)," + "(490, '初始化', 200)";
jdbcTemplate.execute(sql);
}
}
其次測試事務的使用,我們需要額外創建一個測試類,后面的測試case都放在類PropagationSample
中; 為了使輸出結果更加友好,提供了一個封裝的call方法
@Component
public class PropagationSample {
@Autowired
private PropagationDemo propagationDemo;
private void call(String tag, int id, CallFunc<Integer> func) {
System.out.println("============ " + tag + " start ========== ");
propagationDemo.query(tag, id);
try {
func.apply(id);
} catch (Exception e) {
System.out.println(e.getMessage());
}
propagationDemo.query(tag, id);
System.out.println("============ " + tag + " end ========== \n");
}
@FunctionalInterface
public interface CallFunc<T> {
void apply(T t) throws Exception;
}
}
1. REQUIRED
也是默認的傳遞屬性,其特點在於
- 如果存在一個事務,則在當前事務中運行
- 如果沒有事務則開啟一個新的事務
使用方式也比較簡單,不設置@Transactional
注解的propagation屬性,或者設置為 REQUIRED即可
/**
* 如果存在一個事務,則支持當前事務。如果沒有事務則開啟一個新的事務
*
* @param id
*/
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void required(int id) throws Exception {
if (this.updateName(id)) {
this.query("required: after updateMoney name", id);
if (this.updateMoney(id)) {
return;
}
}
throw new Exception("事務回滾!!!");
}
上面就是一個基礎的使用姿勢
private void testRequired() {
int id = 420;
call("Required事務運行", id, propagationDemo::required);
}
輸出結果如下
============ Required事務運行 start ==========
Required事務運行 >>>> {id=420, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
required: after updateMoney name >>>> {id=420, name=更新, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
事務回滾!!!
Required事務運行 >>>> {id=420, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
============ Required事務運行 end ==========
2. SUPPORTS
其特點是在事務里面,就事務執行;否則就非事務執行,即
- 如果存在一個事務,支持當前事務
- 如果沒有事務,則非事務的執行
使用姿勢和前面基本一致
@Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
public void support(int id) throws Exception {
if (this.updateName(id)) {
this.query("support: after updateMoney name", id);
if (this.updateMoney(id)) {
return;
}
}
throw new Exception("事務回滾!!!");
}
這個傳遞屬性比較特別,所以我們的測試case需要兩個,一個事務調用,一個非事務調用
測試事務調用時,我們新建一個bean: PropagationDemo2
,下面的support方法支持事務運行
@Component
public class PropagationDemo2 {
@Autowired
private PropagationDemo propagationDemo;
@Transactional(rollbackFor = Exception.class)
public void support(int id) throws Exception {
// 事務運行
propagationDemo.support(id);
}
}
對於非事務調用,則是直接在測試類中調用(請注意下面的call方法,調用的是兩個不同bean中的support方法)
private void testSupport() {
int id = 430;
// 非事務方式,異常不會回滾
call("support無事務運行", id, propagationDemo::support);
// 事務運行
id = 440;
call("support事務運行", id, propagationDemo2::support);
}
輸出結果如下:
============ support無事務運行 start ==========
support無事務運行 >>>> {id=430, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
support: after updateMoney name >>>> {id=430, name=更新, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
事務回滾!!!
support無事務運行 >>>> {id=430, name=更新, money=210, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
============ support無事務運行 end ==========
============ support事務運行 start ==========
support事務運行 >>>> {id=440, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
support: after updateMoney name >>>> {id=440, name=更新, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
事務回滾!!!
support事務運行 >>>> {id=440, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
============ support事務運行 end ==========
從上面的輸出,也可以得出結果:非事務執行時,不會回滾;事務執行時,回滾
3. MANDATORY
需要在一個正常的事務內執行,否則拋異常
使用姿勢如下
@Transactional(propagation = Propagation.MANDATORY, rollbackFor = Exception.class)
public void mandatory(int id) throws Exception {
if (this.updateName(id)) {
this.query("mandatory: after updateMoney name", id);
if (this.updateMoney(id)) {
return;
}
}
throw new Exception("事務回滾!!!");
}
這種傳播屬性的特點是這個方法必須在一個已有的事務中運行,所以我們的測試case也比較簡單,不再事務中運行時會怎樣?
private void testMandatory() {
int id = 450;
// 非事務方式,拋異常,這個必須在一個事務內部執行
call("mandatory非事務運行", id, propagationDemo::mandatory);
}
輸出結果
============ mandatory非事務運行 start ==========
mandatory非事務運行 >>>> {id=450, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
No existing transaction found for transaction marked with propagation 'mandatory'
mandatory非事務運行 >>>> {id=450, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
============ mandatory非事務運行 end ==========
從上面的輸出可知,直接拋出了異常,並不會執行方法內的邏輯
4. NOT_SUPPORT
這個比較有意思,被它標記的方法,總是非事務地執行,如果存在活動事務,則掛起
(實在是沒有想到,有什么場景需要這種傳播屬性)
一個簡單的使用case如下:
@Transactional(propagation = Propagation.NOT_SUPPORTED, rollbackFor = Exception.class)
public void notSupport(int id) throws Exception {
if (this.updateName(id)) {
this.query("notSupport: after updateMoney name", id);
if (this.updateMoney(id)) {
return;
}
}
throw new Exception("回滾!");
}
接下來需要好好的想一下我們的測試用例,首先是它需要在一個事務中調用,外部事物失敗回滾,並不會影響上面這個方法的執行結果
我們在PropagationDemo2
中,添加測試case如下
@Transactional(rollbackFor = Exception.class)
public void notSupport(int id) throws Exception {
// 掛起當前事務,以非事務方式運行
try {
propagationDemo.notSupport(id);
} catch (Exception e) {
}
propagationDemo.query("notSupportCall: ", id);
propagationDemo.updateName(id, "外部更新");
propagationDemo.query("notSupportCall: ", id);
throw new Exception("回滾");
}
輸出結果如下
============ notSupport start ==========
notSupport >>>> {id=460, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
notSupport: after updateMoney name >>>> {id=460, name=更新, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
notSupportCall: >>>> {id=460, name=更新, money=210, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
notSupportCall: >>>> {id=460, name=外部更新, money=210, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
回滾
notSupport >>>> {id=460, name=更新, money=210, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
============ notSupport end ==========
從上面輸出可以看出
- NOT_SUPPORT 標記的方法,屬於非事務運行(因為拋異常,修改沒有回滾)
- 外部事務回滾,不會影響其修改
5. NEVER
總是非事務地執行,如果存在一個活動事務,則拋出異常。
使用姿勢如下
/**
* 總是非事務地執行,如果存在一個活動事務,則拋出異常。
*
* @param id
* @throws Exception
*/
@Transactional(propagation = Propagation.NEVER, rollbackFor = Exception.class)
public void never(int id) throws Exception {
if (this.updateName(id)) {
this.query("notSupport: after updateMoney name", id);
if (this.updateMoney(id)) {
return;
}
}
}
我們的測試就比較簡單了,如果在事務中運行,是不是會拋異常
在PropagationDemo2
中,添加一個事務調用方法
@Transactional(rollbackFor = Exception.class)
public void never(int id) throws Exception {
propagationDemo.never(id);
}
測試代碼
private void testNever() {
int id = 470;
call("never非事務", id, propagationDemo2::never);
}
輸出結果
============ never非事務 start ==========
never非事務 >>>> {id=470, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
Existing transaction found for transaction marked with propagation 'never'
never非事務 >>>> {id=470, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
============ never非事務 end ==========
直接拋出了異常,並沒有執行方法內的業務邏輯
6. NESTED
其主要特點如下
- 如果不存在事務,則開啟一個事務運行
- 如果存在事務,則運行一個嵌套事務;
上面提出了一個嵌套事務的概念,什么是嵌套事務呢?
- 一個簡單的理解:外部事務回滾,內部事務也會被回滾;內部事務回滾,外部無問題,並不會回滾外部事務
接下來設計兩個測試用例,一個是內部事務回滾;一個是外部事務回滾
a. case1 內部事務回滾
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void nested(int id) throws Exception {
if (this.updateName(id)) {
this.query("nested: after updateMoney name", id);
if (this.updateMoney(id)) {
return;
}
}
throw new Exception("事務回滾!!!");
}
在PropagationDemo2
這個bean中,添加一個外部事務,捕獲上面方法的異常,因此外部執行正常
@Transactional(rollbackFor = Exception.class)
public void nested(int id) throws Exception {
propagationDemo.updateName(id, "外部事務修改");
propagationDemo.query("nestedCall: ", id);
try {
propagationDemo.nested(id);
} catch (Exception e) {
}
}
測試代碼
private void testNested() {
int id = 480;
call("nested事務", id, propagationDemo2::nested);
}
輸出結果如下
============ nested事務 start ==========
nested事務 >>>> {id=480, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
nestedCall: >>>> {id=480, name=外部事務修改, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
nested: after updateMoney name >>>> {id=480, name=更新, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
nested事務 >>>> {id=480, name=外部事務修改, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
============ nested事務 end ==========
仔細看一下上面的結果,外部事務修改的結果都被保存了,內部事務的修改被回滾了,沒有影響最終的結果
b. case2 外部事務回滾
@Transactional(propagation = Propagation.NESTED, rollbackFor = Exception.class)
public void nested2(int id) throws Exception {
if (this.updateName(id)) {
this.query("nested: after updateMoney name", id);
if (this.updateMoney(id)) {
return;
}
}
}
在PropagationDemo2
這個bean中,添加一個外部事務,內部事務正常,但是外部事務拋異常,主動回滾
@Transactional(rollbackFor = Exception.class)
public void nested2(int id) throws Exception {
// 嵌套事務,外部回滾,會同步回滾內部事務
propagationDemo.updateName(id, "外部事務修改");
propagationDemo.query("nestedCall: ", id);
propagationDemo.nested2(id);
throw new Exception("事務回滾");
}
測試代碼
private void testNested() {
int id = 490;
call("nested事務2", id, propagationDemo2::nested2);
}
輸出結果如下
============ nested事務2 start ==========
nested事務2 >>>> {id=490, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
nestedCall: >>>> {id=490, name=外部事務修改, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
nested: after updateMoney name >>>> {id=490, name=更新, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:46.0}
事務回滾
nested事務2 >>>> {id=490, name=初始化, money=200, is_deleted=false, create_at=2020-02-02 15:23:26.0, update_at=2020-02-02 15:23:26.0}
============ nested事務2 end ==========
仔細看上面的輸出,對別case1,其特別在於全部回滾了,內部事務的修改也被回滾了
7. REQUIRES_NEW
這個和上面的NESTED有點相似,但是又不一樣
- 當存在活動事務時,新創建一個事務執行
- 當不存在活動事務時,和REQUIRES效果一致,創建一個事務執行
注意
REQUIRES_NEW
和NESTED
相比,兩個事務之間沒有關系,任何一個回滾,對另外一個無影響
測試case和前面差不多,不多做細說...
8. 小結
前面介紹了7中傳播屬性,下面簡單對比和小結一下
事務 | 特點 |
---|---|
REQUIRED | 默認,如果存在事務,則支持當前事務;不存在,則開啟一個新事務 |
SUPPORTS | 如果存在一個事務,支持當前事務。如果沒有事務,則非事務的執行 |
MANDATORY | 需要在一個正常的事務內執行,否則拋異常 |
REQUIRES_NEW | 不管存不存在事務,都開啟一個新事務 |
NOT_SUPPORTED | 不管存不存在,都以非事務方式執行,當存在事務時,掛起事務 |
NEVER | 非事務方式執行,如果存在事務,則拋異常 |
NESTED | 如果不存在事務,則開啟一個事務運行;如果存在事務,則運行一個嵌套事務 |
II. 其他
0. 系列博文&源碼
系列博文
- 180926-SpringBoot高級篇DB之基本使用
- 190407-SpringBoot高級篇JdbcTemplate之數據插入使用姿勢詳解
- 190412-SpringBoot高級篇JdbcTemplate之數據查詢上篇
- 190417-SpringBoot高級篇JdbcTemplate之數據查詢下篇
- 190418-SpringBoot高級篇JdbcTemplate之數據更新與刪除
- 200119-SpringBoot系列教程之聲明式事務Transactional
- 200120-SpringBoot系列教程之事務隔離級別知識點小結
源碼
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 實例源碼: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate-transaction
1. 一灰灰Blog
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛
- 一灰灰Blog個人博客 https://blog.hhui.top
- 一灰灰Blog-Spring專題博客 http://spring.hhui.top