最近比較懶,好幾周沒寫文章了。還是沒能堅持每周更新,愧對關注我的讀者和自己的flag。。。
不過還好調整了過來,還是會繼續堅持周更的。畢竟學習是一件永無止境的事,用輸出倒逼自己輸入,才能有所沉澱和成長,歡迎大家監督和共勉~
這篇文章是介紹幂等的。幂等在分布式和高并發場景下是必須要考慮的一件事。如果不做幂等,輕則産生髒數據,重則産生業務異常,造成資産損失。
什麼是幂等?幂等原本是一個數學上的概念,表達的是N次變換與1次變換的結果相同。即公式:f(x)=f(f(x)) 成立。
用在編程領域,表達的是對于同一個系統,使用同樣的條件,一次請求和重複的多次請求對系統資源的影響是一緻的。
一般來說,讀操作天然都是幂等的(除非你的讀操作有副作用),而寫操作是不幂等的。但是有些業務場景我們需要做到寫操作幂等,所以需要做一些額外的工作。
比如提交訂單請求,有時候可能是同樣一個訂單,如果不做幂等,重複處理,就有可能會造成用戶或者公司的資産損失。
幂等在并發量較高的項目中是一個經常會遇到的問題。主要有以下兩種場景會遇到幂等問題:
下面分别談談這兩種場景的具體的解決方案。
重複提交下的幂等重複提交,很大的概率是前端設計不合理或者客戶端網絡問題,導緻用戶連續提交多次相同的内容。對于這種場景,可以在前端改善用戶交互,在後端也可以簡單地判重和拒絕。這種場景比較适合在外圍做幂等,直接把請求拒絕或者pass,不用把請求放到業務内部。應該做成一種較為通用的能力。
從前端防止用戶重複提交前端交互很重要,因為用戶其實并不管幂等不幂等,有時候客戶端可能由于手機卡死或者網速較慢,提交了遲遲得不到反饋,可能會瘋狂點擊提交按鈕,導緻産生大量的重複請求。
常用的解決方案有:跳轉到其它頁面,如訂單提交後跳轉到提交成功頁面;點擊提交按鈕後清空内容,适用于評論、回複等業務場景。還有按鈕置灰等設計。
我的個人網站上可以找到類似的例子,比如評論組件,評論提交後,會将提交按鈕置灰60秒。
比如點贊組件,對一篇點贊後也會有一段時間的防重限制,防止用戶瘋狂點擊。
使用緩存實現重複提交幂等
要解決重複提交幂等的方案很簡單,用緩存來做就行了。我們把請求參數(JSON序列化為字符串)或者請求參數中的業務唯一ID作為key,存進緩存裡。後續如果有其它一樣的請求過來,就先去緩存裡面查,如果存在,就直接跳過或者返回。
這裡有一個問題,如果寫操作有返回值怎麼辦?比如我們經常會有這樣的設計:數據庫的主鍵是自增的,往數據庫插入一條新的記錄,持久層會返回我們新插入記錄的ID,而我們後續的程序會需要用到這個ID。
如果要實現幂等,肯定是第一時間寫進緩存key,DB生成ID返回後,再把它插入到緩存的value裡。中間有個時間差,如果這個時間差内,其它的請求過來了怎麼辦?
對于這個問題,如果業務上允許後續的請求直接報錯,那可以直接抛異常出去,比如提示用戶:訂單提交失敗。但這樣通常對用戶來說并不友好。另一種解決方案是,提前生成ID,ID通過參數傳進去,而不是用DB生成,這樣寫操作就不會存在這個問題了。而生成ID的是一個很快的讀操作,對這個讀操作也可以做一個幂等,保證短時間内,同樣的請求參數,隻生成同一個ID。
單機幂等還是分布式幂等,取決于我們用的是單機緩存還是分布式緩存。大多數時候,如果我們的業務是分布式的系統,更建議用redis等分布式緩存來實現分布式幂等。
我們也可以實現一個很方便的幂等注解。其實原理很簡單,自定義一個注解,用spring AOP加上緩存來實現幂等。也有現成的輪子:idempotent-spring-boot-starter,可以參考一下。
失敗重試下的幂等失敗重試幂等其實也是會重複提交,但處理方式不同,不應該簡單地拒絕掉,可能會隻是某些步驟做幂等,但是請求會繼續往後走,進入業務流程内部。比如提交訂單,提交後會進行很多寫操作,如優惠券狀态修改、庫存扣減、創建物流信息等。如果中途因為某種意外原因失敗(比如網絡原因等),可以進行重試。
而失敗重試的時候,如果不做幂等,會産生問題。比如庫存扣減操作,如果因為超時原因重試,可能會扣減兩次庫存,造成數據錯誤。但如果直接拒絕後面的請求也不妥,可能第一次請求确實是因為網絡原因,處理失敗了,第二次重試說不定就成功了,所以還是應該讓後續的請求進來,采取其它措施來保證幂等。
對于失敗重試場景下的幂等,下面也有一些解決方案。
唯一ID/token為每一次操作都生成一個單獨的唯一ID或者token(以下簡稱token)。一個token在操作的每一個階段隻有一次執行權,一旦執行成功則保存執行結果。對重複的請求,返回同一個結果。
在訂單提交的場景,訂單ID就可以作為一個token。每一個環節執行時都先檢測一下該訂單ID是否已經執行過這一步驟,對未執行的請求,執行操作并緩存結果,而對已經執行過的ID,則直接返回之前的執行結果,不做任何操作。
在寫操作之前,還可以用這個token進行上鎖,保證同一個token隻進行一次寫操作。
開源輪子可以參考:redis-auto-idempotent-spring-boot-starter
樂觀鎖可以樂觀鎖的思想,利用版本号去做幂等,這個比較适用于更新操作。
每條數據都有一個版本号,數據更新的時候,傳入獲取到的版本号,且版本号 1。在實際進行寫操作的時候,會去比較請求參數裡面的版本号,每個version隻有一次執行成功的機會,一旦失敗必須重新獲取。
UPDATE demo
SET count = count 1, version = version 1
WHERE id = 123 and version = 5;
利用數據庫主鍵或者唯一索引的特性,保證相同的數據隻會被寫入一次。
比如在評論場景,我們可以把文章ID 用戶ID 評論内容作為一個唯一鍵,那對于同一個文章,同一個用戶,就不能提交多條相同的評論了。
但是個人不推薦這種方式。因為性能較差,而且DB的主鍵沖突,一般歸于系統異常,如果用它來做去重,異常類型和日志不太好定義。
任務幂等任務幂等,指的是對于同一段數據,觸發一次任務和多次任務是相同的結果。
任務基本上都會涉及到寫操作。如果同一段數據,被多次任務觸發,進行了多次寫操作,可能會造成髒數據。
其實也很好解決,我們在抽象任務模型的時候,給任務設計好不同的狀态就行了。比如任務初始化、啟動後、成功、失敗、取消等都有不同的狀态。後續的任務觸發請求,如果監測到這段數據的狀态已經做了修改,就根據這個任務的狀态和用戶定義
的策略,來決定是跳過還是重新執行。
可以參考Spring Batch的設計,Spring Batch抽象了Job這個概念,提供了BatchStatus枚舉來作為任務的狀态:
public enum BatchStatus {STARTING, STARTED, STOPPING,
STOPPED, FAILED, COMPLETED, ABANDONED }
相應的,比Job粒度更小的Step也有自己的狀态。同時Spring Batch可以允許用戶自定義地配置skip策略和失敗處理策略。
消息幂等消息幂等也是經常要考慮的問題。還是之前電商系統中訂單提交的例子,比如庫存扣減這種操作,為了保證吞吐量,可能會使用消息來實現。
消息幂等的概念可以總結如下:
如果消息重試多次,消費者端對該重複消息消費多次與消費一次的結果是相同的,并且多次消費沒有對系統産生副作用,那麼我們就稱這個過程是消息幂等的。
對于消息來說,發送端可能産生重複消息,消費端也可能會重複消費同一條消息(大多數都是因為網絡問題)。如果靠消息中間件去實現幂等,是一件比較困難的事情,增加幂等的處理會導緻消息中間件的吞吐量下降。所以絕大多數消息消息中間件本身不處理幂等問題,而是交給了業務端自己去處理。
而不論是發送端重複還是消費端重複,我們隻需要保證消費端幂等就可以了,不需要在發送端做什麼事情。而消費端做幂等,其實本質上也是上面提到的“重複提交下的幂等”,比較适合在消費端的入口處就做幂等處理。
求個支持我是Yasin,一個堅持技術原創的博主,我的微信公衆号是:編了個程
都看到這兒了,如果覺得我的文章寫的還行,不妨支持一下。
文章會首發到公衆号,閱讀體驗最佳,歡迎大家關注。
你的每一個轉發、關注、點贊、評論都是對我最大的支持!
還有學習資源、和一線互聯網公司内推哦
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!