什么是接口幂等性?

数学中:在一次元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同;在二次元运算为幂等时,自己重复运算的结果等于它自己的元素。

计算机学中:幂等指多次操作产生的影响只会跟一次执行的结果相同,通俗的说:某个行为重复的执行,最终获取的结果是相同的,不会因为重复执行对系统造成变化。

例如:用户购买商品后支付扣款成功,但是此时网络发生了异常,导致返回结果失败。因为没收到返回结果,用户就会再次点击付款按钮,就会多付了一笔钱,从而用户付款了两次,这显然是不行的。

即一次操作要么成功要么失败,再次操作则是重新执行。

有如下场景:

  • 用户重复下订单:当用户下单时,因为网络问题或者手速过快,导致重复下单。
  • 消息重复消费:当使用 MQ 消息中间件时候,如果消息中间件发生异常出现错误未及时提交消费信息,导致消息被重复消费。
  • 抽奖活动(券):当用户参加抽奖活动需要消耗抽奖券时,如果出现并发请求导致抽奖券余额更新错误。
  • 重复提交表单:当用户填写表单提交时,可能会因为用户点多次连击提交或者网络波动导致服务端未及时响应,会导致用户重复的提交表单,就出现了同一个表单多次请求。

这些只是我们常见的一些状况,还需要根据自己的项目的实际情况进行分析,判断是否需要幂等操作。

涉及到数据的修改一般会涉及到幂等性问题,就是执行多次与执行一次结果一样

以下操作是基于分布式锁-----token令牌实现的。就是生成全局的唯一 id 实现,这里是基于 redis 来实现的。还可以有其他方案:如snowflake 雪花算法美团 Leaf 算法滴滴 TinyID 算法百度 Uidgenerator 算法uuid

什么是token

Token在计算机身份认证中是令牌(临时)的意思,在词法分析中是标记的意思。一般作为邀请、登录系统使用。

这种机制适用范围较广,有多种不同的实现方式。其核心思想是每一次操作生成唯一的身份码,操作完成身份码也就不存在。

一个 Token 在操作的每一个阶段只有一次执行权,一旦执行成功就直接保存执行结果。

具体操作如图:

一、拦截器实现

1、创建创建redisservice类

见aop实现

2、创建 tokenservice 类

见aop实现

3、自定义注解

见aop实现

4、创建拦截器

拦截器中需要进行前置拦截,也是通过 controller 中的方法是否有自定义的注解来判断

@Component
public class IdempontentInterceptor implements HandlerInterceptor {
    @Autowired
    TokenService tokenService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断 handler 的类型是否为 HandlerMethod
        if (!(handler instanceof HandlerMethod)){
            return true;
        }

        //获取方法
        Method method = ((HandlerMethod) handler).getMethod();
        //获取方法上的注解
        AutoIdempontent autoIdempontent = method.getAnnotation(AutoIdempontent.class);
        //判断方法是否有该注解
        if (autoIdempontent != null){
            try {
                return tokenService.checkToken(request);
            } catch (IdempontentException e) {
                throw e;
            }
        }
        return true;
    }
}

5、配置类配置拦截器

在配置类中配置拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    IdempontentInterceptor interceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //设置拦截的路径
        registry.addInterceptor(interceptor).addPathPatterns("/**");
    }
}

6、测试

见aop实现中的 controller 测试

二、aop实现

1、创建redisservice类

用于对 redis 中存入数据

@Service
public class RedisService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    //1、存储key-value
    public boolean setEx(String key,String value,Long expireTime){
        boolean result = false;
        try {
            //获取操作
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            //redis存入key-value
            ops.set(key, value);
            //设置过期时间
            stringRedisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            //设置返回值
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    //判断key是否存在
    public boolean isExists(String key){
        return stringRedisTemplate.hasKey(key);
    }

    //移除key-value
    public boolean remove(String key){
        if (isExists(key)){
            return stringRedisTemplate.delete(key);
        }
        return false;
    }

}

2、创建 tokenservice 类

用于创建token、判断是否有token

@Service
public class TokenService {
    @Autowired
    RedisService redisService;

    //创建令牌,也就是在 redis 中存入key-value
    public String createToken(){
        String key = UUID.randomUUID().toString();
        redisService.setEx(key, key, 10000L);
        return key;
    }

    //判断token
    public boolean checkToken(HttpServletRequest request) throws IdempontentException {
        String token = request.getHeader("token");
        //1、判断请求头中是否有token
        if (StringUtils.isEmpty(token)){
            //2、如果请求头中为空,则再从请求体中判断
            token = request.getParameter("token");
            if (StringUtils.isEmpty(token)){//判断请求体中token
                throw new IdempontentException("token 不存在");
            }
        }
        //3、判断 token 是否存在
        if (!redisService.isExists(token)){//redis key 可能过期了,被 redis 删除
            throw new IdempontentException("重复操作");
        }
        //4、如果 token 存在,则移除token
        boolean result = redisService.remove(token);
        if (!result){//令牌过期或已消费过
            throw new IdempontentException("重复操作");
        }
        return true;
    }
}

自定义异常

public class IdempontentException extends Exception{
    public IdempontentException(String message) {
        super(message);
    }
}

因为在其中自定义了异常,所以通过全局的异常处理来将异常信息输出

@RestControllerAdvice
public class GlobalException {
    @ExceptionHandler(IdempontentException.class)
    public String idempontentException(IdempontentException e){
        return e.getMessage();
    }
}

3、自定义注解

用于判断方法检查是否有token

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempontent {
}

4、定义切面

@Component
@Aspect
public class IdempontentAspect {
    @Autowired
    TokenService tokenService;

    @Pointcut("@annotation(com.xrr.idempontent.ann.AutoIdempontent)")
    public void pcl(){

    }

    @Before("pcl()")
    public void before() throws IdempontentException {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        try {
            tokenService.checkToken(request);
        } catch (IdempontentException e) {
            throw e;
        }
    }

}

在切面中通过自定义的注解来定义切点,再通过 tokenservice 来检查是否有 token

5、controller测试

在controller中定义三个方法,创建token、测试token

@RestController
public class HelloController {
    @Autowired
    TokenService tokenService;

    @GetMapping("/getToken")
    public String getToken(){
        return tokenService.createToken();
    }

    @PostMapping("/h1")
    @AutoIdempontent
    public String hello1(){
        return "hello1";
    }

    @PostMapping("/h2")
    public String hello2(){
        return "hello2";
    }
}
posted on 2023-06-15 23:15  xashould  阅读(185)  评论(0编辑  收藏  举报