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)
}
}
``
LPush
**关键点**:
*和
RPush决定了元素添加的位置,这对于实现栈(
LPush+
LPop)和队列(
LPush+
RPop)至关重要。
BLPop
*和
BRPop` 是实现可靠任务队列的关键,消费者进程可以安全地等待新任务,而无需进行轮询,大大降低了 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
通过 TxPipelined
或 Watch
方法支持事务。
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)
}
``
KEYS
**关键点**:
* 在 Lua 脚本中,通过数组访问键名,通过
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 应用插上性能的翅膀。