深入浅出接口幂等性的实现方式

摘要:
读取目录I,数据库重复数据消除表II,状态机III,TOKEN机制1.代码实现2.演示如何实现读取目录接口的幂等性?如何实现目录接口的幂等性?另一个是服务器需要使用幂等性来确保一个和多个请求的结果一致。幂等实现方法可以在前端拦截客户端交互界面的一部分,例如防止重复提交表单、灰显、隐藏和不可单击的按钮。

阅读目录

阅读目录

回到目录
接口幂等性如何实现?
 

导读

  • 转载自幂等性如何实现?深入了解一波!!!
  • 现在这个时代大家可能最关心的就是钱了,那么有没有想过你银行转账给你没有一次是转多的,要么失败,要么成功,为什么不能失误一下多转一笔呢?醒醒吧年轻人,别做梦了,做银行的能那么傻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校验来保证幂等操作,流程如下图:

深入浅出接口幂等性的实现方式第1张

  • 主要的流程步骤如下:
    • 客户端先发送获取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

深入浅出接口幂等性的实现方式第2张

  • 携带Token下单第一次:

深入浅出接口幂等性的实现方式第3张

  • 第二次下单:

深入浅出接口幂等性的实现方式第4张

免责声明:文章转载自《深入浅出接口幂等性的实现方式》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇如何用IDEA创建springboot(maven)并且整合mybatis连接mysql数据库和遇到的问题asp.net mvc让我告诉你请求从哪里来下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

微信开放平台开发(2) 网站应用微信登录

关键字:微信公众平台 微信开放平台 微信登录 微信扫码登录 使用微信账号登录网站作者:方倍工作室 原文:http://www.cnblogs.com/txw1958/p/weixin-qrlogin.html   在这篇微信公众平台开发教程中,我们将介绍如何使用微信开放平台接口实现微信扫码登录的功能。 准备工作 网站应用微信登录是基于OAuth2.0协议标...

.net Core 调用微信Jsapi接口,H5解析二维码

项目里需要用到扫描二维码,自己实现,不会。 找到了两种解决方案: 通过reqrcode.js,这是一个前端解析二维码内容的js库。如果二维码比较清晰,用这种效果也不错 调用微信扫一扫功能,这种效果很好。但是调试接口超级麻烦。 具体实现:前端代码(vue)(前端用到 vux) <template> <div class="main"&g...

解决微信公众平台接口配置信息配置失败问题

填写好URL及TOKEN后,点“提交”时,总是提示“配置失败”或其他错误 确认URL指向的后台页面代码没有问题 确认TOKEN配置没有问题 这时请察看一下你的INDEX页面的编码格式,改成GB2312试试吧,也许会令你的问题迎刃而解。 谨以此文献给浮躁的自己。...

深入浅出Node(5) 内存控制

一)V8垃圾回收机制   1.1 V8的内存限制   1.2 V8垃圾回收机制   二) 高效使用内存   2.1 内存空间的释放   2.2 使用堆外内存   2.3 合理使用内存防止内存泄漏     2.3.1 慎将内存当缓存     2.3.2 关注队列状态   2.3 大内存应用 一)V8垃圾回收机制   Node使用的是Chrome的V8引擎执行...

在Web应用中接入微信支付的流程之极简清晰版

背景: 在Web应用中接入微信支付,我以为只是调用几个API稍作调试即可。 没想到微信的API和官方文档里隐坑无数,致我抱着怀疑人生的心情悲愤踩遍了丫们布下的所有坑。 简要介绍几个主要大坑: 坑一:关于WeixinJSBridge这个对象 查阅网页端调起支付API的开发文档,此对象即旁若无人的映入眼帘。 然后我们就理所应当的在代码里调用了丫的。 可是,to...

vue后台管理系统项目

项目介绍1.项目根目录文件 2.源码子目录结构 3.api目录 4.assets目录 5.components目录 6.mixins目录 7.permission目录 8.router目录 9.store目录 10.styles目录 11.utils目录 项目文件介绍1.安装element-ui组件实现按需加载 // 1.1.npm...