接口设计
什么是幂等?
幂等(idempotency)本身是一个数学概念,常见于抽象代数中,表示一个函数或者操作的结果不受其输入或者执行次数的影响。例如,f(n)=1^n,无论n为多少,f(n)的值永远为 1。
在软件开发领域,幂等是对请求操作结果的一个描述,这个描述就是不论执行多少次相同的请求,产生的效果和返回的结果都和发出单个请求是一样的。
针对数据操作来说就是:
- insert 操作要保证不插入重复的数据
- update 操作要保证多次相同请求数据依然正确。
接口幂等性问题通常是由于网络波动、用户重复操作、超时重试、消息重复消费、响应速度慢等原因导致的。
不保证幂等性会怎么样?
没有保证幂等会导致产生严重的生产级别的 Bug,比较典型的就是涉及到钱的业务场景。就比如在没有保证幂等性的情况下,我作为用户在付款的时候,我同时点击了多次付款按钮,后端处理了多次相同的扣款请求,结果导致我的账户被扣了多次钱。
这就是属于非常非常非常严重的 Bug 了, 只要业务涉及到钱就一定要格外注意
综上,保证接口的幂等性至关重要。另外,保证幂等性这个 操作并不是说前端做了就可以的,后端同样也要做。
为什么会产生接口幂等性问题?
- 网络波动,,可能会引起重复请求
- 使用了失效或超时重试机制(Nginx重试、RPC重试或业务层重试等)
- 页面重复刷新
- 使用浏览器后退按钮重复之前的操作,导致重复提交表单
- 用户双击提交按钮
如何保证幂等性?
前端保证幂等性
前端保证幂等性的话比较简单,一般通过当用户提交请求后将按钮致灰来做到。
需要注意,虽然前端可以通过将按钮置灰防止重复点击,但是纯前端无法完美实现幂等性,比如前端调用后端接口超时,有可能后端已经存储了数据,此时前端的按钮已经可点击,用户再次点击就会生成两条数据。
后端保证幂等性就稍微麻烦一点,方法也是有很多种,比如悲观锁、乐观锁 、唯一索引、去重表、分布式锁、Token 机制等等。
悲观锁
在Java 中,可以使用 ReetrantLock类、synchronized 关键字这类 JDK 自带的悲观锁来保证同-时刻只有一个线程能够进行修改。不过,JDK 自带的锁属于是本地锁,分布式环境下无法使用。

除了利用 JDK 提供的悲观锁之外,数据库自身也带了排他锁(X锁)。排他锁又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。
排他锁只能在支持事务的存储引擎(如 InnoDB)中使用,且只能在事务中使用。另外,排他锁只能在有索引的字段上使用,否则会锁住整个表,影响并发性能。
高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。
乐观锁
乐观锁一般会使用版本号机制或 CAS 算法实现。
拿版本号机制来说,通过在表中增加一个版本号字段,每次更新数据时,检查当前的版本号否和数据库中的一致。如果一致,则更新成功,并且版本号加一。如果不一致,则更新失败,表示数据已经被其他请求修改过。
唯一索引
通过在表中加上唯一索引,保证数据的唯一性,如果有重复的数据插入,会抛出异常,程序可以捕获异常并处理。不过,这种方法只适用于插入数据的场景。
不要依靠唯一索引来保证接口幂等,但建议使用唯一索引作为兜底,避免产生脏数据。
去重表本质上也是一种唯一索引方案。去重表是一张专门用于记录请求信息的表,其中某个字段需要建立唯一索引,用于标识请求的唯一性当客户端发出请求时,服务端会将这次请求的一些信息(如订单号、交易流水号等)插入到去重表中,如果插入成功,说明这是第一次请求,可以执行后续的业务辑;如果插入失败,说明这是重复请求,可以直接返回或者忽略。

Token 机制
Token 机制的核心思想是为每一次操作生成一个唯一性的凭证 token。这个 token 需要由服务端生成的,因为服务端可以对 token 进行签名和加密,防止篡改和泄露。如果由客户端生成 token,可能会存在安全隐患,比如客户端伪造或重复 token,导致服务端无法识别和校验,
具体实现就是进入页面时申请一个token,然后后面所有的请求都带上这个token,后端根据token来避免重复请求。
这样的话,就需要两次请求才能完成一次业务操作:
1、请求获取服务器端 token,token 需要设置有效时间(可以设置短一点),服务端将该 token 保存起来(通常保存在缓存中)
2、执行真正的请求,将上一步获取到的 token 放到 header 或者作为请求参数。服务端验证 token 的有效性,如果有效(一般是通过删除 token 的方式来验证,删除成功则有效),执行业务逻辑,并删除token,防止重复提交;如果无效,拒绝请求,返回提示信息。

缓冲队列
将请求都快速地接收下来后放入缓冲队列中,后续使用异步任务处理队列中的数据,过滤掉重复的请求,该解决方案优点是同步处理改成异步处理、高吞吐量,缺点则是不能及时地返回请求结果,需要后续轮询得处理结果。
分布式锁
分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上,需要使用分布式锁
常见分布式锁实现方案如下:
- 基于关系型数据库比如 MySQL 实现分布式锁。
- 基于分布式协调服务 ZooKeeper 实现分布式锁。
- 基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。
关系型数据库的方式一般是通过唯一索引或者排他锁实现。不过,一般不会使用这种方式,问题太多比如性能太差、不具备锁失效机制。我们上面提到的关系型数据的乐观锁、唯一索引和排他锁也算是分布式锁的实现方式,只是一般不会采用这种方式实现分布式锁,问题太多比如性能太差、不具备锁失效机制。
基于 ZooKeeper 或者 Redis 实现分布式锁这两种实现方式要用的更多一些
悲观锁和分布式锁的核心思想都是通过加锁来保证同一时刻只有一个请求能被执行。但仅仅这样是不够的,还需要配合根据业务逻辑进行幂等性判断,例如,注册场景检测指定的电话/邮箱/用户名是否已经被注册、订单支付场景检测订单的状态。实际项目中,一般采用分布式锁这种方案比较多。
防止接口被盗刷/如何保证接口的安全?
接口防盗刷是指通过一些技术手段,防止恶意用户或者黑客对某个接口进行频繁的请求,造成服务器资源的浪费或者服务的不可用。
下面这些是一些必须要要做防盗刷的接口:
短信/邮件发送接口,投票接口,登录接口
防盗刷是接口安全的一个方面,除了防盗刷之外,还需要防止接口被非法调用、数据被篡改(攻击者拦截并篡改请求的数据)和重放攻击(攻击者截获并重复发送有效的请求)。下面我会简单列举一些常用保证接口安全的策略。
前置验证码
建议行为式验证码(点触、滑块、文字点选、推理拼图等等,可使用第三方验证码服务),普通验证码很发送短信/邮件验证码等场景。不过,不能容易被破解。前置验证码是非常实用有效的方法,适用于登录、仅仅依靠前置验证码来防止接口盗刷,需要搭配其他方法共同使用。
限流
根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制,例如将异常 IP 列入黑名单。
使用代理 IP 池可能会绕过简单的 IP 黑名单措施,因为攻击者可以轻松更改其 IP 地址。
缺点
代理 IP 池允许攻击者在请求之间更改 IP,这使得传统的基于IP 的防护措施不再那么有效。针对短信/邮件发送接口,还可以限制同一手机号/邮箱在固定时间之内限制发送短信发送次数,这个方法还是非常有用的。
请求中加入签名sign
请求中加入 sign 是一种常见的保证接口安全的方法(API 签名机制),它可以确保请求的来源和数据的完整性,防止数据被篡改或者重放攻击。
签名机制的原理是,客户端和服务器端预先约定的密钥和签名算法(需要保密,避免泄露),对请求数据(请求体、时间戳、随机数等按照特定规则进行排序和拼接)计算出一个 sign。客户端将生成的 sign 作为请求头或请求参数的一部分,随请求一起发送。服务端接收到请求后,使用相同的密钥和签名算法,对请求数据(和客户端采用一样的排序和拼接方式)计算出一个 sign,并与客户端发送过来的 sign 进行比对如果一致则处理请求,否则拒绝请求。
sign 可以确保请求的来源和数据的完整性,防止数据被篡改或者重放攻击。不过,如果加密规则被破解sign 就失效了。而且,使用签名机制会对接口性能有轻微的影响。
云平台对外开发 API一般用的就是这种方法,一般在使用之前,需要先申请安全凭证(Secretld 和SecretKey)
Secretld 用于标识 API 调用者身份
SecretKey 用于加密签名字符串和服务器端验证签名字符串的密钥。
这里仅仅是多了一个 Secretld,其他的思路都是一样的。
使用建议:
1.对外访问的接口可以考虑使用签名机制,对内接口可以不用。
2.为了避免重放攻击,可以在签名计算规则中加入时间戳(timestamp)和 nonce(随机数)
3.在选择签名算法时,应兼顾效率和安全性,如 HMAC、AES。
4.妥善管理和保护密钥,定期更换密钥。
使用 HTTPS
HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
总结
HTTPS 是必备的,也是最基本的。像登录、发送短信/邮件验证码这类场景建议加一个前置验证码,比较实用。对外访问的接口,尤其是稀缺资源的接口,建议限流一定要加上。监控告警可以直接通过现成的监控系统来做,例如 SkyWalking(可以满足一些基本的监控告警需求)、Prometheus。对外访问的接口可以考虑使用签名机制来确保请求的来源和数据的完整性,防止数据被篡改或者重放攻击,云平台对外开发API 一般用的就是这种方法。请求中加入 timestamp(时间戳)并设置校验位是一种取巧的做法,绝大部分时候还是非常有效的。
接口响应过慢如何排查?
问题定位
首先我们要快速定位接口的哪一个环节比较比较慢,性瓶颈在哪里?这个时候可以采用APM工具(Application Performance Monitor)快速定位,常见的工具:skywalking、pinpoint、cat、zipkin,假如我们应用没有接入APM,可以在生产环境装一下阿里的Arthas,利用trace方法、大概分析哪一块比较慢。
使用Arthas的trace方法
可以使用IDEA的arthas插件快速生成trace指令。

常见的性能分析方案
1)数据库慢SQL
通过explain执行计划分析下
- 未加索引
- 加了索引,索引失效(对索引加方法转换、区分度很低比如枚举值、索引列大量空值)
- 小表驱动大表(尽可能过滤数据)
- SQL太复杂(join超过3张表或者子查询比较多,建议拆分SQL为多个接口,比如先从某个主接口查某个表数据,然后关联字段作为条件从另外一个表查询,进行内存拼接)
- 深度分页问题
- 返回的数据量数据量太大(可以分页多批次查询)
- 单表数据量太大(考虑放分片库或分表或者clickhouse、es存储)
2)程序逻辑慢
- 非法校验逻辑前置,避免无用数据穿透消耗系统资源,减少无效调用
- 循环调用改为单次调用(比如查数据库或查其他rpc或restful接口,能批量调用尽量批量调用,数据在内存组装处理)
- 同步调用改为异步调用(采用completableFuture异步非阻塞,并行调用不同的rpc接口)
- 非核心逻辑剥离(拆分大事务,采用mq异步解耦)
- 线程池合理设置(千万不要创建无界队列线程池,线程池满了以后要重写拒绝策略,考虑告警加数据持久化)
- 锁合理设置(本地读写锁设计不合理或锁力度太大、分布式锁合理使用防止热点key)
- 优化gc参数(考虑young gc、full gc是否太频繁、调整gc算法、新生代老年代比例)
- 只打印必要日志(warn或error级别)
3)调用第三方接口慢
- 调用第三方设置合理的超时时间,比如你的接口是高并发接口,从自身对方接口的要求和对方线上TP95接口的平均rt,综合设置超时时间
- 集成sentinel或hystrix限流熔断框架,防止对方接口拖垮我们自己的接口
- 事务型操作(比如支付,充值)根据实际的情况酌情决定是否重试补偿(本地消息表+job重试),比如新增、修改等操作要考虑对方接口是否支持幂等,防止超发
- 循环调用,改为单次批量调用,减少IO损耗(比如调用AB接口,根据用户ID、分组ID多个,for调用改为一次传多个分组ID
- 缓存查询结果(比如根据用户ID查询用户信息)
4)中间件慢
- redis慢(是否有热Key、大key,热Key;上本地缓存、大Key:拆分大key或者采用set结构的sismember等方法判断)
- Kafka慢(生产端慢,向kafka丢消息慢了,可以使用阻塞队列接收,批量丢消息等优化,消费端:扩分区、增加消费节点、增加消费线程或批量消费批量写库)
5)架构优化
- 高并发读逻辑都走redis,尽可能不穿透到db
- 涉及写逻辑数据(异步、批量处理、分库分表)
- 接口接入限流熔断兜底(sentinel或hystrix)
- 监控告警(error日志告警、接口慢查询或不可用或限流熔断告警、DB告警、中间件告警、应用系统告警)
- 接口加动态配置开关快速切断流量或降级某一些非核心服务调用
- 设计自动对账job,保证数据自动可修复