2. Redis 基础回顾:Redis 核心数据结构及 Go 客户端快速入门
在构建高并发、高可用的 Go 接口服务时,Redis 通常扮演着缓存、分布式锁、计数器、消息队列等关键角色。本章将系统回顾 Redis 的五种核心数据结构,并演示如何使用 go-redis 库在 Go 语言中快速操作这些结构,为后续的缓存一致性实践打下坚实基础。
2.1 Redis 核心数据结构概览
Redis 是一个基于内存的键值存储系统,其核心优势在于支持丰富的数据结构。理解这些结构的特性和适用场景,是合理设计缓存策略的前提。
| 数据结构 | 内部编码 | 主要命令 | 典型应用场景 |
|---|---|---|---|
| String | int / embstr / raw | SET / GET / INCR / DECR / MSET | 缓存对象、计数器、分布式锁、Session 共享 |
| Hash | ziplist / hashtable | HSET / HGET / HGETALL / HDEL / HINCRBY | 存储结构化对象(如用户信息、商品详情) |
| List | quicklist | LPUSH / RPUSH / LPOP / RPOP / LRANGE / LLEN | 消息队列、最新消息列表、时间线 |
| Set | intset / hashtable | SADD / SREM / SMEMBERS / SISMEMBER / SUNION | 标签系统、社交关系(共同好友)、去重 |
| Sorted Set | ziplist / skiplist | ZADD / ZRANGE / ZRANK / ZREM / ZINCRBY | 排行榜、延时队列、优先级任务 |
选型提示: 对于需要频繁修改对象中某个字段的场景(如更新用户年龄),优先使用 Hash 而非 String 序列化整个对象,这样可以减少网络传输和序列化开销。
2.2 核心数据结构详解
2.2.1 String — 最基础的缓存单元
String 是 Redis 中最简单的数据结构,可以存储字符串、整数或浮点数。它支持原子性的增减操作,非常适合实现计数器。
// Go 示例:使用 go-redis 操作 String
import "github.com/redis/go-redis/v9"
func stringExample(ctx context.Context, rdb *redis.Client) {
// 设置键值对
err := rdb.Set(ctx, "user:1001:name", "张三", 0).Err()
if err != nil {
panic(err)
}
// 获取值
name, err := rdb.Get(ctx, "user:1001:name").Result()
if err == redis.Nil {
fmt.Println("key 不存在")
} else if err != nil {
panic(err)
} else {
fmt.Println("用户名:", name)
}
// 原子自增(计数器)
count, err := rdb.Incr(ctx, "page:view:home").Result()
fmt.Printf("首页访问次数: %d\n", count)
}
2.2.2 Hash — 对象存储的利器
Hash 类似于 Go 中的 map[string]string,适合存储一个对象的多个字段。相比 String 序列化整个对象,Hash 支持单独读写某个字段,更加灵活高效。
// Go 示例:使用 go-redis 操作 Hash
func hashExample(ctx context.Context, rdb *redis.Client) {
// 同时设置多个字段
rdb.HSet(ctx, "user:1001", map[string]interface{}{
"name": "李四",
"age": 28,
"city": "北京",
})
// 获取单个字段
age, _ := rdb.HGet(ctx, "user:1001", "age").Int()
fmt.Printf("年龄: %d\n", age)
// 获取所有字段
userInfo, _ := rdb.HGetAll(ctx, "user:1001").Result()
fmt.Println("用户信息:", userInfo)
// 字段自增
rdb.HIncrBy(ctx, "user:1001", "score", 10)
}
2.2.3 List — 消息队列与时间线
List 是一个双向链表,支持从两端插入和弹出元素。结合 BLPOP / BRPOP 命令,可以轻松实现一个轻量级的阻塞消息队列。
// Go 示例:使用 go-redis 操作 List
func listExample(ctx context.Context, rdb *redis.Client) {
// 从右侧推入消息
rdb.RPush(ctx, "queue:task", "task1", "task2", "task3")
// 从左侧弹出消息(FIFO)
task, _ := rdb.LPop(ctx, "queue:task").Result()
fmt.Println("消费任务:", task)
// 获取列表长度
length, _ := rdb.LLen(ctx, "queue:task").Result()
fmt.Printf("剩余任务数: %d\n", length)
// 获取指定范围元素(分页)
tasks, _ := rdb.LRange(ctx, "queue:task", 0, -1).Result()
fmt.Println("所有任务:", tasks)
}
2.2.4 Set — 去重与集合运算
Set 是无序的字符串集合,自动去重。它支持交集、并集、差集等集合运算,非常适合实现标签系统或社交关系。
// Go 示例:使用 go-redis 操作 Set
func setExample(ctx context.Context, rdb *redis.Client) {
// 添加元素
rdb.SAdd(ctx, "user:1001:tags", "golang", "redis", "backend")
rdb.SAdd(ctx, "user:1002:tags", "golang", "frontend", "vue")
// 判断元素是否存在
exists, _ := rdb.SIsMember(ctx, "user:1001:tags", "redis").Result()
fmt.Println("是否包含redis标签:", exists)
// 计算交集(共同标签)
common, _ := rdb.SInter(ctx, "user:1001:tags", "user:1002:tags").Result()
fmt.Println("共同标签:", common)
// 获取所有元素
allTags, _ := rdb.SMembers(ctx, "user:1001:tags").Result()
fmt.Println("用户1001的所有标签:", allTags)
}
2.2.5 Sorted Set — 排行榜与优先级队列
Sorted Set 在 Set 的基础上为每个元素关联了一个 score(分数),元素按分数从小到大排序。它天然适合实现排行榜、延时队列等场景。
// Go 示例:使用 go-redis 操作 Sorted Set
func sortedSetExample(ctx context.Context, rdb *redis.Client) {
// 添加玩家分数
rdb.ZAdd(ctx, "game:rank", redis.Z{Score: 1000, Member: "player1"})
rdb.ZAdd(ctx, "game:rank", redis.Z{Score: 2500, Member: "player2"})
rdb.ZAdd(ctx, "game:rank", redis.Z{Score: 1800, Member: "player3"})
// 获取排名(升序,0为第一名)
rank, _ := rdb.ZRank(ctx, "game:rank", "player2").Result()
fmt.Printf("player2 排名: %d\n", rank+1)
// 获取前三名(按分数降序)
top3, _ := rdb.ZRevRangeWithScores(ctx, "game:rank", 0, 2).Result()
for i, z := range top3 {
fmt.Printf("第%d名: %s (分数: %.0f)\n", i+1, z.Member, z.Score)
}
// 增加分数
rdb.ZIncrBy(ctx, "game:rank", 500, "player1")
}
性能要点: Sorted Set 的插入和查询时间复杂度为 O(log N),适合百万级数据量的排行榜。如果数据量超过千万,建议考虑分片或使用其他方案。
2.3 go-redis 客户端快速入门
go-redis 是 Go 语言中最流行的 Redis 客户端库,支持连接池、哨兵模式、集群模式、管道(Pipeline)和事务(Transaction)等高级特性。
2.3.1 安装与初始化
// 安装 go-redis
// go get github.com/redis/go-redis/v9
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
// 创建 Redis 客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 地址
Password: "", // 密码(无则留空)
DB: 0, // 默认数据库
PoolSize: 10, // 连接池大小
})
// 测试连接
pong, err := rdb.Ping(ctx).Result()
if err != nil {
panic(fmt.Sprintf("连接 Redis 失败: %v", err))
}
fmt.Println("连接成功:", pong)
defer rdb.Close()
}
2.3.2 连接池配置
合理的连接池配置可以显著提升性能。以下是关键参数说明:
| 参数 | 默认值 | 说明 | 推荐值 |
|---|---|---|---|
PoolSize |
10 | 最大连接数 | 根据并发量调整,通常 10~100 |
MinIdleConns |
0 | 最小空闲连接数 | 建议设为 5~10,避免冷启动 |
MaxIdleConns |
0 | 最大空闲连接数 | 通常等于 PoolSize |
ConnMaxLifetime |
0(不限制) | 连接最大存活时间 | 建议 30 分钟~1 小时 |
ConnMaxIdleTime |
0(不限制) | 连接最大空闲时间 | 建议 5~10 分钟 |
2.3.3 管道(Pipeline)与事务(Transaction)
当需要批量执行命令时,使用 Pipeline 可以减少网络往返次数。如果还需要保证原子性,则使用事务(MULTI/EXEC)。
// Pipeline 示例:批量操作
func pipelineExample(ctx context.Context, rdb *redis.Client) {
pipe := rdb.Pipeline()
// 将多个命令加入管道
pipe.Set(ctx, "key1", "value1", 0)
pipe.Set(ctx, "key2", "value2", 0)
pipe.Get(ctx, "key1")
pipe.Get(ctx, "key2")
// 一次性执行
cmds, err := pipe.Exec(ctx)
if err != nil {
panic(err)
}
for _, cmd := range cmds {
fmt.Println(cmd.String())
}
}
// 事务示例:原子性操作
func transactionExample(ctx context.Context, rdb *redis.Client) {
// 使用 TxPipeline 可以同时获得 Pipeline 和事务的能力
tx := rdb.TxPipeline()
tx.Incr(ctx, "counter")
tx.Incr(ctx, "counter")
tx.Decr(ctx, "counter")
_, err := tx.Exec(ctx)
if err != nil {
panic(err)
}
val, _ := rdb.Get(ctx, "counter").Int()
fmt.Println("最终计数:", val) // 输出: 1
}
注意: 在 Redis 集群模式下,Pipeline 和事务中的命令必须落在同一个 slot 上,否则会报错。建议使用
Hash Tag(如 {user:1001}:name)来确保相关键落在同一节点。
2.4 数据结构选型决策流程
为了帮助你在实际项目中快速选择合适的数据结构,下面给出一个决策流程图:
2.5 本章小结
- String 是最通用的缓存结构,适合简单键值对和计数器。
- Hash 是对象存储的首选,支持字段级操作。
- List 适合实现队列和时间线,注意阻塞命令的使用。
- Set 擅长去重和集合运算,适合标签和社交场景。
- Sorted Set 是排行榜和优先级队列的标准方案。
- go-redis 提供了 Pipeline 和事务机制,能有效提升批量操作的性能。
实践建议: 在开始编写缓存代码前,先根据业务场景画出数据流图,明确每个 Redis 键的命名规范(如
业务:对象:ID:字段),这能有效避免后续维护中的混乱。