聊聊幂等设计
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内算是重复提交。
1) (
public class IdempotenceAspect {
private static final Logger logger = LoggerFactory.getLogger(IdempotenceAspect.class);
private JedisPool jedisPool;
/**
* controller切点
*/
"execution(public * link.anzy.web.app.controller..*(..)) && @annotation(link.anzy.annon.Idempotence)") (
private void controllerAspect() {}
/**
* 环绕通知,加锁,如果加锁失败为重复提交,终止后续执行
* @param joinPoint
*/
"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
*/
"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());
}
}
public Response saveOrder( 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到。
以上是关于聊聊幂等设计的主要内容,如果未能解决你的问题,请参考以下文章