一、背景
? ? ? ?在實際系統(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è)務情況而定的。