淺談接口冪等性

前言

冪等性,是開發人員在日常開發中必須要考慮的,尤其是轉賬、支付等涉及金額交易的場景,如果出現冪等性的問題,造成的后果是非常嚴重的。

本文將分享一下什么是冪等性以及如何保證冪等性。

什么是冪等性

冪等(idempotent、idempotence)是一個數學與計算機學概念,常見于抽象代數中。

在編程中一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。冪等函數,或冪等方法,是指可以使用相同參數重復執行,并能獲得相同結果的函數。這些函數不會影響系統狀態,也不用擔心重復執行會對系統造成改變。

冪等性產生原因

  • 前端未做限制,導致用戶重復提交

  • 使用瀏覽器后退,或者按F5刷新,或者使用歷史記錄,重復提交表單

  • 網絡波動,引起重復請求

  • 超時重試,引起接口重復調用

  • 定時任務設置不合理,導致數據重復處理

  • 使用消息隊列時,消息重復消費

如何保證冪等性

1.前端處理

  • 提交按鈕點擊置灰,或者增加loading

  • 頁面重定向(PRG),PRG模式即POST-REDIRECT-GET,當用戶進行表單提交時,會重定向到另外一個提交成功頁面,而不是停留在原先的表單頁面。這樣就避免了用戶刷新導致重復提交。同時防止了通過瀏覽器按鈕前進/后退導致表單重復提交。

2.先select后insert + 唯一索引沖突

在保存數據前,我們需要先select一下數據是否存在。如果數據已存在,則返回失敗(具體操作視業務情況而定),如果數據不存在,則執行insert操作。

但在高并發的場景下,可能會出現兩個請求select的時候,都沒有查到數據,然后都執行了insert操作,所以此時會有重復數據產生,因此在數據庫中,我們需要添加唯一索引來保證冪等。

流程圖如下:

先查后插+唯一索引.png

此方案適用于新增操作的接口,如用戶注冊。

3.建去重表

某些業務場景,是允許重復數據存在的,僅在流程的某個環節才不允許出現重復數據,這種情況直接在表中添加唯一索引是不合適的,所以就需要創建一張去重表。

CREATE TABLE `table_name` (
  `id` bigint(15) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `order_id` varchar(100) NOT NULL COMMENT '訂單號',
  `create_time` datetime DEFAULT NULL COMMENT '創建時間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `index_order_id` (`order_id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='去重表';

流程圖如下:

去重表.png

特別注意,防重表與業務表必須在同一數據庫,并且操作要在同一事務中。

此方案適用于在業務中有唯一標識的插入場景中,比如在支付業務中,若一個訂單只會支付一次,則訂單ID可以作為唯一標識。

4.使用悲觀鎖

悲觀鎖,正如其名,具有強烈的獨占和排他特性。它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處于鎖定狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機制,也無法保證外部系統不會修改數據)。

在交易場景中,用戶賬戶余額有100元,轉出50元,正常情況下用戶的余額剩余50元。

update account set amount-50 where id = 123;

如果此時有多個相同的請求,可能會導致用戶的金額變為負數。所以此時可以使用悲觀鎖,將用戶的行數據鎖住,在同一時刻只允許一個請求獲得鎖,其他請求等待。

select * from account where id = 123 for update;

流程圖如下:

悲觀鎖.png

需要特別注意的是:如果使用的是mysql數據庫,存儲引擎必須用innodb,因為它才支持事務。此外,這里id字段一定要是主鍵或者唯一索引,不然會鎖住整張表。

因為悲觀鎖是需要在同一事務中鎖住一行數據,所以如果事務比較長,會造成大量請求等待,影響接口性能。

5.使用樂觀鎖

樂觀鎖( Optimistic Locking ) 相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。而樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基于數據版本( Version )記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基于數據庫表的版本解決方案中,一般是通過為數據庫表增加一個 “version” 字段來實現。讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據版本號等于數據庫表當前版本號,則予以更新,否則認為是過期數據。

樂觀鎖主要基于版本標識(version)進行操作,即每次操作查詢數據時都要先查詢出版本標識(version),然后根據版本標識(version)進行update操作。

select id,amount,version from account id = 123;
update account set amount=amount-50,version=version+1 where id=123 and version = 1;

當多個相同的請求查詢信息時,版本標識是相同的,當其中一個請求完成update操作,后續請求影響條數均為0。

流程圖如下:

樂觀鎖.png

6.根據狀態機

很多時候,業務流程是有狀態流轉的,這個時候可以使用狀態機來保證冪等性。

如訂單業務中,存在狀態「1-已下單,2-已支付,3-已完成,4-已取消」,按照業務流程,狀態是依次流轉的,所以在update操作時,我們就要根據本次的狀態來更新下一次的狀態。

update order_info set status = 3 where id = 123 and status = 2;

流程圖如下:

狀態機.png

7.使用分布式鎖

分布式鎖的邏輯是,每次請求都通過業務唯一ID來嘗試獲取鎖,如果獲取成功,就進行后續業務邏輯操作,如果獲取失敗,就舍棄請求直接返回。

分布式鎖通常是基于redis來實現的。

流程圖如下:

分布式鎖.png

分布式鎖是通過設置redis的過期時間來進行控制。如果過期時間設置太短,則無法有效防止重復請求;如果過期時間設置太長,則影響redis存儲空間,甚至會影響后續業務操作。因此需要根據具體的業務情況,來設置合理的過期時間。

8.基于token機制

此方案包含兩個請求階段:

1.客戶端請求服務端申請獲取token

2.客戶端攜帶token再次請求,服務端校驗token后進行操作。

流程圖如下:

token機制.png

這里有一個注意的點:

服務端驗證token是否存在,要使用刪除key的方式,即redis.del(key),刪除成功則表示校驗token通過;

不能使用先查再刪的操作,即先redis.get(key),后redis.del(key),這種方式在高并發下無法保證冪等。


參考資料

如何實現接口冪等性

高并發下如何保證接口的冪等性?

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 一. 冪等的概念 用在編程領域里, 則意為: 對同一個系統,使用同樣的條件,一次請求和重復的多次請求對系統資源的影...
    duenboa閱讀 6,320評論 0 3
  • 前言 通常,我們拿到一臺服務器后使用338端口遠程桌面登錄windows系統,使用22端口ssh登錄linux系統...
    Kayden_龍邵仁閱讀 893評論 0 0
  • 一、什么是嵌入式測試 嵌入式軟件測試的概念似乎沒那么大眾,很多人從字面上理解,可能會以為這是個硬件測試,那么嵌入式...
    UTP協同自動化測試閱讀 1,013評論 0 0
  • 戰國時期東周這位大臣的辯才確實高明,但有些不太光彩 文\常清君 鄭重申明:常清君在自媒體平臺發布的每一篇文章,都是...
    此生讀寫伴閱讀 183評論 0 0
  • 1.前言(老司機直接跳過) 為什么js需要加密 談到加密,大多數人應用場景都在于后端接口的加密簽名校驗。這種一般...
    麻瓜三號閱讀 1,372評論 0 0