成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

我們一起聊聊如何保證接口冪等性?高并發下的接口冪等性如何實現?

開發 前端
具體到HTTP接口或者服務間的API調用,接口冪等性就可以理解為當客戶端對同一接口發起多次相同的請求時,服務端系統也應該確保只執行一次相應的操作,并且不論接收到了多少次請求,系統的狀態變更始終是一致的,不會因為重復的請求而導致數據的錯誤。

什么是接口冪等性

接口冪等性這一概念源于數學,原意是指一個操作如果連續執行多次所產生的結果與僅執行一次的效果相同,那么我們就稱這個操作是冪等的。在互聯網領域,特別是在Web服務、API設計和分布式系統中,接口冪等性具有非常重要的意義。

具體到HTTP接口或者服務間的API調用,接口冪等性就可以理解為當客戶端對同一接口發起多次相同的請求時,服務端系統也應該確保只執行一次相應的操作,并且不論接收到了多少次請求,系統的狀態變更始終是一致的,不會因為重復的請求而導致數據的錯誤。

比如我們常常遇到的訂單創建,支付等業務。

  • 如果一個“創建訂單”接口實現了冪等性,當收到兩次同樣的創建請求時,系統應該要么拒絕第二個請求(因為它已經是重復請求),要么確保只有一個訂單被創建,而不是兩個完全一樣的訂單。
  • 對于一個“支付”接口,冪等性要求即便用戶由于網絡原因反復點擊支付按鈕,服務端也只會扣除用戶賬戶一次金額,避免重復扣費。

導致接口冪等性問題的原因

要向杜絕冪等性,那么我們就要之道導致接口冪等性問題的原因有哪些。接口冪等性問題通常由以下多種原因引起:

  1. 網絡波動不穩定:網絡通信中的丟包、延遲等情況可能導致客戶端未收到服務端的響應或服務端未收到客戶端的請求,此時客戶端可能會重試發送請求,導致接口被重復調用。
  2. 用戶操作:用戶快速重復點擊導致,例如用戶在等待響應時,由于不確定是否操作成功,可能會多次點擊提交按鈕,進而發送多次相同的請求。再比如頁用戶頻繁刷新頁面,尤其是在某些提交操作尚未完成時,刷新頁面可能會重新發送請求。還有用戶可能在瀏覽器上點擊回退然后再重復之間的提交操作,這都可能會導致重新發送請求。
  3. 重試機制:在高可用性設計中,客戶端常常設置有重試機制,當請求失敗或超時時會自動重新發起請求。而在分布式系統中,服務間調用也可能有重試策略,以應對臨時故障。比如Nginx重試,RPC重試,或者調用方業務層中進行重試。
  4. 定時任務或異步處理:在定時任務中如果定時任務調度或邏輯設計不當,可能會導致同一任務被執行多次?;蛘咴谙㈥犃兄?,消息可能會因為異常等原因被重復消費。
  5. 并發控制:缺乏有效的并發控制手段,導致在并發環境下,針對同一資源的操作被多次執行。

總的來說,導致接口冪等性問題可以粗略的歸類于兩種情況:前端調用以及服務端調用,那么我們可以針對這兩種情況看一下如何去保證接口冪等。

如何保證接口冪等?

前端調用

頁面控制

頁面調用接口時可以通過禁用(如按鈕置灰或顯示加載狀態)防止用戶在請求未完成前重復點擊,從而減少不必要的重復請求和可能的數據沖突。雖然在前端進行按鈕置灰等操作可以輔助提高系統的冪等性表現,但是這個方式只是從用戶體驗和用戶行為控制的角度來避免重復提交的一種方法,并沒有從系統設計層面完全解決接口本身的冪等性問題。

使用RPG模式

PRG(POST/Redirect/GET)模式是一種前端交互策略,旨在解決用戶刷新頁面時可能導致表單數據重復提交的問題。它巧妙地利用了HTTP協議的特性,具體的交互流程如下:

  1. 用戶在網頁表單中填寫數據,并通過POST請求將其發送至服務器進行處理,例如創建新資源或更新現有數據。
  2. 服務器接收到POST請求后,對提交的數據進行有效處理和持久化存儲,并在操作成功后不直接返回處理結果,而是通過HTTP響應碼302或303實現重定向,指示客戶端發起一個新的GET請求去訪問一個特定的URL。
  3. 客戶端遵照服務器的重定向指示,自動發送GET請求訪問新的URL,此時返回的頁面將展示之前POST操作處理完畢的結果。
  4. 當用戶在此后刷新頁面時,瀏覽器只會按照常規方式重新發起GET請求,而非重新提交POST數據,因此有效地避免了重復提交引發的潛在問題。
Token機制

Token機制是一種廣泛應用互聯網領域的認證與授權方法,特別是Web服務系統。token可以理解為一種安全憑證,它是由服務端生成并頒發給客戶端的一段經過加密處理的字符串或數據結構,用來代表用戶的某種狀態或權限。

通過Token機制,我們可以解決接口冪等性問題。在接口中,我們允許重復提交,但是要保證重復提交不產生副作用,比如點擊n次只產生一條記錄,客戶端每次請求都需要攜帶一個唯一的Token,而服務器則驗證這個Token的有效性。如果服務器收到了一個已經使用過的Token就會認為這是一個重復請求并拒絕處理,從而確保接口的冪等性具體流握如下Token機制是一種常用的方法,用于確保接口的冪等性和防止重復請求。具體流程如下:

  1. 生成Token當用戶開始執行一個需要確保冪等性的操作(如支付、下單、更新用戶信息等)時,服務端會生成一個唯一的、有時效性的token。這個token可以是一個隨機字符串或者帶有時間戳和其他相關信息的哈希值,確保其唯一性。
  2. 存儲Token生成的token會被存儲在服務端的一個臨時存儲介質中,如Redis、Memcached或數據庫,同時設置一個合理的過期時間(例如15分鐘)。
  3. 傳遞Token將生成的token返回給客戶端,客戶端在進行后續的API調用時,需將此token作為請求參數或放在請求頭中一并發送給服務端。
  4. 驗證Token服務端在接收到帶有token的請求時,首先檢查token是否存在并且有效(未過期且未被使用過)。如果token有效且未被使用,則執行相應的業務邏輯,并在執行完成后立即從存儲介質中移除或標記為已使用。若token已失效或已被使用,則拒絕此次請求,返回相應的錯誤提示,確保同一個操作不會被執行兩次。
  5. 限制并發在并發場景下,通過原子操作(如Redis的SETNX命令)確保在驗證token有效的同時,將其刪除或更新狀態,避免多個請求同時通過驗證。

圖片圖片

服務端控制

在服務端接口處理邏輯時,可以通過通過一些特定的標識符或請求參數來校驗請求的冪等性,以確保同樣的請求不會被重復處理。

唯一標識符

客戶端每次發起請求會攜帶一個全局唯一的標識符。服務器接收到請求后就會對這個標識符進行檢查,若服務器發現該標識符已經在系統中存在,表明這是一個重復請求,此時服務器可以選擇忽略該請求,或者向客戶端返回已處理過相同請求的結果信息。若服務器未找到該標識符存在于系統內,則認定該請求為新請求,服務器將繼續對其進行正常處理,并將此唯一標識符保存至系統中,以便于后續對接收的請求進行有效性校驗,防止同一請求的重復處理。比如我們在要求上游ERP系統對接訂單平臺時就會要求上游傳遞一個賬號下全局唯一的一個參考單號,這個參考單號一個很重要的作用就是保證接口冪等性。

請求參數

某些請求參數確實可以用來輔助校驗請求的冪等性。例如,時間戳可以作為一種可能的請求參數,在處理請求時,服務器可以通過比較時間戳與服務器當前時間來判斷請求的有效性。若時間戳與當前時間之間的差異超出預設的合理范圍(如幾秒鐘到幾分鐘不等,具體閾值視業務場景而定),服務器可以推測該請求可能是由于網絡延遲或者其他原因導致的重復提交。

單純依靠時間戳來判斷冪等性和重復請求并不完全準確,因為不同的客戶端時間可能并不精確同步,而且時間戳本身無法保證全局唯一性。但是它可以作為一種有效的輔助手段來減少重復處理的可能性。

狀態機設計

對于狀態轉移類的操作類型的業務,可采用狀態機設計,每次請求只允許合法的狀態變遷,非法狀態變遷(如已經完成的訂單不允許再次支付)將被拒絕。

樂觀鎖

在更新數據時,可以通過版本號或時間戳等機制判斷數據是否已被修改,防止因并發請求導致的多次更新問題。具體做法:

  1. 在數據庫表中增加一個版本號字段(version)或者時間戳字段(timestamp)。
  2. 客戶端第一次請求時獲取數據的版本號或時間戳。
  3. 客戶端發起更新操作時,將上次讀取的版本號或時間戳一起發送回服務器。
  4. 服務器在執行更新操作前,首先檢查當前數據庫中的版本號或時間戳是否與客戶端提交的一致。

如果一致,說明在這期間數據沒有被其他事務修改過,于是更新數據并遞增版本號或更新時間戳。

如果不一致,說明數據已經被修改過,此時服務器拒絕本次更新請求,返回錯誤提示,客戶端可以根據錯誤信息決定是否重新獲取最新數據再嘗試更新。

通過這種方式,即使客戶端因為網絡原因或其他因素導致同一請求被多次發送,樂觀鎖機制能確保只有在數據未被其他事務修改的前提下,才會執行更新操作,從而達到接口冪等的效果。

實現冪等性方案示例

從上述的幾種解決冪等性問題的方案來看,使用token機制可以保證在不同請求動作下的冪等性。所以我們以此作為方案作為示例方案。

準備工作

我們使用Redis保存Token令牌,引入SpringBoot,Redis,ULID相關的依賴。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.7.0</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.0</version>
</dependency>

<dependency>
    <groupId>com.github.f4b6a3</groupId>
    <artifactId>ulid-creator</artifactId>
    <version>5.2.0</version>
</dependency>

Redis相關的配置:

spring.redis.database=0  
spring.redis.host=127.0.0.1  
spring.redis.port=6379  
spring.redis.password=  
spring.redis.pool.max-active=8  
spring.redis.pool.max-wait=-1  
spring.redis.pool.max-idle=8  
spring.redis.pool.min-idle=0  
spring.redis.timeout=60  


server.port=8080  
server.servlet.context-path=/coderacademy

生成Token令牌

使用ULID生成隨機字符串,然后將其保存在Redis當中。這里以idempotent_token+賬戶+請求操作類型+token作為key。

private StringRedisTemplate stringRedisTemplate;

/**
 * 存入 Redis 的 Token 鍵的前綴
 */
private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:%s:$s:%s";


/**
 * 生成token令牌
 *
 * @param accountSecret 賬戶令牌
 * @param operatorType 接口請求類型,可以是接口url或者其他可以區分接口服務類型的值
 * @return token令牌
 */
@Override
public String generateToken(String accountSecret, String operatorType) {
    // 創建或獲取ULID生成器實例
    long timestampInMillis = LocalDateTime.now().atZone(ZoneOffset.systemDefault()).toInstant().toEpochMilli();
    Ulid ulid = UlidCreator.getUlid(timestampInMillis);
    String token = ulid.toString();
    // 設置存入 Redis 的 Key
    String key = String.format(IDEMPOTENT_TOKEN_PREFIX, accountSecret, operatorType, token);
    // 存儲 Token 到 Redis,且設置過期時間為5分鐘
    stringRedisTemplate.opsForValue().set(key, accountSecret, 5, TimeUnit.MINUTES);
    // 返回 Token
    return token;
}

校驗Token令牌

這里我們使用Redis執行Lua命令去查找以及刪除key,Lua 表達式能保證命令執行的原子性。

/**
     * 驗證 Token 正確性
     *
     * @param token token 字符串
     * @param operatorType 接口請求類型,可以是接口url或者其他可以區分接口服務類型的值
     * @return 驗證結果
     */
private boolean validToken(String token, String accountSecret, String operatorType) {
    // 設置 Lua 腳本,其中 KEYS[1] 是 key,KEYS[2] 是 value
    String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
    RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
    // 根據 Key 前綴拼接 Key
    String key = String.format(IDEMPOTENT_TOKEN_PREFIX, accountSecret, operatorType, token);
    // 執行 Lua 腳本
    Long result = stringRedisTemplate.execute(redisScript, Arrays.asList(key, operatorType));
    // 根據返回結果判斷是否成功成功匹配并刪除 Redis 鍵值對,若果結果不為空和0,則驗證通過
    if (result != null && result != 0L) {
        System.out.println(String.format("驗證 token=%s,key=%s,value=%s 成功", token, key, operatorType));
        return true;
    }
    System.err.println(String.format("驗證 token=%s,key=%s,value=%s 失敗", token, key, operatorType));
    return false;
}

業務代碼以及接口

我們在實現模擬創建訂單的服務,在創建訂單之前,首先校驗token令牌。

/**
 * 創建訂單接口
 *
 * @param requestVO     創建訂單參數
 * @param accountSecret 賬戶令牌
 * @param token         token令牌
 * @return 生成的訂單號
 */
@Override
public String createOrder(OrderCreateRequestVO requestVO, String accountSecret, String token) {
    // 根據 Token 和與用戶相關的信息到 Redis 驗證是否存在對應的信息
    boolean result = validToken(token, accountSecret, "createOrder");
    if (!result){
        // 這里需要自定義異常,統一處理異常,再統一響應返回
        throw new RuntimeException("重復的請求");
    }
    // 根據驗證結果響應不同信息
    return "Success";
}

校驗如果不存在token,則說明請求時重復請求,直接拋出異常,由統一異常管理,直接返回客戶端請求失敗的錯誤信息。關于SpringBoot中統一異常處理,統一結果響應,請查看:SpringBoot統一結果返回,統一異常處理,大牛都這么玩。

我們在定義獲取Token令牌的接口,以及創建訂單的接口。

@RestController
@RequestMapping("order")
public class OrderController {

    private IOrderService orderService;

    /**
     * 獲取token接口
     * @param secret 賬戶令牌
     * @return
     */
    @GetMapping("getToken")
    public String getToken(@RequestHeader("secret") String secret){
        return orderService.generateToken(secret, "createOrder");
    }

    /**
     * 創建訂單接口
     * @param requestVO 參數
     * @param token token令牌
     * @param secret 賬戶令牌
     * @return 響應信息
     */
    @PostMapping("create")
    public OrderCreateResponseVO createOrder(@RequestBody OrderCreateRequestVO requestVO,
                                             @RequestHeader("token") String token,
                                             @RequestHeader("secret") String secret){
        OrderCreateResponseVO responseVO = new OrderCreateResponseVO();
        String result = orderService.createOrder(requestVO, secret, token);
        responseVO.setSuccess(Boolean.TRUE);
        responseVO.setMsg(result);
        return responseVO;
    }

    @Autowired
    public void setOrderService(IOrderService orderService) {
        this.orderService = orderService;
    }
}

我們使用Apifox模擬3個請求并發操作。

圖片圖片

執行結果如下:

圖片圖片

控制臺打印日志如下:

圖片圖片

可以看見只有1個請求成功了,并且控制臺中打印只有一個token校驗成功。

總結

冪等性是開發當中很常見也很重要的一個需求,尤其是訂單,支付以及與金錢掛鉤的服務,保證接口冪等性尤其重要。在實際開發中,我們需要針對不同的業務場景我們需要靈活的選擇冪等性的實現方式:

  • 如果是web服務,客戶端可以采取在頁面上使用按鈕置灰禁用,使用PRG模式,或者搭配后端的Token令牌進行解決。
  • 在服務端,我們可以采取唯一標識符,樂觀鎖,Token令牌,狀態機等校驗方式。

最后強調一下,實現冪等性需要先理解自身業務需求,根據業務邏輯來實現這樣才合理,處理好其中的每一個結點細節,完善整體的業務流程設計,才能更好的保證系統的正常運行。

責任編輯:武曉燕 來源: 碼農Academy
相關推薦

2021-03-28 09:45:05

冪等性接口數據

2025-02-26 08:20:18

2020-07-15 08:14:12

高并發

2021-04-14 17:18:27

冪等性數據源MySQL

2022-01-04 12:08:46

設計接口

2021-01-18 14:34:59

冪等性接口客戶端

2023-09-01 15:27:31

2025-02-23 08:00:00

冪等性Java開發

2024-11-27 08:47:12

2022-03-22 07:57:42

Java多線程并發

2024-07-10 12:23:10

2021-01-13 11:23:59

分布式冪等性支付

2024-08-29 09:01:39

2023-10-26 07:32:42

2024-06-24 01:00:00

2024-11-01 09:28:02

2023-03-07 08:19:16

接口冪等性SpringBoot

2022-05-23 11:35:16

jiekou冪等性

2021-01-20 07:16:07

冪等性接口token

2023-08-29 13:53:00

前端攔截HashMap
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美日韩中文字幕在线 | 国产一区二区欧美 | 一区二区国产精品 | 日韩精品一区二区三区老鸭窝 | 欧美一区二区在线观看视频 | 二区视频 | 欧美婷婷 | av片在线播放 | 国产精品免费在线 | 国产黄色大片在线观看 | 免费一级片 | 蜜桃黄网 | 日本精品一区二区三区视频 | 国产99视频精品免视看9 | 国产欧美日韩在线观看 | 青青草精品视频 | 成人久久久久 | 91啪亚洲精品| 蜜月va乱码一区二区三区 | 国产一区二区三区在线 | 久久久久久国产一区二区三区 | 精品av | 欧美aⅴ在线观看 | 久久精品小视频 | 中文字幕一区二区三区乱码图片 | 亚洲激情自拍偷拍 | 欧美男人亚洲天堂 | 亚洲成人免费 | 久久免费观看一级毛片 | 久久久国产亚洲精品 | 中文字幕亚洲欧美 | 久久亚洲国产 | 日韩av成人| 九九久久在线看 | 精品国产黄a∨片高清在线 www.一级片 国产欧美日韩综合精品一区二区 | 日本人做爰大片免费观看一老师 | 国产精品久久久久久久久 | 日韩在线欧美 | 视频1区| 久久久精品网 | 欧美一区二区三区久久精品 |