Go-Zero项目开发38:深入限流器实现与应用

纲要

  • 限流的核心概念
    • 常见限流指标: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/limitrest/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. 压测验证

使用 wrkab 进行测试:

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.goreserveN 方法中,其调用了一段 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 的令牌桶和滑动窗口。在社交服务群的案例中,我们通过拦截器和中间件的方式将这些限流器集成进了 RPCAPI 层,并通过压测验证了效果。最后深入源码,理解了它们背后的核心原理与容错设计。

相关推荐
风吹心凉1 小时前
python3基础2026.7.28
开发语言·python
Rabitebla1 小时前
C++ 内存管理全面复习:从内存分布到 operator new/delete
java·c语言·开发语言·c++·算法·leetcode
xcLeigh1 小时前
Go入门:main包与main函数的特殊地位
开发语言·后端·golang
心念枕惊2 小时前
PHP 在领域驱动(DDD)设计中的核心实践
android·开发语言·php
圣光SG2 小时前
Java操作题练习(二)
java·开发语言·python
Sammyyyyy2 小时前
如何利用本地技术栈构建 0 成本 AI SaaS 雏形
开发语言·人工智能·python·ai·servbay
冻柠檬飞冰走茶3 小时前
PTA基础编程题目集 7-13 日K蜡烛图(C语言实现)
c语言·开发语言·数据结构·算法
csdn_aspnet3 小时前
C# 从凸包中删除点(Deleting points from Convex Hull)
开发语言·c#
cts6183 小时前
Python全栈claude.md文档
开发语言·python