冪等性

一、背景

? ? ? ?在實際系統(tǒng)操作中,因為操作人員的誤操作,或者網(wǎng)絡抖動等一系列原因?qū)е乱粋€接口請求多次,或者一個業(yè)務執(zhí)行多次情況。但是不管操作多少次,產(chǎn)生的操作結(jié)果應該都一致。

????例如1:用戶發(fā)起一筆支付,當遇到網(wǎng)絡重發(fā),系統(tǒng)bug重試,只允許扣除用戶一次錢。

????????????2 :創(chuàng)建業(yè)務數(shù)據(jù),一次請求只能創(chuàng)建一個,創(chuàng)建多個就會出大問題。

? ? ? ? ? ? 3:發(fā)送消息給用戶,不能因為業(yè)務操作問題發(fā)送重發(fā)信息給用戶,否則用戶會奔潰的。

二、冪等性概念

? ? ? ? ? ?冪等操作的特點是其任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。冪等函數(shù),或冪等方法,是指可以使用相同參數(shù)重復執(zhí)行,并能獲得相同結(jié)果的函數(shù)。這些函數(shù)不會影響系統(tǒng)狀態(tài),也不用擔心重復執(zhí)行會對系統(tǒng)造成改變。

我的理解:冪等就是一個操作,不論執(zhí)行多少次,產(chǎn)生的效果和返回的結(jié)果都是一樣的。

三、冪等常用思路

token機制

????????當客戶端請求頁面時,服務器會生成一個隨機數(shù)token,并且將token放置到session當中,然后將token發(fā)給客戶端(一般通過構(gòu)造hidden表單)。下次客戶端提交請求時,token會隨著表單一起提交到服務器端。服務器端第一次驗證相同過后,會將session中的token值更新下,若用戶重復提交,第二次的驗證判斷將失敗,因為用戶提交的表單中的token沒變,但服務器端session中token已經(jīng)改變了。

樂觀鎖(通過版本號實現(xiàn))

????????update table_xxx set name=#name#,version=version+1 where version=#version#;通過條件限制 update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0要求:quality-#subQuality# >= ,這個情景適合不用版本號,只更新是做數(shù)據(jù)安全校驗,適合庫存模型,扣份額和回滾份額,性能更高;

去重表

????????利用數(shù)據(jù)庫表單的特性來實現(xiàn)冪等,常用的一個思路是在表上構(gòu)建唯一性索引。需求是博客點贊問題,要想防止一個人重復點贊,可以設計一張表,將博客id與用戶id綁定建立唯一索引,每當用戶點贊時就往表中寫入一條數(shù)據(jù),這樣重復點贊的數(shù)據(jù)就無法寫入。

我們可以借鑒數(shù)據(jù)庫的樂觀鎖機制來舉個例子

????????1、首先為表添加一個版本字段version

????????2、在執(zhí)行更新操作前呢,會先去數(shù)據(jù)庫查詢這個version

????????3、然后執(zhí)行更新語句,以version作為條件,例如:

????????UPDATE T_REPS SET COUNT = COUNT -1,VERSION = VERSION + 1 WHERE VERSION = 1

????????4、如果執(zhí)行更新時有其他人先更新了這張表的數(shù)據(jù),那么這個條件就不生效了,也就不會執(zhí)行操作了,通過這種樂觀鎖的機制來保障冪等性。

消費端-冪等性保障

什么情況下會出現(xiàn)重復消費?

當消費者消費完消息時,在給生產(chǎn)端返回ack時由于網(wǎng)絡中斷,導致生產(chǎn)端未收到確認信息,該條消息會重新發(fā)送并被消費者消費,但實際上該消費者已成功消費了該條消息,這就是重復消費問題。

如何避免消息的重復消費問題?

消費端實現(xiàn)冪等性,就意味著,我們的消息永遠不會消費多次,即使我們收到了多條一樣的消息

業(yè)界主流的冪等性操作:

· 唯一ID + 指紋碼機制,利用數(shù)據(jù)庫主鍵去重

· 利用Redis的原子性去實現(xiàn)

唯一ID+指紋碼機制

· 唯一ID + 指紋碼機制,利用數(shù)據(jù)庫主鍵去重

· SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID +指紋碼

· 好處:實現(xiàn)簡單

· 壞處:高并發(fā)下有數(shù)據(jù)庫寫入的性能瓶頸

· 解決方案:跟進ID進行分庫分表進行算法路由

整個思路就是首先我們需要根據(jù)消息生成一個全局唯一的ID,然后還需要加上一個指紋碼。這個指紋碼它并不一定是系統(tǒng)去生成的,而是一些外部的規(guī)則或者內(nèi)部的業(yè)務規(guī)則去拼接,它的目的就是為了保障這次操作是絕對唯一的。

將ID + 指紋碼拼接好的值作為數(shù)據(jù)庫主鍵,就可以進行去重了。即在消費消息前呢,先去數(shù)據(jù)庫查詢這條消息的指紋碼標識是否存在,沒有就執(zhí)行insert操作,如果有就代表已經(jīng)被消費了,就不需要管了。

對于高并發(fā)下的數(shù)據(jù)庫性能瓶頸,可以跟進ID進行分庫分表策略,采用一些路由算法去進行分壓分流。應該保證ID通過這種算法,消息即使投遞多次都落到同一個數(shù)據(jù)庫分片上,這樣就由單臺數(shù)據(jù)庫冪等變成多庫的冪等。

利用Redis的原子性去實現(xiàn)

我們都知道redis是單線程的,并且性能也非常好,提供了很多原子性的命令。比如可以使用 setnx 命令。

在接收到消息后將消息ID作為key執(zhí)行 setnx 命令,如果執(zhí)行成功就表示沒有處理過這條消息,可以進行消費了,執(zhí)行失敗表示消息已經(jīng)被消費了。

使用 redis 的原子性去實現(xiàn)主要需要考慮兩個點

· 第一:我們是否要進行數(shù)據(jù)落庫,如果落庫的話,關(guān)鍵解決的問題是數(shù)據(jù)庫和緩存如何做到原子性?

· 第二:如果不進行落庫,那么都存儲到緩存中,如何設置定時同步的策略(同步到關(guān)系型數(shù)據(jù)庫)?緩存又如何做到數(shù)據(jù)可靠性保障呢

關(guān)于不落庫,定時同步的策略,目前主流方案有兩種,第一種為雙緩存模式,異步寫入到緩存中,也可以異步寫到數(shù)據(jù)庫,但是最終會有一個回調(diào)函數(shù)檢查,這樣能保障最終一致性,不能保證100%的實時性。第二種是定時同步,比如databus同步。

1.使用redis的setnx命令的情況下,如果消費者端setnx成功后,進行消息消費,但是此時突然宕機。那么對于接下來一段時間內(nèi)(指代鎖的有效時長),就無法保證消息的及時消費?

答:首先宕機問題要盡量避免,通過一些高可用的方案降低宕機的風險,如果確實宕機了,對于已發(fā)送但未被消費的消息,可以自己去做補償或者投遞到延遲隊列里處理,宕機會造成生產(chǎn)端消息堆積,如果對消息實時處理要求比較高,需要提前預備一些應急方案另起服務去處理這些消息。

2.redis的setnx怎么做冪等性的? 鎖的有效時長設為多少呢

redis實現(xiàn)冪等很簡單,我以redis實現(xiàn)接口的冪等性為例說明。你可以自定義一個冪等注解,然后配合AOP進行方法攔截,對攔截的請求信息(包括方法名+參數(shù)名+參數(shù)值)根據(jù)固定的規(guī)則去生成一個key,然后調(diào)用redis的setnx方法,如果返回ok,則正常調(diào)用方法,否則就是重復調(diào)用了。這樣可以保證重復請求接口在一定時間內(nèi)只會被成功處理一次。至于鎖的有效時長要根據(jù)業(yè)務情況而定的。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容