一、什么是接口冪等性
接口冪等性就是用戶對於同一操作發起的一次請求或者多次請求的結果是一致的,不會因為多次點擊而產生了副作用。舉個最簡單的例子,支付過程中,用戶購買商品后支付,支付扣款成功,但是返回結果的時候網絡異常,此時錢已經扣了,用戶再次點擊按鈕,此時會進行第二次扣款,返回結果成功,用戶查詢余額返發現多扣錢了,流水記錄也變成了兩條,這就沒有保證接口的冪等性。
簡單的說就是 一個用戶對於同一個操作發起一次或多次的請求,請求的結果一致。不會因為多次點擊而產生多條數據。
二、什么情況需要對接口進行冪等性設置
- Get 方法用於獲取資源。其一般不會也不應當對系統資源進行改變,所以是冪等的。
- Post 方法一般用於創建新的資源。其每次執行都會新增數據,所以不是冪等的。
- Put 方法一般用於修改資源。該操作則分情況來判斷是不是滿足冪等,更新操作中直接根據某個值進行更新,也能保持冪等。不過執行累加操作的更新是非冪等。
- Delete 方法一般用於刪除資源。該操作則分情況來判斷是不是滿足冪等,當根據唯一值進行刪除時,刪除同一個數據多次執行效果一樣。不過需要注意,帶查詢條件的刪除則就不一定滿足冪等了。例如在根據條件刪除一批數據后,這時候新增加了一條數據也滿足條件,然后又執行了一次刪除,那么將會導致新增加的這條滿足條件數據也被刪除。
三、接口冪等性的處理方式有很多,數據庫唯一主鍵、數據庫樂觀鎖、令牌表+唯一約束、下游傳遞唯一序列號、同步鎖(單體項目)、分布式鎖如redis 等等,這里只闡述使用token令牌的方式:
- 客戶端請求服務器接口獲取token。
- 服務器將token返給客戶端的同時將信息(這里包括用戶信息、和token)存儲到redis中。
- 請求業務接口時,將token放入header中進行接口請求。
- 服務器通過用戶信息和token檢查token是否還存在,如果存在就刪除,如果不存在直接返回結果。
- 響應服務器請求結果。
四、具體代碼實現
1、創建一個springboot項目。
2、maven依賴:
<dependencies>
<!-- mysql:MyBatis相關依賴 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!-- mysql:mysql驅動 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mysql:阿里巴巴數據庫連接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--springboot data redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
3、redis配置
redis: host: 127.0.0.1 port: 6379 database: 0 timeout: 1000 password: 961230 lettuce: pool: max-active: 100 max-wait: -1 min-idle: 0 max-idle: 20 ssl: false
4、創建controller層
package com.dongliang.lcnorder.controller; import com.dongliang.lcnorder.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * @author D-L * @version 1.0.0 * @ClassName UserController.java * @Description 接口冪等性實現--Token令牌 * @createTime 2021-05-26 16:40:00 */ @Slf4j @RestController public class UserController { @Autowired private UserService userService; /** * 獲取 Token 接口 * @return */ @GetMapping("/getTokenInfo/{uid}") public String getTokenInfo(@PathVariable("uid") String uid) { return userService.generateToken(uid); } /** * 修改用戶賬戶余額 (賬戶余額加100.0) Modify user account balance * @param token * @param uid 用戶編碼 * @return 返回調用結果 */ @PostMapping("/modifyUserAccountBalance/{uid}") public String modifyUserAccountBalance(@RequestHeader(value = "token") String token , @PathVariable("uid") String uid) { // 根據 Token 和與用戶相關的信息到 Redis 驗證是否存在對應的信息 boolean result = userService.modifyUserAccountBalance(token, uid); return result ? "正常調用" : "重復調用"; } }
5、創建service類
package com.dongliang.lcnorder.service; import com.dongliang.lcnorder.dao.UserDao; 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; import java.util.Arrays; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * @author D-L * @version 1.0.0 * @ClassName UserService.java * @Description 接口冪等性實現--Token令牌 * @createTime 2021-05-26 16:27:00 */ @Slf4j @Service public class UserService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private UserDao userDao; /** * 存入 Redis 的 Token 鍵的前綴 */ private static final String IDEMPOTENT_TOKEN_PREFIX = "user_token:"; /** * 創建 Token 存入 Redis,並返回該 Token * @param value * @return */ public String generateToken(String uid) { // 實例化生成 ID 工具對象 String token = UUID.randomUUID().toString(); // 設置存入 Redis 的 Key String key = IDEMPOTENT_TOKEN_PREFIX + token; // 存儲 Token 到 Redis,且設置過期時間為5分鍾 redisTemplate.opsForValue().set(key, uid, 5, TimeUnit.MINUTES); // 返回 Token return token; } /** * 修改賬戶余額 * @param token * @param uid * @return */ public boolean modifyUserAccountBalance(String token, String uid) { //驗證 Token 正確性 boolean validToken = this.validToken(token, uid); if(validToken){ //執行具體業務邏輯 賬戶余額加100.00 int result = userDao.modifyUserAccountBalance(uid); return true; } return false; } /** * 驗證 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<Long> 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; } }