本文目錄
- 背景
- 簡單冪等實現
2.1 數據庫記錄判斷
2.2 併發問題解決
- 通用冪等實現
3.1 設計方案
3.1.1 通用存儲
3.1.2 使用簡單
3.1.3 支持註解
3.1.4 多級存儲
3.1.5 併發讀寫
3.1.6 執行流程
3.2 冪等接口
3.3 冪等註解
3.4 自動區分重複請求
3.5 存儲結構
3.6 源碼地址
背景
回答群友的問題:
冪等有沒有什麼通用的方案和實踐?關於什麼是冪等,本文就不再闡述了。相信大家都知道,並且也都遇到過類似的問題以及有自己的一套解決方案。
基本上所有業務系統中的冪等都是各自進行處理,也不是說不能統一處理,統一處理的話需要考慮的內容會比較多。
我個人認為核心的業務還是適合業務方自己去處理,比如訂單支付,會有個支付記錄表,一個訂單隻能被支付一次,通過支付記錄表就可以達到冪等的效果。
還有一些不是核心的業務,但是也有冪等的需求。比如網絡問題,多次重試。用戶點擊多次等場景。這種場景下還是需要一個通用的冪等框架來處理,會讓業務開發更加簡單。
簡單冪等實現
冪等的實現其實並不複雜,方案也有很多種,首先介紹下基於數據庫記錄的方案來實現,後面再介紹通用方案。
數據庫記錄判斷
以文章開頭講的支付場景來舉例。業務場景是一個訂單隻能支付一次,所以我們在支付之前會判斷這個訂單有沒有支付過,如果沒有支付過則進行支付,如果支付過了,就反正支付成功,冪等。
這種方式需要有一個額外的表來存儲做過的動作,才能判斷之前有沒有做過這件事情。
就好比你年齡大了,然後還是單身的技術宅。這個時候你家裡著急了呀,你老媽天天給你介紹小姐姐。你每個週末都要打扮的非常帥氣,去見你老媽給你介紹的小姐姐。
去之前你得記錄下吧,8 月第一週我見的 XXX, 第二週我見的 YYY, 如果第三週又讓你去見 XXX, 如果這個時候你不喜歡 XXX, 你會翻出你的小本本看下,這個之前見過了,沒必要再見了,不然見了多尷尬啊。
併發問題解決
通過查詢支付記錄,判斷能否進行支付在業務邏輯上沒一點問題。但是在併發場景就會有問題。
1001 的訂單發起了兩次支付請求,當前兩個請求同時查詢支付記錄,都沒有查詢到,然後都開始走支付的邏輯,最後發現同一個訂單支付了兩次,這就是併發導致的冪等問題。
併發解決的方案也有很多種,簡單點的直接用數據庫的唯一索引解決,稍微麻煩點的都會用分佈式鎖來對同一個資源進行加鎖。
比如我們對訂單 1001 進行加鎖,如果同時發起了兩次支付請求,那麼同一時間只能有一個請求可以獲取鎖,另一個請求獲取不到鎖可以直接失敗,也可以等待前面的請求執行完成。
如果等待前面的請求執行完成,接著往下處理,就能查到 1001 已經支付過了,直接返回支付成功了。
通用冪等實現
為了能夠讓大家更專注於業務功能的開發,簡單場景的冪等操作我認為可以進行統一封裝來處理,下面介紹一下通用冪等的實現。
SJwxerVeyL7CyOr0.png
設計方案
通用存儲
一般我們在程序內部做冪等的話都是先查詢,然後根據查詢的結果做對應的操作。同時會對相同的資源進行加鎖來避免併發問題。
加鎖是通用的,不通用的部分就是判斷這個操作之前有沒有操作過,所以我們需要有一個通用的存儲來記錄所有的操作。
使用簡單
提供通用的冪等組件,注入對應的類即可實現冪等,屏蔽加鎖,記錄判斷等邏輯。
支持註解
除了通過代碼的方式來進行冪等的控制,同時為了讓使用更加簡單,還需要提供註解的方式來支持冪等,使用者只需要在對應的業務方法上增加對應的註解,即可實現冪等。
多級存儲
需要支持多級存儲,比如一級存儲可以用 Redis 來實現,優點是性能高,適用於 90%的場景。因為很多場景都是為了防止短時間內請求重複導致的問題,通過設置一定的失效時間,讓 Key 自動失效。
二級存儲可以支持 Mysql, Mongo 等數據庫,適用於時間長或者永久存儲的場景。
可以通過配置指定一級存儲用什麼,二級存儲用什麼。這個場景非常適合用策略模式來實現。
併發讀寫
引入多級存儲勢必會涉及到併發讀寫的場景,可以支持兩種方式,順序和併發。
順序就是先寫一級存儲,再寫二級存儲,讀也是一樣。這樣的問題在於性能會有點損耗。
併發就是多線程同時寫入,同時讀取,提高性能。
冪等執行流程
冪等接口
冪等接口定義
<code>public
interface
DistributedIdempotent
{T
execute
(String key,
int
lockExpireTime,int
firstLevelExpireTime,int
secondLevelExpireTime, TimeUnit timeUnit, ReadWriteTypeEnum readWriteType, Supplier execute, Supplier fail); } /<code>
使用方式
<code>public
String idempotentCode(String key) {return
distributedIdempotent.execute(key,10
,10
,50
, TimeUnit.SECONDS, ReadWriteTypeEnum.ORDER, () -> { System.out
.println("進來了。。。。"
);return
"success"
; }, () -> { System.out
.println("重複了。。。。"
);return
"fail"
; }); } /<code>
冪等註解
使用註解,能夠讓使用更加簡單,比如我們的事務處理,緩存等都使用了註解來簡化邏輯。
冪等的場景也可以定義通用的註解來簡化使用難度,在需要支持冪等的業務方法上增加註解,配置基本信息。
idempotentHandler 是觸發冪等規則後執行的方法,也就是我們用代碼實現冪等時候的 Supplier fail 參數。實現是用的阿里 Sentinel 限流,熔斷後的處理那套邏輯。
在冪等的場景下,如果是重複執行,通常返回跟正常執行一樣的結果即可。
<code> @Idempotent(spelKey ="#key"
, idempotentHandler ="idempotentHandler"
, readWriteType = ReadWriteTypeEnum.PARALLEL, secondLevelExpireTime =60
)
public
void
idempotent
(String key
) { System.out
.println("進來了。。。。"
); }public
void
idempotentHandler
(String key, IdempotentException e
) { System.out
.println(key +":idempotentHandler已經執行過了。。。。"
); } /<code>
自動區分重複請求
代碼方式處理冪等,需要傳入冪等的 Key,註解方式處理冪等,支持配置 Key,支持 SPEL 表達式。這兩種都是需要在使用的時候就確定好根據什麼來作為冪等的唯一性判斷。
還有一種冪等的場景是比較常見的,就是防止重複提交或者網絡問題超時重試。同樣的操作會請求多次,這種場景下可以在操作之前先申請一個唯一的 ID,每次請求的時候帶給後端,這樣就能標識整個請求的唯一性。
我目前做了一個自動生成唯一標識的功能,簡單來說就是根據請求的信息進行 MD5,如果 MD5 值沒有變化就認為是同一次請求。
需要進行 MD5 的內容有請求 URL 參數,請求體,請求頭信息。請求頭的信息在沒有指定用戶相關 Key 的場景下會進行全部拼接,如果配置了請求頭 userId 為用戶的標識,那麼只會用 userId。
會在請求的入口處進行冪等 Key 的自動生成,如果在使用冪等註解的時候沒有指定 spelKey, 就會使用自動生成的 Key。
存儲結構
Redis: 使用 String 類型存儲,Key 是冪等 Key, Value 默認為 1。
Mysql: 需要創建一張記錄表。(過期的數據需要定時清理,也可以永久存儲)
<code>CREATE
TABLE
`idempotent_record`
(`id`
int
(11
)NOT
NULL
AUTO_INCREMENTCOMMENT
'主鍵'
,`key`
varchar
(50
)NULL
DEFAULT
''
,`value`
varchar
(50
)NOT
NULL
DEFAULT
''
,`expireTime`
timestamp
NOT
NULL
COMMENT
'過期時間'
,`addTime`
timestamp
NOT
NULL
DEFAULT
CURRENT_TIMESTAMP
COMMENT
'創建時間'
, PRIMARYKEY
(`id`
) )ENGINE
=InnoDB
DEFAULT
CHARSET
=utf8COMMENT
='冪等記錄'
; /<code>
Mongo: 字段跟 Mysql 一樣,轉換成 Json 格式即可。Mongo 會自動創建集合。
碼字不易,可以的話來個三連擊,感謝!
關於作者:尹吉歡,簡單的技術愛好者,《Spring Cloud 微服務-全棧技術與案例解析》, 《Spring Cloud 微服務 入門 實戰與進階》作者, 公眾號猿天地發起人。