springboot实现全局api幂等(后端实现防抖功能)

1.概念

幂等性原本是数学中的概念,在开发中意为:

对于同一个系统,在同样条件下,一次请求和重复多次请求对资源的影响是一致的,就称该操作为幂等的

2.业务场景

什么意思呢?

用户在实际操作中,有可能因为手抖或者网络波动,短时间内将一个相同的请求重复提交多次。系统也会多次执行这些请求,就有可能会出现一些问题。

例如创建新用户功能:

我们一般会先验证用户名是否已存在,再去insert用户信息。

但由于用户手抖,极短的时间点了两次提交按钮。前端没防住的情况下两个相同参数的请求几乎同时到达了后端。两个请求由于间隔时间太短,验证用户名是否存在的时候都成功了,于是数据库insert了两条除id外,其余字段都相同的数据。这可能会对以后的业务造成意料之外的影响。

对于这种情况,我们有很多解决方式。例如对这个接口采用分布式锁、使用lombok的@Synchronized注解等。

但事实上,大部分的POST、PUT、DEL请求几乎都需要预防幂等问题。每个接口都去逐一实现分布式锁工作量太大了,所以在项目中我通过springframework自带的HandlerInterceptor实现了全局的分布式锁。

3.代码实现

(1)引入依赖
<dependency>
       <groupId>org.redisson</groupId>
       <artifactId>redisson-spring-boot-starter</artifactId>
       <version>3.12.5</version>
</dependency>
(2)我们先来了解一下HandlerInterceptor是什么
​
public interface HandlerInterceptor {
  default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    return true;
  }

  default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
  }

  default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
  }
}

​

这其实是一个springboot的拦截器,有三个接口,可以简单的理解为:

preHandle——在处理请求之前执行

postHandle——在处理请求后、生成视图前执行

afterCompletion——在完全处理完请求、整个请求的周期结束时执行

我们需要用到preHandle来给请求上锁、afterCompletion来给请求释放锁。

(3)写一个自己的拦截器,实现HandlerIntecepter,重写preHandle和afterCompletion
public class MyInterceptor implements HandlerInterceptor {

  private final Redisson redisson;  // 通过构造器的方式注入redisson
  private ThreadLocal<RLock> lockThreadLocal = new ThreadLocal<>();  // 线程隔离

  public MyInterceptor(Redisson redisson) {
    this.redisson = redisson;
  }

  /**
   * 在请求处理请求之前执行该方法
   * @param request
   * @param response
   * @param handler
   * @return
   * @throws Exception
   */
  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   
    String token = request.getHeader("x-token");  //从request中得到token,用你的token解析器解析出用户id(或其他用户唯一标识)
    String userId = MyTokenUtils.parseToken(token);
    String method = request.getMethod(); // 请求方式
    String path = request.getServletPath(); // 请求路径
    if (!"GET".equals(method)) {
      try {
        RLock rLock = null;
        String key = "ApiIdempotent_" + userId + path;
        rLock = redisson.getLock(key);
        boolean b = rLock.tryLock(5, TimeUnit.SECONDS); // 尝试获取锁的时间 , 锁的持有时间 , 时间单位
        if (!b) {
          throw new MyException().setCode(10018).setMsg("操作太快啦,请稍后重试!");
        }
        lockThreadLocal.set(rLock);
      } catch (InterruptedException e) {
        throw new MyException().setCode(10018).setMsg("获取分布式锁被中断");
      }
    }
    
    return true;
  }

  /**
   * 在完全处理完请求后执行该方法
   * @param request
   * @param response
   * @param handler
   * @param ex
   * @throws Exception
   */
  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
    // 释放api幂等的分布式锁
    String method = request.getMethod(); // 请求方式
    if (!"GET".equals(method)) {
      RLock rLock = null;
      rLock = lockThreadLocal.get();
      if (rLock != null && rLock.isHeldByCurrentThread()) {
        rLock.unlock();
      }
    }
  }
  
}
(4)解释代码

1.大致流程是在处理业务前给非Get请求上锁,用请求路径+用户id作为锁的唯一标识。在处理完成业务后释放掉锁。这样的操作确保了两件事:(1)防止单个用户上个请求还没执行完成就开始执行下个请求。(2)多个用户间不影响彼此的调用。

2.rLock.tryLock()方法可以放入三个参数。尝试获取锁的时间、锁的持有时间、时间单位。因为锁一定会在afterCompletion里销毁掉,所以我没设置第二个参数。为了安全期间,大家可以自行设置。

3.代码中我使用了ThreadLocal<RLock>,这是一个线程隔离的安全容器。我使用它来在preHandle里保存锁,在afterCompletion里取出锁并释放掉。如果不用ThreadLocal的话。有可能A线程的锁会被B线程取出并释放掉,不能保证线程安全。

4.这里我拦截了所有的非GET请求。因为GET不会对我们数据库资源用影响,而其余请求会写入数据库所以可以无脑全部拦截。但如果你的项目不是严格的Rest风格,或者不想无脑拦截,可以在这自己写一个黑名单类,只拦截指定的路径。

5.如果写了想测试可以使用JMeter测试工具,自行搜索使用方法。可以模拟多个线程几乎同时发起请求。查看拦截情况。

4.致谢

感谢看完,如果对你有帮助,不要吝惜你的点赞评论收藏关注哟~

有问题欢迎讨论。

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
SpringBoot中,可以通过定义一个全局的异常处理器和返回值处理器来实现全局统一返回。 1. 全局异常处理器 在SpringBoot中,可以通过实现`@ControllerAdvice`注解的类来定义一个全局的异常处理器。这个类中的方法可以捕获所有Controller中抛出的异常,并进行统一处理。 示例代码: ```java @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) @ResponseBody public ResultVO handleException(Exception e) { ResultVO resultVO = new ResultVO(); resultVO.setCode(ResultEnum.ERROR.getCode()); resultVO.setMsg(e.getMessage()); return resultVO; } } ``` 在上面的例子中,我们定义了一个`GlobalExceptionHandler`类,并使用`@ControllerAdvice`注解标记它是一个全局异常处理器。在这个类中,我们定义了一个`handleException`方法,它使用`@ExceptionHandler`注解标记它可以处理所有类型的异常。在方法中,我们可以根据异常类型进行不同的处理,并返回一个统一的`ResultVO`对象。 2. 全局返回值处理器 在SpringBoot中,可以通过实现`ResponseBodyAdvice`接口来定义一个全局的返回值处理器。这个类中的方法可以对Controller返回的结果进行统一处理。 示例代码: ```java @ControllerAdvice public class GlobalResponseHandler implements ResponseBodyAdvice<Object> { @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { return true; } @Override public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { ResultVO resultVO = new ResultVO(); resultVO.setCode(ResultEnum.SUCCESS.getCode()); resultVO.setMsg(ResultEnum.SUCCESS.getMsg()); resultVO.setData(o); return resultVO; } } ``` 在上面的例子中,我们定义了一个`GlobalResponseHandler`类,并实现了`ResponseBodyAdvice`接口。在类中,我们实现了`supports`和`beforeBodyWrite`方法。`supports`方法返回`true`表示这个处理器可以处理所有类型的返回值。`beforeBodyWrite`方法对Controller返回的结果进行统一处理,将返回值包装成一个统一的`ResultVO`对象。 通过定义全局异常处理器和返回值处理器可以实现全局统一返回,统一格式的返回值可以方便前端调用和处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值