tft每日頭條

 > 生活

 > 幂的運算原理及方法

幂的運算原理及方法

生活 更新时间:2024-08-06 05:06:50

最近比較懶,好幾周沒寫文章了。還是沒能堅持每周更新,愧對關注我的讀者和自己的flag。。。

不過還好調整了過來,還是會繼續堅持周更的。畢竟學習是一件永無止境的事,用輸出倒逼自己輸入,才能有所沉澱和成長,歡迎大家監督和共勉~

這篇文章是介紹幂等的。幂等在分布式和高并發場景下是必須要考慮的一件事。如果不做幂等,輕則産生髒數據,重則産生業務異常,造成資産損失。

什麼是幂等?

幂等原本是一個數學上的概念,表達的是N次變換與1次變換的結果相同。即公式:f(x)=f(f(x)) 成立。

用在編程領域,表達的是對于同一個系統,使用同樣的條件,一次請求和重複的多次請求對系統資源的影響是一緻的

一般來說,讀操作天然都是幂等的(除非你的讀操作有副作用),而寫操作是不幂等的。但是有些業務場景我們需要做到寫操作幂等,所以需要做一些額外的工作。

比如提交訂單請求,有時候可能是同樣一個訂單,如果不做幂等,重複處理,就有可能會造成用戶或者公司的資産損失。

幂等在并發量較高的項目中是一個經常會遇到的問題。主要有以下兩種場景會遇到幂等問題:

  1. 請求重複提交/消息重複提交
  2. 失敗重試

下面分别談談這兩種場景的具體的解決方案。

重複提交下的幂等

重複提交,很大的概率是前端設計不合理或者客戶端網絡問題,導緻用戶連續提交多次相同的内容。對于這種場景,可以在前端改善用戶交互,在後端也可以簡單地判重和拒絕。這種場景比較适合在外圍做幂等,直接把請求拒絕或者pass,不用把請求放到業務内部。應該做成一種較為通用的能力。

從前端防止用戶重複提交

前端交互很重要,因為用戶其實并不管幂等不幂等,有時候客戶端可能由于手機卡死或者網速較慢,提交了遲遲得不到反饋,可能會瘋狂點擊提交按鈕,導緻産生大量的重複請求。

常用的解決方案有:跳轉到其它頁面,如訂單提交後跳轉到提交成功頁面;點擊提交按鈕後清空内容,适用于評論、回複等業務場景。還有按鈕置灰等設計。

我的個人網站上可以找到類似的例子,比如評論組件,評論提交後,會将提交按鈕置灰60秒。

幂的運算原理及方法(幂等問題及常見解)1

比如點贊組件,對一篇點贊後也會有一段時間的防重限制,防止用戶瘋狂點擊。

幂的運算原理及方法(幂等問題及常見解)2

使用緩存實現重複提交幂等

要解決重複提交幂等的方案很簡單,用緩存來做就行了。我們把請求參數(JSON序列化為字符串)或者請求參數中的業務唯一ID作為key,存進緩存裡。後續如果有其它一樣的請求過來,就先去緩存裡面查,如果存在,就直接跳過或者返回。

幂的運算原理及方法(幂等問題及常見解)3

這裡有一個問題,如果寫操作有返回值怎麼辦?比如我們經常會有這樣的設計:數據庫的主鍵是自增的,往數據庫插入一條新的記錄,持久層會返回我們新插入記錄的ID,而我們後續的程序會需要用到這個ID。

如果要實現幂等,肯定是第一時間寫進緩存key,DB生成ID返回後,再把它插入到緩存的value裡。中間有個時間差,如果這個時間差内,其它的請求過來了怎麼辦?

對于這個問題,如果業務上允許後續的請求直接報錯,那可以直接抛異常出去,比如提示用戶:訂單提交失敗。但這樣通常對用戶來說并不友好。另一種解決方案是,提前生成ID,ID通過參數傳進去,而不是用DB生成,這樣寫操作就不會存在這個問題了。而生成ID的是一個很快的讀操作,對這個讀操作也可以做一個幂等,保證短時間内,同樣的請求參數,隻生成同一個ID

單機幂等還是分布式幂等,取決于我們用的是單機緩存還是分布式緩存。大多數時候,如果我們的業務是分布式的系統,更建議用redis等分布式緩存來實現分布式幂等。

我們也可以實現一個很方便的幂等注解。其實原理很簡單,自定義一個注解,用spring AOP加上緩存來實現幂等。也有現成的輪子:idempotent-spring-boot-starter,可以參考一下。

失敗重試下的幂等

失敗重試幂等其實也是會重複提交,但處理方式不同,不應該簡單地拒絕掉,可能會隻是某些步驟做幂等,但是請求會繼續往後走,進入業務流程内部。比如提交訂單,提交後會進行很多寫操作,如優惠券狀态修改、庫存扣減、創建物流信息等。如果中途因為某種意外原因失敗(比如網絡原因等),可以進行重試。

而失敗重試的時候,如果不做幂等,會産生問題。比如庫存扣減操作,如果因為超時原因重試,可能會扣減兩次庫存,造成數據錯誤。但如果直接拒絕後面的請求也不妥,可能第一次請求确實是因為網絡原因,處理失敗了,第二次重試說不定就成功了,所以還是應該讓後續的請求進來,采取其它措施來保證幂等。

幂的運算原理及方法(幂等問題及常見解)4

對于失敗重試場景下的幂等,下面也有一些解決方案。

唯一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每日頭條,我们将持续为您更新最新资讯!

查看全部

相关生活资讯推荐

热门生活资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved