聊聊幂等设计

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

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