第55课 - Redis 操作
1. 学习目标
完成本课学习后,你将能够:
- 使用
go-redis库建立与 Redis 服务器的连接。 - 执行基本的字符串和哈希数据结构操作。
- 运用 Redis 管道 (Pipeline) 提升批量操作性能。
- 使用发布/订阅 (Pub/Sub) 模式实现进程间通信。
- 对 Go 结构体进行序列化与反序列化以存储到 Redis。
2. 核心概念
Redis 是什么?
想象 Redis 是一个超高速的共享笔记本。应用程序(Go程序)可以随时在上面读写信息,所有连接的程序都能立刻看到最新内容。它主要作为缓存(临时存储高频数据)和消息中间件(程序间传递消息)使用。
go-redis 库
这是 Go 语言访问 Redis 最流行的客户端库。它像是一个专业的 “Redis 翻译官兼秘书”,负责:
- 连接管理:维护一个连接池,复用 TCP 连接,避免频繁创建销毁。
- 协议转换:将你的 Go 函数调用翻译成 Redis 能理解的命令。
- 并发安全:你可以在多个 goroutine 中安全地使用同一个
*redis.Client实例。
管道 (Pipeline)
想象你要给 Redis 发送 100 个命令。如果一个一个发,每次都要等网络来回,就像寄 100 封平信,等一封回信才寄下一封。管道就是把 100 封信打包成一个包裹,一次性寄出去,Redis 处理完再把 100 个回复一次性寄回来。这能极大减少网络开销,提升性能。
发布/订阅 (Pub/Sub)
这是一个广播电台模型。
- 频道 (Channel):类似电台频率,例如 “news”、“sports”。
- 发布者 (Publisher):电台主持人,往某个频道发送消息。
- 订阅者 (Subscriber):收音机,订阅某个频道后就能收到所有发往该频道的消息。
- 这个模型非常适合实现事件通知、实时消息推送等功能。
3. 代码示例
首先,请确保已安装依赖:
go get github.com/redis/go-redis/v9
示例 1:连接与基本操作
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
"time"
)
func main() {
ctx := context.Background()
// 1. 创建客户端并连接到 Redis 服务器
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 地址
Password: "", // 无密码
DB: 0, // 使用默认 DB
})
// 简单的连接测试
pong, err := rdb.Ping(ctx).Result()
if err != nil {
panic(err)
}
fmt.Println("连接成功:", pong) // 输出: 连接成功: PONG
// 2. 字符串操作 (Set/Get)
err = rdb.Set(ctx, "greeting", "Hello, Redis!", 10*time.Minute).Err() // 设置键值,有效期10分钟
if err != nil {
panic(err)
}
val, err := rdb.Get(ctx, "greeting").Result()
if err != nil {
panic(err)
}
fmt.Println("greeting:", val) // 输出: greeting: Hello, Redis!
// 3. 哈希操作 (适合存储对象)
userData := map[string]interface{}{
"name": "张三",
"email": "[email protected]",
"age": 30,
}
err = rdb.HSet(ctx, "user:1001", userData).Err()
if err != nil {
panic(err)
}
name, _ := rdb.HGet(ctx, "user:1001", "name").Result()
fmt.Println("用户名:", name) // 输出: 用户名: 张三
}
示例 2:使用管道批量操作
// 在 main 函数中继续添加以下代码
func demonstratePipeline(rdb *redis.Client) {
ctx := context.Background()
// 使用管道批量设置多个键值对
pipe := rdb.Pipeline() // 创建一个管道
// 将命令加入队列,而不是立即执行
pipe.Set(ctx, "key1", "value1", 0)
pipe.Set(ctx, "key2", "value2", 0)
pipe.Set(ctx, "key3", "value3", 0)
pipe.Incr(ctx, "counter") // 计数器加1
// 执行管道中的所有命令
cmders, err := pipe.Exec(ctx)
if err != nil {
panic(err)
}
// 检查结果(可选)
for _, cmder := range cmders {
fmt.Println(cmder) // 打印每个命令的执行情况
}
// 批量读取
vals, _ := rdb.MGet(ctx, "key1", "key2", "key3").Result()
fmt.Println("批量读取:", vals) // 输出: 批量读取: [value1 value2 value3]
}
示例 3:发布/订阅模式
func demonstratePubSub(rdb *redis.Client) {
ctx := context.Background()
channel := "notifications"
// 订阅者:启动一个 goroutine 来接收消息
go func() {
pubsub := rdb.Subscribe(ctx, channel)
defer pubsub.Close() // 记得关闭
// 等待订阅确认
_, err := pubsub.Receive(ctx)
if err != nil {
panic(err)
}
fmt.Println("订阅者已就绪,等待消息...")
// 开始接收消息
ch := pubsub.Channel()
for msg := range ch {
fmt.Printf("收到消息 [%s]: %s\n", msg.Channel, msg.Payload)
if msg.Payload == "quit" {
fmt.Println("收到退出指令,订阅者退出")
return
}
}
}()
// 主线程作为发布者,稍作延迟后发送消息
time.Sleep(1 * time.Second) // 确保订阅者已启动
rdb.Publish(ctx, channel, "Hello, 你好!")
time.Sleep(1 * time.Second)
rdb.Publish(ctx, channel, "这是一条测试消息")
time.Sleep(1 * time.Second)
rdb.Publish(ctx, channel, "quit") // 发送退出指令
time.Sleep(1 * time.Second) // 等待订阅者处理完退出消息
}
4. 实践练习
练习 1:实现一个简单的访问计数器
要求:编写一个程序,模拟用户访问。每次运行时,从 Redis 中读取 visit_count 的值,将其加 1,然后存回 Redis,并打印当前总访问次数。
预期输出:
这是第 1 次访问。
这是第 2 次访问。
...
练习 2:构建缓存封装
要求:编写一个通用的缓存函数 GetOrSet,签名为 func GetOrSet(key string, ttl time.Duration, fetcher func() (interface{}, error)) (string, error)。
- 首先尝试从 Redis 获取
key的值。 - 如果存在,直接返回。
- 如果不存在,则调用
fetcher函数获取数据(模拟从数据库查询),将其序列化为 JSON 存入 Redis,并设置 TTL,然后返回。
练习 3:简易聊天室 (选做)
要求:使用发布/订阅模式,实现一个命令行聊天室。启动两个终端,分别运行 subscriber.go 和 publisher.go。publisher.go 可以从标准输入读取消息并发布到 chatroom 频道,subscriber.go 订阅该频道并打印收到的所有消息。
5. 常见错误
-
未处理的连接错误:Redis 服务器宕机或网络问题会导致操作失败。始终检查错误并实现合理的重试或降级逻辑。
// 错误示范:忽略错误 rdb.Set(ctx, "key", "value", 0) // 正确做法 if err := rdb.Set(ctx, "key", "value", 0).Err(); err != nil { log.Printf("设置缓存失败: %v", err) // 考虑重试或记录日志 } -
忘记设置上下文超时:Redis 命令可能因为网络或服务器负载而卡住。使用带超时的
context。ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() val, err := rdb.Get(ctx, "key").Result() -
在错误的类型上调用方法:例如,一个键是字符串,你却用
HGetAll去获取,会得到错误。// 先确保键的类型,或使用更通用的方法 type_, _ := rdb.Type(ctx, "key").Result() switch type_ { case "string": // 使用 Get case "hash": // 使用 HGetAll } -
序列化陷阱:存储结构体时,如果字段未导出(小写字母开头),JSON 序列化会忽略它。
type User struct { name string `json:"name"` // 错误:name 未导出,不会被序列化 Name string `json:"name"` // 正确 } -
Pub/Sub 中的阻塞操作:在订阅者的回调或消息处理循环中执行耗时操作会阻塞后续消息的接收。应该将消息分发给其他 goroutine 处理。
6. 小结
本节课我们学习了 Go 语言中操作 Redis 的核心知识:
- 连接管理:使用
go-redis/v9创建客户端,注意配置和错误处理。 - 基本数据操作:熟练运用
Set/Get操作字符串,HSet/HGet操作哈希,这些是构建缓存的基础。 - 性能优化:管道 (Pipeline) 是提升 Redis 批量操作性能的关键技术,能显著减少网络 RTT。
- 并发通信:发布/订阅 (Pub/Sub) 模式解耦了消息的生产者和消费者,是实现事件驱动架构的利器,天然契合 Go 的并发哲学。
- 数据序列化:存储复杂数据(如结构体)时,需要先将其序列化为 JSON 等格式,并注意字段导出规则。
记住,Redis 客户端是并发安全的,你可以放心地在多个 goroutine 中使用同一个 *redis.Client 实例。下一课我们将学习如何管理数据库 schema 的变更。