什么叫冪等?如何實現冪等性?
本文公眾號來源:無聊學Java
作者:無聊
什么是冪等性?
冪等是一個數學與計算機學概念,在數學中某一元運算為冪等時,其作用在任一元素兩次后會和其作用一次的結果相同。
“在計算機中編程中,一個冪等操作的特點是其任意多次執(zhí)行所產生的影響均與一次執(zhí)行的影響相同。
冪等函數或冪等方法是指可以使用相同參數重復執(zhí)行,并能獲得相同結果的函數。這些函數不會影響系統(tǒng)狀態(tài),也不用擔心重復執(zhí)行會對系統(tǒng)造成改變。
什么是接口冪等性?
在HTTP/1.1中,對冪等性進行了定義。它描述了一次和多次請求某一個資源對于資源本身應該具有同樣的結果(網絡超時等問題除外),即第一次請求的時候對資源產生了副作用,但是以后的多次請求都不會再對資源產生副作用。
這里的副作用是不會對結果產生破壞或者產生不可預料的結果。也就是說,其任意多次執(zhí)行對資源本身所產生的影響均與一次執(zhí)行的影響相同。
為什么需要實現冪等性?
在接口調用時一般情況下都能正常返回信息不會重復提交,不過在遇見以下情況時可以就會出現問題,如:
前端重復提交表單:在填寫一些表格時候,用戶填寫完成提交,很多時候會因網絡波動沒有及時對用戶做出提交成功響應,致使用戶認為沒有成功提交,然后一直點提交按鈕,這時就會發(fā)生重復提交表單請求。
用戶惡意進行刷單:例如在實現用戶投票這種功能時,如果用戶針對一個用戶進行重復提交投票,這樣會導致接口接收到用戶重復提交的投票信息,這樣會使投票結果與事實嚴重不符。
接口超時重復提交:很多時候 HTTP 客戶端工具都默認開啟超時重試的機制,尤其是第三方調用接口時候,為了防止網絡波動超時等造成的請求失敗,都會添加重試機制,導致一個請求提交多次。
消息進行重復消費:當使用 MQ 消息中間件時候,如果發(fā)生消息中間件出現錯誤未及時提交消費信息,導致發(fā)生重復消費。
“使用冪等性最大的優(yōu)勢在于使接口保證任何冪等性操作,免去因重試等造成系統(tǒng)產生的未知的問題。
引入冪等性后對系統(tǒng)有什么影響?
冪等性是為了簡化客戶端邏輯處理,能放置重復提交等操作,但卻增加了服務端的邏輯復雜性和成本,其主要是:
把并行執(zhí)行的功能改為串行執(zhí)行,降低了執(zhí)行效率。
增加了額外控制冪等的業(yè)務邏輯,復雜化了業(yè)務功能;
所以在使用時候需要考慮是否引入冪等性的必要性,根據實際業(yè)務場景具體分析,除了業(yè)務上的特殊要求外,一般情況下不需要引入的接口冪等性。
Restful API 接口冪等性如何?
現在流行的 Restful 推薦的幾種 HTTP 接口方法中,分別存在冪等行與不能保證冪等的方法,如下:
√滿足冪等x不滿足冪等-可能滿足也可能不滿足冪等,根據實際業(yè)務邏輯有關

方案一:數據庫唯一主鍵實現冪等性
數據庫唯一主鍵的實現主要是利用數據庫中主鍵唯一約束的特性,一般來說唯一主鍵比較適用于“插入”時的冪等性,其能保證一張表中只能存在一條帶該唯一主鍵的記錄。
使用數據庫唯一主鍵完成冪等性時需要注意的是,該主鍵一般來說并不是使用數據庫中自增主鍵,而是使用分布式 ID 充當主鍵,這樣才能能保證在分布式環(huán)境下 ID 的全局唯一性。
適用操作
插入操作 刪除操作
使用限制
需要生成全局唯一主鍵 ID;
主要流程

主要流程如下:
客戶端執(zhí)行創(chuàng)建請求,調用服務端接口。
服務端執(zhí)行業(yè)務邏輯,生成一個分布式
ID,將該 ID 充當待插入數據的主鍵,然 后執(zhí)數據插入操作,運行對應的SQL語句。服務端將該條數據插入數據庫中,如果插入成功則表示沒有重復調用接口。如果拋出主鍵重復異常,則表示數據庫中已經存在該條記錄,返回錯誤信息到客戶端。
方案二:數據庫樂觀鎖實現冪等性
數據庫樂觀鎖方案一般只能適用于執(zhí)行更新操作的過程,我們可以提前在對應的數據表中多添加一個字段,充當當前數據的版本標識。
這樣每次對該數據庫該表的這條數據執(zhí)行更新時,都會將該版本標識作為一個條件,值為上次待更新數據中的版本標識的值。
適用操作
更新操作
使用限制
需要數據庫對應業(yè)務表中添加額外字段
描述示例

例如,存在如下的數據表中:

為了每次執(zhí)行更新時防止重復更新,確定更新的一定是要更新的內容,我們通常都會添加一個 version 字段記錄當前的記錄版本,這樣在更新時候將該值帶上,那么只要執(zhí)行更新操作就能確定一定更新的是某個對應版本下的信息。

這樣每次執(zhí)行更新時候,都要指定要更新的版本號,如下操作就能準確更新 version=5 的信息:
UPDATE?my_table?SET?price=price+50,version=version+1?WHERE?id=1?AND?version=5
上面 WHERE 后面跟著條件 id=1 AND version=5 被執(zhí)行后,id=1 的 version 被更新為 6,所以如果重復執(zhí)行該條 SQL 語句將不生效,因為 id=1 AND version=5 的數據已經不存在,這樣就能保住更新的冪等,多次更新對結果不會產生影響。
方案三:防重 Token 令牌實現冪等性
針對客戶端連續(xù)點擊或者調用方的超時重試等情況,例如提交訂單,此種操作就可以用 Token 的機制實現防止重復提交。
簡單的說就是調用方在調用接口的時候先向后端請求一個全局 ID(Token),請求的時候攜帶這個全局 ID 一起請求(Token 最好將其放到 Headers 中),后端需要對這個 Token 作為 Key,用戶信息作為 Value 到 Redis 中進行鍵值內容校驗,如果 Key 存在且 Value 匹配就執(zhí)行刪除命令,然后正常執(zhí)行后面的業(yè)務邏輯。如果不存在對應的 Key 或 Value 不匹配就返回重復執(zhí)行的錯誤信息,這樣來保證冪等操作。
適用操作
插入操作 更新操作 刪除操作
使用限制
需要生成全局唯一 Token串需要使用第三方組件 Redis進行數據效驗
主要流程:

服務端提供獲取 Token 的接口,該 Token 可以是一個序列號,也可以是一個分布式
ID或者UUID串。客戶端調用接口獲取 Token,這時候服務端會生成一個 Token 串。
然后將該串存入 Redis 數據庫中,以該 Token 作為 Redis 的鍵(注意設置過期時間)。
將 Token 返回到客戶端,客戶端拿到后應存到表單隱藏域中。
客戶端在執(zhí)行提交表單時,把 Token 存入到
Headers中,執(zhí)行業(yè)務請求帶上該Headers。服務端接收到請求后從
Headers中拿到 Token,然后根據 Token 到 Redis 中查找該key是否存在。服務端根據 Redis 中是否存該
key進行判斷,如果存在就將該key刪除,然后正常執(zhí)行業(yè)務邏輯。如果不存在就拋異常,返回重復提交的錯誤信息。
“注意,在并發(fā)情況下,執(zhí)行 Redis 查找數據與刪除需要保證原子性,否則很可能在并發(fā)下無法保證冪等性。其實現方法可以使用分布式鎖或者使用
Lua表達式來注銷查詢與刪除操作。
方案四: 下游傳遞唯一序列號實現冪等性
所謂請求序列號,其實就是每次向服務端請求時候附帶一個短時間內唯一不重復的序列號,該序列號可以是一個有序 ID,也可以是一個訂單號,一般由下游生成,在調用上游服務端接口時附加該序列號和用于認證的 ID。
當上游服務器收到請求信息后拿取該 序列號 和下游 認證ID 進行組合,形成用于操作 Redis 的 Key,然后到 Redis 中查詢是否存在對應的 Key 的鍵值對,根據其結果:
如果存在,就說明已經對該下游的該序列號的請求進行了業(yè)務處理,這時可以直接響應重復請求的錯誤信息。
如果不存在,就以該
Key作為 Redis 的鍵,以下游關鍵信息作為存儲的值(例如下游商傳遞的一些業(yè)務邏輯信息),將該鍵值對存儲到 Redis 中 ,然后再正常執(zhí)行對應的業(yè)務邏輯即可。
適用操作
插入操作 更新操作 刪除操作
使用限制
要求第三方傳遞唯一序列號; 需要使用第三方組件 Redis 進行數據效驗;
主要流程

下游服務生成分布式
ID作為序列號,然后執(zhí)行請求調用上游接口,并附帶唯一序列號與請求的認證憑據ID。上游服務進行安全效驗,檢測下游傳遞的參數中是否存在序列號和憑據ID。
上游服務到 Redis 中檢測是否存在對應的序列號與認證ID組成的
Key,如果存在就拋出重復執(zhí)行的異常信息,然后響應下游對應的錯誤信息。如果不存在就以該序列號和認證ID組合作為Key,以下游關鍵信息作為Value,進而存儲到 Redis 中,然后正常執(zhí)行接來來的業(yè)務邏輯。
“上面步驟中插入數據到 Redis 一定要設置過期時間。這樣能保證在這個時間范圍內,如果重復調用接口,則能夠進行判斷識別。如果不設置過期時間,很可能導致數據無限量的存入 Redis,致使 Redis 不能正常工作。
實現接口冪等示例
這里使用防重 Token 令牌方案,該方案能保證在不同請求動作下的冪等性,實現邏輯可以看上面寫的”防重 Token 令牌”方案,接下來寫下實現這個邏輯的代碼。
1. Maven 引入相關依賴
這里使用 Maven 工具管理依賴,這里在 pom.xml中引入 SpringBoot、Redis、lombok 相關依賴。
<dependencies>
????????
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-webartifactId>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-data-redisartifactId>
????????dependency>
????????<dependency>
????????????<groupId>org.apache.commonsgroupId>
????????????<artifactId>commons-pool2artifactId>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.projectlombokgroupId>
????????????<artifactId>lombokartifactId>
????????dependency>
????dependencies>
2. 配置連接 Redis 的參數
在 application 配置文件中配置連接 Redis 的參數,如下:
spring:
??redis:
????ssl:?false
????host:?127.0.0.1
????port:?6379
????database:?0
????timeout:?1000
????password:
????lettuce:
??????pool:
????????max-active:?100
????????max-wait:?-1
????????min-idle:?0
????????max-idle:?20
3. 創(chuàng)建與驗證 Token 工具類
創(chuàng)建用于操作 Token 相關的 Service 類,里面存在 Token 創(chuàng)建與驗證方法,其中:
Token創(chuàng)建方法:使用UUID工具創(chuàng)建Token串,設置以“idempotent_token:“+“Token串”作為Key,以用戶信息當成Value,將信息存入 Redis 中。Token驗證方法:接收 Token 串參數,加上 Key 前綴形成Key,再傳入value值,執(zhí)行Lua表達式(Lua表達式能保證命令執(zhí)行的原子性)進行查找對應Key與刪除操作。執(zhí)行完成后驗證命令的返回結果,如果結果不為空且非0,則驗證成功,否則失敗。
@Slf4j
@Service
public?class?TokenUtilService?{
????@Autowired
????private?StringRedisTemplate?redisTemplate;
????/**
?????*?存入?Redis?的?Token?鍵的前綴
?????*/
????private?static?final?String?IDEMPOTENT_TOKEN_PREFIX?=?"idempotent_token:";
????/**
?????*?創(chuàng)建?Token?存入?Redis,并返回該?Token
?????*
?????*?@param?value?用于輔助驗證的?value?值
?????*?@return?生成的?Token?串
?????*/
????public?String?generateToken(String?value)?{
????????//?實例化生成?ID?工具對象
????????String?token?=?UUID.randomUUID().toString();
????????//?設置存入?Redis?的?Key
????????String?key?=?IDEMPOTENT_TOKEN_PREFIX?+?token;
????????//?存儲?Token?到?Redis,且設置過期時間為5分鐘
????????redisTemplate.opsForValue().set(key,?value,?5,?TimeUnit.MINUTES);
????????//?返回?Token
????????return?token;
????}
????/**
?????*?驗證?Token?正確性
?????*
?????*?@param?token?token?字符串
?????*?@param?value?value?存儲在Redis中的輔助驗證信息
?????*?@return?驗證結果
?????*/
????public?boolean?validToken(String?token,?String?value)?{
????????//?設置?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?redisScript?=?new?DefaultRedisScript<>(script,?Long.class);
????????//?根據?Key?前綴拼接?Key
????????String?key?=?IDEMPOTENT_TOKEN_PREFIX?+?token;
????????//?執(zhí)行?Lua?腳本
????????Long?result?=?redisTemplate.execute(redisScript,?Arrays.asList(key,?value));
????????//?根據返回結果判斷是否成功成功匹配并刪除?Redis?鍵值對,若果結果不為空和0,則驗證通過
????????if?(result?!=?null?&&?result?!=?0L)?{
????????????log.info("驗證?token={},key={},value={}?成功",?token,?key,?value);
????????????return?true;
????????}
????????log.info("驗證?token={},key={},value={}?失敗",?token,?key,?value);
????????return?false;
????}
}
4、創(chuàng)建測試的 Controller 類
創(chuàng)建用于測試的 Controller 類,里面有獲取 Token 與測試接口冪等性的接口,內容如下:
@Slf4j
@RestController
public?class?TokenController?{
????@Autowired
????private?TokenUtilService?tokenService;
????/**
?????*?獲取?Token?接口
?????*
?????*?@return?Token?串
?????*/
????@GetMapping("/token")
????public?String?getToken()?{
????????//?獲取用戶信息(這里使用模擬數據)
????????//?注:這里存儲該內容只是舉例,其作用為輔助驗證,使其驗證邏輯更安全,如這里存儲用戶信息,其目的為:
????????//?-?1)、使用"token"驗證?Redis?中是否存在對應的?Key
????????//?- 2)、使用"用戶信息"驗證 Redis 的 Value 是否匹配。
????????String?userInfo?=?"mydlq";
????????//?獲取?Token?字符串,并返回
????????return?tokenService.generateToken(userInfo);
????}
????/**
?????*?接口冪等性測試接口
?????*
?????*?@param?token?冪等?Token?串
?????*?@return?執(zhí)行結果
?????*/
????@PostMapping("/test")
????public?String?test(@RequestHeader(value?=?"token")?String?token)?{
????????//?獲取用戶信息(這里使用模擬數據)
????????String?userInfo?=?"mydlq";
????????//?根據?Token?和與用戶相關的信息到?Redis?驗證是否存在對應的信息
????????boolean?result?=?tokenService.validToken(token,?userInfo);
????????//?根據驗證結果響應不同信息
????????return?result???"正常調用"?:?"重復調用";
????}
}
最后總結
冪等性是開發(fā)當中很常見也很重要的一個需求,尤其是支付、訂單等與金錢掛鉤的服務,保證接口冪等性尤其重要。在實際開發(fā)中,我們需要針對不同的業(yè)務場景我們需要靈活的選擇冪等性的實現方式:
對于下單等存在唯一主鍵的,可以使用“唯一主鍵方案”的方式實現。
對于更新訂單狀態(tài)等相關的更新場景操作,使用“樂觀鎖方案”實現更為簡單。
對于上下游這種,下游請求上游,上游服務可以使用“下游傳遞唯一序列號方案”更為合理。
類似于前端重復提交、重復下單、沒有唯一ID號的場景,可以通過
Token與Redis配合的“防重 Token 方案”實現更為快捷。
上面只是給與一些建議,再次強調一下,實現冪等性需要先理解自身業(yè)務需求,根據業(yè)務邏輯來實現這樣才合理,處理好其中的每一個結點細節(jié),完善整體的業(yè)務流程設計,才能更好的保證系統(tǒng)的正常運行。最后做一個簡單總結,然后本博文到此結束,如下:

原創(chuàng)電子書
原創(chuàng)思維導圖
已經有8756個初學者都下載了!
?三歪把【大廠面試知識點】、【簡歷模板】、【原創(chuàng)文章】
全部整理成電子書,共有1263頁!掃碼或微信搜 Java3y
回復「888」領取
![]() |
|

47塊半年購買服務器。最近如果要買服務器的同學可以重點關注,錯過了就要等一年!別在活動結束后再問我能不能買了喲!




