咱們天天都在經過Spring和ORM框架簡化咱們的開發工做,框架簡化了咱們對事務處理的開發難度,我在工做中發現不少開發人員只會機械式的添加@Transactional註解,也無論是否真的執行了事務,以及事務的隔離級別,不一樣異常處理對事務的影響(固然出現這種狀況項目管理方面也是存在問題的)。我構思良久將本身的經驗寫成這篇博文,但願與你們共勉。java
什麼是事務和事務的特性等基礎問題就不在此贅述,若是你還不知道那你應該反思了 面試
要搞清楚這個問題咱們,先看下什麼是隔離 "隔離,指斷絕接觸;斷絕往來。",這個含義很明顯就是要斷絕一個事務和外界的往來。在來看一下隔離級別,解決的業務場景——隔離級別解決的業務場景是併發和多線程。綜上所述,事務的隔離級別就是咱們多個線程或者併發開啓事務操做的時候,數據庫要進行隔離操做,保證數據的準確性,它解決了數據髒讀、不可重複讀、幻讀等問題。經過一個表解釋這幾種錯誤的含義。數據庫
術語 | 含義 |
---|---|
髒讀 | A事務讀取到了B事務還未提交的數據,若是B未提交的事務回滾了,那麼A事務讀取的數據就是無效的,這就是數據髒讀 |
不可重複讀 | 在同一個事務中,屢次讀取同一數據返回的結果不一致,這是因爲讀取事務在進行操做的過程當中,若是出現更新事務,它必須等待更新事務執行成功提交完成後才能繼續讀取數據,這就致使讀取事務在先後讀取的數據不一致的情況出現 |
幻讀 | A事務讀取了幾行記錄後,B事務插入了新數據,而且提交了插入操做,在後續操做中A事務就會多出幾行本來不存在的數據,就像A事務出現幻覺,這就是幻讀 |
隔離級別 | 含義 | 未解決的問題 | 解決的問題 |
---|---|---|---|
Read uncommitted | 容許一個事務讀取另外一個事務未提交的數據 | 髒讀 | 無 |
Read committed | 容許併發事務在已提交後讀取(就是在並事務中一個事務要等待另外一個事務提交後才能讀取) | 不可重複讀、幻讀 | 髒讀 |
Repeatable read | 可重複讀,對相同數據屢次讀取是一致的,除非數據被這個事務自己修改,也就是說在讀取事務開啓後,不容許在提交事務,必須等讀取事務結束 | 幻讀 | 不可重複讀 |
Serializable | 串行化事務,它是最高的隔離級別,可是也是全部隔離級別中最慢的 | 無 | 髒讀、不可重複讀、幻讀 |
便於理解我寫一個例子, 使用Spring Boot + Mybatis,表結構以下所示: 編程
/** * 模擬查詢用戶帳戶信息 */ @Transactional(rollbackFor = Exception.class, isolation = Isolation.READ_UNCOMMITTED) public void findOne() throws InterruptedException { Thread.sleep(1000); Account account = accountMapper.findOne(1); System.out.println(account); } /** * 模擬更新用戶餘額 */ @Transactional(rollbackFor = Exception.class) public void updateAccount() throws InterruptedException { int modifyNum = accountMapper.updateAccount(1); Thread.sleep(1000); int i = 1 / 0; } 複製代碼
<select id = "findOne" parameterType = "java.lang.Integer" resultMap = "baseColum">
select id, name, money from account where id = #{id}
</select>
<update id = "updateAccount">
update account set money = money - 100 where id = #{id}
</update>
複製代碼
@Test public void readUnCommitTest() { CountDownLatch downLatch = new CountDownLatch(2); new Thread(() -> { try { accountService.updateAccount(); } catch (Exception e) { e.printStackTrace(); } downLatch.countDown(); }).start(); new Thread(() -> { try { accountService.findOne(); } catch (Exception e) { e.printStackTrace(); } downLatch.countDown(); }).start(); downLatch.await(6, TimeUnit.SECONDS); } 複製代碼
這裏我定義了兩個方法,分別是一個查詢方法和一個更新方法,同時建立了兩個線程同時執行測試方法,經過定義不一樣的隔離級別咱們分別來看一下,事務的執行狀況。這裏數據庫Money的初始值均設置爲1000。緩存
咱們將查詢方法的隔離級別定義爲Read uncommitted,模擬更新過程當中出現異常的狀況,執行測試方法,控制檯輸出 Account{id=1, name='張三', money=900.0}, **查詢方法查詢到了更新方法還未提交的數據,可是更新方法遇到異常後執行了回滾操做,實際數據庫數據併爲發生改變,數據庫發生了髒讀。**若是這種狀況發生在生產環境中,你的老闆必定會砍死你的。 markdown
咱們首先更改一下查詢方法的隔離級別(代碼就不貼了),一樣的模擬更新出現異常,執行測試方法,控制檯輸出 Account{id=1, name='張三', money=1000.0},雖然查詢方法出現了異常回滾了數據,可是咱們查詢方法查詢的數據依舊是準確的,解決了髒讀問題。多線程
咱們首先看看可重複讀的效果,直接經過語句模擬: 架構
讓咱們在看看設置隔離級別爲,不可重複讀取以後的效果:併發
和以前同樣,搞清楚一個問題時咱們先要搞清楚它到底表明着什麼,它的應用場景是什麼,這樣對於咱們理解是很是有好處的,那麼到底啥是事務的傳播行爲了,傳播確定是要兩我的在一塊兒才能夠,一我的也無法傳播呀(開車了)。事務的傳播確定也是須要在兩個或以上事務中進行的,因此事務的傳播行爲定義爲在一個事務中調用另外一個事務,或者事務之間相互調用,事務如何傳播即事務如何傳遞,它繼續使用這個事務,仍是新建一個事務?,至於場景顯而易見,一個事務方法被其餘事務方法調用時使用。app
類型 | 解釋 |
---|---|
PROPAGATION_REQUIRED | 支持當前事務,若是不存在則建立一個事務,這也是Spring 默認的傳播行爲 |
PROPAGATION_SUPPORTS | 支持當前事務,若是當前事務不存在,就不使用事務 |
PROPAGATION_MANDATORY | 支持當前事務,若是事務不存在則拋出異常 |
PROPAGATION_REQUIRES_NEW | 若是當前事務存在,則掛起當前事務,新建一個事務(會造成兩個獨立的事務,互不干涉 |
PROPAGATION_NOT_SUPPORTED | 以非事務方法運行,若是事務存在,則掛起事務,須要 JtaTransactionManager 的支持 |
PROPAGATION_NEVER | 以非事務的方式運行,若是有事務存在則拋出異常 |
PROPAGATION_NESTED | 若是當前事務存在則嵌套事務存在 |
咱們經過代碼演示一下這種傳播行爲,修改以前的代碼以下:
/** * 新增帳戶信息 */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void addAccount() { Account account = new Account(); account.setId(4); account.setName("趙六"); account.setMoney(1000); int i = accountMapper.save(account); updateAccount(); } /** * 模擬更新用戶餘額 */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void updateAccount() { int modifyNum = accountMapper.updateAccount(1); System.out.println("update success"); //int i = 1 / 0; } 複製代碼
xml:
<insert id = "save" parameterType="com.xiaoxiao.entity.Account"> insert into account(id, name, money) values(#{account.id}, #{account.name}, #{account.money}) </insert> 複製代碼
單獨執行 updateAccount 方法時會建立一個新的事務方法繼續執行,當執行 addAccount 方法是建立了一個新的事務執行到更新方法時,就會加入這個事務,同時能夠看到我在後面加了個by zero 的異常,若是更新方法發生異常,會致使兩個方法同時回滾,由於他們自己就在同一個事務中。
將 updateAccount 傳播行爲更改成PROPAGATION_SUPPORTS,addAccount 則不變,當單獨執行更新方法時它老是以非事務的方式執行,即便遇到錯誤也並未回滾,這是由於它是以非事務方式執行的,當咱們調用插入方法它則會加入到插入方法的事務中執行,這時遇到錯誤都會執行回滾操做。
將更新方法的傳播行爲修改成 PROPAGATION_MANDATORY, 當單獨執行更新方法時,程序拋出 IllegalTransactionStateException 異常,當執行插入方法時更新方法加入到插入方法的事務中繼續執行,遇到錯誤同時回滾。
這種傳播行爲須要 JtaTransactionManager 事務管理器,咱們修改一下代碼
/** * 新增帳戶信息 */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void addAccount() { Account account = new Account(); account.setId(4); account.setName("趙六"); account.setMoney(1000); int i = accountMapper.save(account); updateAccount(); int num = 1 / 0; } /** * 模擬更新用戶餘額 */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) public void updateAccount() { int modifyNum = accountMapper.updateAccount(1); System.out.println("update success"); } 複製代碼
這裏咱們在插入方法中人爲的製造了一個異常,單獨執行更新方法的時候它會新建立一個事務,執行插入方法的時候由於,這裏已經存在一個事務了,因此在執行更新方法的時候它會將外層事務掛起,直到執行完畢,若是在執行更新方法結束後遇到異常,更新方法仍是會提交,外層事務則會回滾。
將更新方法的傳播行爲修改成 PROPAGATION_NESTED, 單獨執行更新方法的時候由於上下文中並無事務存在,它會按照 PROPAGATION_REQUIRED 行爲進行執行,若是調用插入方法的時候,它則會嵌套在插入事務中執行,若是插入方法後續執行過程當中出現異常,則插入更新方法也會回滾,這和 PROPAGATION_REQUIRES_NEW 行爲正好相反。若是更新方法出現異常,它並不會回滾外層方法。
編程式事務和聲明式事務在面試中常常會被提起,其實區分二者很簡單,編程式事務就是須要咱們對事務進行手動管理,咱們須要手動編寫事務處理代碼的均可以被稱爲編程事務。 由於事務管理的代碼多有重複,爲了將其複用,利用了AOP的思想,經過對須要事務管理的方法進行先後攔截,來進行事務管理的,同時框架甚至幫助咱們簡化了這部分代碼,咱們只須要手動加註解的,這類都是聲明式事務。 立刻咱們就能夠知道,編程事務比較複雜,容易致使重複代碼,代碼侵入大,可是相對於聲明式事務更爲靈活,可定製性高。聲明式事務簡單,代碼重複少,耦合低,可是靈活性較差。
雖然聲明簡化了咱們工做,可是你永遠不知道,提需求的同窗的需求有多麼慘絕人寰,我在實際工做中遇到過一個方法中部分須要事務管理,部分不須要事務管理,部分出錯須要回滾,部分不讓回滾的需求。
在進行事務編程過程當中,不一樣的異常處理也會致使事務最終結果差別,下面我就具體討論一下不一樣異常處理對事務的影響。 若是你讀過《阿里巴巴Java開發規約》,或者使用過規約插件的話,必定會發現規約中有一條以下:
最後插件還貼心的幫你準備了三個正例,爲何阿里巴巴要求你這寫,咱們首先看個例子:
/** * 反例1 */ @Transactional public void updateAccount { try { int modifyNum = accountMapper.updateAccount(1); int num = 1 / 0; } catch (Exception e) { e.printStackTrace(); } } 複製代碼
有不少人處理異常的時候喜歡大而全,使用一個try塊包含整個代碼塊,包括我剛開始工做的時候也是這樣的,這樣處理異常實際上是很是LOW的,並且它還會帶來隱患,觀察上面所述代碼,你認爲事務在碰到異常時真的會回滾嗎,答案是是否認的,應爲聲明式事務是基於AOP來幫咱們處理異常的,你卻在這裏處理了異常,對於AOP來講它是感知不到異常的,因此事務並不會回滾。
/** * 反例2 */ @Transactional public void updateAccount() throws FileNotFoundException { int modifyNum = accountMapper.updateAccount(1); //個人電腦並無這個文件 InputStream inputStream = new FileInputStream(new File("G://A.txt")); } 複製代碼
前面提到了《阿里巴巴Java開發規約》要求咱們必須指定異常類型,爲何要制定異常類型,由於**Spring框架的事務基礎架構代碼將默認只在拋出運行時和unchecked exceptions時才標識事務回滾。**這裏首先咱們須要明白異常的分類:
**Java中的異常分爲錯誤和異常,其中錯誤是咱們沒法解決和處理的好比OOM,可是異常是咱們能夠進行處理的,其中Excetion 又分爲運行時異常和分運行時異常,RuntimeException 及其子類都屬於運行時異常,其餘則相反。異常又分檢查異常和非檢查異常,其中Exception中除RuntimeException都屬於可查異常,不可查異常RuntimeException及其子類和Error都屬於不可查異常。很好理解基本上咱們在平常工做中強制咱們捕獲的都屬於檢查異常如 IOException,FileNotFoundException。**若是你對異常處理感興趣,我接下來會寫一篇關於異常處理的博文,能夠繼續關注哦。回到事務,上例的事務會成功回滾嗎,不會由於Spring默認只回滾運行時異常和非檢查異常,即便咱們拋出了FileNotFoundException,也不會回滾,由於他是檢查異常,稍微修改一下代碼。
/** * 正例 */ @Transactional(rollbackFor = Exception.class) public void updateAccount() throws FileNotFoundException { int modifyNum = accountMapper.updateAccount(1); //個人電腦並無這個文件 InputStream inputStream = new FileInputStream(new File("G://A.txt")); } 複製代碼
或
@Transactional(rollbackFor = Exception.class) public void updateAccount() { try { int modifyNum = accountMapper.updateAccount(1); InputStream inputStream = new FileInputStream(new File("G://A.txt")); } catch (FileNotFoundException e) { throw new RuntimeException("文件不存在", e); } } 複製代碼
讓檢查異常回滾:在整個方法前加上 @Transactional(rollbackFor=Exception.class)
讓非檢查異常不回滾: @Transactional(notRollbackFor=RunTimeException.class)
在嵌套方法中異常處理尤其重要,舉個例子:
/** * 新增帳戶信息 */ @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) public void addAccount() { Account account = new Account(); account.setId(4); account.setName("趙六"); account.setMoney(1000); int i = accountMapper.save(account); updateAccount(); } /** * 模擬更新用戶餘額 */ @Transactional(rollbackFor = Exception.class) public void updateAccount() { try { int modifyNum = accountMapper.updateAccount(1); int num = 1 / 0; } catch (Exception e) { e.printStackTrace(); } } 複製代碼
若是像上面這樣,更新方法出現異常,添加方法並不會回滾,咱們是使用了 REQUIRED 隔離級別,顯而易見咱們是想它們在同一個事務中的,當時異常捕獲不當,就沒法達到這種效果,這樣的例子還有不少,在對事物作異常處理的時候必定要謹慎,多測試。對大段代碼進行 try-catch,這是不負責任的表現。catch 時請分清穩定代碼和非穩定代碼,穩定代碼指的是不管如何不會出錯的代碼。對於非穩定代碼的 catch 儘量進行區分異常類型,再作對應的異常處理。阿里規範告訴咱們Transactional註解事務不要濫用。事務會影響數據庫的QPS,另外使用事務的地方須要考慮各方面的回滾方案,包括緩存回滾、搜索引擎回滾、消息補償、統計修正等。
事務處理在開發中絕對是重中之重,特別是對於金融銀行類項目,若是出了問題確定是要捲鋪蓋走人的。若是你真正作跟錢相關的項目你必定會理解我說的。必定要帶着敬畏的心情組織你的代碼,同時必定要多測試、測試、測試,多思考多從開發過程當中總結經驗。
很是感謝你的閱讀,若是你以爲不錯的話就點個贊吧,若是你發現錯誤也能夠在評論區給出批評。