本文目录导读:
- 核心挑战
- 方案一:基于“中心化存储”+“网络通信”(最常用)
- 方案二:基于“分布式协调服务”(一致性要求高)
- 方案三:基于“网络流量调度层”(网关层实现)
- 方案四:混合模式(最优实践)
- 网络实现中的关键挑战与解决
分布式限流的“网络实现”,本质上是通过网络通信将分散在各个服务器节点上的流量计数集中协调起来,从而在全局层面执行限流策略。
这与单机限流(如Guava RateLimiter)完全不同,后者只依赖本地内存,无法感知其他节点的请求。
以下是分布式限流在网络层面常见的三种实现模式,以及它们的技术细节、优缺点和适用场景。
核心挑战
分布式限流需要解决的核心问题是原子性和一致性,当多个节点同时读写同一个计数器时,必须保证数据不冲突。
基于“中心化存储”+“网络通信”(最常用)
所有节点的请求都通过网络访问同一个中央存储节点(如Redis、MySQL)来获取或扣减令牌/计数。
基于 Redis + Lua 脚本(最主流)
这是最流行的实现方式,Redis提供了高性能的原子操作。
-
网络流程:
- 用户请求到达任意一个服务节点。
- 服务节点通过网络向Redis集群发送一个预先编写好的Lua脚本。
- Redis服务器单线程执行Lua脚本,进行令牌或计数器的GET、INCR、EXPIRE等操作。
- Redis返回结果(成功/失败/剩余配额)。
- 服务节点根据结果决定放行或拒绝。
-
关键技术:
- Lua脚本:保证判断和扣减操作的原子性,避免并发问题。
- Redis Sentinel/Cluster:解决Redis单点故障,实现高可用。
-
实现示例(滑动窗口限流脚本逻辑):
-- KEYS[1]: 限流key (rate_limiter:user:123) -- ARGV[1]: 时间窗口(毫秒) -- ARGV[2]: 窗口内最大请求数 -- ARGV[3]: 当前时间戳(毫秒) -- 移除窗口之外的历史记录 redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[3] - ARGV[1]) -- 统计窗口内请求数 local current = tonumber(redis.call('ZCARD', KEYS[1])) if current < tonumber(ARGV[2]) then -- 在窗口内添加当前请求 redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3] .. '_' .. math.random()) redis.call('EXPIRE', KEYS[1], ARGV[1] / 1000 + 1) return 1 -- 允许访问 else return 0 -- 拒绝访问 end -
优点:实现简单,性能好(Redis单机QPS可达10万+),社区有成熟实现(如Redisson的
RRateLimiter)。 -
缺点:引入网络延迟(毫秒级),请求量极大时Redis可能成为瓶颈;依赖Redis高可用性。
-
适用场景:绝大多数分布式系统,如API网关、微服务对用户、接口的限流。
基于 MySQL
- 网络流程:每个请求都去查询和更新MySQL表中的计数器(
SELECT ... FOR UPDATE或UPDATE ... SET count = count + 1 WHERE ...)。 - 技术实现:
- 使用悲观锁(
FOR UPDATE)或乐观锁(版本号)。 - 利用数据库的唯一键或行锁实现原子操作。
- 使用悲观锁(
- 优点:数据持久化强,与业务系统集成方便。
- 缺点:性能极低(高并发下MySQL磁盘IO和行锁竞争严重),网络延迟高,通常不作为高并发限流的主方案。
- 适用场景:对性能要求不高、并发量小、但需要强数据一致性的内部管理后台或批处理系统。
基于“分布式协调服务”(一致性要求高)
利用如Zookeeper、etcd、Consul等分布式协调服务。
- 网络流程:
- 服务节点通过网络向Zookeeper/etcd集群发起请求。
- 协调服务利用其强一致性协议(如ZAB、Raft)在集群内完成写操作。
- 协调服务返回结果。
- 关键技术:
- 临时节点 + 监听:用于实现分布式信号量或互斥锁。
- Watch机制:节点等待下一轮令牌发放。
- 优点:强一致性,高可用(自身就是分布式集群),可靠。
- 缺点:性能差(相比Redis,Zookeeper写性能通常只有几千QPS),网络延迟高,设计复杂。
- 适用场景:对一致性要求极高、并发量不高的场景(如分布式任务调度、配置中心的配额限制)。
基于“网络流量调度层”(网关层实现)
不在业务代码里实现限流逻辑,而是将限流能力下沉到流量入口。
- 网络流程:
- 所有外网请求先到达API网关(如Nginx + Lua、Kong、Zuul等)。
- 网关自己维护一个全局的计数器(或通过Lua脚本与Redis交互)。
- 网关在网络请求进入集群之前直接判断和拦截。
- 关键技术:
- Nginx + Lua (OpenResty):Lua脚本可以直接在Nginx worker进程内执行,也可以访问共享内存(lua_shared_dict)或Redis。
- Kong / APISIX:利用插件(如
rate-limiting插件),底层通常基于Redis。
- 优点:对业务代码完全无侵入;统一管控入口;性能极高(Nginx是异步非阻塞)。
- 缺点:限流粒度较粗(通常只能基于IP、URL、Header等网络层信息);灵活性不如业务层。
- 适用场景:所有对外暴露的API服务。
混合模式(最优实践)
结合“网络层”和“业务层”优点。
- L1:网关层(网络软限流)
- 在Nginx等网关做粗略的、高吞吐的前置保护,防止突发流量打穿后端,限制每个IP每秒最多1000次请求。
- L2:业务层(精确硬限流)
- 在业务代码中,结合Redis + Lua做精准的、自定义的限流,针对特定的用户ID、订单类型进行限流。
- 网络流程: 请求先过Nginx(读本地共享内存,无网络开销或极少网络开销) -> 通过后进入应用 -> 应用通过网络访问Redis做精确判断。
网络实现中的关键挑战与解决
- 网络延迟:每次限流请求都经过网络(到Redis),会增加毫秒级延迟。
- 解决:使用批量操作(Pipeline)、本地缓存预取(如Redis中批量获取Quota后缓存在本地一段时间)、异步化(将限流结果异步上报,但有一定概率不准确)。
- 网络分区:当Redis集群与业务节点网络不通时,是“熔断”(拒绝所有请求)还是“降级”(放行所有请求)?
- 解决:采用降级策略,如果检测到与Redis网络连通失败,暂时放行请求(降级到本地限流或直接放行),待网络恢复后同步计数,这是为了避免网络抖动导致整个系统瘫痪。
- 数据一致性:在分布式环境下,严格全局一致很难做到(尤其是高性能场景)。
- 解决:通常采用最终一致性,允许少量短时误差(比如多放行几个请求),如果必须精确,则要牺牲性能,使用Zookeeper或分布式事务。
| 方案 | 网络成本 | 性能 | 一致性 | 典型实现 | 适用场景 |
|---|---|---|---|---|---|
| Redis + Lua | 低(3-5ms) | 高(万级QPS) | 强(原子脚本) | Redisson RRateLimiter | 最通用,微服务、API限流 |
| MySQL | 高(10ms+) | 极低 | 强(行锁) | UPDATE ... WHERE |
低并发管理后台 |
| Zookeeper/etcd | 高(10ms+) | 低 | 极强(共识算法) | 分布式信号量 | 配置中心、任务调度 |
| Nginx/网关 | 无/极低 | 极高(百万级QPS) | 较弱(本地计数) | limit_req 模块 |
第一层防线,防DDoS |
最佳网络实现路径建议:
- 第一选择:Nginx(L1)+ Redis(L2),Nginx扛住流量毛刺,Redis做精确限流。
- 高性能场景:使用Redis Lua脚本,并做好本地缓存预取 + 网络降级策略。
- 避免:直接使用MySQL或Zookeeper做高并发限流的网络存储。