Appearance
23|缓存与 Redis
网关需要频繁读取路由配置和后端健康状态。每次请求都查数据库,性能差且给数据库带来不必要的压力。缓存把热点数据放到内存或 Redis 里,读取速度比数据库快一到两个数量级。go-gateway 用 Redis 缓存后端健康状态,管理 API 修改配置时同时更新缓存,保证数据一致性。
一、Redis 在网关中的角色
| 数据 | 存储位置 | 说明 |
|---|---|---|
| 路由配置 | SQLite/MySQL + Redis | 持久化在数据库,缓存加速读取 |
| 后端健康状态 | Redis | 探活结果带 TTL,过期自动清理 |
| 限流计数 | Redis | 分布式限流需要共享计数器 |
| 会话/Token | Redis | JWT 黑名单、登录状态(可选) |
二、go-redis 连接
bash
go get github.com/redis/go-redis/v9go
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
client := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
PoolSize: 10,
})
ctx := context.Background()
if err := client.Ping(ctx).Err(); err != nil {
fmt.Println("redis connect failed:", err)
return
}
fmt.Println("redis connected")
}go-redis 支持连接池,默认 PoolSize 是 10。并发量大时适当调大,但不要超过 Redis 服务器的 maxclients 配置。
三、基本操作
go
ctx := context.Background()
// 设置键值,带过期时间
err := client.Set(ctx, "backend:users", "healthy", 30*time.Second).Err()
// 读取
val, err := client.Get(ctx, "backend:users").Result()
if err == redis.Nil {
fmt.Println("key not found")
} else if err != nil {
fmt.Println("get failed:", err)
} else {
fmt.Println("status:", val)
}
// 删除
client.Del(ctx, "backend:users")
// 检查是否存在
exists, _ := client.Exists(ctx, "backend:users").Result()redis.Nil 表示键不存在,不是错误,需要单独判断。
四、哈希表存储结构化数据
路由配置可以用 Redis Hash 存储:
go
func cacheRoute(ctx context.Context, client *redis.Client, route Route) error {
return client.HSet(ctx, "route:"+route.Prefix, map[string]interface{}{
"prefix": route.Prefix,
"backend": route.Backend,
}).Err()
}
func getCachedRoute(ctx context.Context, client *redis.Client, prefix string) (*Route, error) {
result, err := client.HGetAll(ctx, "route:"+prefix).Result()
if err != nil {
return nil, err
}
if len(result) == 0 {
return nil, nil
}
return &Route{
Prefix: result["prefix"],
Backend: result["backend"],
}, nil
}五、缓存策略
Cache-Aside(旁路缓存)
最常用的策略:读数据时先查缓存,没有则查数据库并写入缓存;写数据时先更新数据库,再删除缓存。
go
func (s *RouteService) GetRoute(prefix string) (*Route, error) {
// 先查缓存
if route, _ := getCachedRoute(ctx, s.redis, prefix); route != nil {
return route, nil
}
// 缓存未命中,查数据库
var route Route
if err := s.db.Where("prefix = ?", prefix).First(&route).Error; err != nil {
return nil, err
}
// 写入缓存
cacheRoute(ctx, s.redis, route)
return &route, nil
}缓存穿透
查询一个一定不存在的数据,缓存和数据库都没有,每次请求都打到数据库。解决:缓存空值(带短 TTL),或用布隆过滤器拦截。
缓存击穿
热点 key 突然过期,大量请求同时打到数据库。解决:加互斥锁,只有一个线程去加载数据;或用逻辑过期,异步更新缓存。
缓存雪崩
大量 key 同时过期,数据库压力剧增。解决:过期时间加随机偏移,避免同时失效。
六、TTL 与过期策略
Redis 的过期策略是惰性删除 + 定期删除。键过期后不会立即删除,而是在被访问时检查并删除,或后台定期扫描删除。这意味着过期键可能短暂存在,但不会返回给客户端。
go
// 设置 60 秒过期
client.Set(ctx, "health:web01", "ok", 60*time.Second)
// 查看剩余 TTL
ttl, _ := client.TTL(ctx, "health:web01").Result()七、常见错误
不处理 redis.Nil
go
val, err := client.Get(ctx, "key").Result()
if err != nil {
return err // 错误:redis.Nil 被当成错误返回
}缓存与数据库不一致
更新数据库后忘记删缓存,或删缓存失败。写操作通常采用"先更新数据库,再删缓存"的顺序,删缓存失败时可以记录日志或发消息重试。
大 Key 问题
单个 Hash、List 或 String 存储了几 MB 甚至几十 MB 的数据,会导致 Redis 阻塞。路由配置通常很小,但如果缓存整个路由表,要注意拆分成多个小 key。