2. Redis 核心数据结构在 Go 中的使用
在 Go 项目中操作 Redis,最常用的客户端库是 go-redis/redis(v9+)。本章将回顾 Redis 的五种核心数据结构——String、Hash、List、Set、Sorted Set,并展示它们在 Go 中的典型用法。理解这些数据结构的特性和适用场景,是构建缓存一致性方案的基础。
2.1 数据结构概览与选型对照
不同的数据结构对应不同的业务场景。下表总结了五种核心结构的特点、常用命令及典型 Go 应用场景:
| 数据结构 | 内部编码 | 核心特点 | 常用命令 | Go 典型场景 |
|---|---|---|---|---|
| String | int / embstr / raw | 二进制安全,最大 512MB;支持原子计数 | SET / GET / INCR / DECR / MSET | 缓存单个对象(JSON)、计数器、分布式锁 |
| Hash | ziplist / hashtable | 字段-值映射,适合存储结构化对象 | HSET / HGET / HGETALL / HDEL | 用户信息、商品详情、配置项 |
| List | quicklist | 双向链表,支持左右插入/弹出 | LPUSH / RPUSH / LPOP / BRPOP | 消息队列、最新消息列表、时间线 |
| Set | intset / hashtable | 无序唯一集合,支持交并差运算 | SADD / SMEMBERS / SINTER / SUNION | 标签系统、好友关系、去重统计 |
| Sorted Set | ziplist / skiplist | 有序唯一集合,按 score 排序 | ZADD / ZRANGE / ZREVRANGE / ZINCRBY | 排行榜、延时队列、范围查询 |
选型建议: 优先使用 Hash 存储对象字段,避免大 String 频繁全量读写;排行榜场景必须使用 Sorted Set;消息队列场景推荐 List + BRPOP 阻塞读取。
2.2 String:最基础的缓存单元
String 是 Redis 最基础的数据结构,在 Go 中通常用于缓存序列化后的 JSON 对象或原子计数器。
import (
"context"
"encoding/json"
"github.com/redis/go-redis/v9"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
// 缓存用户对象(JSON 序列化)
func CacheUser(ctx context.Context, rdb *redis.Client, user User) error {
data, _ := json.Marshal(user)
return rdb.Set(ctx, "user:"+string(rune(user.ID)), data, 10*time.Minute).Err()
}
// 获取缓存用户
func GetCachedUser(ctx context.Context, rdb *redis.Client, userID int) (*User, error) {
data, err := rdb.Get(ctx, "user:"+string(rune(userID))).Bytes()
if err != nil {
return nil, err
}
var user User
json.Unmarshal(data, &user)
return &user, nil
}
// 原子计数器(如访问次数)
func IncrVisitCount(ctx context.Context, rdb *redis.Client, pageKey string) (int64, error) {
return rdb.Incr(ctx, "visit:"+pageKey).Result()
}
注意: 使用 String 缓存大对象时,每次读写都是全量操作。如果对象字段频繁更新,建议改用 Hash 结构,只更新变更字段,减少网络开销。
2.3 Hash:结构化对象的最佳映射
Hash 允许对对象的单个字段进行读写,非常适合存储用户信息、商品详情等结构化数据。Go 中通过 HSet 和 HGetAll 操作。
// 存储用户信息到 Hash
func CacheUserHash(ctx context.Context, rdb *redis.Client, user User) error {
return rdb.HSet(ctx, "user:"+string(rune(user.ID)),
"id", user.ID,
"name", user.Name,
).Err()
}
// 获取用户所有字段
func GetUserHash(ctx context.Context, rdb *redis.Client, userID int) (map[string]string, error) {
return rdb.HGetAll(ctx, "user:"+string(rune(userID))).Result()
}
// 仅更新用户名称
func UpdateUserName(ctx context.Context, rdb *redis.Client, userID int, newName string) error {
return rdb.HSet(ctx, "user:"+string(rune(userID)), "name", newName).Err()
}
性能优势: Hash 的字段操作时间复杂度为 O(1),更新单个字段时无需传输整个对象。在缓存一致性方案中,Hash 配合字段级失效策略,能显著减少缓存更新冲突。
2.4 List:消息队列与时间线
List 基于双向链表实现,支持左/右两端插入和弹出。在 Go 中常用于实现轻量级消息队列或存储最新动态。
// 生产者:向队列右侧推送消息
func PushMessage(ctx context.Context, rdb *redis.Client, queue string, msg interface{}) error {
data, _ := json.Marshal(msg)
return rdb.RPush(ctx, queue, data).Err()
}
// 消费者:从队列左侧阻塞弹出消息(超时 5 秒)
func PopMessage(ctx context.Context, rdb *redis.Client, queue string) (string, error) {
result, err := rdb.BLPop(ctx, 5*time.Second, queue).Result()
if err != nil {
return "", err
}
// result[0] 是队列名,result[1] 是消息内容
return result[1], nil
}
// 获取最新 N 条消息(如用户时间线)
func GetLatestMessages(ctx context.Context, rdb *redis.Client, key string, count int64) ([]string, error) {
return rdb.LRange(ctx, key, 0, count-1).Result()
}
阻塞 vs 非阻塞:
BRPOP/BLPOP 在队列为空时阻塞等待,适合消费者轮询;RPOP/LPOP 立即返回 nil,适合定时拉取。
2.5 Set:标签与关系运算
Set 提供无序唯一集合,支持高效的集合运算(交集、并集、差集)。在 Go 中常用于标签系统、好友推荐等场景。
// 为用户添加标签
func AddTags(ctx context.Context, rdb *redis.Client, userID int, tags []string) error {
key := "tags:user:" + string(rune(userID))
return rdb.SAdd(ctx, key, tags).Err()
}
// 获取用户的所有标签
func GetUserTags(ctx context.Context, rdb *redis.Client, userID int) ([]string, error) {
key := "tags:user:" + string(rune(userID))
return rdb.SMembers(ctx, key).Result()
}
// 计算两个用户的共同标签(交集)
func CommonTags(ctx context.Context, rdb *redis.Client, userA, userB int) ([]string, error) {
keyA := "tags:user:" + string(rune(userA))
keyB := "tags:user:" + string(rune(userB))
return rdb.SInter(ctx, keyA, keyB).Result()
}
去重统计: Set 的
SCARD 命令可快速获取集合基数,适合 UV 去重统计。但注意 Set 在元素数量超过 intset 阈值(默认 512)时会转为 hashtable,内存占用上升。
2.6 Sorted Set:排行榜与范围查询
Sorted Set 每个元素关联一个 double 类型的 score,按 score 排序。Go 中通过 ZAdd 写入,ZRevRange 获取排名。
// 更新用户积分(排行榜)
func UpdateScore(ctx context.Context, rdb *redis.Client, userID int, score float64) error {
key := "leaderboard:game1"
return rdb.ZAdd(ctx, key, redis.Z{
Score: score,
Member: string(rune(userID)),
}).Err()
}
// 获取排行榜 Top10(按分数从高到低)
func GetTopN(ctx context.Context, rdb *redis.Client, n int64) ([]redis.Z, error) {
key := "leaderboard:game1"
return rdb.ZRevRangeWithScores(ctx, key, 0, n-1).Result()
}
// 获取用户排名(从 0 开始)
func GetUserRank(ctx context.Context, rdb *redis.Client, userID int) (int64, error) {
key := "leaderboard:game1"
return rdb.ZRevRank(ctx, key, string(rune(userID))).Result()
}
// 按分数范围查询(如积分在 1000-2000 之间的用户)
func GetUsersByScoreRange(ctx context.Context, rdb *redis.Client, min, max float64) ([]string, error) {
key := "leaderboard:game1"
return rdb.ZRangeByScore(ctx, key, &redis.ZRangeBy{
Min: strconv.FormatFloat(min, 'f', -1, 64),
Max: strconv.FormatFloat(max, 'f', -1, 64),
}).Result()
}
Score 精度: Redis 的 score 使用 IEEE 754 双精度浮点数,当分数为整数时建议直接使用 int64 避免精度丢失。对于需要精确排序的场景,可将 score 设计为
时间戳 + 业务权重 的组合值。
2.7 数据结构选型决策流程
下图总结了在 Go 项目中如何根据业务需求选择 Redis 数据结构:
2.8 连接池与基础配置
在 Go 项目中使用 Redis 前,需要正确初始化客户端并配置连接池参数:
import (
"github.com/redis/go-redis/v9"
)
func NewRedisClient() *redis.Client {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // 无密码时留空
DB: 0, // 默认数据库
PoolSize: 10, // 连接池大小(默认 10)
MinIdleConns: 5, // 最小空闲连接数
MaxIdleConns: 10, // 最大空闲连接数
ConnMaxIdleTime: 5 * time.Minute, // 连接最大空闲时间
ConnMaxLifetime: 30 * time.Minute, // 连接最大存活时间
})
return rdb
}
连接池调优建议:
PoolSize 一般设置为 GOMAXPROCS 的 2~4 倍;MinIdleConns 避免冷启动时频繁建连;生产环境务必设置 ConnMaxLifetime 防止连接长时间未释放。
2.9 本章小结
Redis 的五种核心数据结构各有适用场景:
- String:简单缓存、计数器、分布式锁
- Hash:结构化对象、字段级更新
- List:消息队列、时间线
- Set:标签系统、去重统计、集合运算
- Sorted Set:排行榜、延时队列、范围查询