天天看點

完蛋,我的事務怎麼不生效?

事務大家平時應該都有寫,之前寫事務的時候遇到一點坑,居然不生效,後來排查了一下,複習了一下各種事務失效的場景,想着不如來一個總結,這樣下次排查問題,就能有恃無恐了。那麼先來複習一下事務相關知識,事務是指操作的最小工作機關,作為一個單獨且不可切割的單元操作,要麼全部成功,要麼全部失敗。事務有四大特性(<code>ACID</code>):

原子性(<code>Atomicity</code>):事務包含的操作,要麼全部成功,要麼全部失敗復原,不會存在一半成功一半失敗的中間狀态。比如<code>A</code>和<code>B</code>一開始都有<code>500</code>元,<code>A</code>給<code>B</code>轉賬<code>100</code>,那麼<code>A</code>的錢少了<code>100</code>,<code>B</code>的錢就必須多了<code>100</code>,不能<code>A</code>少了錢,<code>B</code>也沒收到錢,那這個錢就不翼而飛了,不符合原子性了。

一緻性(<code>Consistency</code>):一緻性是指事務執行之前和之後,保持整體狀态的一緻,比如<code>A</code>和<code>B</code>一開始都有<code>500</code>元,加起來是<code>1000</code>元,這個是之前的狀态,<code>A</code>給<code>B</code>轉賬<code>100</code>,那麼最後<code>A</code>是<code>400</code>,<code>B</code>是<code>600</code>,兩者加起來還是<code>1000</code>,這個整體狀态需要保證。

隔離性(<code>Isolation</code>):前面兩個特性都是針對同一個事務的,而隔離性指的是不同的事務,當多個事務同時在操作同一個資料的時候,需要隔離不同僚務之間的影響,并發執行的事務之間不能互相幹擾。

持久性(<code>Durability</code>):指事務如果一旦被送出了,那麼對資料庫的修改就是永久性的,就算是資料庫發生故障了,已經發生的修改也必然存在。

事務的幾個特性并不是資料庫事務專屬的,廣義上的事務是一種工作機制,是并發控制的基本機關,保證操作的結果,還會包括分布式事務之類的,但是一般我們談論事務,不特指的話,說的就是與資料庫相關的,因為我們平時說的事務基本都基于資料庫來完成。

事務不僅是适用于資料庫。我們可以将此概念擴充到其他元件,類似隊列服務或外部系統狀态。是以,“一系列資料操作語句必須完全完成或完全失敗,以一緻的狀态離開系統”

前面我們已經部署過了一些demo項目,以及用docker快速搭建環境,本文基于的也是之前的環境:

JDK 1.8

Maven 3.6

Docker

Mysql

正常的事務樣例,包含兩個接口,一個是擷取所有的使用者中的資料,另外一個更新的,是<code>update</code>使用者資料,其實就是每個使用者的年齡<code>+1</code>,我們讓一次操作完第一個之後,抛出異常,看看最後的結果:

資料庫操作:

先擷取<code>http://localhost:8081/getUserList</code>所有的使用者看看:

完蛋,我的事務怎麼不生效?

在調用更新接口,頁面抛出錯誤了:

完蛋,我的事務怎麼不生效?

控制台也出現了異常,意思是除以0,異常:

然後我們再次請求<code>http://localhost:8081/getUserList</code>,看到資料兩個都是<code>11</code>說明資料都沒有發生變化,第一個操作完之後,異常,復原成功了:

那什麼時候事務不正常復原呢?且聽我細細道來:

我們知道,<code>Mysql</code>其實有一個資料庫引擎的概念,我們可以用<code>show engines</code>來檢視<code>Mysql</code>支援的資料引擎:

完蛋,我的事務怎麼不生效?

可以看到<code>Transactions</code>那一列,也就是事務支援,隻有<code>InnoDB</code>,那就是隻有<code>InnoDB</code>支援事務,是以要是引擎設定成其他的事務會無效。

我們可以用<code>show variables like 'default_storage_engine'</code>看預設的資料庫引擎,可以看到預設是<code>InnoDB</code>:

那我們看看我們示範的資料表是不是也是用了<code>InnoDB</code>,可以看到确實是使用<code>InnoDB</code>

完蛋,我的事務怎麼不生效?

那我們把該表的引擎修改成<code>MyISAM</code>會怎麼樣呢?試試,在這裡我們隻修改資料表的資料引擎:

然後再<code>update</code>,不出意料,還是會報錯,看起來錯誤沒有什麼不同:

完蛋,我的事務怎麼不生效?

但是擷取全部資料的時候,第一個資料更新成功了,第二個資料沒有更新成功,說明事務沒有生效。

結論:必須設定為<code>InnoDB</code>引擎,事務才生效。

事務必須是<code>public</code>方法,如果用在了<code>private</code>方法上,那麼事務會自動失效,但是在<code>IDEA</code>中,隻要我們寫了就會報錯:<code>Methods annotated with '@Transactional' must be overrideable</code>,意思是事務的注解加上的方法,必須是可以重寫的,<code>private</code>方法是不可以重寫的,是以報錯了。

完蛋,我的事務怎麼不生效?

同樣的<code>final</code>修飾的方法,如果加上了注解,也會報錯,因為用<code>final</code>就是不想被重寫:

完蛋,我的事務怎麼不生效?

<code>Spring</code>中主要是用放射擷取<code>Bean</code>的注解資訊,然後利用基于動态代理技術的<code>AOP</code>來封裝了整個事務,理論上我想調用<code>private</code>方法也是沒有問題的,在方法級别使用<code>method.setAccessible(true);</code>就可以,但是可能<code>Spring</code>團隊覺得<code>private</code>方法就是開發人員意願上不願意公開的接口,沒有必要破壞封裝性,這樣容易導緻混亂。

<code>Protected</code>方法可不可以?不可以!

下面我們為了實作,魔改代碼結構,因為接口不能用<code>Portected</code>,如果用了接口,就不可能用<code>protected</code>方法,會直接報錯,而且必須在同一個包裡面使用,我們把<code>controller</code>和<code>service</code>放到同一個包下:

完蛋,我的事務怎麼不生效?

測試後發現事務不生效,結果依然是一個更新了,另外一個沒有更新:

結論:必須使用在<code>public</code>方法上,不能用在<code>private</code>,<code>final</code>,<code>static</code>方法上,否則不會生效。

<code>Springboot</code>管理異常的時候,隻會對運作時的異常(<code>RuntimeException</code> 以及它的子類) 進行復原,比如我們前面寫的<code>i=1/0;</code>,就會産生運作時的異常。

從源碼來看也可以看到,<code>rollbackOn(ex)</code>方法會判斷異常是<code>RuntimeException</code>或者<code>Error</code>:

異常主要分為以下類型:

所有的異常都是<code>Throwable</code>,而<code>Error</code>是錯誤資訊,一般是程式發生了一些不可控的錯誤,比如沒有這個檔案,記憶體溢出,<code>IO</code>突然錯誤了。而<code>Exception</code>下,除了<code>RuntimeException</code>,其他的都是<code>CheckException</code>,也就是可以處理的異常,<code>Java</code>程式在編寫的時候就必須處理這個異常,否則編譯是通不過去的。

完蛋,我的事務怎麼不生效?

由下面的圖我們可以看出,<code>CheckedException</code>,我列舉了幾個常見的<code>IOException</code> IO異常,<code>NoSuchMethodException</code>沒有找到這個方法,<code>ClassNotFoundException</code> 沒找到這個類,而<code>RunTimeException</code>有常見的幾種:

數組越界異常:<code>IndexOutOfBoundsException</code>

類型轉換異常:<code>ClassCastException</code>

空指針異常:<code>NullPointerException</code>

完蛋,我的事務怎麼不生效?

事務預設復原的是:運作時異常,也就是<code>RunTimeException</code>,如果抛出其他的異常是無法復原的,比如下面的代碼,事務就會失效:

方法上需要使用<code>@Transactional</code>才能開啟事務

多個資料源配置或者多個事務管理器的時候,注意如果操作資料庫<code>A</code>,不能使用<code>B</code>的事務,雖然這個問題很幼稚,但是有時候用錯難查找問題。

如果在<code>Spring</code>中,需要配置<code>@EnableTransactionManagement</code>來開啟事務,等同于配置<code>xml</code>檔案<code>*&lt;tx:annotation-driven/&gt;*</code>,但是在<code>Springboot</code>中已經不需要了,在<code>springboot</code>中<code>SpringBootApplication</code>注解包含了<code>@EnableAutoConfiguration</code>注解,會自動注入。

<code>@EnableAutoConfiguration</code>自動注入了哪些東西呢?在<code>jetbrains://idea/navigate/reference?project=springDocker&amp;path=~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.5.6/spring-boot-autoconfigure-2.5.6.jar!/META-INF/spring.factories</code>下有自動注入的配置:

裡面配置了一個<code>TransactionAutoConfiguration</code>,這是事務自動配置類:

值得注意的是,<code>@Transactional</code>除了可以用于方法,還可以用于類,表示這個類所有的<code>public</code>方法都會配置事務。

想要進行事務管理的方法隻能在其他類裡面被調用,不能在目前類被調用,否則會失效,為了實作這個目的,如果同一個類有不少事務方法,還有其他方法,這個時候有必要抽取出一個事務類,這樣分層會比較清晰,避免後繼者寫的時候在同一個類調用事務方法,造成混亂。

事務失效的例子:

比如我們将<code>service</code>事務方法改成:

在<code>controller</code>裡面調用的是沒有事務注解的方法,再間接調用事務方法:

調用之後,發現事務失效,一個更新另外一個沒有更新:

為什麼會這樣呢?

<code>Spring</code>用切面對方法進行包裝,隻對外部調用方法進行攔截,内部方法沒有進行攔截。

看源碼:實際上我們調用事務方法的時候,會進入<code>DynamicAdvisedInterceptor</code>的<code>public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)()</code>方法:

完蛋,我的事務怎麼不生效?

裡面調用了<code>AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice()</code>,這裡是擷取調用調用鍊。而沒有<code>@Transactional</code>注解的方法<code>userService.testTransaction()</code>,根本擷取不到代理調用鍊,調用的還是原來的類的方法。

<code>spring</code>裡面要想對一個方法進行代理,用的就是<code>aop</code>,肯定需要一個辨別,辨別哪一個方法或者類需要被代理,<code>spring</code>裡面定義了<code>@Transactional</code>作為切點,我們定義這個辨別,就會被代理。

代理的時機是什麼時候呢?

<code>Spring</code>統一管理了我們的<code>bean</code>,代理的時機自然就是建立<code>bean</code>的過程,看看哪一個類帶了這個辨別,就生成代理對象。

<code>SpringTransactionAnnotationParser</code>這個類有一個方法是用來判斷<code>TransactionAttribute</code>注解的:

假設我們在多線程裡面像以下方式使用事務,那麼事務是不能正常復原的:

因為不同的線程使用的是不同<code>SqlSession</code>,相當于另外一個連接配接,根本不會用到同一個事務:

首先事務是有傳播機制的:

<code>REQUIRED</code>(預設):支援使用目前事務,如果目前事務不存在,建立一個新事務,如果有直接使用目前的事務。

<code>SUPPORTS</code>:支援使用目前事務,如果目前事務不存在,就不會使用事務。

<code>MANDATORY</code>:支援使用目前事務,如果目前事務不存在,則抛出<code>Exception</code>,也就是必須目前處于事務裡面。

<code>REQUIRES_NEW</code>:建立新事務,如果目前事務存在,把目前事務挂起。

<code>NOT_SUPPORTED</code>:沒有事務執行,如果目前事務存在,把目前事務挂起。

<code>NEVER</code>:沒有事務執行,如果目前有事務則抛出<code>Exception</code>。

<code>NESTED</code>:嵌套事務,如果目前事務存在,那麼在嵌套的事務中執行。如果目前事務不存在,則表現跟`REQUIRED

查不多。

預設的是<code>REQUIRED</code>,也就是事務裡面調用另外的事務,實際上不會重新建立事務,而是會重用目前的事務。那如果我們這樣來寫嵌套事務:

調用的另外一個事務:

會抛出以下錯誤:

我們但是實際事務是正常復原掉了,結果是對的,之是以出現這個問題,是因為裡面到方法抛出了異常,用的是同一個事務,說明事務必須被復原掉的,但是外層被<code>catch</code>住了,本來就是同一個事務,一個說復原,一個<code>catch</code>住不讓<code>spring</code>感覺到<code>Exception</code>,那不是自相沖突麼?是以<code>spring</code>報錯說:這個事務被辨別了必須復原掉,最終還是復原掉了。

怎麼處理呢?

外層主動抛出錯誤,<code>throw new RuntimeException()</code>

使用<code>TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();</code>主動辨別復原

有些時候,我們不僅操作自己的資料庫,還需要同時考慮外部的請求,比如同步資料,同步失敗,需要復原掉自己的狀态,在這種場景下,必須考慮網絡請求是否會出錯,出錯如何處理,錯誤碼是哪一個的時候才成功。

如果網絡逾時了,實際上成功了,但是我們判定為沒有成功,復原掉了,可能會導緻資料不一緻。這種需要被調用方支援重試,重試的時候,需要支援幂等,多次調用儲存狀态的一緻,雖然整個主流程很簡單,裡面的細節還是比較多的。

完蛋,我的事務怎麼不生效?

事務被<code>Spring</code>包裹了複雜性,很多東西可能源碼很深,我們用的時候注意模拟測試一下調用是不是能正常復原,不能理所當然,人是會出錯的,而很多時候黑盒測試根本測試這種異常資料,如果沒有正常復原,後面需要手動處理,考慮到系統之間同步的問題,會造成很多不必要的麻煩,手動改資料庫這流程就必須走。

完蛋,我的事務怎麼不生效?

【作者簡介】:

秦懷,公衆号【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:<code>Java源碼解析</code>,<code>JDBC</code>,<code>Mybatis</code>,<code>Spring</code>,<code>redis</code>,<code>分布式</code>,<code>劍指Offer</code>,<code>LeetCode</code>等,認真寫好每一篇文章,不喜歡标題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正确,但是我保證所寫的均經過實踐或者查找資料。遺漏或者錯誤之處,還望指正。

劍指Offer全部題解PDF

2020年我寫了什麼?

開源程式設計筆記

繼續閱讀