1.冪等性
在編程中一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。通俗的講就一個數據,或者一個請求,給你重復來多次,你得確保對應的數據是不會改變的,不能出錯;類似於數據庫中的樂觀鎖機制,如果更新數據庫中的一條SQL;在並發場景,為了性能和數據可靠性,會在更新時加上查詢時的版本,並且更新這個版本信息。
我們可以借鑒數據庫的樂觀鎖機制來舉個例子
- 首先為表添加一個版本字段version
- 在執行更新操作前呢,會先去數據庫查詢這個version
- 然后執行更新語句,以version作為條件,例如:
UPDATE T_REPS SET COUNT = COUNT -1,VERSION = VERSION + 1 WHERE VERSION = 1 - 如果執行更新時有其他人先更新了這張表的數據,那么這個條件就不生效了,也就不會執行操作了,通過這種樂觀鎖的機制來保障冪等性。
2.消費端的冪等性保障
1,發送端MQ-client將消息發給服務端MQ-server
2,服務端MQ-server將消息落地
3,服務端MQ-server回ACK給發送端MQ-client
如果3丟失,發送端MQ-client超時后會重發消息,可能導致服務端MQ-server收到重復消息。
此時重發是MQ-client發起的,消息的處理是MQ-server,為了避免步驟2落地重復的消息,對每條消息,MQ系統內部必須生成一個inner-msg-id,作為去重和冪等的依據,這個內部消息ID的特性是:
(1)全局唯一
(2)MQ生成,具備業務無關性,對消息發送方和消息接收方屏蔽
有了這個inner-msg-id,就能保證上半場重發,也只有1條消息落到MQ-server的DB中,實現上半場冪等
在海量訂單生成的業務高峰期,生產端有可能就會重復發生了消息,這時候消費端就要實現冪等性,這就意味着我們的消息永遠不會被消費多次,即使我們收到了一樣的消息。
業界主流的冪等性有兩種操作:
1.唯一 ID + 指紋碼 機制,利用數據庫主鍵去重
為了應對用戶在一瞬間的頻繁操作,這個指紋碼可能是我們的一些規則或者時間戳加別的服務給到的唯一信息碼,它並不一定是我們系統生成的,基本都是由我們的業務規則拼接而來,但是一定要保證唯一性,然后就利用查詢語句進行判斷這個id是否存在數據庫中。
好處就是實現簡單,就一個拼接,然后查詢判斷是否重復。
壞處就是在高並發時,如果是單個數據庫就會有寫入性能瓶頸
解決方案 :根據 ID 進行分庫分表,對 id 進行算法路由,落到一個具體的數據庫,然后當這個 id 第二次來又會落到這個數據庫,這時候就像我單庫時的查重一樣了。利用算法路由把單庫的冪等變成多庫的冪等,分攤數據流量壓力,提高性能。
2.利用redis的原子性去實現
使用 redis 的原子性去實現需要考慮兩個點
一是 是否 要進行數據落庫,如果落庫的話,關鍵解決的問題是數據庫和緩存如何做到原子性? 數據庫與緩存進行同步肯定要進行寫操作,到底先寫 redis 還是先寫數據庫,這是個問題,涉及到緩存更新與淘汰的問題
二是如果不落庫,那么都存儲到緩存中,如何設置定時同步的策略? 不入庫的話,可以使用雙重緩存等策略,保障一個消息副本,具體同步可以使用類似 databus 這種同步工具。
1)比如你拿個數據要寫庫,你先根據主鍵查一下,如果這數據都有了,你就別插入了,update一下好吧
(2)比如你是寫redis,那沒問題了,反正每次都是set,天然冪等性
(3)比如你不是上面兩個場景,那做的稍微復雜一點,你需要讓生產者發送每條數據的時候,里面加一個全局唯一的id,類似訂單id之類的東西,然后你這里消費到了之后,先根據這個id去比如redis里查一下,之前消費過嗎?如果沒有消費過,你就處理,然后這個id寫redis。如果消費過了,那你就別處理了,保證別重復處理相同的消息即可。
還有比如基於數據庫的唯一鍵來保證重復數據不會重復插入多條,我們之前線上系統就有這個問題,就是拿到數據的時候,每次重啟可能會有重復,因為kafka消費者還沒來得及提交offset,重復數據拿到了以后我們插入的時候,因為有唯一鍵約束了,所以重復數據只會插入報錯,不會導致數據庫中出現臟數據