单机限流和集群限流的区别是什么?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
- 基础掌握度:面试官真正想看的是,你对这两个概念的作用范围、实现方式、适用场景心里有没有数,能不能根据业务 QPS 来选型。
- 分布式理解深度:集群限流涉及到分布式环境下的协调问题(原子性、一致性、网络分区),考察你对 Redis + Lua、网关限流、Token Server 这些方案的理解。
- 实战经验:生产环境怎么选?单机限流扛不住时怎么过渡到集群限流?集群限流的坑(比如 Redis 宕机、时钟不同步)踩过没有?
核心答案
先给结论,一张表说清楚:
| 对比维度 | 单机限流 | 集群限流 |
|---|---|---|
| 作用范围 | 单个应用实例 | 整个集群所有实例 |
| 实现位置 | 应用内(JVM 内存) | 中心化存储(Redis / 网关 / Token Server) |
| 常见算法 | 令牌桶、漏桶、滑动窗口 | Redis + Lua、网关限流、Sentinel Token Server |
| 性能开销 | 极低(纳秒级,本地变量) | 较高(一次网络往返,毫秒级) |
| 限流精度 | 高(本地计数无竞争) | 受网络与时钟影响 |
| 容错性 | 实例挂了限流也挂了 | 中心化组件挂了全局限流失效 |
| 典型场景 | 单机保护、下游隔离 | 全局配额、API 网关、开放平台 |
一句话总结:单机限流保护自己,集群限流保护全局。生产环境通常是 "网关层集群限流 + 应用层单机限流" 双层兜底。
深度解析
一、单机限流:JVM 内的事,自己说了算
单机限流的核心思想是:每个实例维护自己的计数器或桶,只对本机的请求做限制,实例之间互不感知。
上图说明了单机限流的关键特征:每个实例独立计数,互不感知。
- 三台实例各限 100 QPS,理论上集群总流量峰值能到 300 QPS,这就叫 "本地精准,全局失控"
- 好处:本地内存操作,性能极高,没有网络开销
- 坏处:没法控制集群总流量,做不了 "整个服务 100 QPS" 这种全局配额
单机限流常用的算法就那几个:
- 令牌桶:匀速往桶里放令牌,桶满了丢弃,请求拿不到令牌就拒绝。允许突发流量,最常用
- 漏桶:匀速流出,超出桶容量直接丢。强行平滑流量
- 滑动窗口:把时间切成小格子滑动统计,比固定窗口更精准,解决临界突刺
- 计数器:最简单的固定窗口,临界点容易 2 倍突刺,不推荐生产用
二、集群限流:需要一个 "裁判"
集群限流要解决的问题是:让整个集群共享一个总配额。这就需要一个所有实例都信任的中心化组件来当 "裁判"。
上图就是典型的 Redis + Lua 集群限流方案。所有实例都向同一个 Redis 申请令牌,Redis 作为全局唯一的计数器,保证集群总流量被精确控制。
- 关键点:Lua 脚本保证 "判断 + 扣减" 是原子的,否则并发下计数器会乱
- 代价:每次请求都要走一次网络,RT 增加,Redis 也成了单点
集群限流主流实现方案有三种:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Redis + Lua | 用 Lua 脚本原子操作计数器 | 简单、成熟、通用 | 依赖 Redis,网络开销 |
| 网关层限流 | Nginx / Spring Cloud Gateway / Kong 在入口处统一限流 | 入口收敛,业务零侵入 | 网关单点压力 |
| Token Server | Sentinel 集群限流模式,独立服务器发牌 | 性能比 Redis 好,客户端有本地缓存 | 部署复杂,多一个组件 |
三、Redis + Lua 限流代码示例
看一段最经典的 Redis + Lua 滑动窗口限流实现:
-- KEYS[1]: 限流 key
-- ARGV[1]: 时间窗口(毫秒)
-- ARGV[2]: 最大请求数
-- ARGV[3]: 当前时间戳
-- ARGV[4]: 唯一请求 ID
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local uid = ARGV[4]
-- 清除窗口外的旧记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 当前窗口内的请求数
local count = redis.call('ZCARD', key)
if count < limit then
-- 未超限,记录本次请求
redis.call('ZADD', key, now, uid)
redis.call('PEXPIRE', key, window)
return 1 -- 允许
else
return 0 -- 拒绝
end
为什么必须用 Lua? 因为 "判断 + 扣减" 是两步操作,如果不原子,并发下两个请求同时判断为 "未超限",就都过了,限流就形同虚设。Lua 脚本在 Redis 里是单线程串行执行的,天然保证原子性。
四、生产环境怎么选?给三个原则
1. 先单机,再集群。 绝大多数场景,单机限流够用了。只有当你有明确的 "全局配额" 需求(比如开放平台给某个商户每秒 100 QPS),才考虑集群限流。我见过不少团队一上来就搞 Redis 集群限流,结果 Redis 自己成了瓶颈,得不偿失。
2. 多层兜底,各管一摊。 经典架构是:
- 网关层做集群限流:挡住突发流量,保护整个集群
- 应用层做单机限流:保护单个实例不被打挂
- 调用下游时再做客户端限流:保护下游服务,这块很多团队会漏,其实挺关键
3. 集群限流一定要有降级方案。 Redis 挂了怎么办?Token Server 宕机了怎么办?Sentinel 的做法是:中心化组件不可用时,降级为本地限流。这个降级逻辑必须有,否则中心化组件一挂,系统要么完全放开(限流失效),要么完全拒绝(雪崩),两个都不能接受。
面试高频追问
-
追问一:为什么 Sentinel 集群限流要用 Token Server,而不是直接用 Redis?
- Token Server 模式下,客户端会异步批量拉取令牌(默认每 100ms 向 Server 申请一批),缓存在本地,不用每次请求都跑一趟 Token Server,性能比纯 Redis 方案高一个量级。Redis 方案虽然简单,但每次请求都要走网络,高 QPS 下 Redis 容易扛不住。
-
追问二:Redis + Lua 限流,Redis 挂了怎么办?
- 两种思路:一是降级为本地单机限流(Sentinel 的做法),保证基本防护;二是用 Redis Cluster 保证高可用,但要做好客户端重试和超时控制,避免 Redis 抖动时把应用拖垮。
-
追问三:令牌桶和漏桶的区别?集群限流用哪个?
- 令牌桶允许突发流量(桶里攒的令牌可以一次性用掉),漏桶强行平滑输出。大部分业务场景用令牌桶,因为它对突发流量更友好。Redis + Lua 实现的通常是令牌桶或滑动窗口。
-
追问四:单机限流能完全替代集群限流吗?
- 不能。假设集群有 10 台实例,你想限制总 QPS 为 100,平均分每台 10 QPS?理论上对,但负载均衡不可能完全均匀,某台可能扛 50 QPS,某台只有 2 QPS,本机限流根本兜不住全局。所以全局配额必须靠集群限流。
常见面试变体
- "集群限流怎么保证原子性?"
- "Sentinel 的单机限流和集群限流切换是怎么做的?"
- "API 网关怎么做限流?和业务层限流冲突吗?"
- "分布式环境下,限流和熔断降级怎么配合?"
记忆口诀
单机保自己,集群保全局;网关挡入口,应用护自身;中心挂了别慌,降级本地兜底。
总结
单机限流和集群限流本来就是搭档关系,各管一摊。单机限流轻量高效,适合保护单个实例;集群限流重但精准,适合控制全局配额。生产架构基本都是 "网关集群限流 + 应用单机限流 + 客户端限流" 三层兜底。生产环境的限流,从来都是一套组合拳,单点防御顶不住真实流量。
