go-redis 核心用法详解与代码示例 – wiki基地


Go-Redis 核心用法详解与代码示例

引言

在现代后端开发中,缓存系统扮演着至关重要的角色。Redis 以其卓越的性能、丰富的数据结构和强大的功能,成为了当之无愧的内存数据库王者。对于 Go 语言生态而言,与 Redis 进行交互的客户端库有很多,其中 go-redis (github.com/redis/go-redis) 是目前社区最活跃、功能最完备、使用最广泛的选择之一。它提供了对 Redis 各项命令的全面支持,并封装了连接池、管道、事务、发布订阅等高级功能,使得 Go 开发者能够轻松、高效地利用 Redis 的强大能力。

本文将深入探讨 go-redis v9 版本的核心用法,从最基础的连接与配置开始,详细讲解五大核心数据结构的操作,并进一步探索管道(Pipelining)、事务(Transaction)、发布/订阅(Pub/Sub)以及 Lua 脚本等进阶功能。通过丰富的代码示例和详尽的解释,旨在为 Go 开发者提供一份全面而实用的 go-redis 使用指南。

一、 基础入门:安装与连接

在开始之前,请确保你已经安装了 Go 环境和 Redis 服务。

1.1 安装 go-redis

通过 Go-Module,安装 go-redis 非常简单,只需在你的项目目录下执行:

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

1.2 连接到 Redis

go-redis 通过 redis.NewClient 函数创建一个客户端实例。这个函数接收一个 redis.Options 结构体指针,用于配置所有连接参数。

一个最基本的连接示例如下:

“`go
package main

import (
“context”
“fmt”
“github.com/redis/go-redis/v9”
“time”
)

var (
rdb *redis.Client
ctx = context.Background()
)

func init() {
// 创建一个新的 Redis 客户端
rdb = redis.NewClient(&redis.Options{
Addr: “localhost:6379”, // Redis 服务器地址
Password: “”, // Redis 密码,如果没有则留空
DB: 0, // 使用默认的 DB 0
PoolSize: 10, // 连接池大小
})

// 通过 Ping 命令测试连接
_, err := rdb.Ping(ctx).Result()
if err != nil {
    panic(fmt.Sprintf("无法连接到 Redis: %v", err))
}

fmt.Println("成功连接到 Redis!")

}

func main() {
// 在这里执行你的 Redis 操作
// 为了演示,我们在这里暂停一下
time.Sleep(1 * time.Second)
fmt.Println(“应用执行完毕。”)
}
“`

代码解析
* context.Context: go-redis 的所有命令方法都接收 context.Context 作为第一个参数。这是一种非常好的实践,它允许你控制命令的执行时间(超时)和取消操作。在绝大多数 Web 应用场景中,你应该传递请求的 context。在此示例中,我们使用 context.Background() 表示没有超时或截止日期。
* redis.Options: 这个结构体包含了所有配置项。
* Addr: Redis 服务器的地址和端口。
* Password: 访问密码。
* DB: 选择要操作的数据库,Redis 默认有 16 个数据库(0-15)。
* PoolSize: go-redis 内部维护了一个连接池,PoolSize 指定了池中最大连接数。对于大多数应用,默认值已经足够。
* rdb.Ping(ctx): 这是一个用于检查与 Redis 服务器的连接是否正常的命令。如果连接成功,它会返回 “PONG”;否则返回错误。在应用启动时执行 Ping 是一种验证配置正确性的好方法。
* 客户端实例管理: 通常情况下,一个应用程序只需要一个 redis.Client 实例,并在整个应用的生命周期内共享它。go-redis 客户端是并发安全的,其内部的连接池会高效地管理连接。

二、 核心数据结构操作

Redis 的强大之处在于其支持多种数据结构。下面我们将详细介绍 go-redis 如何操作 Redis 的五种基本数据类型:String、Hash、List、Set 和 Sorted Set。

2.1 String (字符串)

String 是 Redis 最基本的数据类型,可以存储字符串、整数或浮点数。常用于缓存用户信息、计数器等场景。

“`go
func stringOperations() {
// 1. 设置键值
// Set(ctx, key, value, expiration)
// expiration: 0 表示永不过期
err := rdb.Set(ctx, “user:1:name”, “Alice”, 0).Err()
if err != nil {
panic(err)
}
fmt.Println(“设置 user:1:name 成功”)

// 设置一个带过期时间的键 (1分钟)
err = rdb.Set(ctx, "session:123", "some_session_data", 1*time.Minute).Err()
if err != nil {
    panic(err)
}
fmt.Println("设置 session:123 并在一分钟后过期")

// 2. 获取键值
// Get(ctx, key)
name, err := rdb.Get(ctx, "user:1:name").Result()
if err != nil {
    if err == redis.Nil {
        fmt.Println("user:1:name 键不存在")
        return
    }
    panic(err)
}
fmt.Printf("获取到 user:1:name 的值为: %s\n", name)

// 当键不存在时,会返回 redis.Nil 错误
_, err = rdb.Get(ctx, "non_existent_key").Result()
if err == redis.Nil {
    fmt.Println("non_existent_key 键不存在 (符合预期)")
}

// 3. 数值操作 (原子性)
// Incr(ctx, key)
// 将键的值加 1,如果键不存在,则先初始化为 0 再加 1
views, err := rdb.Incr(ctx, "article:1:views").Result()
if err != nil {
    panic(err)
}
fmt.Printf("文章 article:1 的浏览量为: %d\n", views)

// DecrBy(ctx, key, decrement)
// 将键的值减去指定数值
stock, err := rdb.DecrBy(ctx, "product:1:stock", 2).Result()
if err != nil {
    panic(err)
}
fmt.Printf("商品 product:1 的库存剩余: %d\n", stock)

}
``
**关键点**:
* 所有操作方法(如
Set,Get)都会返回一个*Cmd结果对象。你可以通过调用.Result()来获取结果和错误,或者直接调用.Err()只检查错误。
* 当
Get一个不存在的键时,go-redis会返回redis.Nil` 错误。检查这个特定错误是判断键是否存在的标准方式,而不是将其视为一个真正的程序异常。

2.2 Hash (哈希)

Hash 是一个键值对集合,非常适合存储对象。例如,一个用户的 ID 作为键,其属性(如姓名、年龄、邮箱)存储在 Hash 的字段中。

“`go
func hashOperations() {
userKey := “user:2”

// 1. 设置单个字段
// HSet(ctx, key, field, value)
err := rdb.HSet(ctx, userKey, "name", "Bob").Err()
if err != nil {
    panic(err)
}

// 2. 同时设置多个字段
// HMSet(ctx, key, values)
// 注意: v8版本后推荐直接用 HSet 传递 map 或 struct
userData := map[string]interface{}{
    "age":   30,
    "email": "[email protected]",
}
err = rdb.HSet(ctx, userKey, userData).Err()
if err != nil {
    panic(err)
}
fmt.Printf("设置用户 %s 的信息成功\n", userKey)

// 3. 获取单个字段
// HGet(ctx, key, field)
name, err := rdb.HGet(ctx, userKey, "name").Result()
if err != nil {
    panic(err)
}
fmt.Printf("用户 %s 的名字是: %s\n", userKey, name)

// 4. 获取所有字段和值
// HGetAll(ctx, key)
allFields, err := rdb.HGetAll(ctx, userKey).Result()
if err != nil {
    panic(err)
}
fmt.Printf("用户 %s 的所有信息: %v\n", userKey, allFields) // 返回 map[string]string

// 5. 删除字段
// HDel(ctx, key, fields...)
err = rdb.HDel(ctx, userKey, "email").Err()
if err != nil {
    panic(err)
}
fmt.Printf("删除了用户 %s 的 email 字段\n", userKey)

}
``
**关键点**:
*
HGetAll返回的是一个map[string]string`。这意味着即使你存入的是数字,取出来也会是字符串形式,需要手动转换。
* 使用 Hash 结构可以有效地组织相关数据,减少键的数量,并节省内存(相比为每个字段创建一个顶级键)。

2.3 List (列表)

List 是一个字符串列表,按照插入顺序排序。你可以从列表的头部(左侧)或尾部(右侧)添加或弹出元素。非常适合实现消息队列、任务列表、最新动态等功能。

“`go
func listOperations() {
listKey := “tasks”
// 清理一下,方便演示
rdb.Del(ctx, listKey)

// 1. 从左侧推入元素 (LPush)
// LPush(ctx, key, values...)
err := rdb.LPush(ctx, listKey, "task1", "task2", "task3").Err()
if err != nil {
    panic(err)
}
// 此时列表内容: ["task3", "task2", "task1"]

// 2. 从右侧推入元素 (RPush)
// RPush(ctx, key, values...)
err = rdb.RPush(ctx, listKey, "task0").Err()
if err != nil {
    panic(err)
}
// 此时列表内容: ["task3", "task2", "task1", "task0"]

// 3. 获取列表长度
// LLen(ctx, key)
length, err := rdb.LLen(ctx, listKey).Result()
if err != nil {
    panic(err)
}
fmt.Printf("任务列表 %s 的长度为: %d\n", listKey, length)

// 4. 获取指定范围的元素
// LRange(ctx, key, start, stop)
// -1 表示最后一个元素
tasks, err := rdb.LRange(ctx, listKey, 0, -1).Result()
if err != nil {
    panic(err)
}
fmt.Printf("任务列表 %s 的所有任务: %v\n", listKey, tasks)

// 5. 从左侧弹出一个元素 (LPop)
// LPop(ctx, key)
task, err := rdb.LPop(ctx, listKey).Result()
if err != nil {
    panic(err)
}
fmt.Printf("从左侧弹出的任务是: %s\n", task) // 应该是 task3
// 此时列表内容: ["task2", "task1", "task0"]

// 6. 阻塞式弹出 (BLPop)
// BLPop(ctx, timeout, keys...)
// 当列表为空时,此命令会阻塞,直到有新元素被推入或超时
// 这里设置 2 秒超时
fmt.Println("等待从空列表 'empty_tasks' 中阻塞弹出元素 (2秒超时)...")
result, err := rdb.BLPop(ctx, 2*time.Second, "empty_tasks").Result()
if err != nil {
    // 超时会返回 redis.Nil 错误
    if err == redis.Nil {
        fmt.Println("没有等到任何元素,超时返回。")
    } else {
        panic(err)
    }
} else {
    fmt.Printf("阻塞式弹出结果: %v\n", result)
}

}
``
**关键点**:
*
LPushRPush决定了元素添加的位置,这对于实现栈(LPush+LPop)和队列(LPush+RPop)至关重要。
*
BLPopBRPop` 是实现可靠任务队列的关键,消费者进程可以安全地等待新任务,而无需进行轮询,大大降低了 CPU 和网络开销。

2.4 Set (集合)

Set 是一个无序且唯一的字符串集合。它提供了高效的成员检查、并集、交集、差集等操作。适合用于标签系统、共同好友、抽奖等场景。

“`go
func setOperations() {
setKey1 := “user:1:following”
setKey2 := “user:2:following”
// 清理
rdb.Del(ctx, setKey1, setKey2)

// 1. 添加成员
// SAdd(ctx, key, members...)
rdb.SAdd(ctx, setKey1, "user:3", "user:4", "user:5")
rdb.SAdd(ctx, setKey2, "user:4", "user:5", "user:6")

// 2. 获取所有成员
// SMembers(ctx, key)
following1, err := rdb.SMembers(ctx, setKey1).Result()
if err != nil {
    panic(err)
}
fmt.Printf("用户1关注的人: %v\n", following1)

// 3. 检查成员是否存在
// SIsMember(ctx, key, member)
isMember, err := rdb.SIsMember(ctx, setKey1, "user:4").Result()
if err != nil {
    panic(err)
}
fmt.Printf("用户1是否关注了用户4? %t\n", isMember)

// 4. 计算交集 (共同关注)
// SInter(ctx, keys...)
commonFollowing, err := rdb.SInter(ctx, setKey1, setKey2).Result()
if err != nil {
    panic(err)
}
fmt.Printf("用户1和用户2的共同关注: %v\n", commonFollowing)

// 5. 计算差集 (用户1关注了,但用户2没关注的)
// SDiff(ctx, keys...)
diffFollowing, err := rdb.SDiff(ctx, setKey1, setKey2).Result()
if err != nil {
    panic(err)
}
fmt.Printf("用户1关注但用户2未关注的人: %v\n", diffFollowing)

}
“`

2.5 Sorted Set (有序集合)

Sorted Set (ZSet) 与 Set 类似,都是唯一的字符串成员集合,但每个成员都会关联一个 double 类型的分数(score)。Redis 正是根据这个分数对成员进行排序。它非常适合实现排行榜、带权重的任务队列等。

“`go
func sortedSetOperations() {
zsetKey := “leaderboard”
rdb.Del(ctx, zsetKey)

// 1. 添加成员和分数
// ZAdd(ctx, key, members...)
members := []redis.Z{
    {Score: 100, Member: "player1"},
    {Score: 95, Member: "player2"},
    {Score: 110, Member: "player3"},
    {Score: 80, Member: "player4"},
}
err := rdb.ZAdd(ctx, zsetKey, members...).Err()
if err != nil {
    panic(err)
}

// 2. 增加成员分数
// ZIncrBy(ctx, key, increment, member)
newScore, err := rdb.ZIncrBy(ctx, zsetKey, 10, "player2").Result()
if err != nil {
    panic(err)
}
fmt.Printf("player2 的新分数是: %.2f\n", newScore) // 现在是 105

// 3. 按分数从高到低获取排行榜 (Top 3)
// ZRevRangeWithScores(ctx, key, start, stop)
topPlayers, err := rdb.ZRevRangeWithScores(ctx, zsetKey, 0, 2).Result()
if err != nil {
    panic(err)
}
fmt.Println("排行榜 Top 3:")
for _, player := range topPlayers {
    fmt.Printf("  - %s: %.2f\n", player.Member, player.Score)
}

// 4. 获取指定成员的排名 (分数从高到低)
// ZRevRank(ctx, key, member)
rank, err := rdb.ZRevRank(ctx, zsetKey, "player1").Result()
if err != nil {
    if err == redis.Nil {
        fmt.Println("player1 不在排行榜上")
    } else {
        panic(err)
    }
}
// 排名是从 0 开始的
fmt.Printf("player1 的排名是: %d\n", rank+1)

}
``
**关键点**:
*
ZAdd时,使用redis.Z结构体来封装成员和分数。
*
ZRevRange系列函数用于按分数从高到低排序,而ZRange` 系列则从低到高。这对于获取“Top N”和“Bottom N”都很有用。

三、 进阶用法

掌握了基本数据结构后,我们来探索 go-redis 提供的一些高级功能,它们可以极大地提升应用性能和处理复杂业务逻辑的能力。

3.1 管道 (Pipelining)

客户端与 Redis 服务器之间的每次通信都有网络延迟(RTT)。如果你需要连续执行多个命令,一次次地发送和等待会累积大量的 RTT。管道允许你将多个命令打包,一次性发送给 Redis,然后一次性接收所有响应。这能显著降低网络开销,提升吞吐量。

“`go
func pipelineExample() {
// 使用 Pipelined 函数,它会自动执行 Exec
// 这是一个更简洁的方式
var incr redis.IntCmd
var get
redis.StringCmd

cmds, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
    incr = pipe.Incr(ctx, "pipeline_counter")
    get = pipe.Get(ctx, "user:1:name")
    pipe.Set(ctx, "pipeline_test", "value", time.Hour)
    return nil
})
if err != nil && err != redis.Nil {
    panic(err)
}

fmt.Println("--- Pipelined 执行结果 ---")
for i, cmd := range cmds {
    val, err := cmd.Result()
    if err != nil {
        fmt.Printf("命令 %d 失败: %v\n", i, err)
    } else {
        fmt.Printf("命令 %d 结果: %v\n", i, val)
    }
}

// 手动获取特定命令的结果
fmt.Printf("计数器的新值: %d\n", incr.Val())
fmt.Printf("获取到的用户名: %s\n", get.Val())

}
``
**关键点**:
*
Pipelined函数内的所有命令都不会立即发送,而是被缓存到pipe对象中。函数返回后,go-redis会自动将所有命令发送出去,并返回一个[]Cmder` 切片,包含了每个命令的结果对象。
* 管道内的命令不是原子性的。在命令执行期间,其他客户端的命令可能会穿插进来。如果需要原子性,请使用事务。

3.2 事务 (Transaction)

Redis 事务可以将一组命令打包执行,保证这组命令的原子性(要么全部执行,要么全不执行)。go-redis 通过 TxPipelinedWatch 方法支持事务。

Watch 提供了乐观锁(CAS – Check-And-Set)机制,它监视一个或多个键。如果在事务执行(EXEC)之前,任何被监视的键被其他客户端修改了,那么整个事务将失败。

“`go
func transactionExample() {
// 场景:更新用户积分,需要先读取旧积分,计算后再写回。
// 这个过程需要是原子的,防止并发问题。
const userScoreKey = “user:1:score”
rdb.Set(ctx, userScoreKey, “100”, 0) // 初始化积分为 100

err := rdb.Watch(ctx, func(tx *redis.Tx) error {
    // 在 Watch 闭包内的操作都在一个事务中
    scoreStr, err := tx.Get(ctx, userScoreKey).Result()
    if err != nil && err != redis.Nil {
        return err
    }

    score, _ := strconv.Atoi(scoreStr)
    newScore := score + 10

    // 使用 TxPipelined 来执行写操作
    // 在 Watch 中,tx.Pipelined() 会被 MULTI/EXEC 包裹
    _, err = tx.Pipelined(ctx, func(pipe redis.Pipeliner) error {
        pipe.Set(ctx, userScoreKey, newScore, 0)
        return nil
    })
    return err
}, userScoreKey) // 告诉 Redis 我们要监视这个 key

if err != nil {
    // 如果是 redis.TxFailedErr,说明在 Watch 期间 key 被修改,事务被中止
    if err == redis.TxFailedErr {
        fmt.Println("事务失败:user:1:score 在操作期间被其他客户端修改。")
        // 在实际应用中,这里通常会进行重试
    } else {
        panic(err)
    }
} else {
    newScore, _ := rdb.Get(ctx, userScoreKey).Result()
    fmt.Printf("事务成功!用户新积分为: %s\n", newScore)
}

}
``
**关键点**:
*
Watch是实现乐观锁的关键。它在MULTI命令之前监视键。
* 传递给
Watch的函数可能会被执行多次(如果发生冲突并重试),因此函数本身应该是幂等的。
* 如果事务因为
Watch的键被修改而失败,go-redis会返回redis.TxFailedErr`。

3.3 发布/订阅 (Pub/Sub)

Pub/Sub 是一种消息通信模式,发送者(Publisher)将消息发送到特定频道(Channel),而订阅者(Subscriber)可以监听这些频道并接收消息。

“`go
func pubSubExample() {
channel := “my-channel”

// 1. 创建一个订阅者
pubsub := rdb.Subscribe(ctx, channel)
defer pubsub.Close()

// 等待订阅确认
_, err := pubsub.Receive(ctx)
if err != nil {
    panic(err)
}

// 2. 在一个单独的 goroutine 中接收消息
ch := pubsub.Channel()
go func() {
    fmt.Println("开始监听频道:", channel)
    for msg := range ch {
        fmt.Printf("从频道 '%s' 收到消息: %s\n", msg.Channel, msg.Payload)
    }
    fmt.Println("监听结束。")
}()

// 3. 发布消息
fmt.Println("发布消息 'hello'...")
rdb.Publish(ctx, channel, "hello")
time.Sleep(100 * time.Millisecond)

fmt.Println("发布消息 'world'...")
rdb.Publish(ctx, channel, "world")
time.Sleep(100 * time.Millisecond)

// 为了演示,这里只等待一小段时间
fmt.Println("Pub/Sub 演示结束。")

}
``
**关键点**:
*
Subscribe返回一个*PubSub对象,该对象不是并发安全的。
*
pubsub.Channel()返回一个 Go channel,可以方便地用for…range循环来处理接收到的消息。这个循环会一直阻塞,直到pubsub` 对象被关闭。
* Pub/Sub 是“即发即弃”的,如果消息发布时没有订阅者在线,消息就会丢失。对于需要可靠消息传递的场景,应考虑使用 Redis Streams 或专门的消息队列(如 RabbitMQ, Kafka)。

3.4 Lua 脚本

当一个操作需要多个 Redis 命令,并且要求原子性时,除了事务,还可以使用 Lua 脚本。将逻辑封装在 Lua 脚本中,由 Redis 服务器原子地执行,可以减少网络往返,同时保证操作的原子性。

``go
func luaScriptExample() {
// 脚本:原子地比较并设置值。如果传入的 key 的值等于 arg1,则将其设置为 arg2。
// 返回 1 表示成功,0 表示失败。
luaScript :=

if redis.call(“get”, KEYS[1]) == ARGV[1] then
return redis.call(“set”, KEYS[1], ARGV[2])
else
return 0
end
`
// 创建脚本对象
script := redis.NewScript(luaScript)

key := "lua_test_key"
val1 := "hello"
val2 := "world"

rdb.Set(ctx, key, val1, 0)

// 执行脚本
// Run(ctx, script, keys[], args...)
result, err := script.Run(ctx, rdb, []string{key}, val1, val2).Result()
if err != nil {
    panic(err)
}

// Redis 返回的是 "OK" 字符串,但脚本逻辑里返回的是数字
// 这里我们需要根据脚本逻辑来判断
fmt.Printf("Lua 脚本执行结果: %v\n", result) // result 可能是 "OK"

// 再次执行,此时 key 的值是 "world",不等于 val1("hello")
result, err = script.Run(ctx, rdb, []string{key}, val1, val2).Int()
if err != nil {
    panic(err)
}
fmt.Printf("第二次执行(值不匹配),脚本返回: %d (0表示失败)\n", result)

}
``
**关键点**:
* 在 Lua 脚本中,通过
KEYS数组访问键名,通过ARGV数组访问参数。
*
redis.NewScript会在首次执行时将脚本加载到 Redis 的脚本缓存中(使用SCRIPT LOAD),并返回脚本的 SHA1 哈希。后续执行会使用EVALSHA,如果缓存失效,go-redis会自动回退到EVAL`,对开发者透明。

四、 总结

go-redis 为 Go 开发者提供了一个功能强大且接口友好的 Redis 客户端。本文从最基础的连接配置讲起,系统地梳理了对 Redis 五大核心数据结构(String, Hash, List, Set, Sorted Set)的操作方法,并通过丰富的代码示例展示了它们的典型应用场景。更进一步,我们深入探讨了 Pipelining、事务、Pub/Sub 和 Lua 脚本等高级特性,这些是构建高性能、高并发应用的关键。

掌握 go-redis 的核心用法,你将能够:
* 高效缓存:利用 String 和 Hash 结构,轻松实现对象和会话缓存。
* 构建功能模块:通过 List 实现消息队列,通过 Set 实现社交关系,通过 Sorted Set 打造排行榜。
* 优化性能:使用 Pipelining 大幅减少网络延迟。
* 保证数据一致性:通过事务和 Lua 脚本执行原子操作。
* 实现实时通信:利用 Pub/Sub 构建简单的实时消息系统。

go-redis 的功能远不止于此,它还支持 Redis Cluster、Redis Sentinel、Streams 等更多高级特性。希望本文能为你打下坚实的基础,鼓励你继续探索 go-redis 和 Redis 的广阔世界,为你的 Go 应用插上性能的翅膀。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部