設計一個回調要注意哪些事情


設計一個回調要注意哪些事情

回調是我們在設計系統的時候經常會使用到的, A服務調用B服務, 但是如果B服務提供的是一個較長時間的、異步的接口, 那么我們就會想到使用一個回調, 讓B服務在異步處理結束之后, 來調用A的一個回調接口. 但是細品一下, 這一來一回的設計, 需要思考的點遠不是一個回調接口這么簡單.

20210220182454

回調是天生的並發

首先, 回調是一個天生的並發操作. 如果你的A服務在調用B服務等待回調的時候, 所有上下文都會保持原狀, 只有回調才會修改, 那么是不會有並發問題的. 但是如果A服務在等待回調的過程中, 上下文會根據某個情況進行變化, 那么這里就有一個並發問題. 如果A的狀態變化在回調之前? 如果A的狀態變化在回調之后? 如果A的狀態變化與回調同時進行?

前面兩種, 如果狀態變化和回調在前后, 那么唯一要注意的是需要做好防御編程. 狀態的變更是有順序的, 不應該讓狀態有任何躍遷或者回退行為. 否則很容易產生臟數據.

后面那種情況, 如果狀態變化和回調同時進行, 這種情況其實是非常難處理的. 一般回調會是一個請求, 請求的鏈路是非常長的, 如何保證整個請求的原子性, 甚至於考慮日志的原子性, 是一個不小的挑戰. 大致想了下, 可以有如下幾個方法:

1 粒度比較大的鎖, 這是通用性方法了. 將整個上下文鎖住, 鎖期間只有回調或者狀態變化能進行操作. 當然, 如果不介意臟讀的話, 完全可以使用讀鎖來保證讀的高可用.

2 (讀狀態標記位 + 寫狀態標記位) + 防御編程. 需要保證這個狀態標記位是原子性的, 這個還是比較好找的, 比如redis的某個key, mysql的某個字段. 但是需要保證讀和寫是一個原子行為. 當一個行為已經修改了狀態標記位后, 另外一個行為會被防御編程攔下來.

3 隊列化. 先使用隊列, 將某個狀態的變更都隊列化, 然后異步一個個處理隊列. 這個也是一個很好用的方法. 將所有的操作串行化自然能解決並發問題. 如果像go這樣的天生協程的語言, 可以不依賴外部隊列, 不妨開一個協程使用channel來進行串行化.

回調的超時時間

給一個回調設置超時時間, 這往往是個很難的事情. 它難的地方有兩個: 第一, 被調用的B服務往往給不出這個時間. 既然是異步, B需要考慮的鏈路一定很長. 加之既然是服務間調用, 基本上你們兩個服務會屬於兩個組織結構, 跨組織結構的溝通, 在所有公司都是一個不大不小的門檻. 第二, 超時之后的處理, 如果被調用方B服務給了一個超時時間, 那么A在超時時間之后要做些什么? B在超時時間之后還是否要發送回調呢? 這又涉及到了一個補刀機制.

但是反之思考, 如果不設置超時時間, 那么程序的健壯性又會是個很大的問題. 不管由於什么原因, 網絡抖動, 程序bug, 回調丟失了. 被調用方B以為已經發了回調, 調用方A卻沒有收到回調. 這種不一致性會是一個更大的災難, 它可能導致各種補刀策略失效.

所以還是建議需要給回調設置一個超時時間的, 至於超時后的處理, 則可以再定義一個機制進行補償.

回調需要心跳么?

這也是一個很有意思的方案. 沿着回調設置超時時間的思路, 可能就有一種解決方案是我設置心跳是否可行. A調用B之后, 在等待B回調的過程中, B不斷發送心跳給A, 告訴A我正在處理中, 一旦不發送心跳了. 那么就代表我死亡了.

首先這種就是一個有點悖論的方案. 這個心跳如果是從B到A, 那么為何不調整一個方向, 從A到B進行狀態查詢呢? 這種狀態查詢也可以充當心跳的功能. 再進一步, 既然都有了這種狀態查詢的心跳, 那為何還需要回調呢? 狀態結束的時候, 這種狀態查詢心跳自然也就會檢測到的. 當然這里可能唯一的差別就是實時性, 回調是一種結束即通知的機制, 心跳是一種定期得到通知的機制. 這又是另外一個需要考量的點了. 在異步絕大多數的場景下, 是可以容忍心跳時長的延遲的, 畢竟..都走異步化了, 多等一個心跳時間又有何妨呢?

回調的重試機制

被調用方B往往也是會知道回調的重要性, 所以一般會進行重試. 但是這種重試,如果不注意的話, 在有的時候, 就是殺死A服務的最后一根稻草.

其實就一點, 我們需要防止B服務的回調在短時間內堆積發送給A. 但是往往這種情況又是很可能發生的. 因為發生的原因很多, 比如B服務的隊列堆積, 重啟之后的瘋狂發送. 又比如A服務的服務對接, 同一時間給B服務發送了很多任務, B服務的任務處理時長基本恆定, 導致同一時間一堆任務需要回調通知. 而這個時候, A服務如果在扛不住的情況下, 又會又導致很多回調失敗, 觸發回調重試機制.

當然, 這里的回調重試機制, 不是回調特有的, 而是重試機制特有的. 好的重試機制應該是散列的, 重試時長遞增的. 這里可以參考TCP的慢啟動機制.

能不用回調就不用回調

這個就是我整篇的觀點, 能不用回調就不用回調, 因為回調要考慮的東西確實不少. 當然特定場景有特定的方法, 並不是所有場景都有並發,原子性的需求. 如果上述的理由還不夠, 我想從業務架構層面再叨叨幾個回調缺點.

回調使得鏈路變長且無向

我們最舒服的模塊鏈路是A調用B再調用C. 但是一旦引入了回調, 就有可能A調用B,B回調A, A再調用B或者C, 如果頻繁使用回調. 這個鏈路是一個很不舒服的鏈路. 即使服務只有少數幾個, 也能讓鏈路長度幾何性增長. 並且最致命還是鏈路的無向性. A和B可以互相調用, 會導致分層非常不合理.

主導權喪失

對於一個任務, A是發起方, 但是A不是結束的發起方, 而是結束的被調用方,其實這就把主動權丟失一部分給B服務了. 業務邏輯就不閉合在A服務了. 這也算是一種主導權的喪失把.

服務耦合性增加

A調用B, B回調A, 這種設計就把A和B綁定在一起了. 耦合性增加的缺點一大堆, 這里就不贅述了.

總結

當然, 如果你看了上面的那么多回調的弊端, 還是在某個場景還是決定使用回調, 那么我相信, 這個場景一定有不得不用回調的原因. 瑾告訴, 慎用之. 因為, 我就是這么踩坑過來的...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM