寫一個(gè)通用的冪等組件司恳,我覺(jué)得很有必要

本文目錄

  1. 背景

  2. 簡(jiǎn)單冪等實(shí)現(xiàn)

2.1 數(shù)據(jù)庫(kù)記錄判斷

2.2 并發(fā)問(wèn)題解決

  1. 通用冪等實(shí)現(xiàn)

3.1 設(shè)計(jì)方案

3.1.1 通用存儲(chǔ)

3.1.2 使用簡(jiǎn)單

3.1.3 支持注解

3.1.4 多級(jí)存儲(chǔ)

3.1.5 并發(fā)讀寫

3.1.6 執(zhí)行流程

3.2 冪等接口

3.3 冪等注解

3.4 自動(dòng)區(qū)分重復(fù)請(qǐng)求

3.5 存儲(chǔ)結(jié)構(gòu)

3.6 源碼地址

背景

回答群友的問(wèn)題:冪等有沒(méi)有什么通用的方案和實(shí)踐途乃?

關(guān)于什么是冪等,本文就不再闡述了扔傅。相信大家都知道耍共,并且也都遇到過(guò)類似的問(wèn)題以及有自己的一套解決方案烫饼。

基本上所有業(yè)務(wù)系統(tǒng)中的冪等都是各自進(jìn)行處理,也不是說(shuō)不能統(tǒng)一處理试读,統(tǒng)一處理的話需要考慮的內(nèi)容會(huì)比較多杠纵。

我個(gè)人認(rèn)為核心的業(yè)務(wù)還是適合業(yè)務(wù)方自己去處理,比如訂單支付钩骇,會(huì)有個(gè)支付記錄表淡诗,一個(gè)訂單只能被支付一次,通過(guò)支付記錄表就可以達(dá)到冪等的效果伊履。

還有一些不是核心的業(yè)務(wù),但是也有冪等的需求款违。比如網(wǎng)絡(luò)問(wèn)題唐瀑,多次重試。用戶點(diǎn)擊多次等場(chǎng)景插爹。這種場(chǎng)景下還是需要一個(gè)通用的冪等框架來(lái)處理哄辣,會(huì)讓業(yè)務(wù)開(kāi)發(fā)更加簡(jiǎn)單。

簡(jiǎn)單冪等實(shí)現(xiàn)

冪等的實(shí)現(xiàn)其實(shí)并不復(fù)雜赠尾,方案也有很多種力穗,首先介紹下基于數(shù)據(jù)庫(kù)記錄的方案來(lái)實(shí)現(xiàn),后面再介紹通用方案气嫁。

數(shù)據(jù)庫(kù)記錄判斷

以文章開(kāi)頭講的支付場(chǎng)景來(lái)舉例当窗。業(yè)務(wù)場(chǎng)景是一個(gè)訂單只能支付一次,所以我們?cè)谥Ц吨皶?huì)判斷這個(gè)訂單有沒(méi)有支付過(guò)寸宵,如果沒(méi)有支付過(guò)則進(jìn)行支付崖面,如果支付過(guò)了,就反正支付成功梯影,冪等巫员。

這種方式需要有一個(gè)額外的表來(lái)存儲(chǔ)做過(guò)的動(dòng)作桐腌,才能判斷之前有沒(méi)有做過(guò)這件事情闹获。

就好比你年齡大了任斋,然后還是單身的技術(shù)宅葡秒。這個(gè)時(shí)候你家里著急了呀博助,你老媽天天給你介紹小姐姐玉吁。你每個(gè)周末都要打扮的非常帥氣锉桑,去見(jiàn)你老媽給你介紹的小姐姐癌蚁。

去之前你得記錄下吧唱遭,8 月第一周我見(jiàn)的 XXX, 第二周我見(jiàn)的 YYY, 如果第三周又讓你去見(jiàn) XXX, 如果這個(gè)時(shí)候你不喜歡 XXX, 你會(huì)翻出你的小本本看下戳寸,這個(gè)之前見(jiàn)過(guò)了,沒(méi)必要再見(jiàn)了拷泽,不然見(jiàn)了多尷尬啊疫鹊。

并發(fā)問(wèn)題解決

通過(guò)查詢支付記錄袖瞻,判斷能否進(jìn)行支付在業(yè)務(wù)邏輯上沒(méi)一點(diǎn)問(wèn)題。但是在并發(fā)場(chǎng)景就會(huì)有問(wèn)題拆吆。

1001 的訂單發(fā)起了兩次支付請(qǐng)求聋迎,當(dāng)前兩個(gè)請(qǐng)求同時(shí)查詢支付記錄,都沒(méi)有查詢到枣耀,然后都開(kāi)始走支付的邏輯霉晕,最后發(fā)現(xiàn)同一個(gè)訂單支付了兩次,這就是并發(fā)導(dǎo)致的冪等問(wèn)題捞奕。

并發(fā)解決的方案也有很多種牺堰,簡(jiǎn)單點(diǎn)的直接用數(shù)據(jù)庫(kù)的唯一索引解決,稍微麻煩點(diǎn)的都會(huì)用分布式鎖來(lái)對(duì)同一個(gè)資源進(jìn)行加鎖颅围。

比如我們對(duì)訂單 1001 進(jìn)行加鎖伟葫,如果同時(shí)發(fā)起了兩次支付請(qǐng)求,那么同一時(shí)間只能有一個(gè)請(qǐng)求可以獲取鎖院促,另一個(gè)請(qǐng)求獲取不到鎖可以直接失敗筏养,也可以等待前面的請(qǐng)求執(zhí)行完成。

如果等待前面的請(qǐng)求執(zhí)行完成常拓,接著往下處理渐溶,就能查到 1001 已經(jīng)支付過(guò)了,直接返回支付成功了弄抬。

通用冪等實(shí)現(xiàn)

為了能夠讓大家更專注于業(yè)務(wù)功能的開(kāi)發(fā)茎辐,簡(jiǎn)單場(chǎng)景的冪等操作我認(rèn)為可以進(jìn)行統(tǒng)一封裝來(lái)處理,下面介紹一下通用冪等的實(shí)現(xiàn)眉睹。

SJwxerVeyL7CyOr0.png

設(shè)計(jì)方案

通用存儲(chǔ)

一般我們?cè)诔绦騼?nèi)部做冪等的話都是先查詢荔茬,然后根據(jù)查詢的結(jié)果做對(duì)應(yīng)的操作。同時(shí)會(huì)對(duì)相同的資源進(jìn)行加鎖來(lái)避免并發(fā)問(wèn)題竹海。

加鎖是通用的慕蔚,不通用的部分就是判斷這個(gè)操作之前有沒(méi)有操作過(guò),所以我們需要有一個(gè)通用的存儲(chǔ)來(lái)記錄所有的操作斋配。

使用簡(jiǎn)單

提供通用的冪等組件孔飒,注入對(duì)應(yīng)的類即可實(shí)現(xiàn)冪等,屏蔽加鎖艰争,記錄判斷等邏輯坏瞄。

支持注解

除了通過(guò)代碼的方式來(lái)進(jìn)行冪等的控制,同時(shí)為了讓使用更加簡(jiǎn)單甩卓,還需要提供注解的方式來(lái)支持冪等鸠匀,使用者只需要在對(duì)應(yīng)的業(yè)務(wù)方法上增加對(duì)應(yīng)的注解,即可實(shí)現(xiàn)冪等逾柿。

多級(jí)存儲(chǔ)

需要支持多級(jí)存儲(chǔ)缀棍,比如一級(jí)存儲(chǔ)可以用 Redis 來(lái)實(shí)現(xiàn)宅此,優(yōu)點(diǎn)是性能高,適用于 90%的場(chǎng)景爬范。因?yàn)楹芏鄨?chǎng)景都是為了防止短時(shí)間內(nèi)請(qǐng)求重復(fù)導(dǎo)致的問(wèn)題父腕,通過(guò)設(shè)置一定的失效時(shí)間,讓 Key 自動(dòng)失效青瀑。

二級(jí)存儲(chǔ)可以支持 Mysql, Mongo 等數(shù)據(jù)庫(kù)璧亮,適用于時(shí)間長(zhǎng)或者永久存儲(chǔ)的場(chǎng)景。

可以通過(guò)配置指定一級(jí)存儲(chǔ)用什么斥难,二級(jí)存儲(chǔ)用什么枝嘶。這個(gè)場(chǎng)景非常適合用策略模式來(lái)實(shí)現(xiàn)。

并發(fā)讀寫

引入多級(jí)存儲(chǔ)勢(shì)必會(huì)涉及到并發(fā)讀寫的場(chǎng)景哑诊,可以支持兩種方式躬络,順序和并發(fā)。

順序就是先寫一級(jí)存儲(chǔ)搭儒,再寫二級(jí)存儲(chǔ),讀也是一樣提茁。這樣的問(wèn)題在于性能會(huì)有點(diǎn)損耗淹禾。

并發(fā)就是多線程同時(shí)寫入,同時(shí)讀取茴扁,提高性能铃岔。

冪等執(zhí)行流程

冪等接口

冪等接口定義

public interface DistributedIdempotent {
    /**
     * 冪等執(zhí)行
     * @param key 冪等Key
     * @param lockExpireTime 鎖的過(guò)期時(shí)間
     * @param firstLevelExpireTime 一級(jí)存儲(chǔ)過(guò)期時(shí)間
     * @param secondLevelExpireTime 二級(jí)存儲(chǔ)過(guò)期時(shí)間
     * @param timeUnit 存儲(chǔ)時(shí)間單位
     * @param readWriteType 讀寫類型
     * @param execute 要執(zhí)行的邏輯
     * @param fail Key已經(jīng)存在,冪等攔截后的執(zhí)行邏輯
     * @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("進(jìn)來(lái)了峭火。毁习。。卖丸。");
        return "success";
    }, () -> {
        System.out.println("重復(fù)了纺且。。稍浆。载碌。");
        return "fail";
    });
}

冪等注解

使用注解,能夠讓使用更加簡(jiǎn)單衅枫,比如我們的事務(wù)處理嫁艇,緩存等都使用了注解來(lái)簡(jiǎn)化邏輯。

冪等的場(chǎng)景也可以定義通用的注解來(lái)簡(jiǎn)化使用難度弦撩,在需要支持冪等的業(yè)務(wù)方法上增加注解步咪,配置基本信息。

idempotentHandler 是觸發(fā)冪等規(guī)則后執(zhí)行的方法益楼,也就是我們用代碼實(shí)現(xiàn)冪等時(shí)候的 Supplier<T> fail 參數(shù)猾漫。實(shí)現(xiàn)是用的阿里 Sentinel 限流点晴,熔斷后的處理那套邏輯。

在冪等的場(chǎng)景下静袖,如果是重復(fù)執(zhí)行觉鼻,通常返回跟正常執(zhí)行一樣的結(jié)果即可。

/**
 * 注解方式冪等-指定冪等規(guī)則觸發(fā)后執(zhí)行的方法
 * @param key
 */
@Idempotent(spelKey = "#key", idempotentHandler = "idempotentHandler", readWriteType = ReadWriteTypeEnum.PARALLEL, secondLevelExpireTime = 60)
public void idempotent(String key) {
    System.out.println("進(jìn)來(lái)了队橙。坠陈。。捐康。");
}
public void idempotentHandler(String key, IdempotentException e) {
    System.out.println(key + ":idempotentHandler已經(jīng)執(zhí)行過(guò)了仇矾。。解总。贮匕。");
}

自動(dòng)區(qū)分重復(fù)請(qǐng)求

代碼方式處理冪等,需要傳入冪等的 Key花枫,注解方式處理冪等刻盐,支持配置 Key,支持 SPEL 表達(dá)式劳翰。這兩種都是需要在使用的時(shí)候就確定好根據(jù)什么來(lái)作為冪等的唯一性判斷敦锌。

還有一種冪等的場(chǎng)景是比較常見(jiàn)的,就是防止重復(fù)提交或者網(wǎng)絡(luò)問(wèn)題超時(shí)重試佳簸。同樣的操作會(huì)請(qǐng)求多次乙墙,這種場(chǎng)景下可以在操作之前先申請(qǐng)一個(gè)唯一的 ID,每次請(qǐng)求的時(shí)候帶給后端生均,這樣就能標(biāo)識(shí)整個(gè)請(qǐng)求的唯一性听想。

我目前做了一個(gè)自動(dòng)生成唯一標(biāo)識(shí)的功能,簡(jiǎn)單來(lái)說(shuō)就是根據(jù)請(qǐng)求的信息進(jìn)行 MD5马胧,如果 MD5 值沒(méi)有變化就認(rèn)為是同一次請(qǐng)求汉买。

需要進(jìn)行 MD5 的內(nèi)容有請(qǐng)求 URL 參數(shù),請(qǐng)求體佩脊,請(qǐng)求頭信息录别。請(qǐng)求頭的信息在沒(méi)有指定用戶相關(guān) Key 的場(chǎng)景下會(huì)進(jìn)行全部拼接,如果配置了請(qǐng)求頭 userId 為用戶的標(biāo)識(shí)邻吞,那么只會(huì)用 userId组题。

會(huì)在請(qǐng)求的入口處進(jìn)行冪等 Key 的自動(dòng)生成,如果在使用冪等注解的時(shí)候沒(méi)有指定 spelKey, 就會(huì)使用自動(dòng)生成的 Key抱冷。

存儲(chǔ)結(jié)構(gòu)

Redis: 使用 String 類型存儲(chǔ)崔列,Key 是冪等 Key, Value 默認(rèn)為 1。

Mysql: 需要?jiǎng)?chuàng)建一張記錄表。(過(guò)期的數(shù)據(jù)需要定時(shí)清理赵讯,也可以永久存儲(chǔ))

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 '過(guò)期時(shí)間',
  `addTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='冪等記錄';

Mongo: 字段跟 Mysql 一樣盈咳,轉(zhuǎn)換成 Json 格式即可。Mongo 會(huì)自動(dòng)創(chuàng)建集合边翼。

碼字不易鱼响,可以的話來(lái)個(gè)三連擊,感謝组底!

關(guān)于作者:尹吉?dú)g丈积,簡(jiǎn)單的技術(shù)愛(ài)好者,《Spring Cloud 微服務(wù)-全棧技術(shù)與案例解析》, 《Spring Cloud 微服務(wù) 入門 實(shí)戰(zhàn)與進(jìn)階》作者, 公眾號(hào)猿天地發(fā)起人债鸡。

微信搜索 猿天地 回復(fù) kitty 獲取源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末江滨,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子厌均,更是在濱河造成了極大的恐慌唬滑,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棺弊,死亡現(xiàn)場(chǎng)離奇詭異晶密,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)模她,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門惹挟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人缝驳,你說(shuō)我怎么就攤上這事」椴裕” “怎么了用狱?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拼弃。 經(jīng)常有香客問(wèn)我夏伊,道長(zhǎng),這世上最難降的妖魔是什么吻氧? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任溺忧,我火速辦了婚禮,結(jié)果婚禮上盯孙,老公的妹妹穿的比我還像新娘鲁森。我一直安慰自己,他們只是感情好振惰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布歌溉。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪痛垛。 梳的紋絲不亂的頭發(fā)上草慧,一...
    開(kāi)封第一講書(shū)人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音匙头,去河邊找鬼漫谷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蹂析,可吹牛的內(nèi)容都是我干的舔示。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼识窿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼斩郎!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起喻频,我...
    開(kāi)封第一講書(shū)人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤缩宜,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后甥温,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體锻煌,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年姻蚓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宋梧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡狰挡,死狀恐怖捂龄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情加叁,我是刑警寧澤倦沧,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站它匕,受9級(jí)特大地震影響展融,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜豫柬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一告希、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧烧给,春花似錦燕偶、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春涧尿,著一層夾襖步出監(jiān)牢的瞬間系奉,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工姑廉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缺亮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓桥言,卻偏偏與公主長(zhǎng)得像萌踱,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子号阿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355