1. 程式人生 > >系統冪等以及常用實現方式

系統冪等以及常用實現方式

集中式 如何 每次 答案 等等 一些事 例子 規模 工具

現在稍具規模的網站和大型應用都不再是單機模式,而是分布式應用,基於多機的集群構建的應用,這樣服務能力就可以基本實現橫向擴容(scale out),不會像單機模式下的縱向擴容(scale up)會受到單機服務能力上限的限制。另外,隨著“微服務”概念的火爆,很多應用在構建之初就已經走在了分布式的路線上了,所以就目前行業的發展來看,基於分布式的應用會越來越普遍,甚至變成常態。加上docker這些容器技術的出現,應用分布式化的工具也越來越成熟。

眾所周知,構建分布式應用所面臨的復雜度遠遠超出集中式的單一應用,導致復雜性的因素有很多,在此只提其中一點:網絡的不可靠性。在單一進程內部,對一個函數的調用,結果只有兩種——成功和失敗,失敗的情況下,調用者可以決定做一些事情彌補。但是在跨進程的調用中,對一個遠程(也可以在同一個節點上)進程上運行的函數調用除了會得到成功和失敗,還會有第三種的情況——超時,這個現象被稱為分布式的三態。這也是困擾分布式應用構建的最核心因素之一,很多分布式應用的復雜度之所以上升這麽多也是因為三態之中的超時引起的。

簡單看看超時給我們帶來的困擾,進程A調用進程B上的函數f,對於成功和失敗的結果,相信和單機下一樣,進程A都可以進行很好地的處理,因為結果是很明確的。如果進程A調用f之後,在允許的等待最大時間內沒有返回結果,就是調用超時了,此時進程A能做什麽?其實進程A什麽都做不了,因為超時是一個不明確的結果——成功和失敗都有可能。詳細解釋下可能的情況:

  • 成功的情況:進程A把數據通過網絡傳輸到進程B上,f執行成功,通 網絡返回執行結果給進程A,可是網絡不太好,傳輸失敗了,進程A並 未在指定時間內收到結果,認為超時了。
  • 失敗的情況:情況和成功的情況差不多,只是f執行失敗了,但是結 果依然傳輸失敗,進程A也認為執行超時了。
  • 未執行的情況:進程A的數據發送到進程B所在的節點過程中網絡失敗 了,或者發送到了進程B所在的機器上,但是進程B沒有消費掉在TCP 網絡層的數據等等

由此可見,進程A對於超時確實無能為力,有太多的可能存在的情況了。但是分布式協作過程中又必須解決這個問題,不然分布式應用是沒意義的,這種情況下,一般會采用讓進程A嘗試重試——即重復發起之前的調用。但是這樣也可能會帶來問題,因為超時的那次調用可能已經成功了,再次以同樣的參數調用f會不會帶來額外的問題?這就引出本文的主角——冪等性。

冪等性本來是一個數學概念,在計算機方面用來表示對同一個過程應用相同的參數多次和應用一次產生的效果是一樣,這樣的過程即被稱為滿足冪等性。

有了這個概念之後,假如之前的f是滿足冪等性的,那麽是不是意味著進程A在調用f超時之後,可以繼續重復調用f多次?這樣最起碼進程A可以在超時情況下做一些促進事情正向發展的努力。所以這種方式是分布式節點間常用的方式,那麽如何保證冪等呢?

在考慮實現冪等之前,先看看有哪些操作是天然冪等的,以SQL為例。update tab1 set col1 = 1 where id = 2這樣的更新語句,無論執行多少次結果都是不受影響的,所以是冪等的。update tab1 set col1 = col1 + 1 where id = 2這樣的更新語句會隨著每次更新不斷變化,所以不是冪等的。所以在考慮之前,先識別出冪等和非冪等操作。

業務系統實現冪等的通用方式:一般是排重表校驗,在業務操作所在的庫建一張小表,名稱暫時搞成dup_forbidden,核心字段就一個biz_id,並且在這個字段上建立一個unique index,其他字段可以根據業務需求來擴充。那麽原來的業務f實現冪等的偽代碼如下:

begin transaction;
count = insert ignore dup_forbidden (...biz_id...) value(...biz_id...)
if (count > 0) {
  f(biz_id)
} 
commit;

可以認為這是一套業務系統實現冪等的模板做法,通過insert ignore返回值來判斷是否已經執行過了,但是針對不同的情況可能還有變化。使用事務的目的是為了保證f和dup_forbidden的操作同時成功和失敗。本質上來看,dup_forbidden表就是通過unique index來屏蔽對f的多次調用,事實上很多業務已經存在dup_forbidden表的功能。

考慮如下場景:在一個面向交易的分布式應用中,支付子系統完成了支付功能,支付子系統通知訂單子系統,通知的方式無非是調用訂單子系統的一個函數f而已,只是調用的方式分為同步和異步。無論是同步還是異步,f都可能存在超時,所以為了支持重試,f必須是冪等的。f會首先根據傳入的訂單號來查找訂單,檢查訂單狀態。如果是已經支付,就會直接返回成功。如果是待支付狀態,那麽會嘗試鎖定(悲觀鎖和樂觀鎖)訂單,修改狀態,指定其他操作,其中鎖定只是為了防止並發操作。偽代碼實現如下:

begin transaction;
count = update deal_tab set status = paid where id = xx_id and status = unpaid
if (count > 0) {
  f(xx_id)
}
commit;

從這個例子可以看出deal_tab訂單表本身已經可以作為dup_forbidden表的作用了,所以insert防重操作變成update來實現,當然這個是樂觀鎖的版本。悲觀鎖的版本如下:

begin transaction;
deal = select * from deal_tab where id = xx_id for update
if (deal.status == paid) {
  return true;
} else if (deal.status = unpaid) {
  f(xx_id)
}
commit;

當然基於悲觀鎖的做法對於高並發的系統是不建議的,畢竟長時間鎖定記錄會降低系統的TPS。

當然,所有這些方案都是基於業務存在唯一的業務編號來設計實現的,可能會存在完全沒有業務編號的嗎?答案是it depends。即使沒有完全唯一的編號,我們也可以人為生成編號,比如調用方負責生成調用編號,同一個調用編號發起的多次調用都被視為一次調用,既可以作為唯一鍵來排重。事實上,這種情況確實比較少!

業務系統實現冪等性的方式基本確定。系統關鍵接口的冪等性為以後系統的長期發展,特別是往分布式方向發展打下了很好的根基,可以大大簡化分布式應用的構建復雜度。

原文鏈接:http://yongpoliu.com/idempotent/

系統冪等以及常用實現方式