接口幂等性就是使用者對于同一操作發起的一次請求或者多次請求的結果是一緻的,不會因為多次點選而産生了副作用;比如說支付場景,使用者購買了商品支付扣款成功,但是傳回結果的時候網絡異常,此時錢已經扣了,使用者再次點選按鈕,此時會進行第二次扣款,傳回結果成功,使用者查詢餘額返發現多扣錢了,流水記錄也變成了兩條...,這就沒有保證接口的幂等性。
使用者多次點選按鈕
使用者頁面回退再次送出
微服務互相調用,由于網絡問題,導緻請求失敗。feign 觸發重試機制
其他業務情況
以 SQL 為例,有些操作是天然幂等的。
SELECT * FROM table WHER id=?,無論執行多少次都不會改變狀态,是天然的幂等。
UPDATE tab1 SET col1=1 WHERE col2=2,無論執行成功多少次狀态都是一緻的,也是幂等操作。
delete from user where userid=1,多次操作,結果一樣,具備幂等性
insert into user(userid,name) values(1,'a') 如 userid 為唯一主鍵,即重複操作上面的業務,隻
會插入一條使用者資料,具備幂等性。
UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次執行的結果都會發生變化,不是幂等的。
insert into user(userid,name) values(1,'a') 如 userid 不是主鍵,可以重複,那上面業務多次操
作,資料都會新增多條,不具備幂等性。
1、服務端提供了發送 token 的接口。我們在分析業務的時候,哪些業務是存在幂等問題的,就必須在執行業務前,先去擷取 token,伺服器會把 token 儲存到 redis 中。
2、然後調用業務接口請求時,把 token 攜帶過去,一般放在請求頭部。
3、伺服器判斷 token 是否存在 redis 中,存在表示第一次請求,然後删除 token,繼續執行業務。
4、如果判斷 token 不存在 redis 中,就表示是重複操作,直接傳回重複标記給 client,這樣就保證了業務代碼,不被重複執行。
危險性:
1、先删除 token 還是後删除 token;
(1) 先删除可能導緻,業務确實沒有執行,重試還帶上之前 token,由于防重設計導緻, 請求還是不能執行。
(2) 後删除可能導緻,業務處理成功,但是服務閃斷,出現逾時,沒有删除 token,别人繼續重試,導緻業務被執行兩邊
(3) 我們最好設計為先删除 token,如果業務調用失敗,就重新擷取 token 再次請求。
2、Token 擷取、比較和删除必須是原子性
(1) redis.get(token) 、token.equals、redis.del(token)如果這兩個操作不是原子,可能導緻,高并發下,都 get 到同樣的資料,判斷都成功,繼續業務并發執行
(2) 可以在 redis 使用 lua 腳本完成這個操作
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
select * from xxxx where id = 1 for update;
悲觀鎖使用時一般伴随事務一起使用,資料鎖定時間可能會很長,需要根據實際情況選用。另外要注意的是,id 字段一定是主鍵或者唯一索引,不然可能造成鎖表的結果,處理起來會非常麻煩。
這種方法适合在更新的場景中,
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
根據 version 版本,也就是在操作庫存前先擷取目前商品的 version 版本号,然後操作的時候帶上此 version 号。我們梳理下,我們第一次操作庫存時,得到 version 為 1,調用庫存服務version 變成了 2;但傳回給訂單服務出現了問題,訂單服務又一次發起調用庫存服務,當訂單服務傳如的 version 還是 1,再執行上面的 sql 語句時,就不會執行;因為 version 已經變為 2 了,where 條件就不成立。這樣就保證了不管調用幾次,隻會真正的處理一次。 樂觀鎖主要使用于處理讀多寫少的問題
如果多個機器可能在同一時間同時處理相同的資料,比如多台機器定時任務都拿到了相同資料處理,我們就可以加分布式鎖,鎖定此資料,處理完成後釋放鎖。擷取到鎖的必須先判斷這個資料是否被處理過。
插入資料,應該按照唯一索引進行插入,比如訂單号,相同的訂單就不可能有兩條記錄插入。 我們在資料庫層面防止重複。
這個機制是利用了資料庫的主鍵唯一限制的特性,解決了在 insert 場景時幂等問題。但主鍵的要求不是自增的主鍵,這樣就需要業務生成全局唯一的主鍵。 如果是分庫分表場景下,路由規則要保證相同請求下,落地在同一個資料庫和同一表中,要不然資料庫主鍵限制就不起效果了,因為是不同的資料庫和表主鍵不相關。
很多資料需要處理,隻能被處理一次,比如我們可以計算資料的MD5将其放入redis的set,每次處理資料,先看這個MD5是否已經存在,存在就不處理。
使用訂單号 orderNo 做為去重表的唯一索引,把唯一索引插入去重表,再進行業務操作,且他們在同一個事務中。這個保證了重複請求時,因為去重表有唯一限制,導緻請求失敗,避免了幂等問題。這裡要注意的是,去重表和業務表應該在同一庫中,這樣就保證了在同一個事務,即使業務操作失敗了,也會把去重表的資料復原。這個很好的保證了資料一緻性。
之前說的 redis 防重也算
調用接口時,生成一個唯一 id,redis 将資料儲存到集合中(去重),存在即處理過。可以使用 nginx 設定每一個請求的唯一 id;
proxy_set_header X-Request-Id $request_id;