接口冪等性這一概念源于數學,原意是指一個操作如果連續執行多次所產生的結果與僅執行一次的效果相同,那么我們就稱這個操作是冪等的。在互聯網領域,特別是在Web服務、API設計和分布式系統中,接口冪等性具有非常重要的意義。
具體到HTTP接口或者服務間的API調用,接口冪等性就可以理解為當客戶端對同一接口發起多次相同的請求時,服務端系統也應該確保只執行一次相應的操作,并且不論接收到了多少次請求,系統的狀態變更始終是一致的,不會因為重復的請求而導致數據的錯誤。
比如我們常常遇到的訂單創建,支付等業務。
要向杜絕冪等性,那么我們就要之道導致接口冪等性問題的原因有哪些。接口冪等性問題通常由以下多種原因引起:
總的來說,導致接口冪等性問題可以粗略的歸類于兩種情況:前端調用以及服務端調用,那么我們可以針對這兩種情況看一下如何去保證接口冪等。
頁面調用接口時可以通過禁用(如按鈕置灰或顯示加載狀態)防止用戶在請求未完成前重復點擊,從而減少不必要的重復請求和可能的數據沖突。雖然在前端進行按鈕置灰等操作可以輔助提高系統的冪等性表現,但是這個方式只是從用戶體驗和用戶行為控制的角度來避免重復提交的一種方法,并沒有從系統設計層面完全解決接口本身的冪等性問題。
PRG(POST/Redirect/GET)模式是一種前端交互策略,旨在解決用戶刷新頁面時可能導致表單數據重復提交的問題。它巧妙地利用了HTTP協議的特性,具體的交互流程如下:
Token機制是一種廣泛應用互聯網領域的認證與授權方法,特別是Web服務系統。token可以理解為一種安全憑證,它是由服務端生成并頒發給客戶端的一段經過加密處理的字符串或數據結構,用來代表用戶的某種狀態或權限。
通過Token機制,我們可以解決接口冪等性問題。在接口中,我們允許重復提交,但是要保證重復提交不產生副作用,比如點擊n次只產生一條記錄,客戶端每次請求都需要攜帶一個唯一的Token,而服務器則驗證這個Token的有效性。如果服務器收到了一個已經使用過的Token就會認為這是一個重復請求并拒絕處理,從而確保接口的冪等性具體流握如下Token機制是一種常用的方法,用于確保接口的冪等性和防止重復請求。具體流程如下:
圖片
在服務端接口處理邏輯時,可以通過通過一些特定的標識符或請求參數來校驗請求的冪等性,以確保同樣的請求不會被重復處理。
客戶端每次發起請求會攜帶一個全局唯一的標識符。服務器接收到請求后就會對這個標識符進行檢查,若服務器發現該標識符已經在系統中存在,表明這是一個重復請求,此時服務器可以選擇忽略該請求,或者向客戶端返回已處理過相同請求的結果信息。若服務器未找到該標識符存在于系統內,則認定該請求為新請求,服務器將繼續對其進行正常處理,并將此唯一標識符保存至系統中,以便于后續對接收的請求進行有效性校驗,防止同一請求的重復處理。比如我們在要求上游ERP系統對接訂單平臺時就會要求上游傳遞一個賬號下全局唯一的一個參考單號,這個參考單號一個很重要的作用就是保證接口冪等性。
某些請求參數確實可以用來輔助校驗請求的冪等性。例如,時間戳可以作為一種可能的請求參數,在處理請求時,服務器可以通過比較時間戳與服務器當前時間來判斷請求的有效性。若時間戳與當前時間之間的差異超出預設的合理范圍(如幾秒鐘到幾分鐘不等,具體閾值視業務場景而定),服務器可以推測該請求可能是由于網絡延遲或者其他原因導致的重復提交。
單純依靠時間戳來判斷冪等性和重復請求并不完全準確,因為不同的客戶端時間可能并不精確同步,而且時間戳本身無法保證全局唯一性。但是它可以作為一種有效的輔助手段來減少重復處理的可能性。
對于狀態轉移類的操作類型的業務,可采用狀態機設計,每次請求只允許合法的狀態變遷,非法狀態變遷(如已經完成的訂單不允許再次支付)將被拒絕。
在更新數據時,可以通過版本號或時間戳等機制判斷數據是否已被修改,防止因并發請求導致的多次更新問題。具體做法:
如果一致,說明在這期間數據沒有被其他事務修改過,于是更新數據并遞增版本號或更新時間戳。
如果不一致,說明數據已經被修改過,此時服務器拒絕本次更新請求,返回錯誤提示,客戶端可以根據錯誤信息決定是否重新獲取最新數據再嘗試更新。
通過這種方式,即使客戶端因為網絡原因或其他因素導致同一請求被多次發送,樂觀鎖機制能確保只有在數據未被其他事務修改的前提下,才會執行更新操作,從而達到接口冪等的效果。
從上述的幾種解決冪等性問題的方案來看,使用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
使用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令牌 */@Overridepublic 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;}
這里我們使用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 生成的訂單號 */@Overridepublic 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校驗成功。
冪等性是開發當中很常見也很重要的一個需求,尤其是訂單,支付以及與金錢掛鉤的服務,保證接口冪等性尤其重要。在實際開發中,我們需要針對不同的業務場景我們需要靈活的選擇冪等性的實現方式:
最后強調一下,實現冪等性需要先理解自身業務需求,根據業務邏輯來實現這樣才合理,處理好其中的每一個結點細節,完善整體的業務流程設計,才能更好的保證系統的正常運行。
本文鏈接:http://www.www897cc.com/showinfo-26-76559-0.html我們一起聊聊如何保證接口冪等性?高并發下的接口冪等性如何實現?
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com