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 数据结构选型决策流程

为了帮助你在实际项目中快速选择合适的数据结构,下面给出一个决策流程图:

Redis 数据结构选型决策流程 开始选型 是否需要排序或排名? 是 Sorted Set 否 是否需要自动去重? 是 Set 否 是否需要 FIFO 队列? 是 List 否 是否操作对象的部分字段? 是 Hash 否 String 提示:实际项目中可能组合使用多种结构,例如用 Hash 存储对象,用 Sorted Set 维护其排名

2.5 本章小结

  • String 是最通用的缓存结构,适合简单键值对和计数器。
  • Hash 是对象存储的首选,支持字段级操作。
  • List 适合实现队列和时间线,注意阻塞命令的使用。
  • Set 擅长去重和集合运算,适合标签和社交场景。
  • Sorted Set 是排行榜和优先级队列的标准方案。
  • go-redis 提供了 Pipeline 和事务机制,能有效提升批量操作的性能。
实践建议: 在开始编写缓存代码前,先根据业务场景画出数据流图,明确每个 Redis 键的命名规范(如 业务:对象:ID:字段),这能有效避免后续维护中的混乱。