接口冪等性校驗
一、概念
冪等(idempotent、idempotence)是一個數學與計算機學概念,常見於抽象代數中。
在編程中.一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。冪等函數,或冪等方法,是指可以使用相同參數重複執行,並能獲得相同結果的函數。
這些函數不會影響系統狀態,也不用擔心重複執行會對系統造成改變。例如,“getUsername()和setTrue()”函數就是一個冪等函數.
冪等性, 通俗的說就是一個接口, 多次發起同一個請求, 必須保證操作只能執行一次,比如:
- 訂單接口, 不能多次創建訂單
- 支付接口, 重複支付同一筆訂單隻能扣一次錢
- 支付寶回調接口, 可能會多次回調, 必須處理重複回調
- 普通表單提交接口, 因爲網絡超時等原因多次點擊提交, 只能成功一次
等等
二、常見解決方案
-
唯一索引 – 防止新增髒數據
-
token機制 – 防止頁面重複提交
-
悲觀鎖 – 獲取數據的時候加鎖(鎖表或鎖行)
-
樂觀鎖 – 基於版本號version實現, 在更新數據那一刻校驗數據
-
分佈式鎖 – redis(jedis、redisson)或zookeeper實現
-
狀態機 – 狀態變更, 更新數據時判斷狀態
三、本文實現方案
本文采用第2種方式實現, 即通過redis + token機制實現接口冪等性校驗
爲需要保證冪等性的每一次請求創建一個唯一標識token, 先獲取token, 並將此token存入redis, 請求接口時, 將此token放到header或者作爲請求參數請求接口, 後端接口判斷redis中是否存在此token,如果存在, 正常處理業務邏輯, 並從redis中刪除此token, 那麼, 如果是重複請求, 由於token已被刪除, 則不能通過校驗, 返回請勿重複操作提示,如果不存在, 說明參數不合法或者是重複請求, 返回提示即可
四、核心代碼
依賴庫
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
implementation 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
自定義註解
/**
* 在需要保證 接口冪等性 的Controller的方法上使用此註解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
TokenServiceImpl
/**
* token業務處理,提供token創建、token驗證接口
* Created by double on 2019/7/11.
*/
@Service
public class TokenServiceImpl implements TokenService {
private static final String TOKEN_NAME = "token";
@Autowired
StringRedisTemplate stringRedisTemplate;
@Override
public ServerResponse createToken() {
//通過UUID來生成token
String tokenValue = "idempotent:token:" + UUID.randomUUID().toString();
//將token放入redis中,設置有效期爲60S
stringRedisTemplate.opsForValue().set(tokenValue, "0", 60, TimeUnit.SECONDS);
return ServerResponse.success(tokenValue);
}
/**
* @param request
*/
@Override
public void checkToken(HttpServletRequest request) {
String token = request.getHeader(TOKEN_NAME);
if (StringUtils.isBlank(token)) {
token = request.getParameter(TOKEN_NAME);
if (StringUtils.isBlank(token)) {
//沒有攜帶token,拋異常,這裏的異常需要全局捕獲
throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg());
}
}
//token不存在,說明token已經被其他請求刪除或者是非法的token
if (!stringRedisTemplate.hasKey(token)) {
throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());
}
boolean del = stringRedisTemplate.delete(token);
if (!del) {
//token刪除失敗,說明token已經被其他請求刪除
throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());
}
}
}
IdempotentTokenInterceptor
/**
* 接口冪等性校驗攔截器
* Created by double on 2019/7/11.
*/
public class IdempotentTokenInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
//冪等性校驗, 校驗通過則放行, 校驗失敗則拋出異常, 並通過統一異常處理返回友好提示
ApiIdempotent apiIdempotent = handlerMethod.getMethod().getAnnotation(ApiIdempotent.class);
if (apiIdempotent != null) {
tokenService.checkToken(request);
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
完成代碼參考我的github:https://github.com/huchao1009/idempotent.git
五、測試
測試接口Controller
/**
* 冪等性測試接口
* Created by double on 2019/7/11.
*/
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private TestService testService;
@ApiIdempotent
@PostMapping("testIdempotent")
public ServerResponse testIdempotent() {
return testService.testIdempotence();
}
}
1、獲取token
http://localhost:8081/token
{
"status": 0,
"msg": "idempotent:token:80dd47ed-fa63-4b30-82fa-f3ee4fd64a50",
"data": null
}
2、驗證接口安全性
http://localhost:8081/test/testIdempotent?token=idempotent:token:b9ae797d-ed1a-4dbc-a94f-b7e45897f0f5
第一次請求
{
"status": 0,
"msg": "test idempotent success",
"data": null
}
重複請求
{
"status": 1,
"msg": "請勿重複操作",
"data": null
}
3、利用jmeter測試工具模擬50個併發請求, 獲取一個新的token作爲參數
設置50個線程請求一次
設置請求IP、Path、參數等信息
查看執行結果,可以看到只有一個請求成功,其他的請求都返回錯誤