解決方案.gif
典型冪等策略
冪等處理的一般流程.png
唯一索引策略(極少使用)
1.select+insert+唯一索引沖突
//1.根據冪等號查詢冪等記錄
Record record = dao.select(param);
if (record != null){
//2.冪等記錄存在,直接返回冪等結果或根據記錄狀態(如失敗可重試)進一步處理
}else{
try{
//3.冪等記錄不存在,插入冪等記錄
dao.insert(entity);
//4.插入成功,執行業務邏輯
}catch(DuplicateKeyException e){
//5/插入失敗,若為重復異常,直接返回冪等結果或進一步處理
record = dao.select(param);
//6.處理邏輯
}catch(Throwable tr){
//7.其他異常處理邏輯
}
}
2.insert+唯一索引沖突
//1.插入冪等記錄
try{
//2.插入成功,執行業務邏輯
dao.insert(entity);
}catch(DuplicateKeyException e){
//3.插入失敗且為重復異常,直接返回冪等結果或進一步處理
dao.select(param);
//4.處理邏輯
}catch(Throwable tr){
//5.其他異常處理邏輯
}
數據庫開銷大,如果重復請求發生的概率較小,可優先選擇2.insert+唯一索引沖突。
唯一索引需結合冪等記錄狀態變更管控+事務機制(數據庫事務/分布式事務)+鎖機制。
悲觀鎖策略(金融賬務清算域常用方式)
//1.開啟事務
begin;
//2.基于冪等號 biz_no 鎖行查詢
record = select * from table_name where biz_no='xxxx' for update
//3.沒有相關冪等記錄,說明是首次請求
if (record == null){
//4.初始化并插入冪等記錄
insert(init(param))
//5.再次基于冪等號biz_no鎖行查詢
record = select * from table_name where biz_no='xxxx' for update
}
//6.鎖行查詢成功,根據記錄判斷決策處理方式
if (record.getStatus() != 預期狀態){
//7.非預期狀態,結束處理
return;
}
//8.預期狀態,執行業務邏輯,如查詢,更新流水等
//9.更新記錄
update table_name set status ='目標狀態' where biz_no='xxx';
//10.提交事務
commit
通過串性化實現冪等,資源占用較多
依靠數據庫自身的特性來實現,實現成本低,結合了事務機制保障了較強的數據一致性,解決了請求并發、亂序等的問題
1.查詢冪等數據的狀態,如果是不可重試終態,直接返回;
2.如果是可重試終態,則進行重試(同一冪等號),涉及到下游則需要結合分布式事務
3.插入冪等記錄TransactionTemplate REQUIRED_NEW
4.下游處理成功,則變更冪等記錄狀態
5.下游也必須是冪等的。
分布式鎖
分布式冪等.png
SETNX keyName value,若keyName已存在于Redis中,則返回0;
若不存在,則返回1.
//1.嘗試將keyName寫入緩存
if(redis.setNx(keyName, 1) == 1){
//2.寫入成功,即獲取鎖成功,繼續執行業務邏輯
//3.執行完成,釋放鎖(為了防止誤釋放,可采用LUA腳本)
}else{
//4.寫入失敗,即獲取鎖失敗,要么直接返回,要么等待、重試
}
需結合事務機制和重試機制形成完整方案。
事務機制用于保證業務邏輯的數據一致性;重試機制是基于持久化的冪等記錄進行失敗重試,保證最終一致性。
還存在釋放鎖操作可能失敗的情況。一旦釋放鎖操作失敗,就會導致一段時間內記錄一直在緩沖中,其他線程無法獲得鎖。
即使設置了失效時間,在有效期內仍會存在問題。失效時間過短,業務邏輯執行完前釋放鎖,失效時間過長,其他嘗試獲取鎖的過程就需要等待,甚至可能超時。
解決冪等問題的關鍵
- 1.唯一性約束
- 2.執行唯一性檢查
解決冪等問題的實現方式
唯一索引:數據庫唯一索引,唯一索引可以基于業務流水建立,也可以單獨建表實現;
唯一數據:悲觀鎖、樂觀鎖、分布式鎖等鎖機制;
狀態機約束:對于存在狀態流轉的業務,通過狀態機的流轉約束,可以實現有限狀態機的冪等。