如何实现一个通用的接口限流、防重

接入与开发

给后端工程师的通用方案:用限流挡住超额流量、用幂等键挡住重复提交,含键设计、存储选型与降级顺序。

接口被前端重试、被爬、被用户连点,结果要么把下游打挂,要么一笔订单落两条。限流和防重听着简单,真写起来全是细节:键按什么维度拼、放内存还是 Redis、限流器自己挂了要不要放行。这篇给一套能直接搬的落地做法,从维度选择到排查顺序讲一遍。

接口被前端重试、被爬、被用户连点,结果要么把下游打挂,要么一笔订单落两条。限流和防重复提交听着简单,真写起来全是细节:键按什么维度拼、放内存还是放共享存储、限流器自己挂了要不要放行。这篇给一套能直接搬的落地做法,从维度选择讲到排查顺序。

先记住一个分工:限流解决「量太大」,防重解决「同一次跑两遍」。两者目标不同,键的拼法、失败后的行为也不同,千万别用一个计数器同时干两件事。

先定维度:限的是谁,重的是哪一次

维度没定好,后面算法再漂亮也白搭。判断标准很简单:问自己「谁在消耗我的容量」和「哪两次请求其实是同一件事」,答案就是你的键。

  • 常见限流维度:用户 ID、API Key、IP、租户、全局;一般做两级——单用户级防刷、全局级保底,两级阈值各自独立配置
  • 不要只按 IP 限:同一出口 IP 后面可能是整个办公网,误伤面比想象的大;有登录态就优先用用户或 Key 维度
  • 防重的键要包含「谁 + 做什么 + 关键参数」,例如用户 ID 拼接口名拼业务单号;只拼用户 ID 会把正常的连续操作也拦掉
  • 全局维度一定留一条:单用户维度再细,也挡不住大量不同账号同时打进来

限流算法与存储:放哪、怎么放

算法选型看精度要求和团队维护成本,别一上来就上最复杂的。存储选型看部署形态,单机和多实例是两套完全不同的答案。

方案适用情况代价
固定窗口计数QPS 不高、容忍临界突刺窗口边界可能放过接近两倍的量
令牌桶 / 漏桶需要平滑放行、允许突发实现和参数调试成本更高
滑动窗口精度要求高、调用方体验敏感内存和存储开销明显更大
本地内存单实例部署或对精度要求很松多副本各限各的,总量变成 N 倍

用共享存储时,把「判断 + 扣减」写成一次原子操作,比如一段脚本或一条原子指令。先读再写的两步做法在并发下必然超发,这一点没有例外。

  1. 先定放行策略存储不可用时,核心交易接口倾向放行保可用,公开数据接口倾向拒绝保下游。这个取舍要在上线前写进配置和文档,别等故障现场临时拍。
  2. 再定过期与清理每个键都要带过期时间,过期时长按业务窗口长度往上留一档余量。没有过期的计数器就是内存泄漏。
  3. 最后压一遍并发用固定并发打一轮,看计数是否随请求线性增长、有没有明显超发;这一轮比看代码更能发现问题。

幂等键怎么发、怎么落地

幂等键的核心思路是「第一次占位,之后复用结果」,而不是「每次都跑一遍再去重」。两种做法的副作用完全不同。

  • 由客户端生成唯一键随请求带上,服务端第一次处理时占位,处理完成后写入结果,重复请求直接返回首次结果
  • 占位要带过期时间,时长按业务最长处理时间往上留一档余量,否则键越积越多
  • 键的校验范围要明确:同一键但请求体不一致时怎么处理,通常直接报冲突,比静默返回旧结果更好排查
  • 异步任务类接口要把幂等做在任务入队那一步,而不是执行那一步,否则重复入队照样跑两遍

如果调用方是浏览器或移动端,键的生成要放在发请求之前而不是重试时。同一笔业务的重试必须复用同一个键,否则重试本身就成了重复提交的来源。

分层顺序与超时降级怎么排

限流和防重不是两个孤立开关,它们有先后顺序。顺序错了,轻则误判,重则把真实流量的特征掩盖掉。

  1. 接入层挡明显超量在第一跳做粗粒度限流,成本最低,能挡掉大部分异常流量。字段只记录键和命中规则即可。
  2. 业务层做幂等在真正写数据或调下游之前占位。这一层是唯一能保证「同一件事只落一次」的地方。
  3. 下游调用带自己的预算每个下游调用设独立超时和重试次数上限,不要沿用上游的总超时,否则一层抖动会逐级放大。
  4. 重试复用同一个键重试必须配退避和次数上限,且请求体里的幂等键不变。做不到这一点,重试就是重复提交的制造机。

降级方案至少准备一档:存储抖动时切本地计数、队列积压时先收后放。每一档都要写清楚触发条件和恢复动作,光有代码没有触发说明,值班的人不敢用。

上线后怎么观察和调阈值

阈值不是一次拍死的,是调出来的。观察手段不够,调阈值就变成凭感觉,出了问题也定位不到。

  • 日志至少记录:限流键、命中规则、当前计数、被拒原因;没有这些字段,事后只能靠猜
  • 阈值先按观察期峰值往上留量,再按被拒比例慢慢收紧;每次调整留变更记录
  • 定期回看被拒量最高的几个键,区分是真实攻击、客户端 bug 还是阈值定低了,三种情况的处理完全不同
  • 区分「被限流」和「被幂等拦下」两类指标,它们的处置动作不一样,混在一起看会误判
现象先查什么常见原因
被拒量突然抬高被拒日志里的键分布客户端重试放大或阈值定低
同一业务落两条幂等键生成位置和过期时间键由服务端生成或重试换了键
下游仍被打挂下游自身的超时和重试预算接入层放行但下游没设上限
存储抖动后计数异常降级是否触发、恢复后计数是否归位降级逻辑没有恢复动作

自测时用固定并发打一轮,看计数是否线性增长,这比读代码有效得多。想先量一条延迟基线,可以到 /tools/speed-test 这类站内测速页跑一遍;想对照别人的实际观测数字,去 /price-watch 看当时的记录,别把别人文章里的旧数字当成自己的目标。

下一步把键维度和降级顺序写成一页配置说明,再拿真实流量压一轮;如果你还在选限流组件的接入方式,可以先去站内工具目录看看现成方案再决定自己写还是直接用。

常见问题

限流和防重是一回事吗,能不能用同一套代码?

不是一回事。限流控制单位时间内的请求数量,超了就拒;防重保证同一次业务操作只生效一次,跟频率无关。实现上可以共用存储和键生成逻辑,但判断分支要分开写,混在一起很容易出现「频率没超所以重复提交被放过」的漏洞。

限流器自己挂了,应该放行还是拒绝?

取决于接口性质。涉及钱、库存、写状态的接口,宁可短暂失败也别超发,倾向拒绝并快速返回明确的错误码;读为主、可容忍略超的接口倾向放行,只是这段时间限流失效。这个选择要在设计阶段写进文档,不要留到线上出问题再拍。

幂等键应该由客户端生成还是服务端生成?

由客户端生成、服务端只做校验和存储。服务端生成的话,重复请求会被当成两次新请求,各自拿到不同的键,防重就失效了。客户端生成时要保证键在重试之间保持不变,很多重复提交问题其实出在客户端每次重试都换了个新键。

本文依据各平台公开文档与官方公告整理,只做中文归纳;具体做法、参数与限制以官方文档为准。