纲要
- 限流的核心概念
- 常见限流指标:
QPS、并发数、TPS、资源使用率等 - 分层限流体系:前端验证码 → 接入层
Nginx→ 服务层算法 → 数据层消息队列
- 常见限流指标:
- 典型限流算法解析
- 令牌桶算法原理与示意图
- 滑动窗口算法原理与示意图
go-zero中的限流器- 并发限流器:基于缓冲
channel的轻量级拦截器 - 令牌桶限流器:基于
Redis+Lua脚本实现,自带本地兜底 - 滑动窗口限流器:基于
Redis+Lua的滚动计数
- 并发限流器:基于缓冲
- 实战:社交服务群接口限流
- 项目结构概览
- 并发限流拦截器实现与压测
- 令牌桶限流中间件实现与压测
- 源码原理解析
- 并发限流:
channel的发送/接收语义 - 令牌桶:
Lua脚本中的令牌生成逻辑,Redis与本地切换机制 - 滑动窗口:
ZSET+ 过期时间实现的窗口滑动
- 并发限流:
- 总结与相关度分析
限流的核心概念
在高并发系统中,限流是保障服务稳定的重要手段。它的本质是控制单位时间内的请求量,防止系统因过载而雪崩。常见的限流指标有:
- QPS(每秒查询数)
- 并发连接数
- TPS(每秒事务数)
- CPU / 内存利用率
- 请求响应时间
限流并非只在某个单点生效,而是可以贯穿整个调用链,形成分层限流体系:
| 层级 | 手段 | 作用 |
|---|---|---|
| 前端 / 客户端 | 验证码、按钮置灰、点击间隔限制 | 延缓请求发出,过滤机器流量 |
| 接入层 | Nginx limit_req / limit_conn 模块 |
基于 IP、URL 等维度快速限流 |
| 服务层 | 令牌桶、滑动窗口、并发控制等算法 | 细粒度保护业务逻辑 |
| 数据层 | 消息队列削峰、数据库连接池限制 | 保护存储资源,平滑流量 |
流量从上到下逐层递减,每一层都可以根据自身特点设置不同的限流策略。
典型限流算法解析
令牌桶算法
令牌桶的核心思想是以恒定速率 向桶中放入令牌,请求到来时必须从桶中获取一个令牌才能通过;当令牌耗尽时,新请求会被拒绝。桶的容量决定了允许的突发流量大小。
#mermaid-svg-M6tHS9XhuLRXRZHE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-M6tHS9XhuLRXRZHE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-M6tHS9XhuLRXRZHE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-M6tHS9XhuLRXRZHE .error-icon{fill:#552222;}#mermaid-svg-M6tHS9XhuLRXRZHE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-M6tHS9XhuLRXRZHE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-M6tHS9XhuLRXRZHE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-M6tHS9XhuLRXRZHE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-M6tHS9XhuLRXRZHE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-M6tHS9XhuLRXRZHE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-M6tHS9XhuLRXRZHE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-M6tHS9XhuLRXRZHE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-M6tHS9XhuLRXRZHE .marker.cross{stroke:#333333;}#mermaid-svg-M6tHS9XhuLRXRZHE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-M6tHS9XhuLRXRZHE p{margin:0;}#mermaid-svg-M6tHS9XhuLRXRZHE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-M6tHS9XhuLRXRZHE .cluster-label text{fill:#333;}#mermaid-svg-M6tHS9XhuLRXRZHE .cluster-label span{color:#333;}#mermaid-svg-M6tHS9XhuLRXRZHE .cluster-label span p{background-color:transparent;}#mermaid-svg-M6tHS9XhuLRXRZHE .label text,#mermaid-svg-M6tHS9XhuLRXRZHE span{fill:#333;color:#333;}#mermaid-svg-M6tHS9XhuLRXRZHE .node rect,#mermaid-svg-M6tHS9XhuLRXRZHE .node circle,#mermaid-svg-M6tHS9XhuLRXRZHE .node ellipse,#mermaid-svg-M6tHS9XhuLRXRZHE .node polygon,#mermaid-svg-M6tHS9XhuLRXRZHE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-M6tHS9XhuLRXRZHE .rough-node .label text,#mermaid-svg-M6tHS9XhuLRXRZHE .node .label text,#mermaid-svg-M6tHS9XhuLRXRZHE .image-shape .label,#mermaid-svg-M6tHS9XhuLRXRZHE .icon-shape .label{text-anchor:middle;}#mermaid-svg-M6tHS9XhuLRXRZHE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-M6tHS9XhuLRXRZHE .rough-node .label,#mermaid-svg-M6tHS9XhuLRXRZHE .node .label,#mermaid-svg-M6tHS9XhuLRXRZHE .image-shape .label,#mermaid-svg-M6tHS9XhuLRXRZHE .icon-shape .label{text-align:center;}#mermaid-svg-M6tHS9XhuLRXRZHE .node.clickable{cursor:pointer;}#mermaid-svg-M6tHS9XhuLRXRZHE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-M6tHS9XhuLRXRZHE .arrowheadPath{fill:#333333;}#mermaid-svg-M6tHS9XhuLRXRZHE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-M6tHS9XhuLRXRZHE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-M6tHS9XhuLRXRZHE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M6tHS9XhuLRXRZHE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-M6tHS9XhuLRXRZHE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M6tHS9XhuLRXRZHE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-M6tHS9XhuLRXRZHE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-M6tHS9XhuLRXRZHE .cluster text{fill:#333;}#mermaid-svg-M6tHS9XhuLRXRZHE .cluster span{color:#333;}#mermaid-svg-M6tHS9XhuLRXRZHE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-M6tHS9XhuLRXRZHE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-M6tHS9XhuLRXRZHE rect.text{fill:none;stroke-width:0;}#mermaid-svg-M6tHS9XhuLRXRZHE .icon-shape,#mermaid-svg-M6tHS9XhuLRXRZHE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-M6tHS9XhuLRXRZHE .icon-shape p,#mermaid-svg-M6tHS9XhuLRXRZHE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-M6tHS9XhuLRXRZHE .icon-shape .label rect,#mermaid-svg-M6tHS9XhuLRXRZHE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-M6tHS9XhuLRXRZHE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-M6tHS9XhuLRXRZHE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-M6tHS9XhuLRXRZHE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 放入令牌
获取令牌
成功
失败
定时器 恒定速率
令牌桶 容量为N
请求
处理请求
拒绝/限流
- 若桶初始满,瞬间可以处理
N个请求,之后受速率限制。 - 适合允许一定突发流量、但需要长期平滑控制的场景。
滑动窗口算法
滑动窗口将时间轴划分为多个小的子窗口,窗口整体向前滑动,每次统计当前窗口范围内的请求总数。超过预设阈值时触发限流。
#mermaid-svg-ll4w1vaPnAwv15PQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ll4w1vaPnAwv15PQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ll4w1vaPnAwv15PQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ll4w1vaPnAwv15PQ .error-icon{fill:#552222;}#mermaid-svg-ll4w1vaPnAwv15PQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ll4w1vaPnAwv15PQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ll4w1vaPnAwv15PQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ll4w1vaPnAwv15PQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ll4w1vaPnAwv15PQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ll4w1vaPnAwv15PQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ll4w1vaPnAwv15PQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ll4w1vaPnAwv15PQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ll4w1vaPnAwv15PQ .marker.cross{stroke:#333333;}#mermaid-svg-ll4w1vaPnAwv15PQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ll4w1vaPnAwv15PQ p{margin:0;}#mermaid-svg-ll4w1vaPnAwv15PQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ll4w1vaPnAwv15PQ .cluster-label text{fill:#333;}#mermaid-svg-ll4w1vaPnAwv15PQ .cluster-label span{color:#333;}#mermaid-svg-ll4w1vaPnAwv15PQ .cluster-label span p{background-color:transparent;}#mermaid-svg-ll4w1vaPnAwv15PQ .label text,#mermaid-svg-ll4w1vaPnAwv15PQ span{fill:#333;color:#333;}#mermaid-svg-ll4w1vaPnAwv15PQ .node rect,#mermaid-svg-ll4w1vaPnAwv15PQ .node circle,#mermaid-svg-ll4w1vaPnAwv15PQ .node ellipse,#mermaid-svg-ll4w1vaPnAwv15PQ .node polygon,#mermaid-svg-ll4w1vaPnAwv15PQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ll4w1vaPnAwv15PQ .rough-node .label text,#mermaid-svg-ll4w1vaPnAwv15PQ .node .label text,#mermaid-svg-ll4w1vaPnAwv15PQ .image-shape .label,#mermaid-svg-ll4w1vaPnAwv15PQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-ll4w1vaPnAwv15PQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ll4w1vaPnAwv15PQ .rough-node .label,#mermaid-svg-ll4w1vaPnAwv15PQ .node .label,#mermaid-svg-ll4w1vaPnAwv15PQ .image-shape .label,#mermaid-svg-ll4w1vaPnAwv15PQ .icon-shape .label{text-align:center;}#mermaid-svg-ll4w1vaPnAwv15PQ .node.clickable{cursor:pointer;}#mermaid-svg-ll4w1vaPnAwv15PQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ll4w1vaPnAwv15PQ .arrowheadPath{fill:#333333;}#mermaid-svg-ll4w1vaPnAwv15PQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ll4w1vaPnAwv15PQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ll4w1vaPnAwv15PQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ll4w1vaPnAwv15PQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ll4w1vaPnAwv15PQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ll4w1vaPnAwv15PQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ll4w1vaPnAwv15PQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ll4w1vaPnAwv15PQ .cluster text{fill:#333;}#mermaid-svg-ll4w1vaPnAwv15PQ .cluster span{color:#333;}#mermaid-svg-ll4w1vaPnAwv15PQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ll4w1vaPnAwv15PQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ll4w1vaPnAwv15PQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-ll4w1vaPnAwv15PQ .icon-shape,#mermaid-svg-ll4w1vaPnAwv15PQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ll4w1vaPnAwv15PQ .icon-shape p,#mermaid-svg-ll4w1vaPnAwv15PQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ll4w1vaPnAwv15PQ .icon-shape .label rect,#mermaid-svg-ll4w1vaPnAwv15PQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ll4w1vaPnAwv15PQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ll4w1vaPnAwv15PQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ll4w1vaPnAwv15PQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 时间轴
落入最新子窗口
是
否
窗口滑动后丢弃
窗口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/limit |
Redis + Lua 脚本,本地兜底 |
平滑限流,允许突发 |
| 滑动窗口 | github.com/zeromicro/go-zero/core/limit |
Redis + Lua 脚本 (ZSET) |
精确控制时间窗口内请求量 |
下面以一个社交服务群模块为例,展示如何在项目中使用这些限流器。
项目结构
dir
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 中实现:
go
package middleware
import (
"context"
"github.com/zeromicro/go-zero/core/limit"
"github.com/zeromicro/go-zero/zrpc"
"google.golang.org/grpc"
)
// TokenLimiterInterceptor 基于并发数的限流拦截器
func TokenLimiterInterceptor(maxConcurrency int) grpc.UnaryServerInterceptor {
limiter := limit.NewTokenLimiter(maxConcurrency)
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
if limiter.Allow() {
defer limiter.Release()
return handler(ctx, req)
}
return nil, zrpc.ErrLimited()
}
}
2. 在服务端应用拦截器
修改 social/internal/server/socialServer.go:
go
package server
import (
"github.com/zeromicro/go-zero/zrpc"
"social/internal/middleware"
"social/internal/logic/groups"
)
func Register(server *zrpc.RpcServer) {
// 限制最大并发数为 10
server.AddUnaryInterceptors(middleware.TokenLimiterInterceptor(10))
social.RegisterSocialServer(server, groups.New())
}
3. 压测验证
使用 ghz 等工具进行压测:
bash
ghz --insecure \
--proto social.proto \
--call social.Social/ListGroups \
--data '{"user_id":1}' \
--concurrency=50 \
--total=200 \
localhost:8080
结果示例:在 10 并发限制下,总请求 200 个,成功通过约 10~20 个(因为请求处理很快,channel 不断释放槽位),其余请求返回限流错误。
令牌桶限流中间件
对于需要平滑限流的 API 层,可使用基于 Redis 的令牌桶。go-zero 的令牌桶支持传入速率和桶容量,并自动将状态同步到 Redis,同时内置了本地兜底逻辑,防止 Redis 故障导致服务不可用。
1. 实现限流中间件
在 social/internal/middleware/tokenbucket.go:
go
package middleware
import (
"net/http"
"github.com/zeromicro/go-zero/core/limit"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/rest"
)
type TokenBucketLimiter struct {
limiter *limit.TokenLimiter
}
// NewTokenBucketLimiter 创建令牌桶限流器
// rate: 每秒生成的令牌数
// burst: 桶容量
// redisCfg: redis 配置
// key: 限流器在 redis 中的键名
func NewTokenBucketLimiter(rate, burst int, redisCfg redis.RedisConf, key string) *TokenBucketLimiter {
store := redis.MustNewRedis(redisCfg)
return &TokenBucketLimiter{
limiter: limit.NewTokenLimiter(rate, burst, store, key),
}
}
func (t *TokenBucketLimiter) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if t.limiter.Allow() {
next(w, r)
} else {
http.Error(w, "too many requests", http.StatusTooManyRequests)
}
}
}
2. 在 API 路由中注册
在 api 服务的启动文件(如 socialapi.go)中:
go
package main
import (
"log"
"net/http"
"github.com/zeromicro/go-zero/core/stores/redis"
"github.com/zeromicro/go-zero/rest"
"social/internal/middleware"
)
func main() {
var c 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),
},
},
)
defer server.Stop()
server.Start()
}
3. 压测验证
使用 wrk 或 ab 进行测试:
bash
ab -n 500 -c 100 http://localhost:8888/groups/list
结果:首次并发 100 个请求全部成功(因为桶初始容量 100,且生成速率 1/s,瞬间允许 100 个),后续大量请求被限流返回 429,成功数稳定在桶容量附近。
源码原理解析
并发限流的 channel 实现
在 core/limit/tokenlimiter.go 中,核心结构极其简洁:
go
type TokenLimiter struct {
ch chan struct{}
}
func NewTokenLimiter(n int) *TokenLimiter {
return &TokenLimiter{
ch: make(chan struct{}, n),
}
}
func (tl *TokenLimiter) Allow() bool {
select {
case tl.ch <- struct{}{}:
return true
default:
return false
}
}
func (tl *TokenLimiter) Release() {
<-tl.ch
}
Allow()向带缓冲的channel发送空结构体,若缓冲区满则进入default分支返回false,表示限流。Release()从channel中读取,释放一个位置。- 该实现极其轻量,适合控制单进程内的最大并发数,但不适合跨进程的分布式限流。
令牌桶的 Lua 脚本与兜底
go-zero 的令牌桶限流支持两种模式:基于 Redis 和本地内存 。核心逻辑在 core/limit/tokenlimiter.go 的 reserveN 方法中,其调用了一段 Lua 脚本(大致逻辑):
lua
local rate = tonumber(ARGV[1]) -- 每秒令牌生成速率
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3]) -- 当前时间
local requested = tonumber(ARGV[4]) -- 请求令牌数(通常为1)
local last = redis.call('HGET', KEYS[1], 'last_time')
local tokens = redis.call('HGET', KEYS[1], 'tokens')
if last == false then
tokens = capacity
else
local delta = math.max(0, now - last)
local filled = delta * rate / 1000
tokens = math.min(capacity, tokens + filled)
end
if tokens >= requested then
tokens = tokens - requested
redis.call('HSET', KEYS[1], 'tokens', tokens)
redis.call('HSET', KEYS[1], 'last_time', now)
return 1
end
return 0
go-zero 在执行时,首先尝试使用 Redis 执行该脚本,如果返回"令牌获取成功"则放行;如果出现 Redis 连接异常、超时等错误,则自动降级 到本地内存限流器(基于同样的令牌桶算法)。同时,框架会启动一个后台 goroutine 持续监听 Redis 的健康状态,一旦恢复,便切回 Redis 模式,保证一致性。
滑动窗口的 ZSET 实现
滑动窗口同样依赖 Redis,核心 Lua 脚本使用了有序集合 ZSET:
lua
local key = KEYS[1] -- 限流 key
local window = tonumber(ARGV[1]) -- 窗口大小(毫秒)
local limit = tonumber(ARGV[2]) -- 阈值
local now = tonumber(ARGV[3]) -- 当前时间(毫秒)
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count >= limit then
return 0
end
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('PEXPIRE', key, window)
return 1
- 每次请求都会将当前时间戳作为成员加入
ZSET。 - 删除窗口以外的记录,统计剩余数量。
- 如果未超过阈值则允许,否则拒绝。
- 通过设置
PEXPIRE使键自动过期,避免内存泄漏。
这种实现精确且高效,广泛用于需要严格控制时间窗口内调用量的场景。
总结
本文从限流的基础概念出发,梳理了令牌桶和滑动窗口两种经典算法,并结合 go-zero 框架展示了三种限流器的工程实现 :基于 channel 的并发限流、基于 Redis + Lua 的令牌桶和滑动窗口。在社交服务群的案例中,我们通过拦截器和中间件的方式将这些限流器集成进了 RPC 和 API 层,并通过压测验证了效果。最后深入源码,理解了它们背后的核心原理与容错设计。