文章目錄
- 接口冪等概念
- 典型場景
- 核心解決方案
- 一鎖
- 二判
- 三更新
- 方案選型對比
接口冪等概念
定義:無論調用接口多少次,對系統的影響與單次調用一樣
范疇:在后端開發中,通常更關注寫接口的冪等,因為寫接口才會對系統數據造成不良影響;讀接口多次調用,我們作為下游通常不好控制,讀取到的數據不一樣也更多是數據實時性導致的問題
典型場景
需要關注冪等性主要由于現代分布式系統面臨三大不可靠因素:
- 用戶不可靠(手抖多點)
用戶在表單頁多次點擊按鈕,前端提交了多次創建訂單的請求 - 網絡不可靠(超時重傳)
當接口超時的時候,由網關層或者業務層控制重試次數,比方說第三方支付接口超時后通過指數回退算法計算重試間隔再重試 - 系統不可靠(服務重試)
MQ 中間件消費消息的時候,由于 Rebalance 或者消費失敗重試等原因,同樣的消息在被舊的消費者處理過后,有可能被再次分配到新的消費者身上繼續處理
核心解決方案
核心流程可以總結為:一鎖二判三更新,這三步每一步涉及到的方案會有分布式鎖、樂觀鎖;Token機制、唯一索引、狀態機、請求序列號等。
偽代碼如下:
// 1. 一鎖: 加上悲觀鎖或者樂觀鎖
Lock.lock();
try {// 2. 二判:判斷請求是否已經被執行過Order order = orderService.queryOrder();if (order.hasSucceeded) {return;}// 3. 三更新:執行更新業務邏輯order.update();return;
} finally {Lock.unlock();
}
很多文章會將分布式鎖、樂觀鎖單獨拿出來作為一種冪等的方案,我覺得這樣理解有失偏頗,因為鎖本質上只是為了保證程序原子化互斥執行的手段,本身就不是專門用來保證冪等的,在這三步里就是為了保證「二判」、「三更新」這兩步的并發安全。舉個極端的例子理解,Redis 鎖確實在一定程度上保證了并發安全情況下的冪等,兩個相同的資源請求同時進來的時候,只有一個請求能夠競爭到鎖,另一個請求直接獲取不到鎖就不阻塞返回了;但假設這兩個請求錯開進入系統,此時不存在鎖的競爭,這個時候兩個請求就都能執行了,如果是創建訂單就創建了多筆訂單,仍然沒有達到冪等。所以將鎖作為「高并發」場景下保證冪等的一種手段,但不是實現冪等的通用范式。
一鎖
鎖主要是為了保證并發場景下「二判」、「三更新」這兩步的并發安全,因為不鎖的話有可能多個線程都沒有用最新值去判斷,如果評估到業務場景下并發確實很低,其實這一步也可以省略。鎖的話就有多種實現方案了,悲觀鎖或者樂觀鎖,但是一定要是互斥鎖。
- 分布式悲觀鎖
分布式鎖的實現方式也比較多 Redis、Zk、MySQL,但業界主流的實現方式還是 Redis, 主要還是考慮到 Redis 的高性能、AP 高可用,通過非阻塞的方式實現并發校驗。
- 數據庫悲觀鎖
開啟事務并對數據庫中的記錄加上排他行鎖,其他事務必須等待本次事務提交后才能執行,同時需要記得行鎖都是基于索引的,如果不加索引可能會導致鎖表的不良后果。偽代碼如下:
// 1. 開啟事務
begin;
// 2. 查詢出商品信息并加行鎖
select quantity from items where id = 1 for update;
// 3. 修改商品信息
update items set quantity = 2 where id = 1;
// 4. 提交事務
commit;
數據庫悲觀鎖效率低,串行執行,更新失敗概率低,適用于并發寫入比較頻繁的場景。
- 數據庫樂觀鎖
并未顯示加鎖,通過版本號 version 實現 CAS 機制
// 1. 查詢原版本號
select quantity, origin_version from item where id = 1;
// 2. CAS 更新
update item set quantity = 2 and version = origin_version + 1 where id = 1 and version = origin_version
數據庫樂觀鎖在讀多寫少的場景效率高,一旦放到并發沖突比較多的場景下或者鎖的粒度沒有掌控好,更新失敗的概率就會變高
二判
判斷這一步主要是進行冪等判斷,Token機制、業務字段、請求序列號(前三種其實都是生成業務冪等號的方式)、唯一索引、狀態機、都能實現類似的功能
- 冪等號 - Token 機制
常用在防重復下單的場景,用戶每次訪問頁面時都先向后端請求一個 token,之后在本頁面的操作都需要將此 token 帶過來,頁面不刷新 token 也不變。
Token 生成:
String token = UUID.randomUUID().toString();
jedis.setex(token, 60 * 60, "1");
jedis.close();
Token 校驗,del 會返回被刪除 key 的數量,返回1代表刪除了1條,一個操作來保證原子
Transaction tx = jedis.multi();
if (tx.del(token) == 1) {// 成功
} else {// 失敗
}
jedis.close();
- 冪等號 - 業務字段
常用在重復消費的場景下,上下游約定好一個冪等字段的生成方式,通過特定業務字段的拼接傳遞給下游,供下游做判斷
- 冪等號 - 請求序列號
通過操作流水來做冪等,常用在金融系統中,或者有流水記錄的業務系統中
- 唯一索引
這一步是最終兜底方案,通過數據庫的唯一索引充當最后的防線
CREATE TABLE orders (id BIGINT PRIMARY KEY,order_no VARCHAR(32) UNIQUE,...
);
異常處理示例:
try {orderDao.insert(order);
} catch (DuplicateKeyException e) {log.warn("重復訂單:{}", order.getOrderNo());return Result.error("訂單已存在");
}
- 狀態機(業務流程控制)
碰到這種多狀態的實體一定要設計好狀態流轉
Order order = orderService.queryOrder();
if (order.status != init) {return;
} else {order.setStatus(finished);
}
三更新
這沒有什么好說的,就是在第二步判斷為首次操作的情況下,更新數據庫狀態
方案選型對比
- 并發較低的情況下不必要上鎖
- 請求序列號可靠性最高,但是實現復雜度也高;狀態機流轉適合多狀態業務,實現復雜度適中。