接口冪等性這一概念源于數(shù)學(xué),原意是指一個(gè)操作如果連續(xù)執(zhí)行多次所產(chǎn)生的結(jié)果與僅執(zhí)行一次的效果相同,那么我們就稱這個(gè)操作是冪等的。在互聯(lián)網(wǎng)領(lǐng)域,特別是在Web服務(wù)、API設(shè)計(jì)和分布式系統(tǒng)中,接口冪等性具有非常重要的意義。
具體到HTTP接口或者服務(wù)間的API調(diào)用,接口冪等性就可以理解為當(dāng)客戶端對(duì)同一接口發(fā)起多次相同的請(qǐng)求時(shí),服務(wù)端系統(tǒng)也應(yīng)該確保只執(zhí)行一次相應(yīng)的操作,并且不論接收到了多少次請(qǐng)求,系統(tǒng)的狀態(tài)變更始終是一致的,不會(huì)因?yàn)橹貜?fù)的請(qǐng)求而導(dǎo)致數(shù)據(jù)的錯(cuò)誤。
比如我們常常遇到的訂單創(chuàng)建,支付等業(yè)務(wù)。
要向杜絕冪等性,那么我們就要之道導(dǎo)致接口冪等性問題的原因有哪些。接口冪等性問題通常由以下多種原因引起:
總的來說,導(dǎo)致接口冪等性問題可以粗略的歸類于兩種情況:前端調(diào)用以及服務(wù)端調(diào)用,那么我們可以針對(duì)這兩種情況看一下如何去保證接口冪等。
頁面調(diào)用接口時(shí)可以通過禁用(如按鈕置灰或顯示加載狀態(tài))防止用戶在請(qǐng)求未完成前重復(fù)點(diǎn)擊,從而減少不必要的重復(fù)請(qǐng)求和可能的數(shù)據(jù)沖突。雖然在前端進(jìn)行按鈕置灰等操作可以輔助提高系統(tǒng)的冪等性表現(xiàn),但是這個(gè)方式只是從用戶體驗(yàn)和用戶行為控制的角度來避免重復(fù)提交的一種方法,并沒有從系統(tǒng)設(shè)計(jì)層面完全解決接口本身的冪等性問題。
PRG(POST/Redirect/GET)模式是一種前端交互策略,旨在解決用戶刷新頁面時(shí)可能導(dǎo)致表單數(shù)據(jù)重復(fù)提交的問題。它巧妙地利用了HTTP協(xié)議的特性,具體的交互流程如下:
Token機(jī)制是一種廣泛應(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機(jī)制,我們可以解決接口冪等性問題。在接口中,我們允許重復(fù)提交,但是要保證重復(fù)提交不產(chǎn)生副作用,比如點(diǎn)擊n次只產(chǎn)生一條記錄,客戶端每次請(qǐng)求都需要攜帶一個(gè)唯一的Token,而服務(wù)器則驗(yàn)證這個(gè)Token的有效性。如果服務(wù)器收到了一個(gè)已經(jīng)使用過的Token就會(huì)認(rèn)為這是一個(gè)重復(fù)請(qǐng)求并拒絕處理,從而確保接口的冪等性具體流握如下Token機(jī)制是一種常用的方法,用于確保接口的冪等性和防止重復(fù)請(qǐng)求。具體流程如下:
圖片
在服務(wù)端接口處理邏輯時(shí),可以通過通過一些特定的標(biāo)識(shí)符或請(qǐng)求參數(shù)來校驗(yàn)請(qǐng)求的冪等性,以確保同樣的請(qǐng)求不會(huì)被重復(fù)處理。
客戶端每次發(fā)起請(qǐng)求會(huì)攜帶一個(gè)全局唯一的標(biāo)識(shí)符。服務(wù)器接收到請(qǐng)求后就會(huì)對(duì)這個(gè)標(biāo)識(shí)符進(jìn)行檢查,若服務(wù)器發(fā)現(xiàn)該標(biāo)識(shí)符已經(jīng)在系統(tǒng)中存在,表明這是一個(gè)重復(fù)請(qǐng)求,此時(shí)服務(wù)器可以選擇忽略該請(qǐng)求,或者向客戶端返回已處理過相同請(qǐng)求的結(jié)果信息。若服務(wù)器未找到該標(biāo)識(shí)符存在于系統(tǒng)內(nèi),則認(rèn)定該請(qǐng)求為新請(qǐng)求,服務(wù)器將繼續(xù)對(duì)其進(jìn)行正常處理,并將此唯一標(biāo)識(shí)符保存至系統(tǒng)中,以便于后續(xù)對(duì)接收的請(qǐng)求進(jìn)行有效性校驗(yàn),防止同一請(qǐng)求的重復(fù)處理。比如我們在要求上游ERP系統(tǒng)對(duì)接訂單平臺(tái)時(shí)就會(huì)要求上游傳遞一個(gè)賬號(hào)下全局唯一的一個(gè)參考單號(hào),這個(gè)參考單號(hào)一個(gè)很重要的作用就是保證接口冪等性。
某些請(qǐng)求參數(shù)確實(shí)可以用來輔助校驗(yàn)請(qǐng)求的冪等性。例如,時(shí)間戳可以作為一種可能的請(qǐng)求參數(shù),在處理請(qǐng)求時(shí),服務(wù)器可以通過比較時(shí)間戳與服務(wù)器當(dāng)前時(shí)間來判斷請(qǐng)求的有效性。若時(shí)間戳與當(dāng)前時(shí)間之間的差異超出預(yù)設(shè)的合理范圍(如幾秒鐘到幾分鐘不等,具體閾值視業(yè)務(wù)場景而定),服務(wù)器可以推測該請(qǐng)求可能是由于網(wǎng)絡(luò)延遲或者其他原因?qū)е碌闹貜?fù)提交。
單純依靠時(shí)間戳來判斷冪等性和重復(fù)請(qǐng)求并不完全準(zhǔn)確,因?yàn)椴煌目蛻舳藭r(shí)間可能并不精確同步,而且時(shí)間戳本身無法保證全局唯一性。但是它可以作為一種有效的輔助手段來減少重復(fù)處理的可能性。
對(duì)于狀態(tài)轉(zhuǎn)移類的操作類型的業(yè)務(wù),可采用狀態(tài)機(jī)設(shè)計(jì),每次請(qǐng)求只允許合法的狀態(tài)變遷,非法狀態(tài)變遷(如已經(jīng)完成的訂單不允許再次支付)將被拒絕。
在更新數(shù)據(jù)時(shí),可以通過版本號(hào)或時(shí)間戳等機(jī)制判斷數(shù)據(jù)是否已被修改,防止因并發(fā)請(qǐng)求導(dǎo)致的多次更新問題。具體做法:
如果一致,說明在這期間數(shù)據(jù)沒有被其他事務(wù)修改過,于是更新數(shù)據(jù)并遞增版本號(hào)或更新時(shí)間戳。
如果不一致,說明數(shù)據(jù)已經(jīng)被修改過,此時(shí)服務(wù)器拒絕本次更新請(qǐng)求,返回錯(cuò)誤提示,客戶端可以根據(jù)錯(cuò)誤信息決定是否重新獲取最新數(shù)據(jù)再嘗試更新。
通過這種方式,即使客戶端因?yàn)榫W(wǎng)絡(luò)原因或其他因素導(dǎo)致同一請(qǐng)求被多次發(fā)送,樂觀鎖機(jī)制能確保只有在數(shù)據(jù)未被其他事務(wù)修改的前提下,才會(huì)執(zhí)行更新操作,從而達(dá)到接口冪等的效果。
從上述的幾種解決冪等性問題的方案來看,使用token機(jī)制可以保證在不同請(qǐng)求動(dòng)作下的冪等性。所以我們以此作為方案作為示例方案。
我們使用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使用ULID生成隨機(jī)字符串,然后將其保存在Redis當(dāng)中。這里以idempotent_token+賬戶+請(qǐng)求操作類型+token作為key。
private StringRedisTemplate stringRedisTemplate;/** * 存入 Redis 的 Token 鍵的前綴 */private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:%s:$s:%s";/** * 生成token令牌 * * @param accountSecret 賬戶令牌 * @param operatorType 接口請(qǐng)求類型,可以是接口url或者其他可以區(qū)分接口服務(wù)類型的值 * @return token令牌 */@Overridepublic String generateToken(String accountSecret, String operatorType) { // 創(chuàng)建或獲取ULID生成器實(shí)例 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); // 存儲(chǔ) Token 到 Redis,且設(shè)置過期時(shí)間為5分鐘 stringRedisTemplate.opsForValue().set(key, accountSecret, 5, TimeUnit.MINUTES); // 返回 Token return token;}這里我們使用Redis執(zhí)行Lua命令去查找以及刪除key,Lua 表達(dá)式能保證命令執(zhí)行的原子性。
/** * 驗(yàn)證 Token 正確性 * * @param token token 字符串 * @param operatorType 接口請(qǐng)求類型,可以是接口url或者其他可以區(qū)分接口服務(wù)類型的值 * @return 驗(yàn)證結(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 鍵值對(duì),若果結(jié)果不為空和0,則驗(yàn)證通過 if (result != null && result != 0L) { System.out.println(String.format("驗(yàn)證 token=%s,key=%s,value=%s 成功", token, key, operatorType)); return true; } System.err.println(String.format("驗(yàn)證 token=%s,key=%s,value=%s 失敗", token, key, operatorType)); return false;}我們在實(shí)現(xiàn)模擬創(chuàng)建訂單的服務(wù),在創(chuàng)建訂單之前,首先校驗(yàn)token令牌。
/** * 創(chuàng)建訂單接口 * * @param requestVO 創(chuàng)建訂單參數(shù) * @param accountSecret 賬戶令牌 * @param token token令牌 * @return 生成的訂單號(hào) */@Overridepublic String createOrder(OrderCreateRequestVO requestVO, String accountSecret, String token) { // 根據(jù) Token 和與用戶相關(guān)的信息到 Redis 驗(yàn)證是否存在對(duì)應(yīng)的信息 boolean result = validToken(token, accountSecret, "createOrder"); if (!result){ // 這里需要自定義異常,統(tǒng)一處理異常,再統(tǒng)一響應(yīng)返回 throw new RuntimeException("重復(fù)的請(qǐng)求"); } // 根據(jù)驗(yàn)證結(jié)果響應(yīng)不同信息 return "Success";}校驗(yàn)如果不存在token,則說明請(qǐng)求時(shí)重復(fù)請(qǐng)求,直接拋出異常,由統(tǒng)一異常管理,直接返回客戶端請(qǐng)求失敗的錯(cuò)誤信息。關(guān)于SpringBoot中統(tǒng)一異常處理,統(tǒng)一結(jié)果響應(yīng),請(qǐ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個(gè)請(qǐng)求并發(fā)操作。
圖片
執(zhí)行結(jié)果如下:
圖片
控制臺(tái)打印日志如下:
圖片
可以看見只有1個(gè)請(qǐng)求成功了,并且控制臺(tái)中打印只有一個(gè)token校驗(yàn)成功。
冪等性是開發(fā)當(dāng)中很常見也很重要的一個(gè)需求,尤其是訂單,支付以及與金錢掛鉤的服務(wù),保證接口冪等性尤其重要。在實(shí)際開發(fā)中,我們需要針對(duì)不同的業(yè)務(wù)場景我們需要靈活的選擇冪等性的實(shí)現(xiàn)方式:
最后強(qiáng)調(diào)一下,實(shí)現(xiàn)冪等性需要先理解自身業(yè)務(wù)需求,根據(jù)業(yè)務(wù)邏輯來實(shí)現(xiàn)這樣才合理,處理好其中的每一個(gè)結(jié)點(diǎn)細(xì)節(jié),完善整體的業(yè)務(wù)流程設(shè)計(jì),才能更好的保證系統(tǒng)的正常運(yùn)行。
本文鏈接:http://m.www897cc.com/showinfo-26-76559-0.html我們一起聊聊如何保證接口冪等性?高并發(fā)下的接口冪等性如何實(shí)現(xiàn)?
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com