聊聊幂等设计

Posted JAVA后端技术之路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊幂等设计相关的知识,希望对你有一定的参考价值。

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


以上是关于聊聊幂等设计的主要内容,如果未能解决你的问题,请参考以下文章

聊聊接 API 口幂等性设计

聊聊幂等设计

每个工程师都应该了解的:聊聊幂等

每个工程师都应该了解的:聊聊幂等

后端程序员逃不掉的幂等设计

聊聊“幂等”