单机限流和集群限流的区别是什么?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(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+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 基础掌握度:面试官真正想看的是,你对这两个概念的作用范围、实现方式、适用场景心里有没有数,能不能根据业务 QPS 来选型。
  2. 分布式理解深度:集群限流涉及到分布式环境下的协调问题(原子性、一致性、网络分区),考察你对 Redis + Lua、网关限流、Token Server 这些方案的理解。
  3. 实战经验:生产环境怎么选?单机限流扛不住时怎么过渡到集群限流?集群限流的坑(比如 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 集群限流

三、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 的做法是:中心化组件不可用时,降级为本地限流。这个降级逻辑必须有,否则中心化组件一挂,系统要么完全放开(限流失效),要么完全拒绝(雪崩),两个都不能接受。

集群限流降级本地
集群限流降级本地

面试高频追问

  1. 追问一:为什么 Sentinel 集群限流要用 Token Server,而不是直接用 Redis?

    • Token Server 模式下,客户端会异步批量拉取令牌(默认每 100ms 向 Server 申请一批),缓存在本地,不用每次请求都跑一趟 Token Server,性能比纯 Redis 方案高一个量级。Redis 方案虽然简单,但每次请求都要走网络,高 QPS 下 Redis 容易扛不住。
  2. 追问二:Redis + Lua 限流,Redis 挂了怎么办?

    • 两种思路:一是降级为本地单机限流(Sentinel 的做法),保证基本防护;二是用 Redis Cluster 保证高可用,但要做好客户端重试和超时控制,避免 Redis 抖动时把应用拖垮。
  3. 追问三:令牌桶和漏桶的区别?集群限流用哪个?

    • 令牌桶允许突发流量(桶里攒的令牌可以一次性用掉),漏桶强行平滑输出。大部分业务场景用令牌桶,因为它对突发流量更友好。Redis + Lua 实现的通常是令牌桶或滑动窗口。
  4. 追问四:单机限流能完全替代集群限流吗?

    • 不能。假设集群有 10 台实例,你想限制总 QPS 为 100,平均分每台 10 QPS?理论上对,但负载均衡不可能完全均匀,某台可能扛 50 QPS,某台只有 2 QPS,本机限流根本兜不住全局。所以全局配额必须靠集群限流

常见面试变体

  • "集群限流怎么保证原子性?"
  • "Sentinel 的单机限流和集群限流切换是怎么做的?"
  • "API 网关怎么做限流?和业务层限流冲突吗?"
  • "分布式环境下,限流和熔断降级怎么配合?"

记忆口诀

单机保自己,集群保全局;网关挡入口,应用护自身;中心挂了别慌,降级本地兜底。

总结

单机限流和集群限流本来就是搭档关系,各管一摊。单机限流轻量高效,适合保护单个实例;集群限流重但精准,适合控制全局配额。生产架构基本都是 "网关集群限流 + 应用单机限流 + 客户端限流" 三层兜底。生产环境的限流,从来都是一套组合拳,单点防御顶不住真实流量。