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 数据结构:

Redis 数据结构选型决策流程 业务数据需求 是否需要排序? Sorted Set 是否需要去重? Set 是否需要消息队列? List 是否操作对象字段? Hash String 决策路径:排序 → 去重 → 消息队列 → 对象字段 → 最终结构

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:排行榜、延时队列、范围查询