自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

我們一起聊聊如何保證接口冪等性?高并發(fā)下的接口冪等性如何實現(xiàn)?

開發(fā) 前端
具體到HTTP接口或者服務(wù)間的API調(diào)用,接口冪等性就可以理解為當(dāng)客戶端對同一接口發(fā)起多次相同的請求時,服務(wù)端系統(tǒng)也應(yīng)該確保只執(zhí)行一次相應(yīng)的操作,并且不論接收到了多少次請求,系統(tǒng)的狀態(tài)變更始終是一致的,不會因為重復(fù)的請求而導(dǎo)致數(shù)據(jù)的錯誤。

什么是接口冪等性

接口冪等性這一概念源于數(shù)學(xué),原意是指一個操作如果連續(xù)執(zhí)行多次所產(chǎn)生的結(jié)果與僅執(zhí)行一次的效果相同,那么我們就稱這個操作是冪等的。在互聯(lián)網(wǎng)領(lǐng)域,特別是在Web服務(wù)、API設(shè)計和分布式系統(tǒng)中,接口冪等性具有非常重要的意義。

具體到HTTP接口或者服務(wù)間的API調(diào)用,接口冪等性就可以理解為當(dāng)客戶端對同一接口發(fā)起多次相同的請求時,服務(wù)端系統(tǒng)也應(yīng)該確保只執(zhí)行一次相應(yīng)的操作,并且不論接收到了多少次請求,系統(tǒng)的狀態(tài)變更始終是一致的,不會因為重復(fù)的請求而導(dǎo)致數(shù)據(jù)的錯誤。

比如我們常常遇到的訂單創(chuàng)建,支付等業(yè)務(wù)。

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

導(dǎo)致接口冪等性問題的原因

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

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

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

如何保證接口冪等?

前端調(diào)用

頁面控制

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

使用RPG模式

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

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

Token機制是一種廣泛應(yīng)用互聯(lián)網(wǎng)領(lǐng)域的認(rèn)證與授權(quán)方法,特別是Web服務(wù)系統(tǒng)。token可以理解為一種安全憑證,它是由服務(wù)端生成并頒發(fā)給客戶端的一段經(jīng)過加密處理的字符串或數(shù)據(jù)結(jié)構(gòu),用來代表用戶的某種狀態(tài)或權(quán)限。

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

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

圖片圖片

服務(wù)端控制

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

唯一標(biāo)識符

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

請求參數(shù)

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

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

狀態(tài)機設(shè)計

對于狀態(tài)轉(zhuǎn)移類的操作類型的業(yè)務(wù),可采用狀態(tài)機設(shè)計,每次請求只允許合法的狀態(tài)變遷,非法狀態(tài)變遷(如已經(jīng)完成的訂單不允許再次支付)將被拒絕。

樂觀鎖

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

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

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

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

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

實現(xiàn)冪等性方案示例

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

準(zhǔn)備工作

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

<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相關(guān)的配置:

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當(dāng)中。這里以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或者其他可以區(qū)分接口服務(wù)類型的值
 * @return token令牌
 */
@Override
public String generateToken(String accountSecret, String operatorType) {
    // 創(chuàng)建或獲取ULID生成器實例
    long timestampInMillis = LocalDateTime.now().atZone(ZoneOffset.systemDefault()).toInstant().toEpochMilli();
    Ulid ulid = UlidCreator.getUlid(timestampInMillis);
    String token = ulid.toString();
    // 設(shè)置存入 Redis 的 Key
    String key = String.format(IDEMPOTENT_TOKEN_PREFIX, accountSecret, operatorType, token);
    // 存儲 Token 到 Redis,且設(shè)置過期時間為5分鐘
    stringRedisTemplate.opsForValue().set(key, accountSecret, 5, TimeUnit.MINUTES);
    // 返回 Token
    return token;
}

校驗Token令牌

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

/**
     * 驗證 Token 正確性
     *
     * @param token token 字符串
     * @param operatorType 接口請求類型,可以是接口url或者其他可以區(qū)分接口服務(wù)類型的值
     * @return 驗證結(jié)果
     */
private boolean validToken(String token, String accountSecret, String operatorType) {
    // 設(shè)置 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);
    // 根據(jù) Key 前綴拼接 Key
    String key = String.format(IDEMPOTENT_TOKEN_PREFIX, accountSecret, operatorType, token);
    // 執(zhí)行 Lua 腳本
    Long result = stringRedisTemplate.execute(redisScript, Arrays.asList(key, operatorType));
    // 根據(jù)返回結(jié)果判斷是否成功成功匹配并刪除 Redis 鍵值對,若果結(jié)果不為空和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;
}

業(yè)務(wù)代碼以及接口

我們在實現(xiàn)模擬創(chuàng)建訂單的服務(wù),在創(chuàng)建訂單之前,首先校驗token令牌。

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

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

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

@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");
    }

    /**
     * 創(chuàng)建訂單接口
     * @param requestVO 參數(shù)
     * @param token token令牌
     * @param secret 賬戶令牌
     * @return 響應(yīng)信息
     */
    @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個請求并發(fā)操作。

圖片圖片

執(zhí)行結(jié)果如下:

圖片圖片

控制臺打印日志如下:

圖片圖片

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

總結(jié)

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

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

最后強調(diào)一下,實現(xiàn)冪等性需要先理解自身業(yè)務(wù)需求,根據(jù)業(yè)務(wù)邏輯來實現(xiàn)這樣才合理,處理好其中的每一個結(jié)點細(xì)節(jié),完善整體的業(yè)務(wù)流程設(shè)計,才能更好的保證系統(tǒng)的正常運行。

責(zé)任編輯:武曉燕 來源: 碼農(nóng)Academy
相關(guān)推薦

2021-03-28 09:45:05

冪等性接口數(shù)據(jù)

2020-07-15 08:14:12

高并發(fā)

2025-02-26 08:20:18

2021-04-14 17:18:27

冪等性數(shù)據(jù)源MySQL

2021-01-18 14:34:59

冪等性接口客戶端

2022-01-04 12:08:46

設(shè)計接口

2023-09-01 15:27:31

2025-02-23 08:00:00

冪等性Java開發(fā)

2022-03-22 07:57:42

Java多線程并發(fā)

2024-11-27 08:47:12

2021-01-13 11:23:59

分布式冪等性支付

2024-07-10 12:23:10

2024-06-24 01:00:00

2024-08-29 09:01:39

2023-10-26 07:32:42

2023-03-07 08:19:16

接口冪等性SpringBoot

2024-11-01 09:28:02

2022-05-23 11:35:16

jiekou冪等性

2021-01-20 07:16:07

冪等性接口token

2020-11-12 07:43:06

Redis冪等性接口
點贊
收藏

51CTO技術(shù)棧公眾號