4. Go连接Redis:go-redis库的安装、初始化连接池、基础CRUD操作
在Go生态中,go-redis 是目前最流行、维护最活跃的Redis客户端库。它提供了连接池管理、哨兵/集群支持、管道/事务等高级特性,且API设计简洁,非常适合高并发业务场景。本章将带你完成从安装、连接池配置到基础CRUD的全流程实战。
4.1 安装go-redis库
使用Go Modules管理依赖,在项目根目录执行以下命令安装最新稳定版:
go get github.com/redis/go-redis/v9
安装完成后,在代码中导入:
import "github.com/redis/go-redis/v9"
版本提示: v9版本要求Go 1.18+,且全面支持context.Context。若你仍在使用Go 1.17及以下,请使用v8版本(
go get github.com/go-redis/redis/v8)。
4.2 初始化Redis客户端与连接池
go-redis通过 redis.NewClient 创建客户端实例,内部自动维护一个连接池。连接池参数直接影响高并发下的吞吐量和资源消耗。
4.2.1 基础连接配置
| 配置项 | 类型 | 说明 | 推荐值 |
|---|---|---|---|
Addr |
string | Redis服务器地址,格式 host:port |
localhost:6379 |
Password |
string | 认证密码,无密码留空 | "" |
DB |
int | 默认数据库编号(0-15) | 0 |
PoolSize |
int | 连接池最大连接数 | 10 * runtime.GOMAXPROCS(0) |
MinIdleConns |
int | 最小空闲连接数,避免频繁建连 | 5 |
MaxIdleConns |
int | 最大空闲连接数,超过则关闭 | PoolSize |
ConnMaxLifetime |
time.Duration | 连接最大存活时间,防止长时间占用 | 30 * time.Minute |
ConnMaxIdleTime |
time.Duration | 连接最大空闲时间,超时关闭 | 5 * time.Minute |
4.2.2 初始化代码示例
package main
import (
"context"
"fmt"
"runtime"
"time"
"github.com/redis/go-redis/v9"
)
var rdb *redis.Client
var ctx = context.Background()
func initRedis() {
rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
PoolSize: 10 * runtime.GOMAXPROCS(0), // 根据CPU核数动态调整
MinIdleConns: 5,
MaxIdleConns: 20,
ConnMaxLifetime: 30 * time.Minute,
ConnMaxIdleTime: 5 * time.Minute,
})
// 验证连接是否成功
pong, err := rdb.Ping(ctx).Result()
if err != nil {
panic(fmt.Sprintf("Redis连接失败: %v", err))
}
fmt.Println("Redis连接成功:", pong)
}
连接池调优要点:
- PoolSize 并非越大越好,过大会增加Redis服务器和Go协程的上下文切换开销。建议根据业务QPS和Redis响应时间(通常<1ms)计算:
PoolSize ≈ (QPS × 平均响应时间) / 1000。 - 高并发场景下,MinIdleConns 应保持一定数量,避免突发流量时频繁创建新连接。
- 若业务中有大量耗时操作(如
BLPOP、BRPOP),建议单独创建专用客户端,避免阻塞连接池。
4.3 连接池工作原理(SVG图)
下图展示了go-redis连接池的内部工作流程,理解它有助于你正确配置参数:
4.4 基础CRUD操作实战
以下代码演示了最常用的五种数据类型操作:String、Hash、List、Set、Sorted Set。所有操作均使用 context.Context 传递超时和取消信号。
4.4.1 String(字符串)操作
// 设置键值,过期时间10分钟
err := rdb.Set(ctx, "user:1001:name", "张三", 10*time.Minute).Err()
if err != nil {
panic(err)
}
// 获取值
val, err := rdb.Get(ctx, "user:1001:name").Result()
if err == redis.Nil {
fmt.Println("键不存在")
} else if err != nil {
panic(err)
} else {
fmt.Println("user:1001:name =", val)
}
// 批量设置
err = rdb.MSet(ctx, "key1", "value1", "key2", "value2").Err()
// 原子递增(用于计数器)
count, err := rdb.Incr(ctx, "page:view:1001").Result()
fmt.Printf("当前计数: %d\n", count)
4.4.2 Hash(哈希)操作
// 设置哈希字段
err := rdb.HSet(ctx, "user:1001", "name", "李四", "age", 28, "city", "北京").Err()
// 获取单个字段
name, err := rdb.HGet(ctx, "user:1001", "name").Result()
// 获取所有字段
userMap, err := rdb.HGetAll(ctx, "user:1001").Result()
for field, val := range userMap {
fmt.Printf("%s: %s\n", field, val)
}
// 原子递增哈希字段
rdb.HIncrBy(ctx, "user:1001", "score", 10)
4.4.3 List(列表)操作
// 从左侧推入(实现栈)
rdb.LPush(ctx, "queue:task", "task1", "task2", "task3")
// 从右侧弹出(实现队列)
task, err := rdb.RPop(ctx, "queue:task").Result()
// 获取列表范围(分页查询)
tasks, err := rdb.LRange(ctx, "queue:task", 0, 9).Result()
// 获取列表长度
length, _ := rdb.LLen(ctx, "queue:task").Result()
4.4.4 Set(集合)操作
// 添加元素
rdb.SAdd(ctx, "set:tags", "go", "redis", "cache")
// 判断元素是否存在
exists, _ := rdb.SIsMember(ctx, "set:tags", "go").Result()
// 获取所有元素
members, _ := rdb.SMembers(ctx, "set:tags").Result()
// 集合交集(共同标签)
common, _ := rdb.SInter(ctx, "set:tags", "set:other_tags").Result()
4.4.5 Sorted Set(有序集合)操作
// 添加带分数的元素
rdb.ZAdd(ctx, "leaderboard",
redis.Z{Score: 95.5, Member: "Alice"},
redis.Z{Score: 88.0, Member: "Bob"},
redis.Z{Score: 92.3, Member: "Charlie"},
)
// 按分数范围查询(排行榜前3)
top3, _ := rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, 2).Result()
for _, z := range top3 {
fmt.Printf("成员: %s, 分数: %.1f\n", z.Member, z.Score)
}
// 获取指定成员的分数
score, _ := rdb.ZScore(ctx, "leaderboard", "Alice").Result()
4.5 管道(Pipeline)与批量操作
当需要执行多条命令时,使用管道可以减少网络往返,大幅提升吞吐量。go-redis的管道将所有命令打包发送,一次接收所有响应。
// 创建管道
pipe := rdb.Pipeline()
// 批量加入命令
pipe.Set(ctx, "k1", "v1", 0)
pipe.Set(ctx, "k2", "v2", 0)
pipe.Get(ctx, "k1")
pipe.Incr(ctx, "counter")
// 执行管道,返回所有命令的结果
cmds, err := pipe.Exec(ctx)
if err != nil {
panic(err)
}
// 遍历结果
for _, cmd := range cmds {
fmt.Println(cmd.String())
}
性能对比: 在100条命令场景下,管道方式比逐条发送快约 10~20倍(取决于网络延迟)。对于高并发业务,建议将多条无关命令合并到管道中执行。
4.6 连接池监控与健康检查
生产环境中,建议定期检查连接池状态,及时发现异常:
// 获取连接池统计信息
stats := rdb.PoolStats()
fmt.Printf("总连接数: %d\n", stats.TotalConns)
fmt.Printf("空闲连接数: %d\n", stats.IdleConns)
fmt.Printf("等待获取连接的协程数: %d\n", stats.StaleConns)
// 主动健康检查(Ping)
func healthCheck() {
ticker := time.NewTicker(30 * time.Second)
for range ticker.C {
_, err := rdb.Ping(ctx).Result()
if err != nil {
fmt.Printf("Redis健康检查失败: %v\n", err)
// 可在此处触发重连或告警
}
}
}
4.7 常见错误与避坑指南
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
dial tcp: connect: connection refused |
Redis服务未启动或地址错误 | 检查 Addr 配置,确认Redis进程运行 |
NOAUTH Authentication required |
需要密码但未配置 | 设置 Password 字段 |
ERR max number of clients reached |
连接数超过Redis最大客户端限制 | 增大Redis的 maxclients 或减小 PoolSize |
i/o timeout |
网络延迟或Redis响应慢 | 检查网络,设置合理的 DialTimeout 和 ReadTimeout |
redis: nil |
键不存在(非错误,是特殊返回值) | 使用 redis.Nil 判断,不要直接比较 err == nil |
最佳实践:
- 始终使用
context.WithTimeout为每个Redis操作设置超时,防止协程永久阻塞。 - 不要在热路径上频繁创建和销毁
redis.Client,应复用全局单例。 - 对于读多写少的场景,可考虑启用
ReadOnly模式配合Redis副本节点分担读压力。