Wetts's blog

Stay Hungry, Stay Foolish.

0%

接口-0-知识点汇总.md

接口设计

  • 遇到的问题

    • 高并发下如何保证接口的幂等性?

      • 幂等性定义
        • 幂等性就是同一个操作执行多次,产生的效果一样。如 http 的 get 请求、数据库的 select 请求就是幂等的。
        • 如提交订单、扣款等接口都要保证幂等性,不然会造成重复创建订单、重复扣款
      • 解决方法
        • 前端保证幂等性的方法
          • 按钮只能点击一次
            • 用户点击按钮后将按钮置灰,或者显示 loading 状态
          • RPG 模式
            • 即 Post-Redirect-Get,当客户提交表单后,去执行一个客户端的重定向,转到提交成功页面。避免用户按 F5 刷新导致的重复提交,也能消除按浏览器后退键导致的重复提交问题。
        • 后端保证幂等性的方法
          • 使用唯一索引
            • 对业务唯一的字段加上唯一索引,这样当数据重复时,插入数据库会抛异常
            • 接口幂等性_唯一索引
          • 状态机幂等
            • 如果业务上需要修改订单状态,例如订单状态有待支付,支付中,支付成功,支付失败。设计时最好只支持状态的单向改变。这样在更新的时候就可以加上条件,多次调用也只会执行一次。例如想把订单状态更新为支持成功,则之前的状态必须为支付中。
            • 接口幂等性_状态机
          • 悲观锁
            • 接口幂等性_悲观锁
          • 乐观锁
            • 步骤
              1. 查询数据获得版本号
              2. 通过版本号去更新,版本号匹配则更新,版本号不匹配则不更新
            • 接口幂等性_乐观锁
          • 防重表
            • 增加一个防重表,业务唯一的 id 作为唯一索引,如订单号,当想针对订单做一系列操作时,可以向防重表中插入一条记录,插入成功,执行后续操作,插入失败,则不执行后续操作。本质上可以看成是基于 MySQL 实现的分布式锁。根据业务场景决定执行成功后,是否删除防重表中对应的数据。
            • 接口幂等性_防重表
          • select+insert
            • 先查询一下有没有符合要求的数据,如果没有再执行插入。没有并发的系统中可以保证幂等性,高并发下不要用这种方法,也会造成数据的重复插入。我一般做消息幂等的时候就是先 select,有数据直接返回,没有数据加分布式锁进行 insert 操作。
          • 分布式锁
            • 执行方法时,先根据业务唯一的 id 获取分布式锁,获取成功,则执行,失败则不执行。分布式锁可以基于 Redis、zookeeper、MySQL 来实现。
            • 接口幂等性_分布式锁
          • 全局唯一号
            • 通过 source(来源)+ seq(序列号)来判断请求是否重复,重复则直接返回请求重复提交,否则执行。如当多个三方系统调用服务的时候,就可以采用这种方式。
          • 获取 token
            • 该方案跟之前的所有方案都有点不一样,需要两次请求才能完成一次业务操作。
            • 步骤
              1. 第一次请求获取 token
                • 接口幂等性_获取token_1
              2. 第二次请求带着这个 token,完成业务操作。
                • 接口幂等性_获取token_2
    • 多版本共存

      • 解决方案

        • Header 版本控制

          • 此方法需要客户端将指示资源版本的自定义 Header 添加到请求中,如果省略了此 Header,按默认值(一般是最新版)处理。

          • ```

            Request

            GET http://adventure-works.com/customers/3
            Custom-Header: api-version=1

            Response

            HTTP/1.1 200 OK
            Content-Type: application/json; charset=utf-8

            {“id”:3,”name”:”Contoso LLC”,”address”:”1 Microsoft Way Redmond WA 98053”}

            1
            2
            3
            4
            5
            6
            7
            8
              - 优点:纯粹的版本控制机制,符合 RESTful API「每个资源使用唯一的URI定位」的原则
            - 缺点:不直观,无法支持表单直接调用(众所周知 HTML 表单是不能添加自定义 Header 的),缓存不友好(不能认为同一 URI/查询字符串指向的是相同数据,因此难以缓存)
            - 媒体类型版本控制
            - 通常,Accept 标头的用途是由客户端指定响应的正文是 XML、JSON或其他格式。但是,可以定义包括以下信息的自定义媒体类型:该信息使客户端应用程序可以指示它所需的资源版本。
            - ```
            # Request
            GET http://adventure-works.com/customers/3 HTTP/1.1
            Accept: application/vnd.adventure-works.v1+json
          • 服务端负责处理 Accept 标头并尽可能采用该值(可以在 Accept 标头中指定多种格式,在这种情况下,服务端在其中选择最适合的格式用于响应正文)。返回结果中的 Content-Type 标头确认响应正文中的数据格式:

          • ```

            Response

            Content-Type: application/vnd.company.myapp-v3+xml

            1
            2
            3
            4
            - URI 版本控制
            - ```
            # Request
            GET http://adventure-works.com/v2/customers/3
          • 优点:直观,缓存友好,支持表单直接调用

          • 缺点:不符合「每个资源使用唯一的URI定位」的原则

        • 查询字符串版本控制

          • # Request
            GET http://adventure-works.com/customers/3?version=2
            
          • 优点:直观,缓存友好,支持表单直接调用,符合「每个资源使用唯一的URI定位」的原则
          • 缺点:某些较旧的 Web 浏览器和代理不会缓存在 URI 中包含字符串的请求的响应,这会对性能产生影响
    • 如何保证 API 接口数据安全?

      1. 登陆验证
      2. 权限验证
      3. HTTPS
      4. 接口签名
        • 签名流程
          • 接口安全性_签名流程
        • 签名规则
          • 线下分配 appid 和 appsecret,针对不同的调用方分配不同的 appid 和 appsecret
            • appSecret 的作用主要是区分不同客户端 app。并且利用获取到的 appSecret 参与到 sign 签名,保证了客户端的请求签名是由我们后台控制的,我们可以为不同的客户端颁发不同的 appSecret。
          • 加入 timestamp(时间戳),5 分钟内数据有效
          • 加入临时流水号 nonce(防止重复提交),至少为 10 位。针对查询接口,流水号只用于日志落地,便于后期日志核查。针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。
          • 加入签名字段 signature,所有数据的签名信息。
        • 客户端签名生成
          • 所有动态参数 = 请求头部分 + 请求 URL 地址 + 请求 Request 参数 + 请求 Body
            • 上面的动态参数以 key-value 的格式存储,并以 key 值正序排序,进行拼接
          • 最后拼接的字符串再拼接 appSecret
          • 拼接成一个字符串,然后做md5不可逆加密
            • signature = DigestUtils.md5DigestAsHex(sortParamsMap + appSecret)
        • 服务端签名验证
          • 验证流程
            1. 验证必须的头部参数
            2. 获取头部参数,request 参数,Url 请求路径,请求体 Body,把这些值放入 SortMap 中进行排序
            3. 对 SortMap 里面的值进行拼接
            4. 对拼接的值进行加密,生成 sign
            5. 把生成的 sign 和前端传入的 sign 进行比较,如果不相同就返回错误
          • 附加功能
            • 可以通过对请求的 timestamp 进行时间验证,如果大于 10 分钟表示此链接已经超时,防止别人来到这个链接去请求。
            • 利用 nonce 参数,防止重复提交

限流

  • 计数器(固定窗口)算法
    • 计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。
    • 限流算法_计数器(固定窗口)算法_1
    • 这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在一个非常严重的问题,那就是临界问题。
      • 假设 1min 内服务器的负载能力为 100,因此一个周期的访问量限制在 100,然而在第一个周期的最后 5 秒和下一个周期的开始 5 秒时间段内,分别涌入 100 的访问量,虽然没有超过每个周期的限制量,但是整体上 10 秒内已达到 200 的访问量,已远远超过服务器的负载能力,由此可见,计数器算法方式限流对于周期比较长的限流,存在很大的弊端。
      • 限流算法_计数器(固定窗口)算法_2
  • 滑动窗口算法
    • 滑动窗口算法是将时间周期分为 N 个小周期,分别记录每个小周期内访问次数,并且根据时间滑动删除过期的小周期。
      • 假设时间周期为 1min,将 1min 再分为 2 个小周期,统计每个小周期的访问数量,则第一个时间周期内,访问数量为 75,第二个时间周期内,访问数量为 100,超过 100 的访问则被限流掉了。
      • 限流算法_滑动窗口算法
    • 当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
    • 此算法可以很好的解决固定窗口算法的临界问题。
  • 漏桶算法
    • 漏桶算法是访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。
    • 限流算法_漏桶算法
  • 令牌桶算法
    • 令牌桶算法是程序以 r(r=时间周期/限流值)的速度向令牌桶中增加令牌,直到令牌桶满,请求到达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略。
    • 限流算法_令牌桶算法
    • Google 开源工具包 Guava 提供了限流工具类 RateLimiter,该类基于令牌桶算法来完成限流,非常易于使用。