事務那些事兒

前言

有段時間沒更新博客了,最近在學習一些有關事務的知識點,今天來總結一下,本文會涉及到如下幾個知識點:mysql

  • MySQL事務
  • Spring事務
  • 分佈式事務

什麼是事務

事務是一系列操做組成的工做單元,該工做單元內的操做是不可分割的,即要麼全部操做都作,要麼全部操做都不作,這就是事務。

舉個例子:算法

張三要給李四轉帳100元,那麼咱們會有這樣的一段SQL:spring

begin transaction;
    update account set money = money-100 where name = '張三';
    update account set money = money+100 where name = '李四';
commit transaction;

事務的體現:這兩個SQL要麼所有成功,要麼所有失敗。sql

事務可否生效數據庫引擎是否支持事務是關鍵。好比經常使用的 MySQL 數據庫默認使用支持事務的innodb引擎。可是,若是把數據庫引擎變爲 myisam,那麼程序也就再也不支持事務了!數據庫

ACID

事務具備如下四個特性:編程

  • 原子性
  • 一致性
  • 隔離性
  • 持久性

原子性(Atomicity)

通常來講,原子是指不能分解成小部分的東西。例如,在多線程編程中,若是一個線程執行一個原子操做,這意味着另外一個線程沒法看到該操做的一半結果。系統只能處於操做以前或操做以後的狀態,而不是介於二者之間的狀態。服務器

一致性(Consistency)

事務一致性是指數據庫中的數據在事務操做先後都必須知足業務規則約束。
好比A轉帳給B,那麼轉帳先後,AB的帳戶總金額應該是一致的。網絡

隔離性(Isolation)

一個事務的執行不能被其它事務干擾。即一個事務內部的操做及使用的數據對其它併發事務是隔離的,併發執行的各個事務之間不能互相干擾。
(設置不一樣的隔離級別,互相干擾的程度會不一樣)多線程

持久性(Durability)

事務一旦提交,結果即是永久性的。即便發生宕機,仍然能夠依靠事務日誌完成數據的持久化。併發

日誌包括回滾日誌(undo)和重作日誌(redo),當咱們經過事務修改數據時,首先會將數據庫變化的信息記錄到重作日誌中,而後再對數據庫中的數據進行修改。這樣即便數據庫系統發生奔潰,咱們還能夠經過重作日誌進行數據恢復。

MySQL事務隔離級別

MySQL有如下四個事務隔離級別:

  • 未提交讀(READ UNCOMMITTED)
  • 已提交讀(READ COMMITTED)
  • 可重複讀(REPEATABLE READ)
  • 串行化(SERIALIZABLE)

各個隔離級別可能會存在如下的問題:

那麼什麼是髒讀、不可重複讀和幻讀?

髒讀:指一個事務能夠看到另外一個事務未提交的數據

好比說事務A修改了一個值可是還未提交,這時事務B能夠看到A修改的值,這就是髒讀。

不可重複讀:一個事務執行兩次一樣的查詢語句,先後得出的數據卻不一致

好比說事務A執行了select語句,事務B修改了某個值,事務A再次執行select語句時發現結果和上次不一致,所以叫作不可重複讀。

幻讀:在同一個事務中,同一個查詢屢次返回的記錄行數不一致(這裏的結果特指查詢到的記錄行數,幻讀能夠看作不可重複讀的一種特殊狀況)

好比說事務A執行了select語句,事務B插入數據,事務A再次執行select語句時發現多了幾條記錄,好像出現了幻覺同樣,所以叫作幻讀。

Read Commit(讀已提交)級別是如何解決髒讀的?

先說結論:經過改變鎖的釋放時機來解決髒讀問題

首先先了解一下爲何會出現髒讀?緣由就是在未提交讀這個級別下,當事務A修改了數據以後就立馬釋放了鎖,所以事務B能夠讀取到這個未提交的數據。

已提交讀級別下寫操做加的鎖會到事務提交後釋放,因此事務B不會讀到事務A未提交的數據,經過改變鎖的釋放時機解決了髒讀的問題。

Repeatable Read(可重複讀)級別是如何解決不可重複讀的?

結論:可重複讀級別就是經過MVCC機制來解決不可重複讀問題的

MVCC

多版本併發控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存儲引擎實現隔離級別的一種具體方式,用於實現提交讀和可重複讀這兩種隔離級別。而未提交讀隔離級別老是讀取最新的數據行,無需使用 MVCC。可串行化隔離級別須要對全部讀取的行都加鎖,單純使用 MVCC 沒法實現。

MVCC機制(多版本併發控制)就我我的理解來講其實就是給每行數據都添加了幾個隱藏字段,用來表示數據的版本號,即一個數據在mysql中會有多個不一樣的版本

在講 MVCC 的實現原理以前,我覺頗有必要先去了解一下 MVCC 的兩種讀形式。

有了MVCC以後咱們能夠把SQL操做分爲兩類:

  • 快照讀

讀取當前事務可見的數據,默認的select操做就是快照讀,讀的是歷史版本的數據。

  • 當前讀

讀取最新的數據,除了默認select操做外的select..for updateupdateinsertdelete等操做都是當前讀,讀取的都是最新的數據。

如今咱們有了MVCC,當事務A執行一個普通的select操做(快照讀),MySQL會把此次讀取的數據保存起來,在這期間無論事務B執行update或是insert操做,事務A再次執行select操做讀取到的數據是不會變的,所以經過可重複讀級別經過MVCC解決了不可重複讀問題,順便解決了部分的幻讀問題,沒錯MVCC並無解決全部的幻讀問題,只是解決了一部分。

那麼何時會出現幻讀呢?

當事務A執行的是當前讀,也就是加鎖的select操做時如select * from Employee for update,會去讀取最新的數據,這樣的話仍是能夠看到事務B提交的數據,所以MySQL提供了Next-Key Lock算法來幫助咱們對數據加鎖。

Next-Key Lock

InnoDB有三種行鎖的算法:

  1. Record Lock:單個行記錄上的鎖。
  2. Gap Lock:間隙鎖,鎖定一個範圍,但不包括記錄自己。GAP鎖的目的,是爲了防止同一事務的兩次當前讀,出現幻讀的狀況。

3. Next-Key Lock:1+2,鎖定一個範圍,而且鎖定記錄自己。對於行的查詢,都是採用該方法,主要目的是解決幻讀的問題。

Next-Key Lock 是 MySQL 的 InnoDB 存儲引擎的一種鎖實現。

MVCC 不能解決幻讀的問題,Next-Key Lock 就是爲了解決這個問題而存在的。在可重複讀(REPEATABLE READ)隔離級別下,使用 MVCC + Next-Key Lock 能夠解決幻讀問題。

當查詢的索引含有惟一屬性的時候,Next-Key Lock 會進行優化,將其降級爲Record Lock,即僅鎖住索引自己,不是範圍。

它是 Record LockGap Lock 的結合,不只鎖定一個記錄上的索引,也鎖定索引之間的間隙。

這樣的話當事務A執行了select * from Employee for update以後,事務B插入數據會被阻塞,這樣的話Repeatable Read(可重複讀)級別使用 MVCC + Next-Key Lock 能夠解決了不可重複讀和幻讀的問題。

注意:Repeatable Read(可重複讀)是MySQL的默認隔離級別。

串行化(SERIALIZABLE)

這是最高的隔離級別,它經過強制事務排序,使之不可能相互衝突,從而解決幻讀問題。簡言之,它是在每一個讀的數據行上加上共享鎖。在這個級別,可能致使大量的超時現象和鎖競爭。

Spring事務

@Transaction事務失效

在項目開發中咱們有時候會遇到Spring事務失效的場景,那麼什麼場景會致使事務失效呢?

- @Transactional 註解屬性 propagation 設置錯誤

這種失效是因爲配置錯誤,如果錯誤的配置如下三種 propagation,事務將不會發生回滾。

TransactionDefinition.PROPAGATION_SUPPORTS:若是當前存在事務,則加入該事務;若是當前沒有事務,則以非事務的方式繼續運行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式運行,若是當前存在事務,則把當前事務掛起。
TransactionDefinition.PROPAGATION_NEVER:以非事務方式運行,若是當前存在事務,則拋出異常。

- @Transactional 應用在非 public 修飾的方法上

如下來自 Spring 官方文檔:

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

大概意思就是 @Transactional 只能用於 public 的方法上,不然事務不會失效,若是要用在非 public 方法上,能夠開啓 AspectJ 代理模式。

- @Transactional 註解屬性 rollbackFor 設置錯誤

rollbackFor 能夠指定可以觸發事務回滾的異常類型。Spring默認拋出了未檢查unchecked異常(繼承自 RuntimeException 的異常)或者 Error纔回滾事務;其餘異常不會觸發回滾事務。若是在事務中拋出其餘類型的異常,但卻指望 Spring 可以回滾事務,就須要指定 rollbackFor屬性。

// 但願自定義的異常能夠進行回滾
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class

同一個類中方法調用,致使@Transactional失效

來看兩個示例:

@Service
public class OrderServiceImpl implements OrderService {
    public void update(Order order) {
        updateOrder(order);
    }
    @Transactional
    public void updateOrder(Order order) {
        // update order;
    }
}

update方法上面沒有加 @Transactional 註解,調用有 @Transactional 註解的 updateOrder 方法,updateOrder 方法上的事務管用嗎?

@Service
public class OrderServiceImpl implements OrderService {
    @Transactional
    public void update(Order order) {
        updateOrder(order); 
   }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateOrder(Order order) {
        // update order;
    }
}

此次在 update 方法上加了 @Transactional,updateOrder 加了 REQUIRES_NEW 新開啓一個事務,那麼新開的事務管用麼?

這兩個例子的答案是:無論用!

由於@Transactional註解底層實際上是Spring幫咱們生成了一個代理對象,當其它對象調用帶有@Transactional的方法時,其實調的是代理對象,Spring會在代理對象中幫咱們加上一系列的事務操做。

在上面的例子中它們發生了自身調用,就調用該類本身的方法,而沒有通過 Spring 的代理類,默認只有在外部調用事務纔會生效,這也是老生常談的經典問題了。

異常被吃了

這種狀況是最多見的一種@Transactional註解失效場景

@Autowired
private B b;

@Service
public class OrderServiceImpl implements OrderService {
    @Transactional
    public void A(Order order) {
        try {
            b.insert();
         }catch (Exception e){
            //do something;
        }
    }
}

若是b.insert()方法內部拋了異常,而A方法此時try catch了B方法的異常,那這個事務還能正常回滾嗎?

答案:不能!而是會拋出下面異常:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

由於當ServiceB中拋出了一個異常之後,ServiceB標識當前事務須要rollback。可是ServiceA中因爲你手動的捕獲這個異常並進行處理,ServiceA認爲當前事務應該正常commit。此時就出現了先後不一致,也就是由於這樣,拋出了前面的UnexpectedRollbackException異常。

spring的事務是在調用業務方法以前開始的,業務方法執行完畢以後才執行commit or rollback,事務是否執行取決因而否拋出Runtime異常。若是拋出runtime exception 並在你的業務方法中沒有catch到的話,事務會回滾。

在業務方法中通常不須要catch異常,若是非要catch必定要拋出throw new RuntimeException(),或者註解中指定拋異常類型@Transactional(rollbackFor=Exception.class),不然會致使事務失效,數據commit形成數據不一致,因此有些時候try catch反倒會多此一舉。

分佈式事務

首先看下什麼是分佈式事務:

分佈式事務就是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位於不一樣的分佈式系統的不一樣節點之上。簡單的說,就是一次大的操做由不一樣的小操做組成,這些小的操做分佈在不一樣的服務器上,且屬於不一樣的應用,分佈式事務須要保證這些小操做要麼所有成功,要麼所有失敗。本質上來講,分佈式事務就是爲了保證不一樣數據庫的數據一致性。

那麼爲何須要分佈式事務?直接用Spring提供的@Transaction註解不行嗎?

這裏極其重要的一點:單塊系統是運行在同一個 JVM 進程中的,可是分佈式系統中的各個系統運行在各自的 JVM 進程中。所以你直接加@Transactional 註解是不行的,由於它只能控制同一個 JVM 進程中的事務,可是對於這種跨多個 JVM 進程的事務無能無力。

分佈式的幾種解決方案

可靠消息最終一致性方案

preview

咱們來解釋一下這個方案的大概流程:

  1. A 系統先發送一個 prepared 消息到 mq,若是這個 prepared 消息發送失敗那麼就直接取消操道別執行了,後續操做都再也不執行。
  2. 若是這個消息發送成功過了,那麼接着執行 A 系統的本地事務,若是執行失敗就告訴 mq 回滾消息,後續操做都再也不執行。
  3. 若是 A 系統本地事務執行成功,就告訴 mq 發送確認消息。
  4. 那若是 A 系統遲遲不發送確認消息呢? 此時 mq 會自動定時輪詢全部 prepared 消息,而後調用 A 系統事先提供的接口,經過這個接口反查 A 系統的上次本地事務是否執行成功 若是成功,就發送確認消息給 mq;失敗則告訴 mq 回滾消息(後續操做都再也不執行)。
  5. 此時 B 系統會接收到確認消息,而後執行本地的事務,若是本地事務執行成功則事務正常完成。
  6. 若是系統 B 的本地事務執行失敗了咋辦? 基於 mq 重試咯,mq 會自動不斷重試直到成功,若是實在是不行,能夠發送報警由人工來手工回滾和補償。 這種方案的要點就是能夠基於 mq 來進行不斷重試,最終必定會執行成功的。 由於通常執行失敗的緣由是網絡抖動或者數據庫瞬間負載過高,都是暫時性問題。 經過這種方案,99.9%的狀況都是能夠保證數據最終一致性的,剩下的 0.1%出問題的時候,就人工修復數據唄。

適用場景: 這個方案的使用仍是比較廣,目前國內互聯網公司大都是基於這種思路玩兒的。

最大努力通知方案

整個流程圖以下所示:
preview

這個方案的大體流程:

  1. 系統 A 本地事務執行完以後,發送個消息到 MQ。
  2. 這裏會有個專門消費 MQ 的最大努力通知服務,這個服務會消費 MQ,而後寫入數據庫中記錄下來,或者是放入個內存隊列。接着調用系統 B 的接口。
  3. 假如系統 B 執行成功就萬事 ok 了,可是若是系統 B 執行失敗了呢? 那麼此時最大努力通知服務就定時嘗試從新調用系統 B,反覆 N 次,最後仍是不行就放棄。

這套方案和上面的可靠消息最終一致性方案的區別:

可靠消息最終一致性方案能夠保證的是隻要系統 A 的事務完成,經過不停(無限次)重試來保證系統 B 的事務總會完成。

可是最大努力方案就不一樣,若是系統 B 本地事務執行失敗了,那麼它會重試 N 次後就再也不重試,系統 B 的本地事務可能就不會完成了。

至於你想控制它究竟有「多努力」,這個須要結合本身的業務來配置。

好比對於電商系統,在下完訂單後發短信通知用戶下單成功的業務場景中,下單正常完成,可是到了發短信的這個環節因爲短信服務暫時有點問題,致使重試了 3 次仍是失敗。

那麼此時就再也不嘗試發送短信,由於在這個場景中咱們認爲 3 次就已經算是盡了「最大努力」了。

簡單總結:就是在指定的重試次數內,若是能執行成功那麼皆大歡喜,若是超過了最大重試次數就放棄,再也不進行重試。

適用場景: 通常用在不過重要的業務操做中,就是那種完成的話是錦上添花,但失敗的話對我也沒有什麼壞影響的場景。

好比上邊提到的電商中的部分通知短信,就比較適合使用這種最大努力通知方案來作分佈式事務的保證。

TCC 強一致性方案

TCC 的全稱是:

  • Try(嘗試)
  • Confirm(確認/提交)
  • Cancel(回滾)。

這個實際上是用到了補償的概念,分爲了三個階段:

  1. Try 階段:這個階段說的是對各個服務的資源作檢測以及對資源進行鎖定或者預留;
  2. Confirm 階段:這個階段說的是在各個服務中執行實際的操做;
  3. Cancel 階段:若是任何一個服務的業務方法執行出錯,那麼這裏就須要進行補償,就是執行已經執行成功的業務邏輯的回滾操做。

仍是給你們舉個例子:

preview

好比跨銀行轉帳的時候,要涉及到兩個銀行的分佈式事務,若是用 TCC 方案來實現,思路是這樣的:

  1. Try 階段:先把兩個銀行帳戶中的資金給它凍結住就不讓操做了;
  2. Confirm 階段:執行實際的轉帳操做,A 銀行帳戶的資金扣減,B 銀行帳戶的資金增長;
  3. Cancel 階段:若是任何一個銀行的操做執行失敗,那麼就須要回滾進行補償,就是好比 A 銀行帳戶若是已經扣減了,可是 B 銀行帳戶資金增長失敗了,那麼就得把 A 銀行帳戶資金給加回去。

適用場景:這種方案說實話幾乎不多有人使用,可是也有使用的場景。

由於這個事務回滾其實是嚴重依賴於你本身寫代碼來回滾和補償了,會形成補償代碼巨大,很是之噁心。

好比說咱們,通常來講跟錢相關的,跟錢打交道的,支付、交易相關的場景,咱們會用 TCC,嚴格保證分佈式事務要麼所有成功,要麼所有自動回滾,嚴格保證資金的正確性,在資金上不容許出現問題。

比較適合的場景:除非你是真的一致性要求過高,是你係統中核心之核心的場景,好比常見的就是資金類的場景,那你能夠用 TCC 方案了。 你須要本身編寫大量的業務邏輯,本身判斷一個事務中的各個環節是否 ok,不 ok 就執行補償/回滾代碼。

並且最好是你的各個業務執行的時間都比較短。

可是說實話,通常儘可能別這麼搞,本身手寫回滾邏輯,或者是補償邏輯,實在太噁心了,那個業務代碼很難維護。

總結

今天簡單對事務作了一個總結,有什麼不對的地方請多多指教!

參考

https://zhuanlan.zhihu.com/p/85790242

相關文章
相關標籤/搜索