vault backup: 2026-06-01 00:35:50
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
---
|
||||
tags: [redis, evalsha, script-load, performance, optimization]
|
||||
create time: 2026-06-01 10:30
|
||||
---
|
||||
|
||||
# EVALSHA 预加载与性能优化
|
||||
|
||||
## 概述
|
||||
|
||||
在 Redis 生产环境中,频繁使用 `EVAL` 传输完整脚本会带来**可量化的性能损耗**。`SCRIPT LOAD` + `EVALSHA` 的组合是解决这个问题的标准方案。本节深入讲解它的工作原理、性能收益和最佳实践。
|
||||
|
||||
对应薄弱点 Q14(EVALSHA 预加载的核心好处)。
|
||||
|
||||
## 一、工作原理
|
||||
|
||||
### 1.1 流程分解
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as 应用实例
|
||||
participant Redis as Redis Server
|
||||
|
||||
Note over App,Redis: ── 阶段1:启动时一次性预加载 ──
|
||||
App->>Redis: SCRIPT LOAD "return redis.call('INCR', KEYS[1])"
|
||||
Redis->>Redis: SHA1(脚本内容) = b78d89f7...
|
||||
Redis->>App: "b78d89f7..." (SHA1 指纹)
|
||||
|
||||
Note over App,Redis: ── 阶段2:运行时高频调用 ──
|
||||
loop 每次请求
|
||||
App->>Redis: EVALSHA b78d89f7... 1 mykey
|
||||
Redis->>Redis: 查找缓存中的脚本 → 命中
|
||||
Redis->>Redis: 执行脚本
|
||||
Redis-->>App: 返回值
|
||||
end
|
||||
|
||||
Note over App,Redis: ── 异常路径:NOSCRIPT ──
|
||||
alt Redis 重启 / FLUSHALL
|
||||
App->>Redis: EVALSHA b78d89f7... 1 mykey
|
||||
Redis-->>App: NOSCRIPT 错误
|
||||
App->>Redis: SCRIPT LOAD "..." (重新加载)
|
||||
App->>Redis: EVALSHA <新SHA> ...
|
||||
end
|
||||
```
|
||||
|
||||
### 1.2 两种方式的对比
|
||||
|
||||
| 维度 | EVAL | EVALSHA |
|
||||
|------|------|---------|
|
||||
| **传输内容** | 完整脚本字符串(通常 500B ~ 5KB) | SHA1 指纹(固定 40 字节) |
|
||||
| **Redis 处理** | 计算 SHA1 + 存入缓存 + 执行 | 直接查缓存 + 执行 |
|
||||
| **网络开销** | 大(每次都发脚本) | 小(只发 40 字节) |
|
||||
| **适用场景** | 调试、低频脚本 | 生产环境、高频调用 |
|
||||
|
||||
## 二、性能收益量化
|
||||
|
||||
以典型的令牌桶 Lua 脚本为例(约 1.2KB):
|
||||
|
||||
```
|
||||
场景:每秒 10,000 次限流请求
|
||||
|
||||
EVAL 模式:
|
||||
- 每次传输:~1.2 KB
|
||||
- 每秒总带宽:10,000 × 1.2 KB = 12 MB/s
|
||||
|
||||
EVALSHA 模式:
|
||||
- 每次传输:40 字节(SHA1)
|
||||
- 每秒总带宽:10,000 × 40 B = 400 KB/s
|
||||
|
||||
节省:12 MB/s - 0.4 MB/s ≈ 11.6 MB/s(节省 96.7%)
|
||||
```
|
||||
|
||||
对于高频限流场景,这个差异非常可观。即使脚本只有几百字节,乘以百万级 QPS 后也是显著的网络 IO。
|
||||
|
||||
## 三、Go-Redis 中的实现
|
||||
|
||||
### 3.1 显式预加载(推荐生产使用)
|
||||
|
||||
```go
|
||||
// 程序启动时统一加载
|
||||
func initScripts(ctx context.Context, rdb *redis.Client) error {
|
||||
scripts := map[string]string{
|
||||
"token_bucket": tokenBucketLua,
|
||||
"sliding_window": slidingWindowLua,
|
||||
"fixed_window": fixedWindowLua,
|
||||
}
|
||||
|
||||
shas := make(map[string]string)
|
||||
for name, lua := range scripts {
|
||||
sha, err := rdb.ScriptLoad(ctx, lua).Result()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load %s: %w", name, err)
|
||||
}
|
||||
shas[name] = sha
|
||||
log.Printf("loaded [%s]: SHA=%s", name, sha)
|
||||
}
|
||||
|
||||
// 保存到全局或依赖注入容器
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 NOSCRIPT 容错处理
|
||||
|
||||
```go
|
||||
// 捕获 NOSCRIPT 错误,自动回退到 EVAL
|
||||
func safeEval(ctx context.Context, rdb *redis.Client, sha, lua string, keys []string, args ...interface{}) (interface{}, error) {
|
||||
result, err := rdb.EvalSHA(ctx, sha, keys, args...).Result()
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 不是 NOSCRIPT 错误,直接返回
|
||||
if !isNoScript(err) {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// NOSCRIPT:重新加载并再次尝试
|
||||
newSHA, err := rdb.ScriptLoad(ctx, lua).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, err = rdb.EvalSHA(ctx, newSHA, keys, args...).Result()
|
||||
return result, err
|
||||
}
|
||||
|
||||
func isNoScript(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
errStr := err.Error()
|
||||
return strings.Contains(errStr, "NOSCRIPT") ||
|
||||
strings.Contains(errStr, "no such script")
|
||||
}
|
||||
```
|
||||
|
||||
## 四、什么时候不需要预加载?
|
||||
|
||||
| 场景 | 建议 |
|
||||
|------|------|
|
||||
| 每个请求都调用的限流脚本 | ✅ 必须预加载 |
|
||||
| 每天只跑几次的管理脚本 | ⭕ 用 EVAL 即可,省得维护状态 |
|
||||
| 开发/测试环境 | ⭕ Eval 的自动重试已够用 |
|
||||
| Redis 集群多节点 | ✅ 必须在每个节点上都加载(通过 Sentinel 或 Cluster 客户端自动处理) |
|
||||
|
||||
> [!warning]- 分布式部署注意
|
||||
>
|
||||
> 如果你的 Go 服务连接到 Redis Cluster,`ScriptLoad` 会自动在所有主节点上加载脚本。但如果你用了连接池连接多个独立 Redis 实例(非 Cluster 模式),每个实例都需要单独 ScriptLoad——或者使用统一的代理层(如 Twemproxy、Codis)。
|
||||
|
||||
## 五、常见误区
|
||||
|
||||
| 误区 | 真相 |
|
||||
|------|------|
|
||||
| EVALSHA 执行更快 | ❌ EVALSHA 和 EVAL 的**执行速度几乎一样**,区别只在传输带宽 |
|
||||
| 只需要加载一次永远有效 | ❌ Redis 重启、FLUSHALL、内存淘汰都会清空脚本缓存 |
|
||||
| EVALSHA 可以绕过沙箱限制 | ❌ EVALSHA 的沙箱规则和 EVAL 完全相同 |
|
||||
| 语法更简洁 | ❌ 需要额外传 SHA1,反而多了步骤 |
|
||||
|
||||
## 六、监控指标
|
||||
|
||||
在生产环境中,建议监控以下 EVALSHA 相关指标:
|
||||
|
||||
```bash
|
||||
# Redis 端:脚本缓存统计
|
||||
SCRIPT STATS # Redis 7+,查看各脚本调用次数和缓存命中率
|
||||
|
||||
# Go 端:
|
||||
- EvalSHA 失败率(NOSCRIPT 占比)
|
||||
- EvalSHA 平均延迟 vs Eval 平均延迟
|
||||
- Script Load 次数(过高可能说明缓存丢失频繁)
|
||||
```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[Go-Redis Lua 调用指南]] — Go-Redis 中 Lua 调用的完整封装方式
|
||||
- [[Lua脚本]] — Redis Lua 基础概念
|
||||
- [[分布式限流]] — EVALSHA 在限流架构中的实际位置
|
||||
Reference in New Issue
Block a user