如何保證介面的冪等性?

来源:https://www.cnblogs.com/xiaoniuhululu/archive/2023/10/16/17767217.html
-Advertisement-
Play Games

作者:小牛呼嚕嚕 | https://xiaoniuhululu.com 電腦內功、源碼解析、科技故事、項目實戰、面試八股等更多硬核文章,首發於公眾號「小牛呼嚕嚕」 目錄什麼是冪等性?為什麼需要保證冪等性?介面冪等設計和防止重覆提交可以等同嗎?常用保證冪等性的措施先select再insert資料庫 ...


作者:小牛呼嚕嚕 | https://xiaoniuhululu.com

電腦內功、源碼解析、科技故事、項目實戰、面試八股等更多硬核文章,首發於公眾號「小牛呼嚕嚕

目錄

什麼是冪等性?

大家好,我是呼嚕嚕,所謂冪等性就是:任意次數請求 同一個資源,對資源的狀態產生的影響和執行一次請求是相同的
比如對於介面來說,無論調用多少次同一個介面,對資源的狀態都只產生一次影響

為什麼需要保證冪等性?

為什麼需要做介面的冪等性?如果不做會發生什麼事情?我們在實際企業開發過程中,如果僅是對資料庫進行查詢、刪除指定記錄操作,重覆提交是沒啥問題的。但是如果是新增或者修改操作,就需要考慮重覆提交的問題。
比如,如果一個訂單支付的時候,因各種原因重覆提交多次,那如果沒有冪等性處理的話,這個訂單將會被支付多次的錢,這種和錢有關的錯誤是絕對不能容忍的。

經常發生重覆提交的場景:

  1. 當我們在公司的系統裡面,提交表格,前端沒有對保存按鈕的做控制,可以多次點擊,然後我們又不小心快速點了多次,或者是網路卡頓, 還是其他原因,以為沒有成功提交,就一直點擊保存按鈕,這樣都會產生重覆提交表單請求。
  2. 在實際開發過程中,網路波動是常有的事,所以很多時候 HTTP 客戶端工具都預設開啟超時重試的機制,這樣就無法避免產生重覆的請求。
  3. 還有就是項目可能使用一些中間件,比如kafka消費生產者產生的消息時,可能讀到重覆的消息,這樣也會產生重覆的請求。
  4. ......

介面冪等設計和防止重覆提交可以等同嗎?

介面冪等和防止重覆提交有交集,但是嚴格來說並不完全等同

  1. 防重設計,主要從客戶端/前端的角度來解決,主要為了避免重覆提交,對每次請求的返回結果無限制,前端常見的手段:點擊提交按鈕變灰、點擊後跳轉結果頁、每次頁面初始化生成隨機碼,提交時隨機碼緩存,後續重覆的隨機碼請求直接不提交
  2. 冪等設計,強調更多地是當重覆提交請求無法避免的時候,還能保證每次請求都返回一樣的結果。像我們上面對前端做的限制,是能繞過去的,抓包是能直接把介面給抓出來的,比如惡意批量調用介面,所以企業級系統,前後端都需要做限制,特別是涉及到錢的業務。絕不能偷懶,後面我們來詳細講講對介面冪等的限制。

常用保證冪等性的措施

先select再insert

新手小白,在往資料庫插入數據時,為了防止重覆插入,一般會在insert前,通過關鍵字去先select一下,如果查不到記錄就執行insert操作,否則就不插入


但如果併發場景下,這個就不行了。比如線程2,線上程1插入數據前,執行select,最終它也會去執行插入操作,這樣就會產生2條記錄。所以在實際開發過程中,是不建議如此操作的。

資料庫設置唯一索引或唯一組合索引

資料庫設置唯一索引是我們最常用的方式,一個非常簡單,並且有效的方案。當記錄多次插入資料庫,會由於Id或者關鍵欄位索引唯一的限制,導致後續記錄插入失敗

--創建唯一索引
alter table `order` add UNIQUE KEY `索引名` (`欄位`);

第一條記錄插入到資料庫中,當後面其他相同的請求,再插入時,資料庫會報異常Duplicate entry 'xx' for key 'xx_name',這個異常不會對資料庫中既有的數據有影響,我們只需對異常進行捕獲就行,直接返回,代表已經執行過當前請求。

筆者這裡介紹一個騷操作:INSERT IGNORE

insert ignore INTO tableName VALUES ("id","xxx")

咦,會有讀者覺得,這樣哪怕索引衝突了,資料庫會忽略錯誤返回影響行數0,這樣就不用再在代碼中,手動捕捉異常了,又方便又省事!

但事實真這樣嗎???

如果希望在每次插入新記錄時,自動地創建主鍵欄位的值。一般會將主鍵id的屬性設為AUTO_INCREMENT
如果我們使用INSERT IGNORE時,沒有成功新增記錄,但是AUTO_INCREMENT會自動+1,binlog中也沒有 INSERT IGNORE 語句日誌。這個會導致主從數據一致性問題,如果線上環境資料庫是主從架構,從庫該欄位的AUTO_INCREMENT值會和主庫不一致,切庫(從庫變成總庫)的時候會衝突。

當然,查詢Mysql官方手冊,發現innodb_autoinc_lock_mode用於平衡性能與主從數據一致性innodb_autoinc_lock_mode=0可以解決這個問題,將其設為0後, 所有的insert語句都要在語句開始的時候得到一個表級的auto_inc鎖,在語句結束的時候才釋放這把鎖。也就是說在INSERT未成功執行時AUTO_INCREMENT不會自增,但是其也有缺點,會影響到資料庫的併發插入性能。

Mysql官方手冊明確指出,The setting innodb_autoinc_lock_mode=0 should not be used except for compatibility purposes.除非出於相容性目的,否則不應設置innodb_autoinc_lock_mode=0。所以我們還是老老實實手動捕捉異常,慎用insert ignore

**innodb_autoinc_lock_mode: **在MySQL8中, 預設值為 2 (輕量級鎖) , 在MySQL8之前, 5.1之後, 預設值為 1(混合使用這2種鎖), 在更早的版本是 0(auto_inc鎖)

去重表

去重表,其實也是唯一索引方案的一個變種,原表不太適合再新建唯一索引了,且數據量不大的話。我們可以再新建一張去重表,把唯一標識作為唯一索引,然後把對原表的操作和同時新增去重表 ,放在一個事務中,如果重覆創建,去重表會拋出唯一約束異常,事務里所有的操作就會回滾。

insert中加入exist條件判斷

有時候我們會遇到非常複雜的表,表結構確定了,比如已經有了許多索引欄位,不太適合再新建索引的時候,呼嚕嚕 在這裡再提供一個"騷操作":可以通過insert中加入exist來解決重覆插入的問題。
比如:

insert into order(id,code,password)
select ${id},${code},${password}
from order
where not exists(select 1 from order where code = ${code}) limit 0,1;

上面的sql註意思路就是將查詢和插入寫在同一個sql中,需要註意的是limit 0,1最後一定要加上,不然可能會出現重覆插入的情況

悲觀鎖

悲觀鎖,顧名思義就是,對數據被外界或者內部修改處理時,持"悲觀"態度,總認為會發生併發衝突,所以會在整個數據處理過程中,將數據鎖定。
悲觀鎖的實現,通常依靠資料庫提供的鎖機制實現,在這裡以mysql為例,最典型的就是"for update"。

select * from order where id = "xxxx" for update; 

需要註意的是:使用悲觀鎖,需要先關閉mysql的自動提交功能,將 set autocommit = 0;

for update僅適用於Mysql中lnnoDB引擎,預設是行級鎖,如果sql中有明確指定的主鍵時候,是行級鎖,如果沒有,會鎖表(非常危險的操作)。for update一般和事務配合使用,一旦用戶對某個行施加了行級加鎖,則該用戶可以查詢也可以更新被加鎖的數據行,其它用戶只能查詢但不能更新被加鎖的數據行。直到顯示提交事務(由於關閉了mysql的自動提交)時,for update獲取的鎖會自動釋放。

悲觀鎖雖然保證了數據處理的安全性,但會嚴重影響併發效率,降低系統吞吐量。適用於併發量不大、又對數據一致性比較高的場景。

樂觀鎖

樂觀鎖,和悲觀鎖相反,對數據被外界或者內部修改處理時,持"樂觀"態度,總認為不會發生併發衝突,所以不會上鎖,只需在更新的時候會去判斷一下在此期間有沒有去更新這個數據。

一般是使用版本號或者時間戳,比如

  1. 我們在資料庫中,給訂單表增加一個version 欄位
  2. select數據時,將version一起讀出,當提交數據更新時,判斷版本號是否和取出來的是否一致。如果不一致就代表,已更新,那就不更新。如果一致就繼續執行更新操作。
  3. 每次更新時,除了更新指定的欄位,也要將version進行+1操作
update order set name=#{xxx},version=#{version} where id=#{id} and version < ${version}

不加鎖就能保證冪等性,又增加了系統吞吐量,如果頻繁觸發版本號不一致的情況,反而降低了性能。

狀態機

狀態機也是樂觀鎖的一種,比如企業級貨品管理系統中,訂單的轉單流程,將訂單的狀態,設置為有限的幾個(1-下單、2-已支付、3-完成、4-發貨、5-退貨),通過各個狀態依次執行轉換,來控制訂單轉單的流程,是非常好的選擇。

分散式鎖

上面介紹了許多方案,在單體應用中是沒啥問題的,但是隨著時代的發展,現在微服務大行其道,以上方法就不太適應了。

在分散式系統中,上面唯一索引對於全局來說,是無法確定的,我們可以引入第三方分散式鎖來保證冪等性設計。分散式鎖,主要是用來 當多個進程不在同一個系統中,用分散式鎖控制多個進程對資源的訪問

實現分散式鎖常見的方法有:基於redis實現分散式鎖,基於 Consul 實現分散式鎖,基於 zookeeper實現分散式鎖等等,本文重點介紹最常見的基於redis實現分散式鎖,set NX PX + Lua

  1. 在分散式系統中,插入或者更新的請求,業務邏輯中先獲取唯一業務欄位,比如訂單id之類的,接著需要獲取分散式鎖,對redis執行下述命令
SET key value NX PX 30000

各參數的含義:

  • SET: 在Redis 2.6.12之後,set命令整合了setex命令 的功能,支持了原子命令加鎖和設置過期時間的功能
  • key:業務邏輯中先獲取唯一業務欄位,比如訂單id,code之類,也可以在前面加一些系統參數當首碼,這個完全可以自定義
  • value: 填入是一串隨機值,必須保證全局唯一性(在釋放鎖時,我們需要對value進行驗證,防止誤釋放),一般用uuid來實現
  • NX: 表示key不存在時才設置,如果存在則返回 null。還有另一個參數XX,表示key存在時才設置,如果不存在則返回NULL
  • PX 30000: 表示過期時間30000毫秒,指到30秒後,key將被自動刪除。這個非常的重要,如果設置過短,無法有效的防止重覆請求,過長的話會浪費redis的空間
  1. 然後執行插入或者更新,或者其他相關業務邏輯,在釋放鎖之前,如果有其他中心的服務來請求,由於key是一樣的,無法獲取鎖,就代表這些是重覆請求,不操作,直接返回
  2. 執行完插入或者更新後,需要釋放鎖,一定要判斷釋放的鎖的value和與Redis記憶體儲的value是否一致,不然如果直接刪除的話,會把其他中心服務的鎖釋放調。

這種先查再刪的2步操作,我們可以使用lua腳本,把他們變成一個"原子操作"

Lua 是一種輕量小巧的腳本語言,Redis會將整個腳本作為一個整體執行,中間不會被其他命令打斷插入(l類似與事務),可以減少網路開銷,方便復用

以下是Lua腳本,通過 Redis 的 eval/evalsha 命令來運行:

if redis.call('get', KEYS[1]) == ARGV[1] //判斷value是否一致
    then
        return redis.call('del', KEYS[1])//刪除key
    else
        return 0
end

這樣依靠單體的redis實現的分散式鎖能夠很好的解決,微服務系統的冪等問題。但是有些公司的微服務更加龐大,redis也是集群的話,set NX PX + Lua就不夠看了,這裡介紹Redis作者推薦的方法-Redlock演算法,這裡就先不展開講了,不然文章篇幅過長。先挖個坑,後面有空填一下:)

token機制

最後再補充一個方案利用token機制,每次調用介面時,使用token來標識請求的唯一性。token也叫令牌,天然適合微服務。基於token+redis來設計冪等的思路還是比較簡單的,和分散式鎖類似:

  1. 客戶端發送請求,得去服務端獲取一個全局唯一的一串隨機字元串作為Token 令牌(每次請求獲取到的都是一個全新的令牌),把令牌保存到 redis 中,需要有過期時間,同時把這個 ID 返回給客戶端
  2. 客戶端第二次調用業務請求的時候必須攜帶這個 token,服務端會去校驗redis中是否有該token。如果存在,表示這是第一次請求,刪除緩存中的token(這邊還是建議用lua腳本,保證操作的原子性);如果緩存中不存在,表示重覆請求,直接返回。

尾語

冪等性是系統服務對外一種承諾,特別業務中涉及的錢的部分,一定要慎重再慎重。雖然前端做限制會更容易點,但前後端都需要做努力,除了本文介紹的常見的方案,大家也可以集思廣益,畢竟技術在發展,單體到集群分散式,還會繼續發展,還有有新的問題產生。
本文雖然通篇在將冪等的重要性和如何實現冪等,但不可否認,冪等肯定導致系統吞吐量、併發能力的下降,企業級應用還是得根據業務,權衡利弊,感謝大家的閱讀。

參考資料:
https://www.cnblogs.com/linjiqin/p/9678022.html


全文完,感謝您的閱讀,如果我的文章對你有所幫助的話,還請點個免費的,你的支持會激勵我輸出更高質量的文章,感謝!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 草船借箭 題目: 題目描述: 程式員小周同學這幾天在看《三國演義》。今天他看到了“草船借箭”這一回,在欽佩諸葛亮巧借東風向曹操“借"箭的同 時,小周想到這麼一個問題: 如果諸葛亮一共派出了N條放置草人的船來“借"箭。“悚慨”的曹操向第1條草船上射了A支 箭、第2條草船上射了B支箭,第3條草船上射的箭 ...
  • 寫這篇文章的主要原因是工作中需要寫一個用訓練好的模型批量生圖的腳本,開始是想用python直接載入模型,但後來發現webui的界面中有不少好用的插件和參數,所以最終改成調用WebUI介面的方式來批量生圖。 Stable-diffusion的webui界面使用比較方便,但是它的api文檔比較簡陋,很多 ...
  • 通常情況下我們在編寫套接字通信程式時都會實現一收一發的通信模式,當客戶端發送數據到服務端後,我們希望服務端處理請求後同樣返回給我們一個狀態值,並以此判斷我們的請求是否被執行成功了,另外增加收發同步有助於避免數據包粘包問題的產生,在多數開發場景中我們都會實現該功能。Socket粘包是指在使用TCP協議... ...
  • 目錄Java8 介面初始化的幾種場景通過介面實現類的方式實現代碼實現通過匿名內部類的來實現代碼實現通過JDK8 雙冒號用法方式代碼實現通過箭頭函數Lambda表達式的方式代碼實現將介面作為方法參數代碼實現 Java8 介面初始化的幾種場景 通過介面實現類的方式實現 代碼實現 public inter ...
  • 數組(Array) 數組(Array)應該是最基礎的數據結構之一,它由相同類型的元素組成的集合,並按照一定的順序存儲在記憶體中。每個元素都有一個唯一的索引,可以用於訪問該元素。 // java 數組示例 int[] numbers1 = {2,0,2,3,9,23}; // 或者 int[] numb ...
  • 歡迎訪問我的GitHub 這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos 本篇概覽 本文是《client-go實戰》系列的第八篇,主要內容是新建一個golang工程,用於管理代碼,後面整個系列的代碼都會保存在這個工程中 工程結構 ...
  • Spring Boot 2.0 中的註解 @ModelAttribute 有什麼作用呢? 通常情況下,我們會將 @ModelAttribute 註解放置在 Controller 中的某個方法上,那麼,如果您在請求這個 Controller 中定義的 URI 時,會首先調用這個被註解的方法,並將該方法 ...
  • 哈嘍兄弟們,抖音現在有JS加密,以前的方法爬不了餓了,今天來實現一下某音短視頻的JS逆向解析。 知識點 動態數據抓包`在這裡插入代碼片`requests發送請求X-Bogus 參數逆向 環境模塊 python 3.8 運行代碼pycharm 2022.3 輔助敲代碼requests pip inst ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...