HTTP GET方法用於獲取資源,不應有副作用,所以是冪等的。比如:GET http://www.bank.com/account/123456,不會改變資源的狀態,不論調用一次還是N次都沒有副作用。請注意,這裏強調的是一次和N次具有相同的副作用,而不是每次GET的結果相同。GET http://www.news.com/latest-news這個HTTP請求可能會每次得到不同的結果,但它本身並沒有產生任何副作用,因而是滿足冪等性的。
HTTP DELETE方法用於刪除資源,有副作用,但它應該滿足冪等性。比如:DELETE http://www.forum.com/article/4231,調用一次和N次對系統產生的副作用是相同的,即刪掉id爲4231的帖子;因此,調用者可以多次調用或刷新頁面而不必擔心引起錯誤。
HTTP POST方法用於創建資源,所對應的URI並非創建的資源本身,而是去執行創建動作的操作者,有副作用,不滿足冪等性。比如:POST http://www.forum.com/articles的語義是在http://www.forum.com/articles下創建一篇帖子,HTTP響應中應包含帖子的創建狀態以及帖子的URI。兩次相同的POST請求會在服務器端創建兩份資源,它們具有不同的URI;所以,POST方法不具備冪等性。
HTTP PUT方法用於創建或更新操作,所對應的URI是要創建或更新的資源本身,有副作用,它應該滿足冪等性。比如:PUT http://www.forum/articles/4231的語義是創建或更新ID爲4231的帖子。對同一URI進行多次PUT的副作用和一次PUT是相同的;因此,PUT方法具有冪等性。
綜上所述,post方法下需要考慮冪等問題。
HTTP POST 操作既不是安全的,也不是冪等的(至少在HTTP規範裏沒有保證)。當我們因爲反覆刷新瀏覽器導致多次提交表單,多次發出同樣的POST請求,導致遠端服務器重複創建出了資源。
所以,對於電商應用來說,第一對應的後端 WebService 一定要做到冪等性,第二服務器端收到 POST 請求,在操作成功後必須302跳轉到另外一個頁面,這樣即使用戶刷新頁面,也不會重複提交表單。
解決方案
一、前端解決 js做判斷
二、後端
1、利用AOP+redis加判斷,設置8S內算是重複提交。
@Aspect
@Component
@Order(1)
public class IdempotenceAspect {
private static final Logger logger = LoggerFactory.getLogger(IdempotenceAspect.class);
@Autowired
private JedisPool jedisPool;
/**
* controller切點
*/
@Pointcut("execution(public * link.anzy.web.app.controller..*(..)) && @annotation(link.anzy.annon.Idempotence)")
private void controllerAspect() {}
/**
* 環繞通知,加鎖,如果加鎖失敗爲重複提交,終止後續執行
* @param joinPoint
*/
@Around("controllerAspect()")
public Object methodAround(ProceedingJoinPoint joinPoint) throws Throwable {
String lockKey = generateLockKey(joinPoint);
if (RedisDistributedLock.tryGetDistributedLock(
this.jedisPool.getResource(),
lockKey,
"idempotence",
8000)) {
return joinPoint.proceed();
}
throw new BusinessException(SwellResponseCodeEnum.DO_NOT_REPEAT_SUBMISSION);
}
/**
* 後置通知,解鎖
* @param joinPoint
*/
@After("controllerAspect()")
public void methodAfter(JoinPoint joinPoint) {
String lockKey = generateLockKey(joinPoint);
// 解鎖
RedisDistributedLock.releaseDistributedLock(this.jedisPool.getResource(), lockKey, lockKey);
}
/**
* 生成分佈式鎖的key
* @param joinPoint
* @return
*/
private String generateLockKey(JoinPoint joinPoint) {
StringBuilder sb = new StringBuilder();
sb.append(joinPoint.getTarget().getClass().getName())
.append(".").append(joinPoint.getSignature().getName());
Long currentUserId = CurrentUserUtils.getCurrentUserId();
if (currentUserId != null) {
sb.append(currentUserId).append(CurrentUserUtils.getCurrentUserType());
} else {
sb.append(WebContextUtils.getHttpServletRequest().getHeader(HttpConsts.DEVICE_ID_HEADER));
}
sb.append(JSON.toJSONString(joinPoint.getArgs()));
return MD5Util.getMD5String(sb.toString());
}
}
@Idempotence
@PostMapping(value = "/save/order")
public Response saveOrder(@RequestBody BuyerOrderRequest buyerOrderRequest) throws BusinessException{
SaveBuyerOrderVO saveBuyerOrderVO = buyerOrderService.saveOrder(buyerOrderRequest);
return ResponseUtils.success(saveBuyerOrderVO);
}
2、新增表單時,在action的add()方法中生成uuid,並同時保存在redis中(有效期可爲1小時),跳轉到新增表單頁面,在表單隱藏域中通過tokenid保存uuid,在數據併發保存時,首次進入線程的先通過tokenid拿到redis的key並進行key移除操作,進行數據的操作保存,其他重複點擊的線程從redis中因得不到key,不進行任何數據處理,提示數據已經提交,勿重複操作。
我的方法可能不是最好的,思路希望大家都能夠get到。