纲要限流的核心概念常见限流指标QPS、并发数、TPS、资源使用率等分层限流体系前端验证码 → 接入层Nginx→ 服务层算法 → 数据层消息队列典型限流算法解析令牌桶算法原理与示意图滑动窗口算法原理与示意图go-zero中的限流器并发限流器基于缓冲channel的轻量级拦截器令牌桶限流器基于RedisLua脚本实现自带本地兜底滑动窗口限流器基于RedisLua的滚动计数实战社交服务群接口限流项目结构概览并发限流拦截器实现与压测令牌桶限流中间件实现与压测源码原理解析并发限流channel的发送/接收语义令牌桶Lua脚本中的令牌生成逻辑Redis与本地切换机制滑动窗口ZSET 过期时间实现的窗口滑动总结与相关度分析限流的核心概念在高并发系统中限流是保障服务稳定的重要手段。它的本质是控制单位时间内的请求量防止系统因过载而雪崩。常见的限流指标有QPS每秒查询数并发连接数TPS每秒事务数CPU / 内存利用率请求响应时间限流并非只在某个单点生效而是可以贯穿整个调用链形成分层限流体系层级手段作用前端 / 客户端验证码、按钮置灰、点击间隔限制延缓请求发出过滤机器流量接入层Nginx limit_req/limit_conn模块基于 IP、URL 等维度快速限流服务层令牌桶、滑动窗口、并发控制等算法细粒度保护业务逻辑数据层消息队列削峰、数据库连接池限制保护存储资源平滑流量流量从上到下逐层递减每一层都可以根据自身特点设置不同的限流策略。典型限流算法解析令牌桶算法令牌桶的核心思想是以恒定速率向桶中放入令牌请求到来时必须从桶中获取一个令牌才能通过当令牌耗尽时新请求会被拒绝。桶的容量决定了允许的突发流量大小。放入令牌获取令牌成功失败定时器 恒定速率令牌桶 容量为N请求处理请求拒绝/限流若桶初始满瞬间可以处理N个请求之后受速率限制。适合允许一定突发流量、但需要长期平滑控制的场景。滑动窗口算法滑动窗口将时间轴划分为多个小的子窗口窗口整体向前滑动每次统计当前窗口范围内的请求总数。超过预设阈值时触发限流。时间轴落入最新子窗口是否窗口滑动后丢弃窗口1窗口2窗口3窗口4窗口5新请求窗口内总数 阈值?拒绝通过丢弃例如设定窗口大小1秒子窗口数5则每0.2秒滑动一次。窗口覆盖的时间范围始终为1秒只统计这1秒内的请求数。相比固定窗口它能更平滑地处理边界突发避免“双倍流量”问题。go-zero中的限流器go-zero框架提供了三种常用的限流器分别位于core/limit和rest/handler等包中限流器所在包实现基础适用场景并发限流 (TokenLimiter)github.com/zeromicro/go-zero/core/limit缓冲channel控制最大并发数令牌桶 (TokenLimiter Redis)github.com/zeromicro/go-zero/core/limitRedisLua脚本本地兜底平滑限流允许突发滑动窗口github.com/zeromicro/go-zero/core/limitRedisLua脚本 (ZSET)精确控制时间窗口内请求量下面以一个社交服务群模块为例展示如何在项目中使用这些限流器。项目结构social/ ├── social.proto # 定义 RPC 接口 ├── social.go # 服务实现 ├── internal/ │ ├── logic/ │ │ └── groups/ │ │ └── list.go # 群列表业务逻辑 │ └── server/ │ └── socialServer.go # 服务启动入口 └── etc/ └── social.yaml # 配置文件并发限流拦截器并发限流器基于带缓冲的 channel实现当缓冲区满时新的请求无法写入 channel 而被拒绝请求处理完成后从 channel 中读出一个元素释放一个并发槽位。1. 创建限流拦截器在social/internal/middleware/tokenlimiter.go中实现packagemiddlewareimport(contextgithub.com/zeromicro/go-zero/core/limitgithub.com/zeromicro/go-zero/zrpcgoogle.golang.org/grpc)// TokenLimiterInterceptor 基于并发数的限流拦截器funcTokenLimiterInterceptor(maxConcurrencyint)grpc.UnaryServerInterceptor{limiter:limit.NewTokenLimiter(maxConcurrency)returnfunc(ctx context.Context,reqinterface{},info*grpc.UnaryServerInfo,handler grpc.UnaryHandler)(respinterface{},errerror){iflimiter.Allow(){deferlimiter.Release()returnhandler(ctx,req)}returnnil,zrpc.ErrLimited()}}2. 在服务端应用拦截器修改social/internal/server/socialServer.gopackageserverimport(github.com/zeromicro/go-zero/zrpcsocial/internal/middlewaresocial/internal/logic/groups)funcRegister(server*zrpc.RpcServer){// 限制最大并发数为 10server.AddUnaryInterceptors(middleware.TokenLimiterInterceptor(10))social.RegisterSocialServer(server,groups.New())}3. 压测验证使用ghz等工具进行压测ghz--insecure\--protosocial.proto\--callsocial.Social/ListGroups\--data{user_id:1}\--concurrency50\--total200\localhost:8080结果示例在 10 并发限制下总请求 200 个成功通过约 10~20 个因为请求处理很快channel 不断释放槽位其余请求返回限流错误。令牌桶限流中间件对于需要平滑限流的API层可使用基于Redis的令牌桶。go-zero的令牌桶支持传入速率和桶容量并自动将状态同步到Redis同时内置了本地兜底逻辑防止Redis故障导致服务不可用。1. 实现限流中间件在social/internal/middleware/tokenbucket.gopackagemiddlewareimport(net/httpgithub.com/zeromicro/go-zero/core/limitgithub.com/zeromicro/go-zero/core/stores/redisgithub.com/zeromicro/go-zero/rest)typeTokenBucketLimiterstruct{limiter*limit.TokenLimiter}// NewTokenBucketLimiter 创建令牌桶限流器// rate: 每秒生成的令牌数// burst: 桶容量// redisCfg: redis 配置// key: 限流器在 redis 中的键名funcNewTokenBucketLimiter(rate,burstint,redisCfg redis.RedisConf,keystring)*TokenBucketLimiter{store:redis.MustNewRedis(redisCfg)returnTokenBucketLimiter{limiter:limit.NewTokenLimiter(rate,burst,store,key),}}func(t*TokenBucketLimiter)Handle(next http.HandlerFunc)http.HandlerFunc{returnfunc(w http.ResponseWriter,r*http.Request){ift.limiter.Allow(){next(w,r)}else{http.Error(w,too many requests,http.StatusTooManyRequests)}}}2. 在 API 路由中注册在api服务的启动文件如socialapi.go中packagemainimport(lognet/httpgithub.com/zeromicro/go-zero/core/stores/redisgithub.com/zeromicro/go-zero/restsocial/internal/middleware)funcmain(){varc rest.RestConf// 加载配置...redisCfg:redis.RedisConf{Host:127.0.0.1:6379,Type:node,}limiter:middleware.NewTokenBucketLimiter(1,100,redisCfg,social_api_limit)server:rest.MustNewServer(c)server.AddRoutes([]rest.Route{{Method:http.MethodGet,Path:/groups/list,Handler:limiter.Handle(groupsListHandler),},},)deferserver.Stop()server.Start()}3. 压测验证使用wrk或ab进行测试ab-n500-c100http://localhost:8888/groups/list结果首次并发 100 个请求全部成功因为桶初始容量 100且生成速率 1/s瞬间允许 100 个后续大量请求被限流返回 429成功数稳定在桶容量附近。源码原理解析并发限流的channel实现在core/limit/tokenlimiter.go中核心结构极其简洁typeTokenLimiterstruct{chchanstruct{}}funcNewTokenLimiter(nint)*TokenLimiter{returnTokenLimiter{ch:make(chanstruct{},n),}}func(tl*TokenLimiter)Allow()bool{select{casetl.ch-struct{}{}:returntruedefault:returnfalse}}func(tl*TokenLimiter)Release(){-tl.ch}Allow()向带缓冲的channel发送空结构体若缓冲区满则进入default分支返回false表示限流。Release()从channel中读取释放一个位置。该实现极其轻量适合控制单进程内的最大并发数但不适合跨进程的分布式限流。令牌桶的Lua脚本与兜底go-zero的令牌桶限流支持两种模式基于 Redis和本地内存。核心逻辑在core/limit/tokenlimiter.go的reserveN方法中其调用了一段Lua脚本大致逻辑localratetonumber(ARGV[1])-- 每秒令牌生成速率localcapacitytonumber(ARGV[2])-- 桶容量localnowtonumber(ARGV[3])-- 当前时间localrequestedtonumber(ARGV[4])-- 请求令牌数通常为1locallastredis.call(HGET,KEYS[1],last_time)localtokensredis.call(HGET,KEYS[1],tokens)iflastfalsethentokenscapacityelselocaldeltamath.max(0,now-last)localfilleddelta*rate/1000tokensmath.min(capacity,tokensfilled)endiftokensrequestedthentokenstokens-requested redis.call(HSET,KEYS[1],tokens,tokens)redis.call(HSET,KEYS[1],last_time,now)return1endreturn0go-zero在执行时首先尝试使用Redis执行该脚本如果返回“令牌获取成功”则放行如果出现Redis连接异常、超时等错误则自动降级到本地内存限流器基于同样的令牌桶算法。同时框架会启动一个后台goroutine持续监听Redis的健康状态一旦恢复便切回Redis模式保证一致性。滑动窗口的ZSET实现滑动窗口同样依赖Redis核心Lua脚本使用了有序集合ZSETlocalkeyKEYS[1]-- 限流 keylocalwindowtonumber(ARGV[1])-- 窗口大小毫秒locallimittonumber(ARGV[2])-- 阈值localnowtonumber(ARGV[3])-- 当前时间毫秒redis.call(ZREMRANGEBYSCORE,key,0,now-window)localcountredis.call(ZCARD,key)ifcountlimitthenreturn0endredis.call(ZADD,key,now,now..-..math.random())redis.call(PEXPIRE,key,window)return1每次请求都会将当前时间戳作为成员加入ZSET。删除窗口以外的记录统计剩余数量。如果未超过阈值则允许否则拒绝。通过设置PEXPIRE使键自动过期避免内存泄漏。这种实现精确且高效广泛用于需要严格控制时间窗口内调用量的场景。总结本文从限流的基础概念出发梳理了令牌桶和滑动窗口两种经典算法并结合go-zero框架展示了三种限流器的工程实现基于channel的并发限流、基于RedisLua的令牌桶和滑动窗口。在社交服务群的案例中我们通过拦截器和中间件的方式将这些限流器集成进了RPC和API层并通过压测验证了效果。最后深入源码理解了它们背后的核心原理与容错设计。