1、@Transactional前言
最近在操作springboot的時候,需要來進行操作數據庫,需要來使用事務來進行管理。通過一個注解即可來進行搞定這里的問題。
@Transactional
通過一個注解就可以搞定數據庫操作的事務問題。
在springboot繼承mybatis和mybatis-plus的過程中,已經幫我們自動配置好了事務管理器,我們只需要在使用的時候加上@Transactional注解即可。但是需要注意的是在使用@Transactional注解的時候,小心事務失效的問題。
在介紹下面的之前,還需要了解一下MySQL數據庫中的隔離級別和事務。
一般來說有兩種方式來開啟事務:1、begin/commit/rollback;2、start transaction/commit/rollback;
在springboot中是否需要開啟事務是通過注解@Transactional注解來決定的,開啟了之后需要將自動提交來設置成為手動提交的方式。
這里是需要來進行提前說明的。
2、案例演示
那么這里首先通過一個案例來進行演示一下,參考鏈接
@Transactional
public void sellProduct() throws ClassNotFoundException {
log.info("----------------->>>>>>>開啟日志<<<<<------------------------");
LOCK.lock();
try {
System.out.println(Thread.currentThread().getName() + ":搶到鎖了,進入到方法中來");
// 首先查詢庫存
Product product = productMapper.selectById(1L);
Integer productcount = product.getProductcount();
System.out.println(Thread.currentThread().getName() + ":當前庫存是:" + productcount);
if (productcount > 0) {
product.setProductcount(productcount - 1);
// 更新操作
productMapper.updateById(product);
Buy buy = new Buy();
buy.setProductname(product.getProductname());
buy.setUsername(Thread.currentThread().getName());
// 保存操作
buyMapper.insert(buy);
System.out.println(Thread.currentThread().getName() + ":減庫存,創建訂單完畢!");
} else {
System.out.println(Thread.currentThread().getName() + ":沒有庫存了");
}
} finally {
System.out.println(Thread.currentThread().getName() + ":釋放鎖");
// 釋放鎖
LOCK.unlock();
}
}
2.1、問題
首先先准備兩個問題:
1、事務的開始在哪里進行的?
2、事務的提交和回滾操作是在哪里操作的?
那么帶着這兩個問題來找答案。
2.2、 查詢哪個事務正在執行SQL
首先准備一個SQL語句,如下所示:
select * from information_schema.innodb_trx;
這個語句可以顯示當前數據庫中有哪些事務正在執行SQL語句。
那么在
Product product = productMapper.selectById(1L);
語句上打上斷點,然后利用上面的SQL來進行查詢,發現是沒有的;當SQL語句執行的時候,再次來執行SQL語句,發現可以看到當前事務中正在執行SQL語句。
那么也就說明了事務的開始時間並非是在第一行就開始了。
這里說明一下:在innodb執行引擎下,事務的開啟執行是在執行SQL的時機時才會觸發,那么之前的都是准備工作。
根據上面說的,事務已經有了,那么需要將自動提交改為手動提交,這兩個步驟是在哪里來進行操作的呢?
2.3、手動設置事務
那么這里的測試我使用的是單元測試
@Test
void contextLoads() throws ClassNotFoundException {
productService.sellProduct();
}
那么斷點打在這里:

首先可以看到的是這里顯示的是一個動態代理對象,使用的是CGLIB來進行創建的。
那么繼續跟進去,慢慢看這里的操作:
從這里可以看到將會走動態代理來調用方法獲取得到結果
然后來調用目標方法:
從調用的注解上翻譯:使用 getTransaction 和 commit/rollback 調用進行標准事務划分。
那么這里已經來到了操作的重點了。那么看看對應的操作
首先來對事務來進行檢查,判斷事務上的注解的合理性
返回返回一個事務操作,下面看看里面做了什么操作:
看一下方法上的注解,開啟新的事務。繼續向下進行:
那么看一下這里的操作,首先獲取得到autocommit,來進行判斷是否為true,也就是自動提交,如果是,那么在下面將connection中的自動提交設置成fasle。那么在哪里將connection的autocommit設置成true的?
其實看一下265行,獲取得到連接的操作。
首先hireka數據庫連接池先來進行初始化操作,那么初始化完成之后,肯定是要創建連接,然后獲取得到對應的連接。
看一下這里的調用鏈路,一般人還真找不到。在初始化的時候來進行設置的,但是可以可以看到對autocommit設置的是true。
看一下方法調用棧的效果:
將autocommit設置的時候是在獲取得到連接的地方。然后將其設置成false。
記錄一下設置成true的接口位置:java.sql.Connection#setAutoCommit
2.4、提交事務和回滾事務
那么這里設置完成了之后,接下來就應該執行我們的目標方法了。執行完成之后決定是否是提交事務還是回滾事務。
還是之前的那個方法:將Standard transaction demarcation with getTransaction and commit/rollback calls.進行分離開來的方法
當這行代碼執行完成之后,就來到了下面的使用重點方法了。
現在一旦看到這個retVal就覺得是返回值。那么看try代碼中的注釋:環繞通知,調用目標對象。那么也就是在這里來調用我們在service層中寫的代碼。看這里的
try{
}catch(){
}finnaly{
}
感覺自己又掙到了。哈哈哈
看一下這里的執行邏輯。如果目標方法執行正常,那么執行finnaly代碼塊中的代碼。如果catch中的異常沒有得到處理,那么在finnaly結束之后,依然會向外拋出異常。那么將會由外層的異常來進行處理。那么最終會拋到controller中去,然后拋出到exceptionhandler中去進行處理,然后將值進行返回。那么不管是做了什么。首先現將這里的事務的回滾和提交先繼續看下去。
注意:finnal中的代碼不是提交,而是將我們在connection上有着自定義化的操作的標記給擦除掉。也就是說先把連接上的東西給去掉,因為提交或者是回滾事務后還需要將數據庫連接還回去。我也不知道為什么這里要提前到這里來進行操作,可能是為了防止后期需要做的事情有干擾,所以提前了。
然后再catch代碼塊中可以看到注釋:目標方法出現異常!那么需要來進行執行的操作。
這里回想一下事務失效中的一條規則:當自己手動的進行try...catch,而又沒有將異常拋出去的時候,其實被抓住的異常相對於調用者來說,是沒有異常產生的。所以也會走finnaly中的代碼,然后進行提交事務。所以源碼中很清楚,但是源碼中的這種思想要學會。
2.4.1、正常提交事務
那么首先看一下正常提交時候的吧,然后再看一下異常提交的時候。這里還是比較有意思的。
這里看一下commitTransactionAfterReturning方法,在正常處理完成之后提交事務。不要被這個方法的意思給誤解了!
我以為的不是coding代碼的人以為的提交,但是不是真的提交。
還需要經過什么?還需要經過兩個判斷,判斷是否有rollbackonly!
這個異常就比較常見了!!出現的原因是兩個@Transacitonal方法來進行調用,類似下面這種:
A(){
B();
}
由於B方法在執行中出現了異常,那么導致A也將事務進行了回滾。所以這里也比較有意思了。那么可以將事務傳播行為修改一番,將其變成即使B方法執行失敗了,A方法如果執行成功,也是可以正常提交事務的。當然,這個會在處理完事務中的異常之后來舉例子提及到。
那么正常提交的處理完成之后,看一下處理異常的案例。那么如果說異常的話,springboot中默認能夠處理的異常是什么?
2.4.3、默認異常處理
那么先手動的去制造一個異常來看看:
@Transactional
public void sellProduct() throws ClassNotFoundException {
log.info("----------------->>>>>>>開啟日志<<<<<------------------------");
LOCK.lock();
try {
System.out.println(Thread.currentThread().getName() + ":搶到鎖了,進入到方法中來");
// 首先查詢庫存
Product product = productMapper.selectById(1L);
int i = 1/0;
Integer productcount = product.getProductcount();
System.out.println(Thread.currentThread().getName() + ":當前庫存是:" + productcount);
if (productcount > 0) {
product.setProductcount(productcount - 1);
// 更新操作
productMapper.updateById(product);
Buy buy = new Buy();
buy.setProductname(product.getProductname());
buy.setUsername(Thread.currentThread().getName());
// 保存操作
buyMapper.insert(buy);
System.out.println(Thread.currentThread().getName() + ":減庫存,創建訂單完畢!");
} else {
System.out.println(Thread.currentThread().getName() + ":沒有庫存了");
}
} finally {
System.out.println(Thread.currentThread().getName() + ":釋放鎖");
// 釋放鎖
LOCK.unlock();
}
}
那么肯定會出現異常!
那么接着看一下如何來處理異常的:
看一下方法注釋:處理一個throwable,完成事務。 我們可能會提交或回滾,具體取決於配置。
那么這里的配置指的是什么?指的是我們在@Transactional中配置的一些注解指定的值。
這里來看一下java的異常體系:
那么接着看springboot對事務處理默認的異常是什么?再看一下代碼,關鍵在於這個rollback方法的判斷:
有方法注釋一定要看方法注釋,真的是很有幫助:
1、方法注釋:獲勝規則是最淺的規則(即繼承層次結構中最接近異常的規則)。 如果沒有規則適用 (-1),則返回 false。
2、if判斷中的注釋:如果沒有規則匹配,則用戶超類行為(未選中的回滾)。
這里的規則就是我們在@Transactional注解中配置的值,所以這里面的值不是能夠隨便來進行配置的。一會兒會給一個規范配置。
那么我們從這里也可以知道,如果知道一個異常是回滾還是提交呢?就一直在繼承體系中來進行查找,那么這里也就說明了spring中肯定是存在着默認的異常處理類的。這里的變量命名的十分有意思。
那么對於我手動制造的異常來說,是沒有找到對應的匹配規則的。那么則會走if中的判斷,那么看一下if中的判斷:
默認行為與 EJB 一樣:在未經檢查的異常 (RuntimeException) 上回滾,假設任何業務規則之外的意外結果。 此外,我們還嘗試回滾錯誤,這顯然也是一個意想不到的結果。 相比之下,檢查異常被認為是業務異常,因此是事務性業務方法的常規預期結果,即一種仍然允許資源操作的常規完成的替代返回值。
這在很大程度上與 TransactionTemplate 的默認行為一致,除了 TransactionTemplate 還會回滾未聲明的已檢查異常(極端情況)。 對於聲明性事務,我們希望將檢查異常有意聲明為業務異常,從而導致默認提交。
--------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
看一下這里的注釋:
1、默認是RuntimeException和Error;
2、將檢查時異常需要注意!可能會導致提交(所以阿里體系規范,最好寫Exception);
針對上面的2中的一會兒可以來進行實驗,但是感覺沒有必要來進行操作。
那么代碼執行到了這里,返回為TRUE之后,
將會走到這里的代碼中來:
try {
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
應用程序異常被回滾異常覆蓋
執行回滾操作。還會將異常給拋出來,交給調用者來進行處理。這里是為了記錄一下日志而已。因為我們一路看下來,都是在進行try...catch...,但是又一路向外拋,沒有對異常來進行處理,只是做了一下日志的記錄而已。
但是對於上面的winner==null來說,我們是否可以手動的來更改這個值呢?答案也是可以的:
@Transactional(noRollbackFor = ArithmeticException.class)
在@Transactional注解上加上屬性的值,那么再來進行判斷:
在這里可以看到因為我們在注解上加了ArithmeticException,不讓其進行回滾操作,那么這里匹配到了之后,winner就會被賦值,然后if判斷就不會生效。因為最終返回的是fasle,那么就會執行到了commit操作里面來:
如果這個時候你理解要提交事務的話,那么你又理解錯了。
還是剛剛那個邏輯!因為這里考慮的是事物方法調用事務方法的問題,如果被調用的出現了異常,那么調用者的事務如何來進行處理呢?
2.4.4、使用注解注意細節
那么這里我分別舉例來說明一下幾個問題:
1、事務的開啟和准備工作是有區別的,在innodb數據庫引擎中,只有真正開始執行SQL的時候,這個時候才算是開啟了事務。
2、在上面的基礎之上,如果還沒有來的及執行SQL語句,就已經拋出了異常。那么事務如何來進行處理。
最常見是FileNotFoundException異常等!但是這種會走提交事務的邏輯!因為其不是默認異常的子類,那么返回false,會走commit操作
那么這里就有點問題了。因為對於拋出的是編譯時異常來說,應該都會來進行處理。但是也會有對應的問題出現:
編譯時異常在前;
sql操作在后;
那么這種操作,會導致事務提交,然后拋出異常。
那么這里手動制造一個:
public void test222() throws ClassNotFoundException, IOException {
FileInputStream inputStream = new FileInputStream("dsafdsaf:fdksajflkds");
inputStream.close();
System.out.println("hello");
int i = 1/0;
System.out.println("hello");
}
當編譯時異常發生之后,那么下面的將不會繼續執行了,向上層拋出異常。
然后事務提交。那么這種沒有關系。但是如下所示:
sql1操作在前;
編譯時異常在后;
sql2操作在前;
那么這個地方如果出現了編譯時異常,將會導致上面SQL執行成功並提交,下面的並不會執行。那么將會導致問題的發生。
所以這里建議無論是運行時異常還是編譯時異常,都將其設置成Exception或者是Throwable的類型。
以防萬一!還是這樣來進行設置比較保守。
2.4.5、Rollback
那么再繼續剛剛的話題:如果出現了rollback的場景,該如何來進行解決呢?這種場景下應該是事務方法調用事務方法:
這里需要首先說明一下spring中的事務傳播行為是REQUIRED行為,也就是說,如果上下文存在着事務,那么使用上下文的事務;如果上下文沒有事務,那么創建新的事務。所以下面的兩個方法如果存在着事務方法調用,那么使用的應該是同一個事務。
@Transactional
public void sellProduct() throws ClassNotFoundException {
log.info("----------------->>>>>>>開啟日志<<<<<------------------------");
LOCK.lock();
try {
ProductService productService = SpringApplicationContext.getBean(ProductService.class);
log.info("對應的productService是----------->>>{}", productService);
// 兩個事務方法來進行調用!那么看看對應的效果數據!
productService.sellProductZ();
} catch (Exception e) {
log.error("程序異常,詳細信息是:{}", e.getLocalizedMessage(), e);
}
try {
System.out.println(Thread.currentThread().getName() + ":搶到鎖了,進入到方法中來");
// 首先查詢庫存
Product product = productMapper.selectById(1L);
Integer productcount = product.getProductcount();
System.out.println(Thread.currentThread().getName() + ":當前庫存是:" + productcount);
if (productcount > 0) {
product.setProductcount(productcount - 1);
// 更新操作
productMapper.updateById(product);
Buy buy = new Buy();
buy.setProductname(product.getProductname());
buy.setUsername(Thread.currentThread().getName());
// 保存操作
buyMapper.insert(buy);
System.out.println(Thread.currentThread().getName() + ":減庫存,創建訂單完畢!");
} else {
System.out.println(Thread.currentThread().getName() + ":沒有庫存了");
}
} finally {
System.out.println(Thread.currentThread().getName() + ":釋放鎖");
// 釋放鎖
LOCK.unlock();
}
}
@Transactional
public void sellProductZ() {
// 隨便來制造一個異常
int a = 1 / 0;
}
從這里看到,事務方法sellProduct調用事務方法sellProductZ。
上面是在模擬一種情況,事務方法sellProductZ出現了異常,那么調用者的事務方法,該如何來進行處理?
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
出現了這個異常
那么這里是怎么造成的?肯定是因為事務方法sellProductZ拋出了異常。然后接下來因為在事務方法sellProduct進行了try....catch...操作
但是看看sellProductZ的處理:
追蹤一下:
那么出現了異常肯定會走這里:
因為是算術異常,屬於運行時異常。那么是RuntimeException的子類,所以提交將事務進行回滾:
查看方法注釋:處理實際回滾。 已完成標志已被檢查。那么我們關注的點是:
也就是說sellProductZ方法異常,這里會將一個標記置為true。看上來的日志說明:加載事務失敗,將事務需要進行回滾標記下來。
那么接着看這里進行了標記,看一下sellProduct的處理方式。因為在里面來進行了try...catch...操作,所以內部沒有異常,那么整個方法執行完成之后需要進行提交。
但是之前說過,提交是真的提交嗎?並不是,還需要檢查roll-back,那么一切說起來就很合理了。
做一些清除操作,然后開始提交事務。
那么這里就需要來做檢查,判斷是否是rollback。那么看一下對應的檢查:
protected boolean shouldCommitOnGlobalRollbackOnly() {
return false;
}
這里返回的是false,那么只需要關注的是后面的為什么是true即可。
最終會來到:
@Override
public boolean isRollbackOnly() {
return getConnectionHolder().isRollbackOnly();
}
那么應該是一個公用一個事務,那么在一個連接上的操作。上面sellProductZ在這里將其置為了TRUE,那么表示需要進行回滾。
那么再看上面的圖,就表示的是已經可以回滾事務了。
所以要是sellProductZ方法出現了異常,而sellProduct執行正常可以進行提交的正確使用方式是使用正確的傳播行為。
應該是如果上下文中存在着事務,那么不去使用這個事務,而是掛起,自己新創建一個,那么毫無疑問的是
做一個修改的地方:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sellProductZ() {
// 隨便來制造一個異常
int a = 1 / 0;
}
那么再次來進行調用,查看一下結果。發現結果就是正確的了!這塊的原理還是比較好分析的。
3、總結
那么最后一個關於鎖,很明顯的使用錯誤。鎖已經釋放了,但是事務還沒有提交。所以無論是syncronized還是lock鎖都無法避免。
建議加在controller層中,或者是另外新起一個事務方法來進行調用,亦或者是加入動態對象到當前對象中來。
或者另外一種方式來使用編程式事務,這種方式可以規避很多問題的發生。比如說:
長事務連接
鏈接:https://mp.weixin.qq.com/s/Q1VnZd5rt5OFaRGaXtABMg
編程式事務的使用:https://blog.csdn.net/whhahyy/article/details/48370879?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_title~default-0.fixedcolumn&spm=1001.2101.3001.4242.1
3.1、使用編程式事務
使用TransactionTemplate 不需要顯式地開始事務,甚至不需要顯式地提交事務。這些步驟都由模板完成。但出現異常時,應通過TransactionStatus 的setRollbackOnly 顯式回滾事務。
TransactionTemplate 的execute 方法接收一個TransactionCallback 實例。Callback 也是Spring 的經典設計,用於簡化用戶操作, TransactionCallback 包含如下方法。
• Object dolnTransaction(TransactionStatus status) 。
該方法的方法體就是事務的執行體。
如果事務的執行體沒有返回值,則可以使用TransactionCallbackWithoutResultl類的實例。這是個抽象類,不能直接實例化,只能用於創建匿名內部類。它也是TransactionCallback 接口的子接口,該抽象類包含一個抽象方法:
• void dolnTransactionWithoutResult(TransactionStatus status)該方法與dolnTransaction 的效果非常相似,區別在於該方法沒有返回值,即事務執行體無須返回值。
那么看一下對應的配置信息:
@Configuration
@AllArgsConstructor
public class BianChengShiConfig {
private final DataSource dataSource;
@Bean
public DataSourceTransactionManager dataSourceTransactionManager(){
return new DataSourceTransactionManager(dataSource);
}
@Bean
public TransactionTemplate transactionTemplate(){
TransactionTemplate transactionTemplate = new TransactionTemplate(dataSourceTransactionManager());
// 設置事務傳播行為
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 設置隔離級別
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
// 默認為false,這里可以不需要來進行設置
transactionTemplate.setReadOnly(false);
// 設置超時時間 這里不需要來進行設置
transactionTemplate.setTimeout(4000);
return transactionTemplate;
}
}
然后看一下對應的使用:
@Service
@AllArgsConstructor
public class UserServiceImpl implements UserService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
private final UserMapper userMapper;
private final TransactionTemplate transactionTemplate;
@Override
public boolean transfer(String from, String to, Double money) {
// 模擬出現了異常,如何來進行處理?
transactionTemplate.execute((transactionStatus -> {
userMapper.transfer(from, -money);
// 模擬出現了異常,如何來進行處理?
int i = 1 / 0;
userMapper.transfer(to, money);
return transactionStatus;
}));
return true;
}
}
注意:在業務層不需要手動的來對異常代碼進行try...catch....
如果這樣子做了,那么就意味着我們需要手動的來進行手動回滾事務。
看一下源碼:
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
}
else {
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
result = action.doInTransaction(status);
}
catch (RuntimeException | Error ex) {
// Transactional code threw application exception -> rollback
rollbackOnException(status, ex);
throw ex;
}
catch (Throwable ex) {
// Transactional code threw unexpected exception -> rollback
rollbackOnException(status, ex);
throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
}
this.transactionManager.commit(status);
return result;
}
}
看到:
result = action.doInTransaction(status);
這行代碼就是我們在代碼中進行的邏輯實現:
transactionTemplate.execute((transactionStatus -> {
userMapper.transfer(from, -money);
// 模擬出現了異常,如何來進行處理?
int i = 1 / 0;
userMapper.transfer(to, money);
return transactionStatus;
}));
當我們的代碼中出現了異常,會進入到catch代碼塊中去,然后去嘗試進行回滾操作;說明了這里已經幫我們來實現了這樣的一個過程。
如果代碼執行正常,那么最終會執行到commit代碼塊中來進行執行。
這種使用方式也是比較優雅的。
感覺這種使用方式要比@Transactional注解使用起來好用一些。而且還能夠避免一些出現的問題。
沒有必要將對其他的一些操作,如磁盤IO操作放置在事務中來進行執行。如果需要的話,加上syncronized或者是lock鎖都可以。
參考:
1、https://blog.csdn.net/xrt95050/article/details/18076167
2、https://blog.csdn.net/qq_33404395/article/details/83377382?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-1.pc_relevant_paycolumn_v2&spm=1001.2101.3001.4242.2&utm_relevant_index=4
3、https://www.cnblogs.com/aliger/p/3898869.html