聊聊冪等設計

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到。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章