SpringBoot接口冪等性實現(xiàn)的4種方案色瘩!

圖片

目錄

  • 什么是冪等性

  • 什么是接口冪等性

  • 為什么需要實現(xiàn)冪等性

  • 引入冪等性后對系統(tǒng)的影響

  • Restful API 接口的冪等性

  • 如何實現(xiàn)冪等性

  • 方案一:數(shù)據(jù)庫唯一主鍵

  • 方案二:數(shù)據(jù)庫樂觀鎖

  • 方案三:防重 Token 令牌

  • 方案四采驻、下游傳遞唯一序列號

  • 實現(xiàn)接口冪等示例

  • Maven 引入相關(guān)依賴

  • 配置連接 Redis 的參數(shù)

  • 創(chuàng)建與驗證 Token 工具類

  • 創(chuàng)建測試的 Controller 類

  • 創(chuàng)建 SpringBoot 啟動類

  • 寫測試類進行測試

  • 總結(jié)

系統(tǒng)環(huán)境:

  • Java JDK 版本:1.8

  • SpringBoot 版本:2.3.4.RELEASE

示例地址:

https://github.com/my-dlq/blog-example/tree/master/springboot/springboot-idempotent-token/

一渤涌、什么是冪等性

冪等是一個數(shù)學(xué)與計算機學(xué)概念吹埠,在數(shù)學(xué)中某一元運算為冪等時超营,其作用在任一元素兩次后會和其作用一次的結(jié)果相同眠砾。在計算機中編程中虏劲,一個冪等操作的特點是其任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。

冪等函數(shù)或冪等方法是指可以使用相同參數(shù)重復(fù)執(zhí)行褒颈,并能獲得相同結(jié)果的函數(shù)柒巫。這些函數(shù)不會影響系統(tǒng)狀態(tài),也不用擔(dān)心重復(fù)執(zhí)行會對系統(tǒng)造成改變谷丸。

二堡掏、什么是接口冪等性

在HTTP/1.1中,對冪等性進行了定義刨疼。它描述了一次和多次請求某一個資源對于資源本身應(yīng)該具有同樣的結(jié)果(網(wǎng)絡(luò)超時等問題除外)泉唁,即第一次請求的時候?qū)Y源產(chǎn)生了副作用,但是以后的多次請求都不會再對資源產(chǎn)生副作用揩慕。

這里的副作用是不會對結(jié)果產(chǎn)生破壞或者產(chǎn)生不可預(yù)料的結(jié)果亭畜。也就是說,其任意多次執(zhí)行對資源本身所產(chǎn)生的影響均與一次執(zhí)行的影響相同迎卤。

三拴鸵、為什么需要實現(xiàn)冪等性

在接口調(diào)用時一般情況下都能正常返回信息不會重復(fù)提交,不過在遇見以下情況時可以就會出現(xiàn)問題蜗搔,如:

  • 前端重復(fù)提交表單: 在填寫一些表格時候劲藐,用戶填寫完成提交,很多時候會因網(wǎng)絡(luò)波動沒有及時對用戶做出提交成功響應(yīng)樟凄,致使用戶認(rèn)為沒有成功提交聘芜,然后一直點提交按鈕,這時就會發(fā)生重復(fù)提交表單請求缝龄。

  • 用戶惡意進行刷單: 例如在實現(xiàn)用戶投票這種功能時汰现,如果用戶針對一個用戶進行重復(fù)提交投票挂谍,這樣會導(dǎo)致接口接收到用戶重復(fù)提交的投票信息,這樣會使投票結(jié)果與事實嚴(yán)重不符服鹅。

  • 接口超時重復(fù)提交: 很多時候 HTTP 客戶端工具都默認(rèn)開啟超時重試的機制,尤其是第三方調(diào)用接口時候百新,為了防止網(wǎng)絡(luò)波動超時等造成的請求失敗企软,都會添加重試機制,導(dǎo)致一個請求提交多次饭望。

  • 消息進行重復(fù)消費: 當(dāng)使用 MQ 消息中間件時候仗哨,如果發(fā)生消息中間件出現(xiàn)錯誤未及時提交消費信息,導(dǎo)致發(fā)生重復(fù)消費铅辞。

使用冪等性最大的優(yōu)勢在于使接口保證任何冪等性操作厌漂,免去因重試等造成系統(tǒng)產(chǎn)生的未知的問題。

四斟珊、引入冪等性后對系統(tǒng)的影響

冪等性是為了簡化客戶端邏輯處理苇倡,能放置重復(fù)提交等操作,但卻增加了服務(wù)端的邏輯復(fù)雜性和成本囤踩,其主要是:

  • 把并行執(zhí)行的功能改為串行執(zhí)行旨椒,降低了執(zhí)行效率。

  • 增加了額外控制冪等的業(yè)務(wù)邏輯堵漱,復(fù)雜化了業(yè)務(wù)功能综慎;

所以在使用時候需要考慮是否引入冪等性的必要性,根據(jù)實際業(yè)務(wù)場景具體分析勤庐,除了業(yè)務(wù)上的特殊要求外示惊,一般情況下不需要引入的接口冪等性。

五愉镰、Restful API 接口的冪等性

現(xiàn)在流行的 Restful 推薦的幾種 HTTP 接口方法中米罚,分別存在冪等行與不能保證冪等的方法,如下:

  • √ 滿足冪等

  • x 不滿足冪等

    • 可能滿足也可能不滿足冪等丈探,根據(jù)實際業(yè)務(wù)邏輯有關(guān)
圖片

六阔拳、如何實現(xiàn)冪等性

方案一:數(shù)據(jù)庫唯一主鍵

方案描述

數(shù)據(jù)庫唯一主鍵的實現(xiàn)主要是利用數(shù)據(jù)庫中主鍵唯一約束的特性,一般來說唯一主鍵比較適用于“插入”時的冪等性类嗤,其能保證一張表中只能存在一條帶該唯一主鍵的記錄糊肠。

使用數(shù)據(jù)庫唯一主鍵完成冪等性時需要注意的是,該主鍵一般來說并不是使用數(shù)據(jù)庫中自增主鍵遗锣,而是使用分布式 ID 充當(dāng)主鍵货裹,這樣才能能保證在分布式環(huán)境下 ID 的全局唯一性。

適用操作:

  • 插入操作

  • 刪除操作

使用限制:

  • 需要生成全局唯一主鍵 ID精偿;

主要流程:

圖片

主要流程:

  • ① 客戶端執(zhí)行創(chuàng)建請求弧圆,調(diào)用服務(wù)端接口赋兵。

  • ② 服務(wù)端執(zhí)行業(yè)務(wù)邏輯,生成一個分布式 ID搔预,將該 ID 充當(dāng)待插入數(shù)據(jù)的主鍵霹期,然后執(zhí)數(shù)據(jù)插入操作,運行對應(yīng)的 SQL 語句拯田。

  • ③ 服務(wù)端將該條數(shù)據(jù)插入數(shù)據(jù)庫中历造,如果插入成功則表示沒有重復(fù)調(diào)用接口。如果拋出主鍵重復(fù)異常船庇,則表示數(shù)據(jù)庫中已經(jīng)存在該條記錄吭产,返回錯誤信息到客戶端。

方案二:數(shù)據(jù)庫樂觀鎖

方案描述:

數(shù)據(jù)庫樂觀鎖方案一般只能適用于執(zhí)行“更新操作”的過程,我們可以提前在對應(yīng)的數(shù)據(jù)表中多添加一個字段,充當(dāng)當(dāng)前數(shù)據(jù)的版本標(biāo)識顶猜。這樣每次對該數(shù)據(jù)庫該表的這條數(shù)據(jù)執(zhí)行更新時,都會將該版本標(biāo)識作為一個條件邑蒋,值為上次待更新數(shù)據(jù)中的版本標(biāo)識的值。

適用操作:

  • 更新操作

使用限制:

  • 需要數(shù)據(jù)庫對應(yīng)業(yè)務(wù)表中添加額外字段按厘;

描述示例:

圖片

例如寺董,存在如下的數(shù)據(jù)表中:

圖片

為了每次執(zhí)行更新時防止重復(fù)更新,確定更新的一定是要更新的內(nèi)容刻剥,我們通常都會添加一個 version 字段記錄當(dāng)前的記錄版本遮咖,這樣在更新時候?qū)⒃撝祹希敲粗灰獔?zhí)行更新操作就能確定一定更新的是某個對應(yīng)版本下的信息造虏。
圖片

這樣每次執(zhí)行更新時候御吞,都要指定要更新的版本號,如下操作就能準(zhǔn)確更新 version=5 的信息:

<pre data-tool="mdnice編輯器" style="margin: 10px 0px; padding: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial; color: rgb(0, 0, 0); font-size: 16px; letter-spacing: 2px; text-align: left; background-color: rgb(255, 255, 255); border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">UPDATE my_table SET price=price+50,version=version+1 WHERE id=1 AND version=5 </pre>

上面 WHERE 后面跟著條件 id=1 AND version=5 被執(zhí)行后漓藕,id=1 的 version 被更新為 6陶珠,所以如果重復(fù)執(zhí)行該條 SQL 語句將不生效,因為 id=1 AND version=5 的數(shù)據(jù)已經(jīng)不存在享钞,這樣就能保住更新的冪等揍诽,多次更新對結(jié)果不會產(chǎn)生影響。

方案三:防重 Token 令牌

方案描述:

針對客戶端連續(xù)點擊或者調(diào)用方的超時重試等情況栗竖,例如提交訂單暑脆,此種操作就可以用 Token 的機制實現(xiàn)防止重復(fù)提交。

簡單的說就是調(diào)用方在調(diào)用接口的時候先向后端請求一個全局 ID(Token)狐肢,請求的時候攜帶這個全局 ID 一起請求(Token 最好將其放到 Headers 中)添吗,后端需要對這個 Token 作為 Key,用戶信息作為 Value 到 Redis 中進行鍵值內(nèi)容校驗份名,如果 Key 存在且 Value 匹配就執(zhí)行刪除命令碟联,然后正常執(zhí)行后面的業(yè)務(wù)邏輯妓美。如果不存在對應(yīng)的 Key 或 Value 不匹配就返回重復(fù)執(zhí)行的錯誤信息,這樣來保證冪等操作鲤孵。

適用操作:

  • 插入操作

  • 更新操作

  • 刪除操作

使用限制:

  • 需要生成全局唯一 Token 串壶栋;

  • 需要使用第三方組件 Redis 進行數(shù)據(jù)效驗;

主要流程:

圖片
  • ① 服務(wù)端提供獲取 Token 的接口普监,該 Token 可以是一個序列號贵试,也可以是一個分布式 ID 或者 UUID 串。

  • ② 客戶端調(diào)用接口獲取 Token鹰椒,這時候服務(wù)端會生成一個 Token 串锡移。

  • ③ 然后將該串存入 Redis 數(shù)據(jù)庫中呕童,以該 Token 作為 Redis 的鍵(注意設(shè)置過期時間)漆际。

  • ④ 將 Token 返回到客戶端,客戶端拿到后應(yīng)存到表單隱藏域中夺饲。

  • ⑤ 客戶端在執(zhí)行提交表單時奸汇,把 Token 存入到 Headers 中,執(zhí)行業(yè)務(wù)請求帶上該 Headers往声。

  • ⑥ 服務(wù)端接收到請求后從 Headers 中拿到 Token擂找,然后根據(jù) Token 到 Redis 中查找該 key 是否存在。

  • ⑦ 服務(wù)端根據(jù) Redis 中是否存該 key 進行判斷浩销,如果存在就將該 key 刪除贯涎,然后正常執(zhí)行業(yè)務(wù)邏輯。如果不存在就拋異常慢洋,返回重復(fù)提交的錯誤信息塘雳。

注意,在并發(fā)情況下普筹,執(zhí)行 Redis 查找數(shù)據(jù)與刪除需要保證原子性败明,否則很可能在并發(fā)下無法保證冪等性。其實現(xiàn)方法可以使用分布式鎖或者使用 Lua 表達式來注銷查詢與刪除操作太防。

方案四妻顶、下游傳遞唯一序列號

方案描述:

所謂請求序列號,其實就是每次向服務(wù)端請求時候附帶一個短時間內(nèi)唯一不重復(fù)的序列號蜒车,該序列號可以是一個有序 ID讳嘱,也可以是一個訂單號,一般由下游生成酿愧,在調(diào)用上游服務(wù)端接口時附加該序列號和用于認(rèn)證的 ID呢燥。

當(dāng)上游服務(wù)器收到請求信息后拿取該 序列號 和下游 認(rèn)證ID 進行組合,形成用于操作 Redis 的 Key寓娩,然后到 Redis 中查詢是否存在對應(yīng)的 Key 的鍵值對叛氨,根據(jù)其結(jié)果:

  • 如果存在呼渣,就說明已經(jīng)對該下游的該序列號的請求進行了業(yè)務(wù)處理,這時可以直接響應(yīng)重復(fù)請求的錯誤信息寞埠。

  • 如果不存在屁置,就以該 Key 作為 Redis 的鍵,以下游關(guān)鍵信息作為存儲的值(例如下游商傳遞的一些業(yè)務(wù)邏輯信息)仁连,將該鍵值對存儲到 Redis 中 蓝角,然后再正常執(zhí)行對應(yīng)的業(yè)務(wù)邏輯即可。

適用操作:

  • 插入操作

  • 更新操作

  • 刪除操作

使用限制:

  • 要求第三方傳遞唯一序列號饭冬;

  • 需要使用第三方組件 Redis 進行數(shù)據(jù)效驗使鹅;

主要流程:

圖片

主要步驟:

  • ① 下游服務(wù)生成分布式 ID 作為序列號,然后執(zhí)行請求調(diào)用上游接口昌抠,并附帶“唯一序列號”與請求的“認(rèn)證憑據(jù)ID”患朱。

  • ② 上游服務(wù)進行安全效驗,檢測下游傳遞的參數(shù)中是否存在“序列號”和“憑據(jù)ID”炊苫。

  • ③ 上游服務(wù)到 Redis 中檢測是否存在對應(yīng)的“序列號”與“認(rèn)證ID”組成的 Key裁厅,如果存在就拋出重復(fù)執(zhí)行的異常信息,然后響應(yīng)下游對應(yīng)的錯誤信息侨艾。如果不存在就以該“序列號”和“認(rèn)證ID”組合作為 Key执虹,以下游關(guān)鍵信息作為 Value,進而存儲到 Redis 中唠梨,然后正常執(zhí)行接來來的業(yè)務(wù)邏輯袋励。

上面步驟中插入數(shù)據(jù)到 Redis 一定要設(shè)置過期時間。這樣能保證在這個時間范圍內(nèi)当叭,如果重復(fù)調(diào)用接口茬故,則能夠進行判斷識別。如果不設(shè)置過期時間科展,很可能導(dǎo)致數(shù)據(jù)無限量的存入 Redis均牢,致使 Redis 不能正常工作。

七才睹、實現(xiàn)接口冪等示例

這里使用防重 Token 令牌方案徘跪,該方案能保證在不同請求動作下的冪等性,實現(xiàn)邏輯可以看上面寫的”防重 Token 令牌”方案琅攘,接下來寫下實現(xiàn)這個邏輯的代碼垮庐。

1、Maven 引入相關(guān)依賴

這里使用 Maven 工具管理依賴坞琴,這里在 pom.xml 中引入 SpringBoot哨查、Redis、lombok 相關(guān)依賴剧辐。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
    </parent>

    <groupId>mydlq.club</groupId>
    <artifactId>springboot-idempotent-token</artifactId>
    <version>0.0.1</version>
    <name>springboot-idempotent-token</name>
    <description>Idempotent Demo</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--springboot web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--springboot data redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2寒亥、配置連接 Redis 的參數(shù)

在 application 配置文件中配置連接 Redis 的參數(shù)邮府,如下:

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 相關(guān)的 Service 類溉奕,里面存在 Token 創(chuàng)建與驗證方法褂傀,其中:

  • Token 創(chuàng)建方法: 使用 UUID 工具創(chuàng)建 Token 串,設(shè)置以 “idempotent_token:“+“Token串” 作為 Key加勤,以用戶信息當(dāng)成 Value仙辟,將信息存入 Redis 中。

  • Token 驗證方法: 接收 Token 串參數(shù)鳄梅,加上 Key 前綴形成 Key叠国,再傳入 value 值,執(zhí)行 Lua 表達式(Lua 表達式能保證命令執(zhí)行的原子性)進行查找對應(yīng) Key 與刪除操作戴尸。執(zhí)行完成后驗證命令的返回結(jié)果粟焊,如果結(jié)果不為空且非0,則驗證成功校赤,否則失敗吆玖。

import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

@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();
        // 設(shè)置存入 Redis 的 Key
        String key = IDEMPOTENT_TOKEN_PREFIX + token;
        // 存儲 Token 到 Redis马篮,且設(shè)置過期時間為5分鐘
        redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
        // 返回 Token
        return token;
    }

    /**
     * 驗證 Token 正確性
     *
     * @param token token 字符串
     * @param value value 存儲在Redis中的輔助驗證信息
     * @return 驗證結(jié)果
     */
    public boolean validToken(String token, String value) {
        // 設(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 = IDEMPOTENT_TOKEN_PREFIX + token;
        // 執(zhí)行 Lua 腳本
        Long result = redisTemplate.execute(redisScript, Arrays.asList(key, value));
        // 根據(jù)返回結(jié)果判斷是否成功成功匹配并刪除 Redis 鍵值對浑测,若果結(jié)果不為空和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 與測試接口冪等性的接口,內(nèi)容如下:

import lombok.extern.slf4j.Slf4j;
import mydlq.club.example.service.TokenUtilService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
public class TokenController {

    @Autowired
    private TokenUtilService tokenService;

    /**
     * 獲取 Token 接口
     *
     * @return Token 串
     */
    @GetMapping("/token")
    public String getToken() {
        // 獲取用戶信息(這里使用模擬數(shù)據(jù))
        // 注:這里存儲該內(nèi)容只是舉例滥崩,其作用為輔助驗證岖圈,使其驗證邏輯更安全,如這里存儲用戶信息钙皮,其目的為:
        // - 1)蜂科、使用"token"驗證 Redis 中是否存在對應(yīng)的 Key
        // - 2)、使用"用戶信息"驗證 Redis 的 Value 是否匹配短条。
        String userInfo = "mydlq";
        // 獲取 Token 字符串导匣,并返回
        return tokenService.generateToken(userInfo);
    }

    /**
     * 接口冪等性測試接口
     *
     * @param token 冪等 Token 串
     * @return 執(zhí)行結(jié)果
     */
    @PostMapping("/test")
    public String test(@RequestHeader(value = "token") String token) {
        // 獲取用戶信息(這里使用模擬數(shù)據(jù))
        String userInfo = "mydlq";
        // 根據(jù) Token 和與用戶相關(guān)的信息到 Redis 驗證是否存在對應(yīng)的信息
        boolean result = tokenService.validToken(token, userInfo);
        // 根據(jù)驗證結(jié)果響應(yīng)不同信息
        return result ? "正常調(diào)用" : "重復(fù)調(diào)用";
    }

}

5、創(chuàng)建 SpringBoot 啟動類

創(chuàng)建啟動類茸时,用于啟動 SpringBoot 應(yīng)用贡定。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

6、寫測試類進行測試

寫個測試類進行測試可都,多次訪問同一個接口缓待,測試是否只有第一次能否執(zhí)行成功蚓耽。

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class IdempotenceTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    @Test
    public void interfaceIdempotenceTest() throws Exception {
        // 初始化 MockMvc
        MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
        // 調(diào)用獲取 Token 接口
        String token = mockMvc.perform(MockMvcRequestBuilders.get("/token")
                .accept(MediaType.TEXT_HTML))
                .andReturn()
                .getResponse().getContentAsString();
        log.info("獲取的 Token 串:{}", token);
        // 循環(huán)調(diào)用 5 次進行測試
        for (int i = 1; i <= 5; i++) {
            log.info("第{}次調(diào)用測試接口", i);
            // 調(diào)用驗證接口并打印結(jié)果
            String result = mockMvc.perform(MockMvcRequestBuilders.post("/test")
                    .header("token", token)
                    .accept(MediaType.TEXT_HTML))
                    .andReturn().getResponse().getContentAsString();
            log.info(result);
            // 結(jié)果斷言
            if (i == 0) {
                Assert.assertEquals(result, "正常調(diào)用");
            } else {
                Assert.assertEquals(result, "重復(fù)調(diào)用");
            }
        }
    }

}

顯示如下:

[main] IdempotenceTest:  獲取的 Token 串:980ea707-ce2e-456e-a059-0a03332110b4
[main] IdempotenceTest:  第1次調(diào)用測試接口
[main] IdempotenceTest:  正常調(diào)用
[main] IdempotenceTest:  第2次調(diào)用測試接口
[main] IdempotenceTest:  重復(fù)調(diào)用
[main] IdempotenceTest:  第3次調(diào)用測試接口
[main] IdempotenceTest:  重復(fù)調(diào)用
[main] IdempotenceTest:  第4次調(diào)用測試接口
[main] IdempotenceTest:  重復(fù)調(diào)用
[main] IdempotenceTest:  第5次調(diào)用測試接口
[main] IdempotenceTest:  重復(fù)調(diào)用

八、總結(jié)

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

  • 對于下單等存在唯一主鍵的,可以使用“唯一主鍵方案”的方式實現(xiàn)汇四。

  • 對于更新訂單狀態(tài)等相關(guān)的更新場景操作接奈,使用“樂觀鎖方案”實現(xiàn)更為簡單。

  • 對于上下游這種通孽,下游請求上游序宦,上游服務(wù)可以使用“下游傳遞唯一序列號方案”更為合理。

  • 類似于前端重復(fù)提交背苦、重復(fù)下單互捌、沒有唯一ID號的場景,可以通過 Token 與 Redis 配合的“防重 Token 方案”實現(xiàn)更為快捷行剂。

上面只是給與一些建議秕噪,再次強調(diào)一下,實現(xiàn)冪等性需要先理解自身業(yè)務(wù)需求厚宰,根據(jù)業(yè)務(wù)邏輯來實現(xiàn)這樣才合理腌巾,處理好其中的每一個結(jié)點細(xì)節(jié),完善整體的業(yè)務(wù)流程設(shè)計铲觉,才能更好的保證系統(tǒng)的正常運行澈蝙。最后做一個簡單總結(jié),然后本博文到此結(jié)束撵幽,如下:

圖片

來源 | www.mydlq.club/article/94

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末灯荧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子盐杂,更是在濱河造成了極大的恐慌逗载,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件况褪,死亡現(xiàn)場離奇詭異撕贞,居然都是意外死亡,警方通過查閱死者的電腦和手機测垛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門捏膨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事号涯∧亢” “怎么了?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵链快,是天一觀的道長誉己。 經(jīng)常有香客問我,道長域蜗,這世上最難降的妖魔是什么巨双? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮霉祸,結(jié)果婚禮上筑累,老公的妹妹穿的比我還像新娘。我一直安慰自己丝蹭,他們只是感情好慢宗,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著奔穿,像睡著了一般镜沽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贱田,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天缅茉,我揣著相機與錄音,去河邊找鬼湘换。 笑死宾舅,一個胖子當(dāng)著我的面吹牛统阿,可吹牛的內(nèi)容都是我干的彩倚。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼扶平,長吁一口氣:“原來是場噩夢啊……” “哼帆离!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起结澄,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤哥谷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后麻献,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體们妥,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年勉吻,在試婚紗的時候發(fā)現(xiàn)自己被綠了监婶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖惑惶,靈堂內(nèi)的尸體忽然破棺而出煮盼,到底是詐尸還是另有隱情,我是刑警寧澤带污,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布僵控,位于F島的核電站,受9級特大地震影響鱼冀,放射性物質(zhì)發(fā)生泄漏报破。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一千绪、第九天 我趴在偏房一處隱蔽的房頂上張望泛烙。 院中可真熱鬧,春花似錦翘紊、人聲如沸蔽氨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鹉究。三九已至,卻和暖如春踪宠,著一層夾襖步出監(jiān)牢的瞬間自赔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工柳琢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绍妨,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓柬脸,卻偏偏與公主長得像他去,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子倒堕,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容