在业务开发中,我们常会面对防止重复请求的问题。当服务端对于请求的响应涉及数据的修改,或状态的变更时,可能会造成极大的危害。重复请求的后果在交易系统、售后维权,以及支付系统中尤其严重。

如何避免并发情况下的重复提交

在传统的restful风格的项目中,防止重复提交,通常做法是:后端生成一个唯一的提交令牌(uuid),并存储在服务端。页面提交请求携带这个提交令牌,后端验证并在第一次验证后删除该令牌,保证提交请求的唯一性。

上述的思路其实没有问题的,但是需要前后端都稍加改动,如果在业务开发完在加这个的话,改动量未免有些大了,本节的实现方案无需前端配合,纯后端处理。

利用分布式锁

相同的请求在同一时间只能被处理一次,利用分布式锁可以非常方便地解决这个问题。

思路:

1、自定义注解 @NoRepeatSubmit 标记所有Controller中的提交请求。
2、通过拦截器对所有标记了 @NoRepeatSubmit 的方法拦截。
3、在业务方法执行前,获取当前用户的 token(或者JSessionId)+ 当前请求地址,作为一个唯一 KEY,去获取 Redis 分布式锁(如果此时并发获取,只有一个线程会成功获取锁)。
4、业务方法执行后,释放锁。

拦截器类

  1. import lombok.extern.slf4j.Slf4j;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.stereotype.Component;
  4. import org.springframework.web.method.HandlerMethod;
  5. import org.springframework.web.servlet.HandlerInterceptor;
  6. import org.springframework.web.servlet.ModelAndView;
  7. import javax.servlet.http.HttpServletRequest;
  8. import javax.servlet.http.HttpServletResponse;
  9.  
  10. @Component
  11. @Slf4j
  12. public class ViolationInterceptor extends HandlerInterceptorAdapter {
  13. @Autowired
  14. private RedisLock redisLock;
  15.  
  16. @Override
  17. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex) throws Exception {
  18. }
  19. @Override
  20. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object hadnler, ModelAndView ex) throws Exception {
  21. }
  22. @Override
  23. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  24. String key = request.getHeader("requestId");
  25. boolean isSuccess = redisLock.tryLock(key, clientId, lockSeconds);
  26. if (isSuccess) {
  27. log.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
  28. // 获取锁成功, 执行进程
  29. Object result;
  30. try {
  31. requestIds.add(requestId);
  32. return super.preHandle(request, response, handler);
  33. } finally {
  34. // 释放锁
  35. redisLock.releaseLock(key, clientId);
  36. log.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
  37. }
  38. }else {
  39. throw new IllegalArgumentException("Violation Request; Reason requestId already registered");
  40. }
  41. }
  42. }

利用数据库的唯一索引

如果设置了唯一约束,那么同一条数据再次插入数据库时,数据库会报唯一索引的错误,这个时候后台对这个异常进行处理并返回给前端提示。

思路:

接口A接收到请求之后,对请求信息hash运算,得到hash值hashCodeA;
保存hashCodeA 到数据库,并且对应的数据库的列(column)满足unique约束;
保存成功之后,才进行正常业务逻辑处理,比如提交订单;
服务器B接收到相同的请求后,也得到相同的hash值,hashCodeA,
服务器B保存hashCodeA 到数据库,肯定失败,因为相同的hash值已经存在;
因为保存失败,所以后面的业务逻辑不会执行。

缓存计数器
由于数据库的操作比较消耗性能,了解到redis的计数器也是原子性操作。果断采用计数器。既可以提高性能,还不用存储,而且能提升qps的峰值。

以支付为例子:

每次request进来则新建一个以orderId为key的计数器,然后+1。

如果>1(不能获得锁): 说明有操作在进行,删除。
如果=1(获得锁): 可以操作。
操作结束(删除锁):删除这个计数器。

本文已通过「原本」原创作品认证,转载请注明文章出处及链接。

Java最后更新:2022-11-6
夏日阳光
  • 本文由 夏日阳光 发表于 2019年10月11日
  • 本文为夏日阳光原创文章,转载请务必保留本文链接:https://www.pieruo.com/112.html
匿名

发表评论

匿名网友
:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:
确定

拖动滑块以完成验证
加载中...