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。即使沒有完全唯一的編號,我們也可以人為生成編號,比如呼叫方負責生成呼叫編號,同一個呼叫編號發起的多次呼叫都被視為一次呼叫,既可以作為唯一鍵來排重。事實上,這種情況確實比較少!

總結

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