本文目錄
-
背景
-
簡單冪等實現
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 已經支付過了,直接返回支付成功了。
通用冪等實現
為了能夠讓大家更專注於業務功能的開發,簡單場景的冪等操作我認為可以進行統一封裝來處理,下面介紹一下通用冪等的實現。
設計方案
通用存儲
一般我們在程序內部做冪等的話都是先查詢,然后根據查詢的結果做對應的操作。同時會對相同的資源進行加鎖來避免並發問題。
加鎖是通用的,不通用的部分就是判斷這個操作之前有沒有操作過,所以我們需要有一個通用的存儲來記錄所有的操作。
使用簡單
提供通用的冪等組件,注入對應的類即可實現冪等,屏蔽加鎖,記錄判斷等邏輯。
支持注解
除了通過代碼的方式來進行冪等的控制,同時為了讓使用更加簡單,還需要提供注解的方式來支持冪等,使用者只需要在對應的業務方法上增加對應的注解,即可實現冪等。
多級存儲
需要支持多級存儲,比如一級存儲可以用 Redis 來實現,優點是性能高,適用於 90%的場景。因為很多場景都是為了防止短時間內請求重復導致的問題,通過設置一定的失效時間,讓 Key 自動失效。
二級存儲可以支持 Mysql, Mongo 等數據庫,適用於時間長或者永久存儲的場景。
可以通過配置指定一級存儲用什么,二級存儲用什么。這個場景非常適合用策略模式來實現。
並發讀寫
引入多級存儲勢必會涉及到並發讀寫的場景,可以支持兩種方式,順序和並發。
順序就是先寫一級存儲,再寫二級存儲,讀也是一樣。這樣的問題在於性能會有點損耗。
並發就是多線程同時寫入,同時讀取,提高性能。
冪等執行流程
冪等接口
冪等接口定義
public interface DistributedIdempotent {
/**
* 冪等執行
* @param key 冪等Key
* @param lockExpireTime 鎖的過期時間
* @param firstLevelExpireTime 一級存儲過期時間
* @param secondLevelExpireTime 二級存儲過期時間
* @param timeUnit 存儲時間單位
* @param readWriteType 讀寫類型
* @param execute 要執行的邏輯
* @param fail Key已經存在,冪等攔截后的執行邏輯
* @return
*/
<T> T execute(String key, int lockExpireTime, int firstLevelExpireTime, int secondLevelExpireTime, TimeUnit timeUnit, ReadWriteTypeEnum readWriteType, Supplier<T> execute, Supplier<T> fail);
}
使用方式
/**
* 代碼方式冪等-有返回值
* @param key
* @return
*/
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";
});
}
冪等注解
使用注解,能夠讓使用更加簡單,比如我們的事務處理,緩存等都使用了注解來簡化邏輯。
冪等的場景也可以定義通用的注解來簡化使用難度,在需要支持冪等的業務方法上增加注解,配置基本信息。
idempotentHandler 是觸發冪等規則后執行的方法,也就是我們用代碼實現冪等時候的 Supplier
在冪等的場景下,如果是重復執行,通常返回跟正常執行一樣的結果即可。
/**
* 注解方式冪等-指定冪等規則觸發后執行的方法
* @param key
*/
@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已經執行過了。。。。");
}
自動區分重復請求
代碼方式處理冪等,需要傳入冪等的 Key,注解方式處理冪等,支持配置 Key,支持 SPEL 表達式。這兩種都是需要在使用的時候就確定好根據什么來作為冪等的唯一性判斷。
還有一種冪等的場景是比較常見的,就是防止重復提交或者網絡問題超時重試。同樣的操作會請求多次,這種場景下可以在操作之前先申請一個唯一的 ID,每次請求的時候帶給后端,這樣就能標識整個請求的唯一性。
我目前做了一個自動生成唯一標識的功能,簡單來說就是根據請求的信息進行 MD5,如果 MD5 值沒有變化就認為是同一次請求。
需要進行 MD5 的內容有請求 URL 參數,請求體,請求頭信息。請求頭的信息在沒有指定用戶相關 Key 的場景下會進行全部拼接,如果配置了請求頭 userId 為用戶的標識,那么只會用 userId。
會在請求的入口處進行冪等 Key 的自動生成,如果在使用冪等注解的時候沒有指定 spelKey, 就會使用自動生成的 Key。
存儲結構
Redis: 使用 String 類型存儲,Key 是冪等 Key, Value 默認為 1。
Mysql: 需要創建一張記錄表。(過期的數據需要定時清理,也可以永久存儲)
CREATE TABLE `idempotent_record` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`key` varchar(50) NULL DEFAULT '',
`value` varchar(50) NOT NULL DEFAULT '',
`expireTime` timestamp NOT NULL COMMENT '過期時間',
`addTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='冪等記錄';
Mongo: 字段跟 Mysql 一樣,轉換成 Json 格式即可。Mongo 會自動創建集合。
碼字不易,可以的話來個三連擊,感謝!
關於作者:尹吉歡,簡單的技術愛好者,《Spring Cloud 微服務-全棧技術與案例解析》, 《Spring Cloud 微服務 入門 實戰與進階》作者, 公眾號猿天地發起人。