淺談Redis處理接口冪等性的兩種方案
前言:接口冪等性
問題,對於開發人員來說,是一個跟語言無關的公共問題。對於一些用戶請求,在某些情況下是可能重復發送的,如果是查詢類操作並無大礙,但其中有些是涉及寫入操作的,一旦重復瞭,可能會導致很嚴重的後果,例如交易的接口如果重復請求可能會重復下單。接口冪等性是指用戶對於同一操作發起的一次請求或者多次請求的結果是一致的,不會因為多次點擊而產生瞭副作用。
一、接口冪等性
1.1、什麼是接口冪等性
在HTTP/1.1中,對冪等性進行瞭定義。它描述瞭一次和多次請求某一個資源對於資源本身應該具有同樣的結果,即第一次請求的時候對資源產生瞭副作用,但是以後的多次請求都不會再對資源產生副作用。這裡的副作用是不會對結果產生破壞或者產生不可預料的結果。也就是說,其任意多次執行對資源本身所產生的影響均與一次執行的影響相同。
這類問題多發於接口的:
insert
操作,這種情況下多次請求,可能會產生重復數據。update
操作,如果隻是單純的更新數據,比如:update user set status=1 where id=1
,是沒有問題的。如果還有計算,比如:update user set status=status+1 where id=1
,這種情況下多次請求,可能會導致數據錯誤。
1.2、為什麼需要實現冪等性
在接口調用時一般情況下都能正常返回信息不會重復提交,不過在遇見以下情況時可以就會出現問題,如:
- 前端重復提交表單: 在填寫一些表格時候,用戶填寫完成提交,很多時候會因網絡波動沒有及時對用戶做出提交成功響應,致使用戶認為沒有成功提交,然後一直點提交按鈕,這時就會發生重復提交表單請求。
- 用戶惡意進行刷單: 例如在實現用戶投票這種功能時,如果用戶針對一個用戶進行重復提交投票,這樣會導致接口接收到用戶重復提交的投票信息,這樣會使投票結果與事實嚴重不符。
- 接口超時重復提交: 很多時候 HTTP 客戶端工具都默認開啟超時重試的機制,尤其是第三方調用接口時候,為瞭防止網絡波動超時等造成的請求失敗,都會添加重試機制,導致一個請求提交多次。
- 消息進行重復消費: 當使用 MQ 消息中間件時候,如果發生消息中間件出現錯誤未及時提交消費信息,導致發生重復消費。
本文討論的是如何在服務端優雅地統一處理這種接口冪等性情況,如何禁止用戶重復點擊等客戶端操作不在此次討論范圍。
1.3、引入冪等性後對系統的影響
冪等性是為瞭簡化客戶端邏輯處理,能放置重復提交等操作,但卻增加瞭服務端的邏輯復雜性和成本,其主要是:
- 把並行執行的功能改為串行執行,降低瞭執行效率。
- 增加瞭額外控制冪等的業務邏輯,復雜化瞭業務功能;
所以在使用時候需要考慮是否引入冪等性的必要性,根據實際業務場景具體分析,除瞭業務上的特殊要求外,一般情況下不需要引入的接口冪等性。
二、如何設計冪等
冪等意味著一條請求的唯一性。不管是你哪個方案去設計冪等,都需要一個全局唯一的ID ,去標記這個請求是獨一無二的。
- 如果你是利用唯一索引控制冪等,那唯一索引是唯一的
- 如果你是利用數據庫主鍵控制冪等,那主鍵是唯一的
- 如果你是悲觀鎖的方式,底層標記還是全局唯一的ID
2.1、全局的唯一性ID
全局唯一性ID,我們怎麼去生成呢?你可以回想下,數據庫主鍵Id怎麼生成的呢?
是的,我們可以使用UUID
,但是UUID的缺點比較明顯,它字符串占用的空間比較大,生成的ID過於隨機,可讀性差,而且沒有遞增。
我們還可以使用雪花算法(Snowflake)
生成唯一性ID。
雪花算法是一種生成分佈式全局唯一ID的算法,生成的ID稱為Snowflake IDs
。這種算法由Twitter創建,並用於推文的ID。
一個Snowflake ID有64位。
- 第1位:Java中long的最高位是符號位代表正負,正數是0,負數是1,一般生成ID都為正數,所以默認為0。
- 接下來前41位是時間戳,表示瞭自選定的時期以來的毫秒數。
- 接下來的10位代表計算機ID,防止沖突。
- 其餘12位代表每臺機器上生成ID的序列號,這允許在同一毫秒內創建多個Snowflake ID。
當然,全局唯一性的ID,還可以使用百度的Uidgenerator
,或者美團的Leaf
。
2.2、冪等設計的基本流程
冪等處理的過程,說到底其實就是過濾一下已經收到的請求,當然,請求一定要有一個全局唯一的ID標記
哈。然後,怎麼判斷請求是否之前收到過呢?把請求儲存起來,收到請求時,先查下存儲記錄,記錄存在就返回上次的結果,不存在就處理請求。
一般的冪等處理就是這樣,如下:
三、接口冪等性常見解決方案
3.1、下遊傳遞唯一請求編號
可能會想到的是,隻要請求有唯一的請求編號,那麼就能借用Redis做這個去重——隻要這個唯一請求編號在Redis存在,證明處理過,那麼就認為是重復的。
方案描述:
所謂唯一請求序列號,其實就是每次向服務端請求時候附帶一個短時間內唯一不重復的序列號,該序列號可以是一個有序 ID,也可以是一個訂單號,一般由下遊生成,在調用上遊服務端接口時附加該序列號和用於認證的 ID。
當上遊服務器收到請求信息後拿取該 序列號 和下遊 認證ID 進行組合,形成用於操作 Redis 的 Key,然後到 Redis 中查詢是否存在對應的 Key 的鍵值對,根據其結果:
- 如果存在,就說明已經對該下遊的該序列號的請求進行瞭業務處理,這時可以直接響應重復請求的錯誤信息。
- 如果不存在,就以該 Key 作為 Redis 的鍵,以下遊關鍵信息作為存儲的值(例如下遊商傳遞的一些業務邏輯信息),將該鍵值對存儲到 Redis 中 ,然後再正常執行對應的業務邏輯即可。
適用操作:
- 插入操作
- 更新操作
- 刪除操作
使用限制:
- 要求第三方傳遞唯一序列號;
- 需要使用第三方組件 Redis 進行數據效驗;
主要流程:
主要步驟:
- ① 下遊服務生成分佈式 ID 作為序列號,然後執行請求調用上遊接口,並附帶“唯一序列號”與請求的“認證憑據ID”。
- ② 上遊服務進行安全效驗,檢測下遊傳遞的參數中是否存在“序列號”和“憑據ID”。
- ③ 上遊服務到 Redis 中檢測是否存在對應的“序列號”與“認證ID”組成的 Key,如果存在就拋出重復執行的異常信息,然後響應下遊對應的錯誤信息。如果不存在就以該“序列號”和“認證ID”組合作為 Key,以下遊關鍵信息作為 Value,進而存儲到 Redis 中,然後正常執行接來來的業務邏輯。
上面步驟中插入數據到 Redis 一定要設置過期時間。這樣能保證在這個時間范圍內,如果重復調用接口,則能夠進行判斷識別。如果不設置過期時間,很可能導致數據無限量的存入 Redis,致使 Redis 不能正常工作。
3.2、防重 Token 令牌
方案描述:
針對客戶端連續點擊或者調用方的超時重試等情況,例如提交訂單,此種操作就可以用 Token 的機制實現防止重復提交。簡單的說就是調用方在調用接口的時候先向後端請求一個全局 ID(Token),請求的時候攜帶這個全局 ID 一起請求(Token 最好將其放到 Headers 中),後端需要對這個 Token 作為 Key,用戶信息作為 Value 到 Redis 中進行鍵值內容校驗,如果 Key 存在且 Value 匹配就執行刪除命令,然後正常執行後面的業務邏輯。如果不存在對應的 Key 或 Value 不匹配就返回重復執行的錯誤信息,這樣來保證冪等操作。
使用限制:
- 需要生成全局唯一 Token 串;
- 需要使用第三方組件 Redis 進行數據效驗;
主要流程:
① 服務端提供獲取 Token 的接口,該 Token 可以是一個序列號,也可以是一個分佈式 ID 或者 UUID 串。
② 客戶端調用接口獲取 Token,這時候服務端會生成一個 Token 串。
③ 然後將該串存入 Redis 數據庫中,以該 Token 作為 Redis 的鍵(註意設置過期時間)。
④ 將 Token 返回到客戶端,客戶端拿到後應存到表單隱藏域中。
⑤ 客戶端在執行提交表單時,把 Token 存入到 Headers 中,執行業務請求帶上該 Headers。
⑥ 服務端接收到請求後從 Headers 中拿到 Token,然後根據 Token 到 Redis 中查找該 key 是否存在。
⑦ 服務端根據 Redis 中是否存該 key 進行判斷,如果存在就將該 key 刪除,然後正常執行業務邏輯。如果不存在就拋異常,返回重復提交的錯誤信息。
註意,在並發情況下,執行 Redis 查找數據與刪除需要保證原子性,否則很可能在並發下無法保證冪等性。其實現方法可以使用分佈式鎖或者使用 Lua 表達式來註銷查詢與刪除操作。
參考鏈接:
阿裡面試官:接口的冪等性怎麼設計?
優雅地處理重復請求(並發請求)
SpringBoot 接口冪等性實現的 4 種方案!這個我真的服氣瞭!
到此這篇關於淺談Redis處理接口冪等性的兩種方案的文章就介紹到這瞭,更多相關Redis 接口冪等性內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 淺談java接口的冪等性及解決方案
- MybatisPlus中的insert操作詳解
- Go語言實現Snowflake雪花算法
- PHP利用雪花(SnowFlake)算法生成唯一ID
- Java經典面試題最全匯總208道(六)