網(wǎng)站可以微信支付是怎么做的域名??烤W(wǎng)頁推廣大全
文章目錄
- 六大方案解決接口冪等問題
- 什么是接口冪等?
- 天然冪等
- 不做冪等會怎么樣?
- 解決方案
- 1)insert前先select
- 2)使用唯一索引
- 3)去重表加悲觀鎖
- 4)加樂觀鎖之版本號機制
- 5)使用 Redisson 分布式鎖
- 6)Token 機制
六大方案解決接口冪等問題
什么是接口冪等?
冪等(idempotency)本身是一個數(shù)學(xué)概念,常見與抽象代數(shù)中,代表一個函數(shù)或操作的結(jié)果不受其輸入或者執(zhí)行次數(shù)的影響,例如,f(n) = 1^n,無論 n 為多少,f(n)的值永遠為 1 。
在軟件開發(fā)領(lǐng)域,冪等對請求執(zhí)行結(jié)果的一個描述,這個描述就是無論執(zhí)行多少次相同的請求,產(chǎn)生的效果和返回的結(jié)果和發(fā)出單個請求是一樣的。
舉個例子🌰:
- 有時我們在填寫某些
form表單
時,保存按鈕不小心快速點了兩次,表中竟然產(chǎn)生了兩條重復(fù)的數(shù)據(jù),只是id不一樣。 - 我們在項目中為了解決
接口超時
問題,通常會引入了重試機制
。第一次請求接口超時了,請求方?jīng)]能及時獲取返回結(jié)果(此時有可能已經(jīng)成功了),為了避免返回錯誤的結(jié)果(這種情況不可能直接返回失敗吧?),于是會對該請求重試幾次,這樣也會產(chǎn)生重復(fù)的數(shù)據(jù)。
天然冪等
那有沒有有些情況是天然支持冪等的呢?當然有!比如說我要更新一個某記錄的狀態(tài) status = 1
具體的 sql 為 update table set status = 1 where id = 1
這種情況,無論我執(zhí)行多少次這條 sql 他的效果是一樣的,這就是天然支持冪等的。
不做冪等會怎么樣?
比如說用戶在付款的時候,同時點擊多次付款按鈕,后端處理了多次扣款請求,結(jié)果導(dǎo)致用戶的賬戶扣了多次錢。妥妥 p0 事故呀!
到這你又會說,前端做個置灰按鈕不就行了嗎,第一次付款完畢后,那用戶或者惡意攻擊你服務(wù)器的人直接用腳本搞你不走前端,你是防止不了的。
那接下來,我將介紹六大解決接口冪等的方案,速速點贊上車!!!
解決方案
1)insert前先select
通常情況下,在保存數(shù)據(jù)的接口中,我們?yōu)榱朔乐巩a(chǎn)生重復(fù)數(shù)據(jù),一般會在insert
前,先根據(jù)name
或code
字段select
一下數(shù)據(jù)。如果該數(shù)據(jù)已存在,則執(zhí)行update
操作,如果不存在,才執(zhí)行 insert
操作。
該方案可能是我們平時在防止產(chǎn)生重復(fù)數(shù)據(jù)時,使用最多的方案。但是該方案不適用于并發(fā)場景,在并發(fā)場景中,要配合其他方案一起使用,否則同樣會產(chǎn)生重復(fù)數(shù)據(jù)。
2)使用唯一索引
通過在表中加上唯一索引,保證數(shù)據(jù)的唯一性。如果有重復(fù)的數(shù)據(jù)插入,會拋出DuplicateKeyException
異常,程序可以捕獲異常并處理。不過,這種方法只適用于插入數(shù)據(jù)的場景。
create table t_order(id int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT "主鍵",code varchar(200) not null COMMENT "流水號",user_id int unsigned COMMENT "用戶id",amount decimal(10,2) unsigned not null COMMENT "總金額",UNIQUE unq_code(code)
) COMMENT="訂單表";
不要依靠唯一索引來保證接口冪等,但建議使用唯一索引作為兜底,避免產(chǎn)生臟數(shù)據(jù)。
偽代碼如下:
public void idempotent(OrderDO orderDO){try {// 執(zhí)行核心業(yè)務(wù)...orderMapper.insert(orderDO);}catch(DuplicateKeyException e) {// 有重復(fù)的數(shù)據(jù)插入}
}
3)去重表加悲觀鎖
去重表本質(zhì)上也是一種唯一索引方案。去重表是一張專門用于記錄請求信息的表,其中某個字段需要建立唯一索引,用于標識請求的唯一性當客戶端發(fā)出請求時,服務(wù)端會將這次請求的一些信息(如訂單號、交易流水號等)插入到去重表中,如果插入成功,說明這是第一次請求,可以執(zhí)行后續(xù)的業(yè)務(wù)邏輯;如果插入失敗,說明這是重復(fù)請求,可以直接返回或者忽略。
CREATE TABLE deduplication_table (id int unsigned PRIMARY KEY AUTO_INCREMENT COMMENT "主鍵",processed_code varchar(200) not null COMMENT "已處理的訂單流水號",-- 省略其他字段UNIQUE unq_processed_code(processed_code)
) COMMENT="去重表";
使用for update
加鎖每次查詢到都是最新的數(shù)據(jù)
select * from deduplication_table where processed_code = 'xxx' for update
偽代碼如下:
public boolean idempotent(OrderDO orderDO){// 執(zhí)行核心業(yè)務(wù)之前DoMain domain = deduplicationMapper.selectForUpdate(orderDO.getProcessedCode);if(doamin != null) {// 訂單已經(jīng)支付}
}
4)加樂觀鎖之版本號機制
既然悲觀鎖有性能問題,為了提升接口性能,我們可以使用樂觀鎖。需要在表中增加一個timestamp
或者version
字段,這里以version
字段為例。
在更新數(shù)據(jù)之前先查詢一下數(shù)據(jù):
select id,amount,version from user id=123;
如果數(shù)據(jù)存在,假設(shè)查到的version
等于1
,再使用id
和version
字段作為查詢條件更新數(shù)據(jù):
update user set amount = amount + 100, version = version + 1 where id = 123 and version = 1;
更新數(shù)據(jù)的同時version+1
,然后判斷本次update
操作的影響行數(shù),如果大于0,則說明本次更新成功,如果等于0,則說明本次更新沒有讓數(shù)據(jù)變更。
由于第一次請求version
等于1
是可以成功的,操作成功后version
變成2
了。這時如果并發(fā)的請求過來,再執(zhí)行相同的sql:
update user set amount = amount + 100,version = version + 1 where id = 123 and version = 1;
該update
操作不會真正更新數(shù)據(jù),最終sql的執(zhí)行結(jié)果影響行數(shù)是0
,因為version
已經(jīng)變成2
了,where
中的version=1
肯定無法滿足條件。但為了保證接口冪等性,接口可以直接返回成功,因為version
值已經(jīng)修改了,那么前面必定已經(jīng)成功過一次,后面都是重復(fù)的請求。
具體流程如下:
具體步驟:
- 先根據(jù)id查詢用戶信息,包含version字段
- 根據(jù)id和version字段值作為where條件的參數(shù),更新用戶信息,同時version+1
- 判斷操作影響行數(shù),如果影響1行,則說明是一次請求,可以做其他數(shù)據(jù)操作。
- 如果影響0行,說明是重復(fù)請求,則直接返回成功。
5)使用 Redisson 分布式鎖
基于 MySQL 也可以實現(xiàn)分布式鎖,但一般我們不會采用這種方式。
通常情況下,我們一般會選擇基于 Redis 或者 ZooKeeper 實現(xiàn)分布式鎖,Redis 用的要更多一點。
// 唯一標識
String uniqueId = "orderId123";
// 1. 根據(jù)唯一標識生成分布式鎖對象
RLock lock = redisson.getLock("lock:" + uniqueId);try {// 2. 嘗試獲取鎖(Watch Dog 自動續(xù)期機制) if (lock.tryLock()) {// 3. 如果成功獲取到鎖,說明請求還沒有被處理,執(zhí)行業(yè)務(wù)邏輯} else {// 請求已經(jīng)被處理,直接返回}
} finally {// 4. 釋放鎖lock.unlock();
}
6)Token 機制
Token 機制的核心思想是為每一次操作生成一個唯一性的憑證 token。這個 token 需要由服務(wù)端生成的,因為服務(wù)端可以對 token 進行簽名和加密,防止篡改和泄露。如果由客戶端生成 token,可能會存在安全隱患,比如客戶端偽造或重復(fù) token,導(dǎo)致服務(wù)端無法識別和校驗。
這樣的話,就需要兩次請求才能完成一次業(yè)務(wù)操作:
-
請求獲取服務(wù)器端 token,token 需要設(shè)置有效時間(可以設(shè)置短一點),服務(wù)端將該 token 保存起來。
-
執(zhí)行真正的請求,將上一步獲取到的 token 放到 header 或者作為請求參數(shù)。服務(wù)端驗證 token 的有效性,如果有效(一般是通過刪除 token 的方式來驗證,刪除成功則有效),執(zhí)行業(yè)務(wù)邏輯,并刪除 token,防止重復(fù)提交;如果無效,拒絕請求,返回提示信息。
// 獲取token
public String getToken(Long busId, Long userId){String UUID = UUID.randomUUID().toString();stringRedisTemplate.opsForValue().set(busId+userId, UUID, 20, TimeUnit.SECONDS)return UUID;
}
// 發(fā)起業(yè)務(wù)請求攜帶token
public void doSomeBusiness(Parameter parameter) {Long busId = parameter.getBusId;Long userId = UserContext.getUserId();// 判斷token是否存在Boolean deleted = stringRedisTemplate.delete(busId+userId);if(deleted) {// 刪除成功,代表重復(fù)請求不進行操作,直接返回return;}// doSomeBusiness...
}
具體步驟:
- 用戶訪問頁面時,瀏覽器自動發(fā)起獲取 token 請求。
- 服務(wù)端生成 token,保存到 redis 中,然后返回給瀏覽器。
- 用戶通過瀏覽器發(fā)起請求時,攜帶該 token。
- 從 redis 中嘗試刪除 token 如果刪除失敗,說明是第一次請求,做則后續(xù)的數(shù)據(jù)操作。
- 如果刪除成功,說明是重復(fù)請求,不做任何操作。