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 数据结构选型决策流程
在实际工程中,选择哪种数据结构取决于访问模式和数据关系。以下决策树可帮助快速判断:
- 是否需要过期时间? → 所有类型都支持,但 String 最常用。
- 数据是单值还是对象? → 单值用 String;对象字段需单独读写用 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)
}
避坑指南:
• 使用
• List 做队列时务必配合
• Zset 的 score 使用 float64,注意精度问题;高并发写入时考虑使用 pipeline 减少 RTT。
• 使用
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 或中间件模式确保缓存删除操作一定执行,避免因异常导致缓存与数据库长期不一致。
3.7 本章小结
- Redis 五种数据结构各有适用场景,选型应基于访问模式和数据关系。
- go-redis 提供了简洁的 API,支持 Pipeline 批量操作,是 Go 工程中操作 Redis 的标准选择。
- 缓存一致性治理从“TTL + 主动删除”起步,后续章节将深入延迟双删、异步同步等高级策略。