Springboot處理事務


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM