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

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

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

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

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

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

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

下游服務生成分布式?
ID
?作為序列號,然后執行請求調用上游接口,并附帶唯一序列號與請求的認證憑據ID。上游服務進行安全效驗,檢測下游傳遞的參數中是否存在序列號和憑據ID。
上游服務到 Redis 中檢測是否存在對應的序列號與認證ID組成的?
Key
,如果存在就拋出重復執行的異常信息,然后響應下游對應的錯誤信息。如果不存在就以該序列號和認證ID組合作為?Key
,以下游關鍵信息作為?Value
,進而存儲到 Redis 中,然后正常執行接來來的業務邏輯。
“上面步驟中插入數據到 Redis 一定要設置過期時間。這樣能保證在這個時間范圍內,如果重復調用接口,則能夠進行判斷識別。如果不設置過期時間,很可能導致數據無限量的存入 Redis,致使 Redis 不能正常工作。
實現接口冪等示例
這里使用防重 Token 令牌方案,該方案能保證在不同請求動作下的冪等性,實現邏輯可以看上面寫的”防重 Token 令牌”方案,接下來寫下實現這個邏輯的代碼。
1. Maven 引入相關依賴
這里使用?Maven
?工具管理依賴,這里在?pom.xml
中引入?SpringBoot
、Redis
、lombok
?相關依賴。
????????
????????
????????????org.projectlombok
????????????lombok
????????
????
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. 創建與驗證 Token 工具類
創建用于操作 Token 相關的 Service 類,里面存在 Token 創建與驗證方法,其中:
Token
?創建方法:使用?UUID
?工具創建?Token
?串,設置以?“idempotent_token:“+“Token串”
?作為?Key
,以用戶信息當成?Value
,將信息存入 Redis 中。Token
?驗證方法:接收 Token 串參數,加上 Key 前綴形成?Key
,再傳入?value
?值,執行?Lua
?表達式(Lua
?表達式能保證命令執行的原子性)進行查找對應?Key
?與刪除操作。執行完成后驗證命令的返回結果,如果結果不為空且非0,則驗證成功,否則失敗。
@Slf4j
@Service
public?class?TokenUtilService?{
????@Autowired
????private?StringRedisTemplate?redisTemplate;
????/**
?????*?存入?Redis?的?Token?鍵的前綴
?????*/
????private?static?final?String?IDEMPOTENT_TOKEN_PREFIX?=?"idempotent_token:";
????/**
?????*?創建?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;
????????//?執行?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、創建測試的 Controller 類
創建用于測試的?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?執行結果
?????*/
????@PostMapping("/test")
????public?String?test(@RequestHeader(value?=?"token")?String?token)?{
????????//?獲取用戶信息(這里使用模擬數據)
????????String?userInfo?=?"mydlq";
????????//?根據?Token?和與用戶相關的信息到?Redis?驗證是否存在對應的信息
????????boolean?result?=?tokenService.validToken(token,?userInfo);
????????//?根據驗證結果響應不同信息
????????return?result???"正常調用"?:?"重復調用";
????}
}
最后總結
冪等性是開發當中很常見也很重要的一個需求,尤其是支付、訂單等與金錢掛鉤的服務,保證接口冪等性尤其重要。在實際開發中,我們需要針對不同的業務場景我們需要靈活的選擇冪等性的實現方式:
對于下單等存在唯一主鍵的,可以使用“唯一主鍵方案”的方式實現。
對于更新訂單狀態等相關的更新場景操作,使用“樂觀鎖方案”實現更為簡單。
對于上下游這種,下游請求上游,上游服務可以使用“下游傳遞唯一序列號方案”更為合理。
類似于前端重復提交、重復下單、沒有唯一ID號的場景,可以通過?
Token
?與?Redis
?配合的“防重 Token 方案”實現更為快捷。
上面只是給與一些建議,再次強調一下,實現冪等性需要先理解自身業務需求,根據業務邏輯來實現這樣才合理,處理好其中的每一個結點細節,完善整體的業務流程設計,才能更好的保證系統的正常運行。最后做一個簡單總結,然后本博文到此結束,如下:

—————END—————
特別推薦一個分享架構+算法的優質內容,還沒關注的小伙伴,可以長按關注一下:
長按訂閱更多精彩▼
如有收獲,點個在看,誠摯感謝
免責聲明:本文內容由21ic獲得授權后發布,版權歸原作者所有,本平臺僅提供信息存儲服務。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯系我們,謝謝!