3. Go 连接 Redis:使用 go-redis 库建立连接、连接池配置、基础 CRUD 操作

在 Go 生态中,go-redis 是连接 Redis 的事实标准库。本章将围绕连接建立、连接池调优以及基础 CRUD 操作展开,确保你能够在业务接口中稳定、高效地使用 Redis。

3.1 安装与导入

使用 go get 安装最新版本:

go get github.com/redis/go-redis/v9

在代码中导入:

import (
    "context"
    "github.com/redis/go-redis/v9"
)
版本提示: v9 版本要求 Go 1.18+,并全面支持 Redis 7.x 的新特性(如 RESP3、ACL、Sharded Pub/Sub)。建议生产环境使用 v9 系列。

3.2 建立连接:单节点模式

最常用的连接方式是通过 redis.NewClient 创建单节点客户端:

var rdb *redis.Client

func InitRedis() error {
    rdb = redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",   // Redis 地址
        Password: "",                 // 密码,无则留空
        DB:       0,                  // 默认数据库 0
        PoolSize: 10,                 // 连接池大小
        MinIdleConns: 5,             // 最小空闲连接数
        DialTimeout:  5 * time.Second, // 连接超时
        ReadTimeout:  3 * time.Second, // 读取超时
        WriteTimeout: 3 * time.Second, // 写入超时
    })

    // 验证连接
    ctx := context.Background()
    _, err := rdb.Ping(ctx).Result()
    if err != nil {
        return fmt.Errorf("redis ping failed: %w", err)
    }
    return nil
}
关键点: Ping 是连接验证的标准方式。如果返回 pong 且无错误,说明连接已成功建立。

3.3 连接池配置详解

go-redis 内置了基于 pool 包的连接池。合理的连接池配置能显著提升吞吐量并降低延迟。下表列出核心参数及其推荐值:

参数 类型 默认值 推荐值 说明
PoolSize int 10 CPU 核心数 × 2 ~ 4 最大连接数,超过此值会等待
MinIdleConns int 0 5 ~ 10 最小空闲连接,避免冷启动
MaxIdleConns int PoolSize 与 PoolSize 相同 最大空闲连接数
ConnMaxLifetime time.Duration 0(不限制) 30 分钟 ~ 1 小时 连接最大存活时间,防止长时间连接被 Redis 断开
ConnMaxIdleTime time.Duration 0(不限制) 5 分钟 空闲连接超时回收
DialTimeout time.Duration 5s 5s 建立 TCP 连接超时
ReadTimeout time.Duration 3s 3s 读取 Redis 响应超时
WriteTimeout time.Duration 3s 3s 写入 Redis 命令超时
避坑指南: 不要将 PoolSize 设置过大(如超过 100),否则 Redis 端会因大量空闲连接而消耗内存。同时,ConnMaxLifetime 建议设置,避免因网络中间件(如 Nginx、云负载均衡)断开长时间空闲连接导致 broken pipe 错误。

3.4 连接池状态监控

go-redis 提供了 PoolStats() 方法,用于实时查看连接池状态:

stats := rdb.PoolStats()
fmt.Printf("连接池状态:\n")
fmt.Printf("  TotalConns: %d\n", stats.TotalConns)       // 总连接数
fmt.Printf("  IdleConns:  %d\n", stats.IdleConns)        // 空闲连接数
fmt.Printf("  Hits:       %d\n", stats.Hits)             // 命中次数(从池中获取)
fmt.Printf("  Misses:     %d\n", stats.Misses)           // 未命中次数(新建连接)
fmt.Printf("  Timeouts:   %d\n", stats.Timeouts)         // 等待超时次数

通过监控这些指标,可以判断连接池配置是否合理:

  • Hits 远大于 Misses:连接复用率高,配置良好。
  • Timeouts 持续增长:PoolSize 过小或请求并发过高,需要扩容。
  • IdleConns 长期接近 TotalConns:连接池过大,浪费资源。

3.5 基础 CRUD 操作

以下演示最常用的五种数据类型操作:String、Hash、List、Set、Sorted Set。

3.5.1 String 操作

ctx := context.Background()

// Set
err := rdb.Set(ctx, "user:1001:name", "张三", 0).Err()
if err != nil {
    panic(err)
}

// Get
val, 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:", val)
}

// 带过期时间的 Set
rdb.Set(ctx, "session:token:abc123", "data", 30*time.Minute)

// 批量操作
rdb.MSet(ctx, "key1", "val1", "key2", "val2")
vals, _ := rdb.MGet(ctx, "key1", "key2").Result()

3.5.2 Hash 操作

// HSet
rdb.HSet(ctx, "user:1001", map[string]interface{}{
    "name":  "张三",
    "age":   28,
    "city":  "北京",
})

// HGet
name, _ := rdb.HGet(ctx, "user:1001", "name").Result()

// HGetAll
userMap, _ := rdb.HGetAll(ctx, "user:1001").Result()
fmt.Println(userMap) // map[age:28 city:北京 name:张三]

// HIncrBy
rdb.HIncrBy(ctx, "user:1001", "age", 1)

3.5.3 List 操作

// LPush / RPush
rdb.LPush(ctx, "queue:task", "task1", "task2")
rdb.RPush(ctx, "queue:task", "task3")

// LPop / RPop
task, _ := rdb.LPop(ctx, "queue:task").Result()

// LRange
tasks, _ := rdb.LRange(ctx, "queue:task", 0, -1).Result()

// LLen
length, _ := rdb.LLen(ctx, "queue:task").Result()

3.5.4 Set 操作

// SAdd
rdb.SAdd(ctx, "set:tags", "golang", "redis", "cache")

// SMembers
members, _ := rdb.SMembers(ctx, "set:tags").Result()

// SIsMember
exists, _ := rdb.SIsMember(ctx, "set:tags", "golang").Result()

// SRem
rdb.SRem(ctx, "set:tags", "cache")

3.5.5 Sorted Set 操作

// ZAdd
rdb.ZAdd(ctx, "leaderboard", redis.Z{
    Score:  100,
    Member: "player1",
}, redis.Z{
    Score:  200,
    Member: "player2",
})

// ZRangeByScore
opts := &redis.ZRangeBy{
    Min: "0",
    Max: "150",
}
results, _ := rdb.ZRangeByScoreWithScores(ctx, "leaderboard", opts).Result()
for _, z := range results {
    fmt.Printf("Member: %s, Score: %f\n", z.Member, z.Score)
}

// ZIncrBy
rdb.ZIncrBy(ctx, "leaderboard", 50, "player1")
最佳实践: 对于业务接口中的 CRUD,建议将 Redis 操作封装为独立的 Repository 层,统一处理 key 前缀、序列化(如 JSON/Protobuf)、错误转换和重试逻辑,避免业务代码直接调用 go-redis 原始 API。

3.6 连接与操作流程总览

下图展示了从 Go 应用初始化到执行 CRUD 的完整链路:

Go 应用连接 Redis 与 CRUD 流程 Go 业务代码 go-redis Client 连接池 (Pool) Redis Server 调用 API 获取连接 TCP 连接 响应 基础 CRUD 操作映射 • String: Set / Get / MSet / MGet / Incr / Decr • Hash: HSet / HGet / HGetAll / HIncrBy / HDel • List: LPush / RPush / LPop / RPop / LRange / LLen • Set: SAdd / SMembers / SIsMember / SRem / SInter • Sorted Set: ZAdd / ZRangeByScore / ZIncrBy / ZRem

3.7 连接池与 CRUD 交互时序

以下时序图展示了一次完整的 Get 操作在连接池中的流转过程:

连接池获取连接与 CRUD 时序 业务 Goroutine go-redis 客户端 连接池 Redis Server Get("key") GetConn() 获取空闲连接 GET key value / nil 归还连接到池 返回结果

3.8 常见错误与处理

错误类型 典型原因 处理方式
redis.Nil key 不存在 使用 errors.Is(err, redis.Nil) 判断,不要视为异常
dial tcp: i/o timeout Redis 不可达或网络延迟 检查网络、防火墙;增加 DialTimeout
connection pool exhausted 连接池满,请求排队超时 增大 PoolSize 或优化业务并发
broken pipe 连接被 Redis 或中间件断开 设置 ConnMaxLifetime 定期回收连接
MOVED / ASK Redis 集群模式下 slot 迁移 使用 redis.NewClusterClient 自动处理
生产建议: 在业务接口中,建议对 Redis 操作添加 重试机制(如指数退避重试 3 次),并配合 熔断器(如 gobreaker)防止 Redis 故障时雪崩。同时,所有 Redis 操作应设置合理的 Context 超时,避免 Goroutine 泄漏。