Day 15 / 共 20 天 · 第 3 周 HTTP 与外部调用

Redis 调用

第 3 周收官。昨天(Day 14)学的 HttpCall 是"调外部 HTTP 服务",今天 Redis 调用是同一套异步回调思路,只是后端换成 Redis。限流、计数、分布式锁这类需要"跨请求/跨实例共享状态"的插件,靠 Redis。今天看 RedisClusterClient:怎么初始化、怎么发命令、以及它同样是异步回调式的。

📍 你在整门课的位置(wasm-go 20 天 · 第 3 周收官 → 第 4 周高级特性)
HttpCall D14 Redis 共享状态 D15 日志 D16 Leader D17 Token D18
L01

为什么插件要 Redis

💡 本质:Redis = 全公司唯一的一块公告栏 每个 worker 的 Wasm VM 是各自独立的"小黑板",写在自己黑板上的数字别人看不见。要做"全公司只招 1000 人"这种全局计数,就得所有人都往同一块公告栏上记数——这块公告栏就是 Redis。所有实例、所有 VM 读写同一个 Redis 计数器,才能做全局限流、全局配额、分布式锁。它是插件们的"共享大脑"。
3 个网关实例(各含多个 VM)共享一个 Redis 计数器 实例1 · VM本地黑板只增自己的 实例2 · VM看不见别人的计数 📌 Redis 公告栏 INCR counter → 全局唯一计数 实例3 · VM也读写同一处
图注:本地变量各 VM 独立(小黑板),全局限流必须写到共享的 Redis(公告栏)——所有实例读写同一个计数器。
插件的"共享内存"问题 回想 Day 03:每个 worker 线程一份 Wasm VM,全局变量各自独立。那"全局限流"(所有实例加起来不超过 1000 QPS)怎么算?没法用本地变量,因为每个 VM 各算各的。解法:把计数放进 Redis——所有实例、所有 VM 都读写同一个 Redis 计数器,就能做全局限流、全局配额、分布式锁。Redis 是插件的"共享大脑"。上一站的 ai-token-ratelimit 就靠 Redis 做全局 Token 限流。
L02

RedisClusterClient

redis_wrapper.go:111-118RedisClusterClient[C Cluster]——和 HTTP 一样用 Cluster 指定 Redis 后端(通常是 FQDNCluster 或 StaticIpCluster 指向 Redis 服务)。

读法:接口 RedisClient:33)定义了一长串方法:Init + 各种 Redis 命令(Get/Set/Incr/Del/Expire/LPush/HGet…)。和 HTTP 一样,Redis 也复用 Day 13 的 Cluster 抽象——"调哪个 Redis"用 Cluster 表达。
👶 小白 vs 👨‍🏫 老师 👶:Redis 那么快,直接 count := client.Get("x") 拿返回值不行吗?
👨‍🏫:不行。再快也是"网络往返",而 Wasm 插件不能阻塞(Day 03)。所以 Redis 命令和昨天的 HttpCall 一模一样是异步回调式——你传个 callback,结果在回调里拿,配合 Pause/Resume。学会了 HttpCall,Redis 直接会。
L03

Init 初始化

redis_wrapper.go:261-324Init(username, password, timeout, opts...)

// 把配置项拼进 clusterName 的 query 参数:
//   db=N(选库)、buffer_flush_timeout=3(默认3ms)、max_buffer_size_before_flush=1024
clusterName = fmt.Sprintf("%s?%s", clusterName, strings.Join(params, "&"))
// 真正初始化
err := proxywasm.RedisInit(clusterName, username, password, uint32(timeout))
if err != nil { c.ready = false; return nil }   // 失败不报错,标记未就绪待重试
c.ready = true
在 parseConfig 或 OnPluginStart 里 Init Redis 连接在插件启动阶段初始化一次(不是每请求)。有意思的是失败时不返回错误,而是标记 ready=false 待后续重试:317-321)——因为插件启动时 Redis 可能还没就绪,硬失败会让插件启不来。buffer_flush_timeout/max_buffer_size 是性能调优:攒一批命令再发,减少往返。
L04

命令方法

SDK 封装了大量 Redis 命令(redis_wrapper.go:346-650+),每个都带回调:

client.Get(key, callback)
client.Set(key, value, callback)
client.SetEx(key, value, ttl, callback)      // 带过期
client.SetNX(key, value, ttl, callback)      // 不存在才设(分布式锁)
client.Incr(key, callback) / Decr / IncrBy   // 计数
client.Expire(key, ttl, callback)
client.LPush/RPush/LPop/LRange ...           // 列表
client.HGet/HDel/HExists ...                 // 哈希
client.MGet(keys, cb) / MSet(kvMap, cb)      // 批量
📝 举个例子:每分钟限流 100 次 key = ratelimit:用户张三:2026-07-12T10:30(按分钟)。
第 1 次请求:Incr(key) → 回调收到 1 → 顺手 Expire(key, 60)(60 秒后自动清零)。
第 101 次请求:Incr(key) → 回调收到 101101 > 100SendHttpResponseWithDetail(429, ...) 拦截。
下一分钟:key 已过期不存在 → Incr 重新从 1 开始(滑动窗口的雏形)。
读法:覆盖了 String/List/Hash 等常用类型。限流常用 Incr+Expire(计数+窗口过期),分布式锁常用 SetNX(原子占锁)。每个命令都是异步的——传 callback,结果在回调里拿(和 HTTP 一样的 Pause/Resume 节奏)。
L05

回调解析 resp

回调类型 RedisResponseCallback func(response resp.Value)redis_wrapper.go:31)。resp.Value 来自 github.com/tidwall/resp——Redis 的 RESP 协议解析库。

client.Incr("counter", func(response resp.Value) {
    count := response.Integer()   // Redis 返回的整数
    if count > limit {
        proxywasm.SendHttpResponseWithDetail(429, ...)  // 超限拦截
    }
    proxywasm.ResumeHttpRequest()
})
RESP 是 Redis 的"说话方式" Redis 用 RESP 协议返回结果(整数、字符串、数组、错误各有格式)。resp.Value 帮你解析:.Integer() 取整数、.String() 取字符串、.Array() 取数组、.Error() 看有没有错。你在回调里按预期类型取值。和 HTTP 回调一样,处理完记得 Resume(如果是在请求处理中调的)。
L06

Eval 与原子性

redis_wrapper.go:332-344Eval(script, numkeys, keys, args, callback):执行 Lua 脚本。

为什么限流一定要 Lua 脚本? 限流要"读计数 → 判断 → 加一"三步。如果分三次 Redis 命令,并发下会有竞态(两个请求同时读到 99,都以为没超限,结果加到 101)。Redis 的 Eval 把这三步写成一段 Lua 脚本,Redis 保证脚本原子执行(中间不被打断)——彻底消除竞态。所以生产级限流插件都用 Eval + Lua,而不是分开的 Incr/Get。上一站 Higress 的限流插件(Day 03 提过的 Redis+Lua)正是这么做的。
L07

重认证与就绪

redis_wrapper.go:257-259Ready() 判断连接是否就绪;Command/Eval 等每次执行前先 checkReadyFunc():301-313)——若未就绪就尝试重新 RedisInit 认证。

读法:这是"延迟初始化 + 自愈"设计:Init 失败不致命,后续每次用 Redis 前检查就绪、没就绪就重试认证。这样即使 Redis 启动慢于插件、或中途重连,插件也能自动恢复,不用重启。生产环境的健壮性细节。
L08

今日小结 + 动手(第 3 周收官)

🧠 第 3 周你应该能回答

  • 为什么全局限流必须用 Redis 而不能用本地变量?
  • Redis 命令为什么也是异步回调式?resp.Value 怎么解析?
  • 为什么限流要用 Eval + Lua(原子性)?
  • Init 失败为什么不报错?重认证怎么自愈?
🎯 记忆口诀 "共享状态找 Redis,命令异步带回调;计数 Incr 加 Expire,占锁 SetNX,限流要原子就上 Eval+Lua。"

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/wasm-go
sed -n '31,120p' pkg/wrapper/redis_wrapper.go
sed -n '325,345p' pkg/wrapper/redis_wrapper.go
grep -n "func (c \*RedisClusterClient" pkg/wrapper/redis_wrapper.go | head -40
下周预告 · 第 4 周高级特性与实战——日志与安全日志、Leader 选举与 SharedData(Redis 之外的另一种共享)、AI Token 用量解析、性能重建机制,最后构建部署 + 五站大串讲收官。
← Day 14 Day 16 · 日志 →