接口幂等性如何实现?

导读

  • 转载自幂等性如何实现?深刻了解一波!!!
  • 如今这个时代你们可能最关心的就是钱了,那么有没有想过你银行转帐给你没有一次是转多的,要么失败,要么成功,为何不能失误一下多转一笔呢?醒醒吧年轻人,别作梦了,作银行的能那么傻x吗?
  • 今天咱们就来谈一谈为何银行转帐不能多给我转一笔?关乎到钱的问题,小伙伴们打起精神!!!
  • 要想要理解上述的疑惑,不得不提的一个概念就是幂等性,至于什么是幂等性,如何经过代码实现幂等性,下面将会详细讲述。

什么是幂等性

  • 所谓幂等性通俗的将就是一次请求和屡次请求同一个资源产生相同的反作用。用数学语言表达就是f(x)=f(f(x))
  • 维基百科的幂等性定义以下:
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中一个幂等操做的特色是其任意屡次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可使用相同参数重复执行,并能得到相同结果的函数。这些函数不会影响系统状态,也不用担忧重复执行会对系统形成改变。例如,“setTrue()”函数就是一个幂等函数,不管屡次执行,其结果都是同样的,更复杂的操做幂等保证是利用惟一交易号(流水号)实现.

为何须要幂等性

  • 在系统高并发的环境下,颇有可能由于网络,阻塞等等问题致使客户端或者调用方并不能及时的收到服务端的反馈甚至是调用超时的问题。总之,就是请求方调用了你的服务,可是没有收到任何的信息,彻底懵逼的状态。好比订单的问题,可能会遇到以下的几个问题:
  1. 建立订单时,第一次调用服务超时,再次调用是否产生两笔订单?
  2. 订单建立成功去减库存时,第一次减库存超时,是否会多扣一次?
  3. 订单支付时,服务端扣钱成功,可是接口反馈超时,此时再次调用支付,是否会多扣一笔呢?
  • 做为消费者,前两种能接受,第三种状况就MMP了,哈哈哈!!!这种状况通常有以下两种解决方式
  1. 服务方提供一个查询操做是否成功的api,第一次超时以后,调用方调用查询接口,若是查到了就走成功的流程,失败了就走失败的流程。
  2. 另外一种就是服务方须要使用幂等的方式保证一次和屡次的请求结果一致。

HTTP的幂等性

  • GET:只是获取资源,对资源自己没有任何反作用,自然的幂等性。
  • HEAD:本质上和GET同样,获取头信息,主要是探活的做用,具备幂等性。
  • OPTIONS:获取当前URL所支持的方法,所以也是具备幂等性的。
  • DELETE:用于删除资源,有反作用,可是它应该知足幂等性,好比根据id删除某一个资源,调用方能够调用N次而不用担忧引发的错误(根据业务需求而变)。
  • PUT:用于更新资源,有反作用,可是它应该知足幂等性,好比根据id更新数据,调用屡次和N次的做用是相同的(根据业务需求而变)。
  • POST:用于添加资源,屡次提交极可能产生反作用,好比订单提交,屡次提交极可能产生多笔订单。

幂等性的实现方式

  • 对于客户端交互的接口,能够在前端拦截一部分,例如防止表单重复提交,按钮置灰,隐藏,不可点击等方式。可是前端进行拦截器显然是针对普通用户,懂点技术的均可以模拟请求调用接口,因此后端幂等性很重要。
  • 后端的幂等性如何实现?将会从如下几个方面介绍。

数据库去重表

  • 在往数据库中插入数据的时候,利用数据库惟一索引特性,保证数据惟一。好比订单的流水号,也能够是多个字段的组合。
  • 实现比较简单,读者能够本身实现看看,这里再也不提供demo了。

状态机

  • 不少业务中多有多个状态,好比订单的状态有提交、待支付、已支付、取消、退款等等状态。后端能够根据不一样的状态去保证幂等性,好比在退款的时候,必定要保证这笔订单是已支付的状态。
  • 业务中经常出现,读者能够本身实现看看,再也不提供demo。

TOKEN机制

  • 针对客户端连续点击或者调用方的超时重试等状况,例如提交订单,此种操做就能够用Token的机制实现防止重复提交。
  • TOKEN机制如何实现?简单的说就是调用方在调用接口的时候先向后端请求一个全局ID(TOKEN),请求的时候携带这个全局ID一块儿请求,后端须要对这个全局ID校验来保证幂等操做,流程以下图:

  • 主要的流程步骤以下:
    • 客户端先发送获取token的请求,服务端会生成一个全局惟一的ID保存在redis中,同时把这个ID返回给客户端。
    • 客户端调用业务请求的时候必须携带这个token,通常放在请求头上。
    • 服务端会校验这个Token,若是校验成功,则执行业务。
    • 若是校验失败,则表示重复操做,直接返回指定的结果给客户端。
  • 经过以上的流程分析,惟一的重点就是这个全局惟一ID如何生成,在分布式服务中每每都会有一个生成全局ID的服务来保证ID的惟一性,可是工程量和实现难度比较大,UUID的数据量相对有些大,此处陈某选择的是雪花算法生成全局惟一ID,不了解雪花算法的读者下一篇文章会着重介绍。

代码实现

  • 陈某选择的环境是SpringBoot+Redis单机环境+注解+拦截器的方式实现,只是演示一下思想,具体的代码能够参照实现。
  • redis如何实现,获取Token接口将全局惟一Id存入Redis(必定要设置失效时间,根据业务需求),业务请求的时候直接从redis中删除,根据delete的返回值判断,返回true表示第一次请求,返回false表示重复请求。代码以下:
@Service
public class TokenServiceImpl implements TokenService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public String getToken() {
        //获取全局惟一id
        long nextId = SnowflakeUtil.nextId();
        //存入redis,设置10分钟失效
        stringRedisTemplate.opsForValue().set(String.valueOf(nextId), UUID.randomUUID().toString(),10, TimeUnit.MINUTES);
        return String.valueOf(nextId);
    }

    /**
    * 删除记录,true表示第一次提交,false重复提交
    */
    @Override
    public Boolean checkToken(String token) {
        return stringRedisTemplate.delete(token);
    }
}
  • 注解的实现以下,标注在controller类上表示当前类上所有接口都作幂等,标注单个方法上,表示单个接口作幂等操做。
/**
 * @Description 幂等操做的注解
 * @Author CJB
 * @Date 2020/3/25 10:19
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatLimiter {
}
  • 请求头的拦截器,用于提取请求头和校验请求头,以下:
/**
 * @Description 获取请求头的信息,具体校验逻辑读者本身实现
 * @Author CJB
 * @Date 2020/3/25 11:09
 */
@Component
public class HeaderIntercept implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取token
        String token = request.getHeader(HeaderConstant.TOKEN);
        //校验逻辑
        if (!validToken(token))
            throw new TokenInvalidException("TOKEN失效");
        //获取其余的参数.....
        RequestHeader header = RequestHeader.builder()
                .token(token)
                .build();
        //放入request中
        request.setAttribute(HeaderConstant.HEADER_INFO,header);
        return true;
    }

    /**
     * 校验token,逻辑本身实现
     * @param token
     * @return
     */
    private boolean validToken(String token){
        return Boolean.TRUE;
    }
}
  • 保证幂等性的拦截器,直接从redis中删除token,成功则第一次提交,不成功则重复提交。
@Component
public class RepeatIntercept implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod){
            //获取方法上的参数
            RepeatLimiter repeatLimiter = AnnotationUtils.findAnnotation(((HandlerMethod) handler).getMethod(), RepeatLimiter.class);

            if (Objects.isNull(repeatLimiter)){
                //获取controller类上注解
                repeatLimiter=AnnotationUtils.findAnnotation(((HandlerMethod) handler).getBean().getClass(),RepeatLimiter.class);
            }

            //使用注解,须要拦截验证
            if (Objects.nonNull(repeatLimiter)){
                //获取全局token,表单提交的惟一id
                RequestHeader info = RequestContextUtils.getHeaderInfo();

                //没有携带token,抛异常,这里的异常须要全局捕获
                if (StringUtils.isEmpty(info.getToken()))
                    throw new RepeatException();

                //校验token
                Boolean flag = tokenService.checkToken(info.getToken());

                //删除失败,表示
                if (Boolean.FALSE.equals(flag))
                    //抛出重复提交的异常
                    throw new RepeatException();
            }
        }
        return true;
    }
}
  • 接口幂等实现,代码以下:
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 下单
     * @param order
     * @return
     */
    @PostMapping
    @RepeatLimiter  //幂等性保证
    public CommenResult add(@RequestBody Order order){
        orderService.save(order);
        return new CommenResult("200","下单成功");
    }
}

演示

  • 发送getToken的请求获取Token

  • 携带Token下单第一次:

  • 第二次下单:

相关文章
相关标签/搜索