天天看點

接口幂等性一、什麼是接口幂等性二、哪些情況需要防止 三、什麼情況下需要幂等 四、幂等解決方案

目錄

一、什麼是接口幂等性

二、哪些情況需要防止

三、什麼情況下需要幂等

四、幂等解決方案

1、token 機制

實作

危險性

2、各種鎖機制

1、資料庫悲觀鎖

2、資料庫樂觀鎖

3、業務層分布式鎖

3、各種唯一限制

1、資料庫唯一限制

2、redis set 防重

3、防重表

4、全局請求唯一 id

以token機制作為執行個體示範

一、什麼是接口幂等性

接口幂等性就是使用者對于同一操作發起的一次請求或者多次請求的結果是一緻的 ,不會因 為多次點選而産生了副作用;比如說支付場景,使用者購買了商品支付扣款成功,但是傳回結 果的時候網絡異常,此時錢已經扣了,使用者再次點選按鈕,此時會進行第二次扣款,傳回結 果成功,使用者查詢餘額返發現多扣錢了,流水記錄也變成了兩條... , 這就沒有保證接口 的幂等性。

二、哪些情況需要防止

使用者多次點選按鈕 使用者頁面回退再次送出 微服務互相調用,由于網絡問題,導緻請求失敗。 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 機制

實作

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

2、各種鎖機制

1、資料庫悲觀鎖

select * from xxxx where id = 1 for update; 悲觀鎖使用時一般伴随事務一起使用,資料鎖定時間可能會很長,需要根據實際情況選用。 另外要注意的是, id 字段一定是主鍵或者唯一索引,不然可能造成鎖表的結果,處理起來會 非常麻煩。

2、資料庫樂觀鎖

這種方法适合在更新的場景中, 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 條件就不成立。這樣就保證了不管調用幾次,隻會真正的處理一次。 樂觀鎖主要使用于處理讀多寫少的問題

3、業務層分布式鎖

如果多個機器可能在同一時間同時處理相同的資料,比如多台機器定時任務都拿到了相同數 據處理,我們就可以加分布式鎖,鎖定此資料,處理完成後釋放鎖。擷取到鎖的必須先判斷 這個資料是否被處理過。

3、各種唯一限制

1、資料庫唯一限制

插入資料,應該按照唯一索引進行插入,比如訂單号,相同的訂單就不可能有兩條記錄插入。 我們在資料庫層面防止重複。 這個機制是利用了資料庫的主鍵唯一限制的特性,解決了在 insert 場景時幂等問題。但主鍵 的要求不是自增的主鍵,這樣就需要業務生成全局唯一的主鍵。 如果是分庫分表場景下,路由規則要保證相同請求下,落地在同一個資料庫和同一表中,要 不然資料庫主鍵限制就不起效果了,因為是不同的資料庫和表主鍵不相關。

2、redis set 防重

很多資料需要處理,隻能被處理一次,比如我們可以計算資料的 MD5 将其放入 redis 的 set , 每次處理資料,先看這個 MD5 是否已經存在,存在就不處理。

3、防重表

使用訂單号 orderNo 做為去重表的唯一索引,把唯一索引插入去重表,再進行業務操作,且 他們在同一個事務中。這個保證了重複請求時,因為去重表有唯一限制,導緻請求失敗,避 免了幂等問題。這裡要注意的是,去重表和業務表應該在同一庫中,這樣就保證了在同一個 事務,即使業務操作失敗了,也會把去重表的資料復原。這個很好的保證了資料一緻性。 之前說的 redis 防重也算

4、全局請求唯一 id

調用接口時,生成一個唯一 id , redis 将資料儲存到集合中(去重),存在即處理過。 可以使用 nginx 設定每一個請求的唯一 id ; proxy_set_header X-Request-Id $request_id;

以token機制作為執行個體示範

以訂單業務為例,在渲染訂單确認頁資料時,通過uuid生成token,儲存在redis中,傳給前端

// 防止訂單重複送出,防重令牌 TODO
        String uuid = UUID.randomUUID().toString().replace("-","");
        redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),uuid,30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(uuid);
           

前端送出訂單時攜帶token,與redis中的token進行比對,比對成功進行删除,下一次同樣的請求進來會發現redis中無token,此時告訴client該請求已完成即可。這裡token對比和删除要保證原子性,使用LUA腳本保證原子性。

@Override
    public SubmitOrderRespVo submitOrder(OrderSubmitVo vo) {
        threadLocal.set(vo);
        SubmitOrderRespVo respVo = new SubmitOrderRespVo();
        //去建立、下訂單、驗令牌、驗價格、鎖定庫存...
        // 擷取使用者資訊
        MemberRespVo memberRespVo = OrderInterceptor.threadLocal.get();
        //1、驗證令牌是否合法【令牌的對比和删除必須保證原子性】使用lua腳本保證原子性
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Long res = redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class), Collections.singletonList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), vo.getOrderToken());
        if(Objects.equals(0L,res)) {
            //令牌驗證失敗
            respVo.setCode(1);
            return respVo;
        }
        //令牌驗證成功
        //1、建立訂單、訂單項等資訊
        OrderCreateTo order = createOrder();
        return respVo;
    }