1. 程式人生 > >事務與分散式事務

事務與分散式事務

很多同學在開發中已經不自覺的接觸了很多事務相關的程式碼(尤其是在資料庫操作中),但是事務究竟是做什麼的,有沒有必要必須這麼操作?

一段典型的程式碼如下:

db.beginTransaction();
try {
    // do some CRUD operation
    db.commit();
} catch {
    //Error in between database transaction 
    db.rollback();
} finally { db.endTransaction(); } 

從這段程式碼可以更直觀的感受一下 “事務” 這個抽象的概念,那麼事務是幹什麼用的呢?

事務(Transaction)


wiki的解釋中,事務是一組單元化的操作,這組操作可以保證要麼全部成功,要麼全部失敗(只要有一個失敗的操作,就會把其他已經成功的操作回滾)。這樣的解釋還是不夠直觀,看下面一個經典的例子。

假設有兩個銀行賬戶A和B,現在A要給B轉10塊錢,也就是轉賬。在銀行系統中A和B是兩個獨立的賬戶,所以轉賬操作會被分解:

  1. 從A的賬戶中扣掉10塊錢
  2. 在B的賬戶中新增10塊錢

那麼問題就來了,如果成功的在A賬戶中扣掉了錢,但是沒有在B中加錢怎麼辦?或者A中沒有成功扣款,B中卻加了錢怎麼辦?這些可能性都是有的,比如突然斷電、系統崩潰或者A賬戶本來就沒有錢。所以無論上面哪一張情況發生,都是不應該的。

解決上面問題的一種簡單方法就是事務 - 要麼兩個操作都成功返回一個成功的結果,否則把所有操作回滾,返回一個失敗的結果。所謂回滾就是如果從A中扣錢成功但B中加錢失敗的話,那麼把A中扣得錢再還回去。

注:事務的英文Transaction其實就是交易的意思

事務操作的基本步驟概括如下:

  1. 開始事務
  2. 執行一系列的資料庫操作
  3. 如果沒有錯誤發生,那麼提交事務,返回成功
  4. 如果有錯誤發生,那麼回滾事務,返回失敗

同時事務發展出幾個基本原則 - ACID:

  1. Atomicity 原子性,要不成功要麼失敗,部分成功是不可以的
  2. Consistency
    一致性,在事務開始之前和事務結束以後,資料庫的完整性沒有被破壞(一致性一般是由應用來指定的)
  3. Isolation 隔離性,當一個事務正在進行的時候,假設沒有第二個事務在進行(併發,如果真的發生了,系統需要保證隔離的程度)
  4. Durability 永續性,事務完成後,該事務對資料庫所作的更改便持久地儲存在資料庫之中(防止系統崩潰)

這幾個原則構成了單一資料來源事務的基本原則。

程式碼實現


由於事務的這些特點,所以在事務相關的程式碼中,基本都是類似的風格:

db.beginTransaction();
try {
    // do some CRUD operation
    db.commit();
} catch {
    //Error in between database transaction 
    db.rollback();
} finally { db.endTransaction(); } 

這就是我們經常見到的程式碼(無論哪種語言)。這種相對比較固定的模板導致一種“宣告式”事務的產生,在Spring中會經常見到,所謂的“宣告式”大概表現如下:

@Transactional
public void insertXXX() { // do something... } 

在方法的宣告上,通過 @Transactional 註解把一個方法標註為支援事務的,那麼在執行的時候,容器會自動給這個方法圍繞上面的程式碼塊。

事務的實現原理


在開始的時候,我們說事務可以保證操作的ACID原則,那麼事務究竟是如何保證這些原則的?db.beginTransaction()db.commit()db.rollback()db.endTransaction()究竟幹了什麼事,如果這些操作本身也失敗了怎麼辦?

實現事務功能的系統通常叫 “TransactionProcessingMonitor” 或 “TransactionManager”,這些系統通常會被打包進資料庫引擎中,在分散式系統中也會作為一個獨立的模組存在。

解決ACID問題的兩大技術點是:

  1. 預寫日誌(Write-ahead logging) 保證原子性和永續性
  2. (locking) 保證各隔離性

這裡並沒有提到一致性,這是因為一致性是應用相關的話題,它的定義一般由業務系統來定義 - 什麼樣的狀態才是一致的?而實現一致性的程式碼通常在業務層邏輯的程式碼中得以體現。

是大家熟悉的一個話題,在併發環境中通過讀寫鎖來保證操作的互斥性,沒有太多神奇的東西。根據隔離程度不同,鎖的運用也不同,可能會產生一些問題,具體可以檢視 Isolation (database systems)

本文比較感興趣的是預寫日誌。因為在資料庫的複雜資料結構中更新資料是一個比較慢的操作,保證這種操作的原子性和永續性是很困難的。預寫日誌的工作模式是這樣的:在任何事務操作發生之前,先把所有的變化寫入一個日誌檔案並持久化,然後再開始真實的寫資料庫操作。如果在接下來的操作中系統崩潰,那麼我們可以在系統恢復之後檢查日誌和資料庫中的記錄,來決定是繼續執行完成未盡的任務還是回滾操作 - 把資料庫還原到這次事務之前的一個狀態。

預寫日誌一般採用簡單順序日誌(sequential file)的寫入格式,這樣日誌寫入速度可以很快。這點很重要,因為一般事務操作的吞吐量往往受到日誌系統速度的限制。日誌的格式會同時記錄redo和undo的資訊。

分散式事務


在大型應用開發中,經常要做業務拆分,把原來的單一架構的應用拆分成不同的服務。不同的服務之間鬆耦合可以很好的解決業務耦合問題,但是這樣也會帶來事務處理的問題,如果一個操作會同時寫入兩個資料庫,那麼如何保證這兩個寫入的一致性?

單一資料庫可以通過ACID原則保證自己的事務處理,但是如果有兩個不同的資料庫,如何保證針對這兩個資料庫的事務都成功呢?在 JavaEE 規範中使用 2PC (2 Phase Commit, 兩階段提交) 來處理跨 DB 環境下的事務問題。

簡單來說J2EE的2PC協議是這樣的,先把事物請求傳送給中間協調器,協調器負責各個資料來源的事物處理。處理過程分兩個階段:

  1. 投票階段
    協調器把事物請求傳送給各個資料來源,資料來源負責各自的事物處理。
  2. 完成階段
    協調器根據各個資料來源的返回結果,決定是處理成功或者失敗,只要有一個結果是失敗的,那麼會回滾所有資料來源的事物處理。

這種處理方式,實際上是一個放大版的ACID原則。但是在分散式環境下,2PC 是反可伸縮模式,在事務處理過程中,參與者需要一直持有資源直到整個分散式事務結束。這樣,當業務規模達到千萬級以上時,2PC 的侷限性就越來越明顯,系統可伸縮性會變得很差。

CAP


在過去Inktomi的Eric Brewer曾提出過分散式系統的一種猜想(conjecture),在分散式系統中的三項重要指標:

  • Consistency 一致性
  • Availability 可用性
  • Partition-tolerance 分割槽容忍度

是不能同時成立的 - 在任意時刻,只有兩項能同時成立。對於高流量的網站來說,為了提高系統伸縮性,一般都會犧牲一致性。

BASE


BASE是一種嘗試通過犧牲一部分一致性而達到高可用性的原則,ACID原則中要求系統的每個操作之後都是連續的,但是BASE認為系統是可容忍區域性的/短時間的不一致。

在基於ACID的事務中,事務是簡單可靠的,為了達成這種效果,往往會造成耗盡整個系統的資源造成整體不可用。而BASE的實現是複雜的和業務緊密相關的。BASE 原則體現在(採用這種原則意味著放棄ACID):

  • 基本可用(Basically Available)
  • 軟狀態(Soft state)
  • 最終一致(Eventually consistent)

這是一組非常抽象的概念,通過這組原則你不能領會任何可行的具體的系統設計方案。BASE並沒有指明任何方案,只是在告訴你 - 其實還可以這麼搞!

下面看一下簡單的例子(從這裡偷來的):假如有一個系統可以在上面買賣物品,可以設計著這樣的表結構:

  user and transaction

在這個系統中,如果產生了一項交易,那麼會現在Transaction表中記下一條記錄,然後在買家和賣家表中分別更新記錄。基於ACID原則的事務程式碼是這樣的:

  acid

這裡面的三條SQL操作會分別更新三個不同的資料庫,在ACID系統中會使用2PC實現。如果從BASE的角度來考慮可以這麼設計:

  base

對於整個系統來說Transaction表才是真正有意義的,User表中的相關資料可以認為是為了系統性能而設計的快取(這樣不必查Transaction表即可或許相關的資料)。所以可以設計出上面這種事務模型,這種模型的潛在問題是,如果對於User表的操作失敗了,那麼在使用者端看到的結果是不準確的。現在我們的系統已經出現了不一致的問題,如果我們提前告知使用者,他看到的資料是粗略的估計,那麼這個問題在業務上算是解決了。

但是如果我們不能容忍這種可能會造成永久的不一致,那麼該如何解決問題呢?答案在訊息系統 - 可靠的訊息系統。下面是改進版:

  base and mq

在這種設計中,第一段程式碼保證了Transaction和訊息持久化的事務性,然後在訊息處理系統中在分別更新User表的資料。並且在處理訊息的時候,僅在事務成功的時候才移除訊息,這樣可以保證User可以成功的更新。因為用到了訊息系統,所以必然存在訊息的延遲問題,而這正式前面說的 - 犧牲一部分的一致性(User和Transaction表不是同步更新的)。但是一旦訊息被成功處理,我們最終會達成一致的狀態 - 即最終一致。

看完了這個例子,對於BASE也僅僅是初步的認識,在真實的業務系統中還需要根據自己業務的特色設計相應的解決方案。

注1:分散式事務確實牛逼,寫作過程中深感壓力,如果不足不對的地方,還請高手多多指正。
注2:對於最開始那個轉賬的例子,用BASE思想的實現現在還不方便說,因為這是螞蟻內部的資料。

參考:



作者:ntop
連結:https://www.jianshu.com/p/d322cba3add4