Redis 调用
第 3 周收官。昨天(Day 14)学的 HttpCall 是"调外部 HTTP 服务",今天 Redis 调用是同一套异步回调思路,只是后端换成 Redis。限流、计数、分布式锁这类需要"跨请求/跨实例共享状态"的插件,靠 Redis。今天看 RedisClusterClient:怎么初始化、怎么发命令、以及它同样是异步回调式的。
为什么插件要 Redis
RedisClusterClient
redis_wrapper.go:111-118:RedisClusterClient[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 表达。count := client.Get("x") 拿返回值不行吗?👨🏫:不行。再快也是"网络往返",而 Wasm 插件不能阻塞(Day 03)。所以 Redis 命令和昨天的 HttpCall 一模一样是异步回调式——你传个
callback,结果在回调里拿,配合 Pause/Resume。学会了 HttpCall,Redis 直接会。Init 初始化
redis_wrapper.go:261-324 的 Init(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
ready=false 待后续重试(:317-321)——因为插件启动时 Redis 可能还没就绪,硬失败会让插件启不来。buffer_flush_timeout/max_buffer_size 是性能调优:攒一批命令再发,减少往返。命令方法
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) // 批量
ratelimit:用户张三:2026-07-12T10:30(按分钟)。第 1 次请求:
Incr(key) → 回调收到 1 → 顺手 Expire(key, 60)(60 秒后自动清零)。第 101 次请求:
Incr(key) → 回调收到 101 → 101 > 100 → SendHttpResponseWithDetail(429, ...) 拦截。下一分钟:key 已过期不存在 →
Incr 重新从 1 开始(滑动窗口的雏形)。Incr+Expire(计数+窗口过期),分布式锁常用 SetNX(原子占锁)。每个命令都是异步的——传 callback,结果在回调里拿(和 HTTP 一样的 Pause/Resume 节奏)。回调解析 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.Value 帮你解析:.Integer() 取整数、.String() 取字符串、.Array() 取数组、.Error() 看有没有错。你在回调里按预期类型取值。和 HTTP 回调一样,处理完记得 Resume(如果是在请求处理中调的)。Eval 与原子性
redis_wrapper.go:332-344 的 Eval(script, numkeys, keys, args, callback):执行 Lua 脚本。
Eval 把这三步写成一段 Lua 脚本,Redis 保证脚本原子执行(中间不被打断)——彻底消除竞态。所以生产级限流插件都用 Eval + Lua,而不是分开的 Incr/Get。上一站 Higress 的限流插件(Day 03 提过的 Redis+Lua)正是这么做的。重认证与就绪
redis_wrapper.go:257-259 的 Ready() 判断连接是否就绪;Command/Eval 等每次执行前先 checkReadyFunc()(:301-313)——若未就绪就尝试重新 RedisInit 认证。
今日小结 + 动手(第 3 周收官)
🧠 第 3 周你应该能回答
- 为什么全局限流必须用 Redis 而不能用本地变量?
- Redis 命令为什么也是异步回调式?resp.Value 怎么解析?
- 为什么限流要用 Eval + Lua(原子性)?
- Init 失败为什么不报错?重认证怎么自愈?
✋ 动手
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