3. 缓存基础与Redis核心数据结构:String/Hash/List/Set/Zset在缓存场景中的应用

缓存是解决高并发读场景下数据库压力的核心手段。Redis 作为内存型 NoSQL 数据库,凭借其丰富的数据结构和微秒级响应,成为缓存层的首选。本章将深入剖析 Redis 五种核心数据结构在缓存场景中的选型逻辑,并结合 Go 语言 go-redis 库,演示工程化的接入与操作方式。

3.1 缓存核心概念与Redis定位

缓存系统通常遵循 Cache-Aside(旁路缓存)模式:应用先查询缓存,命中则直接返回;未命中则回查数据库,并将结果写入缓存。Redis 在该模式中承担“高速暂存区”的角色,其核心优势在于:

  • 纯内存操作:读写延迟通常在 1ms 以内。
  • 单线程模型:避免并发竞争,保证原子性。
  • 丰富的数据结构:针对不同业务场景提供专用类型,而非仅支持 KV。
工程要点:缓存不是银弹。必须为缓存设置合理的 TTL(过期时间),并制定缓存与数据库的一致性策略(如延迟双删、异步同步),否则会出现缓存雪崩、穿透、击穿等经典问题。

3.2 五种核心数据结构与缓存场景

下表对比了 Redis 五种数据结构的底层实现、适用缓存场景及典型 Key 设计:

数据结构 底层编码 典型缓存场景 Key 设计示例 注意事项
String int / embstr / raw 单值缓存(用户 Token、验证码、计数器) user:token:9527 最大 512MB,适合存储序列化后的 JSON 对象
Hash ziplist / hashtable 对象缓存(用户信息、商品详情) user:info:9527 支持单独读写字段,节省带宽;避免大 Key
List quicklist 消息队列、最新动态、时间线 feed:latest:1001 可做 LPUSH + LTRIM 实现固定长度队列
Set intset / hashtable 去重集合、标签系统、好友关系 tag:article:42 支持交并差运算,适合做“共同关注”
Zset ziplist / skiplist 排行榜、延时队列、滑动窗口限流 rank:score:2025 按 score 排序,支持范围查询

3.3 数据结构选型决策流程

在实际工程中,选择哪种数据结构取决于访问模式和数据关系。以下决策树可帮助快速判断:

  1. 是否需要过期时间? → 所有类型都支持,但 String 最常用。
  2. 数据是单值还是对象? → 单值用 String;对象字段需单独读写用 Hash。
  3. 是否需要排序? → 是则用 Zset;否则看下一步。
  4. 是否需要去重? → 是则用 Set;否则看下一步。
  5. 是否需要按顺序存取? → 是则用 List(队列/栈);否则用 String。
Redis 数据结构选型决策流程 缓存数据需求 是否对象字段单独读写? Hash(对象缓存) 是否需要排序? Zset(排行榜/延时队列) 是否需要去重? Set(标签/去重) List / String(队列/单值)

3.4 Go 中 go-redis 库的基本使用

go-redis 是 Go 生态中最流行的 Redis 客户端,支持连接池、哨兵、集群模式。以下演示从初始化到五种数据结构操作的完整流程。

3.4.1 初始化客户端

import (
    "context"
    "github.com/redis/go-redis/v9"
)

var rdb *redis.Client

func InitRedis() {
    rdb = redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "",   // 无密码留空
        DB:       0,    // 默认数据库
        PoolSize: 100,  // 连接池大小
    })

    // 验证连接
    ctx := context.Background()
    pong, err := rdb.Ping(ctx).Result()
    if err != nil {
        panic("Redis 连接失败: " + err.Error())
    }
    println(pong) // 输出: PONG
}

3.4.2 五种数据结构操作示例

以下代码片段展示每种数据结构的典型缓存操作,包含错误处理与上下文传递:

ctx := context.Background()
var err error

// 1. String:缓存用户 Token
err = rdb.Set(ctx, "user:token:9527", "eyJhbGciOiJIUzI1NiIs...", 3600*time.Second).Err()
token, err := rdb.Get(ctx, "user:token:9527").Result()

// 2. Hash:缓存用户信息(对象)
userInfo := map[string]interface{}{"name": "张三", "age": 28, "city": "北京"}
err = rdb.HSet(ctx, "user:info:9527", userInfo).Err()
name, err := rdb.HGet(ctx, "user:info:9527", "name").Result()
allInfo, err := rdb.HGetAll(ctx, "user:info:9527").Result()

// 3. List:最新动态(固定长度 100)
err = rdb.LPush(ctx, "feed:latest:1001", "article:42").Err()
err = rdb.LTrim(ctx, "feed:latest:1001", 0, 99).Err() // 只保留前100条
feeds, err := rdb.LRange(ctx, "feed:latest:1001", 0, 9).Result() // 取前10条

// 4. Set:文章标签(去重)
err = rdb.SAdd(ctx, "tag:article:42", "Go", "Redis", "缓存").Err()
members, err := rdb.SMembers(ctx, "tag:article:42").Result()
count, err := rdb.SCard(ctx, "tag:article:42").Result()

// 5. Zset:积分排行榜
err = rdb.ZAdd(ctx, "rank:score:2025", redis.Z{Score: 95.5, Member: "user:1001"}).Err()
rankList, err := rdb.ZRevRangeWithScores(ctx, "rank:score:2025", 0, 9).Result()
for _, z := range rankList {
    fmt.Printf("用户 %s 得分 %.1f\n", z.Member, z.Score)
}
避坑指南:
• 使用 HGetAll 时注意大对象可能产生大量网络传输,建议只取必要字段。
• List 做队列时务必配合 LTrim 或设置 MaxLen,防止内存无限增长。
• Zset 的 score 使用 float64,注意精度问题;高并发写入时考虑使用 pipeline 减少 RTT。

3.5 管道与批量操作优化

当需要批量执行多个 Redis 命令时,使用 Pipeline 可以显著减少网络往返次数。go-redis 的 Pipeline 支持原子性(非事务)和事务性(Multi/Exec)两种模式。

// 批量写入用户缓存(非事务管道)
pipe := rdb.Pipeline()
pipe.Set(ctx, "user:token:1001", "token_1001", 3600*time.Second)
pipe.HSet(ctx, "user:info:1001", "name", "李四", "age", 30)
pipe.SAdd(ctx, "tag:user:1001", "VIP", "新用户")
cmds, err := pipe.Exec(ctx)
if err != nil {
    log.Fatalf("管道执行失败: %v", err)
}
for _, cmd := range cmds {
    fmt.Println(cmd.Name(), cmd.Err())
}

// 事务管道(原子性保证)
txPipe := rdb.TxPipeline()
txPipe.Incr(ctx, "counter:visits")
txPipe.Expire(ctx, "counter:visits", 86400*time.Second)
_, err = txPipe.Exec(ctx)

3.6 缓存一致性初步:过期与主动更新

在缓存场景中,数据一致性是核心挑战。基于 Redis 的缓存一致性治理通常从以下两个基础策略开始:

  • 设置合理的 TTL:强制过期,保证最终一致性。TTL 不宜过长(通常 5~30 分钟),避免脏数据长期存在。
  • 主动更新(Cache-Aside 写策略):更新数据库后,立即删除或更新缓存。推荐“先更新数据库,再删除缓存”模式,配合延迟双删防止并发脏读。
工程实践:在 Go 中,可以使用 defer 或中间件模式确保缓存删除操作一定执行,避免因异常导致缓存与数据库长期不一致。
Cache-Aside 模式读写流程 应用服务 Redis 缓存 MySQL 数据库 读流程 ① 查询缓存 ② 缓存未命中,回查 DB ③ 写入缓存(回填) 写流程 ① 更新数据库 ② 删除缓存(或更新)

3.7 本章小结

  • Redis 五种数据结构各有适用场景,选型应基于访问模式和数据关系。
  • go-redis 提供了简洁的 API,支持 Pipeline 批量操作,是 Go 工程中操作 Redis 的标准选择。
  • 缓存一致性治理从“TTL + 主动删除”起步,后续章节将深入延迟双删、异步同步等高级策略。