接口设计
遇到的问题
高并发下如何保证接口的幂等性?
- 幂等性定义
- 幂等性就是同一个操作执行多次,产生的效果一样。如 http 的 get 请求、数据库的 select 请求就是幂等的。
- 如提交订单、扣款等接口都要保证幂等性,不然会造成重复创建订单、重复扣款
- 解决方法
- 前端保证幂等性的方法
- 按钮只能点击一次
- 用户点击按钮后将按钮置灰,或者显示 loading 状态
- RPG 模式
- 即 Post-Redirect-Get,当客户提交表单后,去执行一个客户端的重定向,转到提交成功页面。避免用户按 F5 刷新导致的重复提交,也能消除按浏览器后退键导致的重复提交问题。
- 按钮只能点击一次
- 后端保证幂等性的方法
- 使用唯一索引
- 对业务唯一的字段加上唯一索引,这样当数据重复时,插入数据库会抛异常
- 状态机幂等
- 如果业务上需要修改订单状态,例如订单状态有待支付,支付中,支付成功,支付失败。设计时最好只支持状态的单向改变。这样在更新的时候就可以加上条件,多次调用也只会执行一次。例如想把订单状态更新为支持成功,则之前的状态必须为支付中。
- 悲观锁
- 乐观锁
- 步骤
- 查询数据获得版本号
- 通过版本号去更新,版本号匹配则更新,版本号不匹配则不更新
- 步骤
- 防重表
- 增加一个防重表,业务唯一的 id 作为唯一索引,如订单号,当想针对订单做一系列操作时,可以向防重表中插入一条记录,插入成功,执行后续操作,插入失败,则不执行后续操作。本质上可以看成是基于 MySQL 实现的分布式锁。根据业务场景决定执行成功后,是否删除防重表中对应的数据。
- select+insert
- 先查询一下有没有符合要求的数据,如果没有再执行插入。没有并发的系统中可以保证幂等性,高并发下不要用这种方法,也会造成数据的重复插入。我一般做消息幂等的时候就是先 select,有数据直接返回,没有数据加分布式锁进行 insert 操作。
- 分布式锁
- 执行方法时,先根据业务唯一的 id 获取分布式锁,获取成功,则执行,失败则不执行。分布式锁可以基于 Redis、zookeeper、MySQL 来实现。
- 全局唯一号
- 通过 source(来源)+ seq(序列号)来判断请求是否重复,重复则直接返回请求重复提交,否则执行。如当多个三方系统调用服务的时候,就可以采用这种方式。
- 获取 token
- 该方案跟之前的所有方案都有点不一样,需要两次请求才能完成一次业务操作。
- 步骤
- 第一次请求获取 token
- 第二次请求带着这个 token,完成业务操作。
- 第一次请求获取 token
- 使用唯一索引
- 前端保证幂等性的方法
- 幂等性定义
多版本共存
解决方案
Header 版本控制
此方法需要客户端将指示资源版本的自定义 Header 添加到请求中,如果省略了此 Header,按默认值(一般是最新版)处理。
```
Request
GET http://adventure-works.com/customers/3
Custom-Header: api-version=1Response
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 接口数据安全?
- 登陆验证
- 权限验证
- HTTPS
- 接口签名
- 签名流程
- 签名规则
- 线下分配 appid 和 appsecret,针对不同的调用方分配不同的 appid 和 appsecret
- appSecret 的作用主要是区分不同客户端 app。并且利用获取到的 appSecret 参与到 sign 签名,保证了客户端的请求签名是由我们后台控制的,我们可以为不同的客户端颁发不同的 appSecret。
- 加入 timestamp(时间戳),5 分钟内数据有效
- 加入临时流水号 nonce(防止重复提交),至少为 10 位。针对查询接口,流水号只用于日志落地,便于后期日志核查。针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。
- 加入签名字段 signature,所有数据的签名信息。
- 线下分配 appid 和 appsecret,针对不同的调用方分配不同的 appid 和 appsecret
- 客户端签名生成
- 所有动态参数 = 请求头部分 + 请求 URL 地址 + 请求 Request 参数 + 请求 Body
- 上面的动态参数以 key-value 的格式存储,并以 key 值正序排序,进行拼接
- 最后拼接的字符串再拼接 appSecret
- 拼接成一个字符串,然后做md5不可逆加密
signature = DigestUtils.md5DigestAsHex(sortParamsMap + appSecret)
- 所有动态参数 = 请求头部分 + 请求 URL 地址 + 请求 Request 参数 + 请求 Body
- 服务端签名验证
- 验证流程
- 验证必须的头部参数
- 获取头部参数,request 参数,Url 请求路径,请求体 Body,把这些值放入 SortMap 中进行排序
- 对 SortMap 里面的值进行拼接
- 对拼接的值进行加密,生成 sign
- 把生成的 sign 和前端传入的 sign 进行比较,如果不相同就返回错误
- 附加功能
- 可以通过对请求的 timestamp 进行时间验证,如果大于 10 分钟表示此链接已经超时,防止别人来到这个链接去请求。
- 利用 nonce 参数,防止重复提交
- 验证流程
- 签名流程
限流
- 计数器(固定窗口)算法
- 计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。
- 这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在一个非常严重的问题,那就是临界问题。
- 假设 1min 内服务器的负载能力为 100,因此一个周期的访问量限制在 100,然而在第一个周期的最后 5 秒和下一个周期的开始 5 秒时间段内,分别涌入 100 的访问量,虽然没有超过每个周期的限制量,但是整体上 10 秒内已达到 200 的访问量,已远远超过服务器的负载能力,由此可见,计数器算法方式限流对于周期比较长的限流,存在很大的弊端。
- 滑动窗口算法
- 滑动窗口算法是将时间周期分为 N 个小周期,分别记录每个小周期内访问次数,并且根据时间滑动删除过期的小周期。
- 假设时间周期为 1min,将 1min 再分为 2 个小周期,统计每个小周期的访问数量,则第一个时间周期内,访问数量为 75,第二个时间周期内,访问数量为 100,超过 100 的访问则被限流掉了。
- 当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
- 此算法可以很好的解决固定窗口算法的临界问题。
- 滑动窗口算法是将时间周期分为 N 个小周期,分别记录每个小周期内访问次数,并且根据时间滑动删除过期的小周期。
- 漏桶算法
- 漏桶算法是访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。
- 令牌桶算法
- 令牌桶算法是程序以 r(r=时间周期/限流值)的速度向令牌桶中增加令牌,直到令牌桶满,请求到达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略。
- Google 开源工具包 Guava 提供了限流工具类 RateLimiter,该类基于令牌桶算法来完成限流,非常易于使用。