Skip to content

23|缓存与 Redis

网关需要频繁读取路由配置和后端健康状态。每次请求都查数据库,性能差且给数据库带来不必要的压力。缓存把热点数据放到内存或 Redis 里,读取速度比数据库快一到两个数量级。go-gateway 用 Redis 缓存后端健康状态,管理 API 修改配置时同时更新缓存,保证数据一致性。

一、Redis 在网关中的角色

数据存储位置说明
路由配置SQLite/MySQL + Redis持久化在数据库,缓存加速读取
后端健康状态Redis探活结果带 TTL,过期自动清理
限流计数Redis分布式限流需要共享计数器
会话/TokenRedisJWT 黑名单、登录状态(可选)

二、go-redis 连接

bash
go get github.com/redis/go-redis/v9
go
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。