接口限流实战:令牌桶与漏桶的落地实现(含分布式场景踩坑记录)

发布时间:2026/10/7 8:25:27

接口限流实战:令牌桶与漏桶的落地实现(含分布式场景踩坑记录) 为什么接口一定要有限流高并发系统中任何一个接口的处理能力都是有上限的。没有限流保护时突发流量会直接打满数据库连接池、耗尽线程资源最终从单个接口超时演变成整条服务雪崩。限流、缓存、降级是服务保护的三板斧而限流是第一道闸门。限流的本质是在流量超过预设阈值时主动拒绝一部分请求用少量请求的失败换取整体服务的可用。本文结合项目中的真实落地过程讲清楚固定窗口、滑动窗口、漏桶、令牌桶四种算法的差异以及分布式场景下最容易踩的坑。一、四种常见限流算法1. 固定窗口计数器最简单的实现把时间切分成固定窗口比如每秒一个窗口每个窗口内计数超过阈值就拒绝窗口切换时计数器归零。publicclassFixedWindowRateLimiter{privatefinallongwindowMillis;privatefinalintmaxRequests;privatelongwindowStartSystem.currentTimeMillis();privateintcounter0;publicFixedWindowRateLimiter(longwindowMillis,intmaxRequests){this.windowMilliswindowMillis;this.maxRequestsmaxRequests;}publicsynchronizedbooleantryAcquire(){longnowSystem.currentTimeMillis();if(now-windowStartwindowMillis){windowStartnow;counter0;}if(countermaxRequests){counter;returntrue;}returnfalse;}}踩坑点1固定窗口存在临界突刺问题。假设限制每秒100次如果在窗口的最后10毫秒涌入100次、下一窗口的最初10毫秒又涌入100次那么在这20毫秒内实际通过了200次瞬时QPS达到阈值的两倍。对边界敏感的接口不能用这种算法。2. 滑动窗口滑动窗口把一个大窗口拆成多个小格子统计时只看最近一段时间内所有格子的计数随着时间推移平滑丢弃旧格子。它解决了固定窗口的临界问题Sentinel的滑动窗口就是类似实现LeapArray。代价是实现更复杂需要维护格子数组。3. 漏桶算法漏桶把请求想象成往桶里倒水水请求从桶底以恒定速率漏出。不管进水多快出水速度始终均匀。桶满时新请求被拒绝。漏桶的特点是严格平滑适合保护下游对流量均匀性要求高的场景比如调用第三方按固定速率计费的接口。4. 令牌桶算法系统以固定速率往桶里放令牌桶有容量上限每个请求必须拿到一个令牌才能处理拿不到就拒绝或排队。令牌桶和漏桶的关键区别在于令牌桶允许一定程度的突发流量——因为桶里可以预先积累令牌短时间内可以连续放行多个请求只要长期平均速率不超过阈值。这也是GuavaRateLimiter和大多数网关限流采用的方案。二、单机令牌桶Guava RateLimiter 的正确用法单机场景直接用Guava的RateLimiter即可// 每秒生成500个令牌支持预占acquire可以透支下一段时间的令牌RateLimiterlimiterRateLimiter.create(500.0,1,RateLimiter.AcquireTimeoutBehavior.THROW_EXCEPTION);publicResponsehandle(Requestreq){// tryAcquire拿不到立即返回false不阻塞if(!limiter.tryAcquire(1,100,TimeUnit.MILLISECONDS)){returnResponse.fail(系统繁忙请稍后再试);}returndoBusiness(req);}踩坑点2acquire()和tryAcquire()行为完全不同混用会引发线上事故。acquire()拿不到令牌时会阻塞线程等待在线上Tomcat线程池里如果大量线程同时阻塞等待令牌线程池会被迅速占满表现为整个服务假活——接口不报错但全部超时。对外提供服务的接口绝大多数情况应该用带超时的tryAcquire()快速失败而不是排队阻塞。踩坑点3RateLimiter默认是预消费模式。突发大请求比如一次acquire(100)可以提前透支未来的令牌后续请求会为此等待。对公平性要求高的场景要评估这个特性避免单个大请求把后续流量全部借空。三、分布式限流Redis Lua 的实现单机限流只保护单个实例服务多实例部署时每台机器各自限流集群总阈值等于单机阈值乘以实例数起不到全局保护作用。分布式限流需要一个所有实例共享的计数中心通常用Redis实现。关键是**读计数判断写回必须是原子操作**否则在并发下会出现计数不准。用Lua脚本保证原子性-- KEYS[1]: 限流key-- ARGV[1]: 桶容量 ARGV[2]: 令牌生成速率(个/秒) ARGV[3]: 当前时间(毫秒) ARGV[4]: 请求令牌数localkeyKEYS[1]localcapacitytonumber(ARGV[1])localratetonumber(ARGV[2])localnowtonumber(ARGV[3])localrequestedtonumber(ARGV[4])-- 从Hash里取上次状态第一次用默认值locallast_tokenstonumber(redis.call(hget,key,tokens)orcapacity)locallast_timetonumber(redis.call(hget,key,timestamp)ornow)-- 按经过时间补充令牌localdeltamath.max(0,now-last_time)/1000.0localfilledlast_tokensdelta*ratelocaltokensmath.min(capacity,filled)localallowed0iftokensrequestedthentokenstokens-requested allowed1endredis.call(hmset,key,tokens,tokens,timestamp,now)redis.call(expire,key,60)returnallowedJava侧调用publicbooleandistributedTryAcquire(StringbizKey,intpermits){Stringlua...上面的脚本...;DefaultRedisScriptLongscriptnewDefaultRedisScript(lua,Long.class);LongallowedredisTemplate.execute(script,Collections.singletonList(rate_limit:bizKey),String.valueOf(capacity),String.valueOf(rate),String.valueOf(System.currentTimeMillis()),String.valueOf(permits));returnallowed!nullallowed1L;}踩坑点4时间戳必须取同一个时钟且注意Redis主从切换的数据丢失。脚本中如果时间基准不统一会导致令牌计算错误更隐蔽的问题是Redis主从异步复制主节点宕机发生主从切换时最近的限流状态可能丢失极端情况下限流失灵一次。对于不能接受任何超限的强一致场景需要考虑使用更可靠的方案或接受这部分误差。一般业务防护场景这个误差是可以接受的。踩坑点5Lua脚本里浮点取最小值要用math.min时间差要防负数。实际环境出现过因客户端时钟回拨NTP校时导致now - last_time为负、令牌被扣成异常值的问题。脚本中用math.max(0, ...)做兜底能避免时钟回拨把令牌数算乱。四、限流落地的几个工程细节限流粒度要分层全局限流保护整个服务、接口级限流保护慢接口、用户级限流防止单用户刷接口要分开配置不能只做一层。用户级限流的key要带上用户标识避免一个恶意用户拖垮全局配额。拒绝后的响应要友好且可识别限流拒绝应返回明确的业务码客户端可以据此做退避重试而不是当成普通错误反复打。重试一定要带随机退避否则所有被拒请求同一时刻重试会制造第二波流量高峰。限流阈值不能拍脑袋阈值应该基于压测得到的单机容量再结合实例数、下游承受能力综合计算。阈值设得过低会误杀正常流量设置上线后要持续观察实际拒绝率配合监控动态调整。限流要和降级联动被限流的请求可以走降级逻辑返回缓存数据、静态兜底页比直接报错体验好。五、算法选型总结算法突发流量平滑性实现复杂度典型场景固定窗口有临界突刺差最低粗粒度统计、要求不高的场景滑动窗口平滑较好中精准计数限流漏桶不允许突发恒定速率中保护下游、对接匀速接口令牌桶允许可控突发长期平均匀速中绝大多数业务接口推荐实际项目中单机保护选Guava令牌桶集群全局限流选RedisLua令牌桶对下游有匀速要求的场景再考虑漏桶。限流不是配完就结束压测定阈值、监控看拒绝率、异常做降级这套闭环跑起来限流才算真正落地。
延伸阅读

更多相关文章

2026/10/7 8:20:27

信息学奥赛一本通1286:怪盗基德滑翔翼线性DP详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 8:20:27

Ponytail插件实战:从碎片收拢到知识卡片库的工作流设计

Ponytail 这个名字,第一眼很容易让人想到马尾辫。我第一次在技术社区刷到这个词的时候还以为是发型教程,点进去才发现,这其实是一款用来整理碎片信息的效率插件。它的核心思路跟扎辫子确实有几分相似:把你散落在各处的内容——代码…

2026/10/7 10:50:49

D435i+IMU联合标定实战指南:ROS视觉惯性里程计精度基石

1. 为什么D435iIMU联合标定是ROS机器人开发绕不开的硬门槛? 在ROS机器人开发里,你可能已经调通了小车底盘、跑起了SLAM建图、甚至让机械臂抓起了水杯——但只要一上真实场景,定位就开始漂、轨迹就发散、建图就错层。这时候老手第一反应不是查…

2026/10/7 10:50:49

联邦学习实战:训练链路、FedAvg聚合与non-IID工程实践

这篇继续聊联邦学习(二)。上一篇文章我们把联邦学习的基本概念、参与角色和典型架构捋了一遍,今天直接进入训练链路。很多人入坑之后会发现,算法书上的联邦学习写得明明白白,真到自己动手跑起来全是问题:模…

2026/10/7 10:50:49

Linux下nl命令实战:从行号控制到逻辑页机制全解析

给文件加行号这件事,看着简单,但真要在 Linux 下把它做得干净、可控,我发现大部分人只会敲cat -n,再往深了问就说不出来了。我个人的习惯是,专门负责行号这件事的nl命令才是真正值得花十分钟吃透的工具。名字“nl”就是…

2026/10/7 10:50:49

OpenClaw公网访问报错control ui requires device identity的排查与解决

前阵子我把自部署的OpenClaw接到公网访问,折腾到差点把键盘砸了。本地打开控制界面一切正常,换成公网域名一访问,页面直接弹出一句“control ui requires device identity”。第一次看到这个报错的时候我整个人是懵的——这设备明明就是我自己…

2026/10/7 10:50:49

Android存储问题排查:从Ext4原理到FUSE、I/O卡顿实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 10:45:44

树莓派5+ Hailo-8L 边缘AI数字人实战:从硬件到口型同步全解析

树莓派5的PCIe接口终于不再是摆设了。M.2 HAT插上一块Hailo-8L加速卡,整机总成本压在千元以内,就能让数字人的人脸检测、关键点追踪、手势识别这些视觉任务完全跑在本地NPU上,不必再去挤那点可怜的CPU资源。这篇文章想聊的,不只是…

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/7 8:18:33

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/6 17:46:51

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:05:03

ESP32免重刷固件:浏览器直接修改NVS键值实现WiFi配置更新

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:05:03

SAP HANA查询结果导出CSV:避开乱码、性能与权限的实用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/7 1:05:03

数字后端Placement阶段Density与Congestion控制实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑