更多个人笔记:(仅供参考,非盈利)
gitee: https://gitee.com/harryhack/it_note
github: https://github.com/ZHLOVEYY/IT_note
(更多GO+redis等见内部,会及时更新~)
- 安装redis客户端:
go get github.com/redis/go-redis/v9
- 注意go mod tidy的时候不要import成了"github.com/go-redis/redis" 需要检查一下
数据持久化(RDB + AOF)
这个也是八股中很多的
Redis 的数据持久化通过 RDB (快照)和 AOF(追加日志)实现。Go 代码无需直接控制持久化(由 Redis 配置文件管理),但可以通过命令触发快照或检查持久化状态。
- RDB(Redis Database Backup) (案例)
- 原理:
- RDB 通过定期生成内存数据的快照(二进制文件,.rdb),保存到磁盘。
- 快照是某一时刻的完整数据副本,文件体积较小,恢复速度快。
- 触发方式:
- 自动触发:根据 save 配置(如 save 900 1 表示 900 秒内至少 1 次变更触发)。
- 手动触发 :
- SAVE:阻塞主线程,生成快照(生产慎用)。
- BGSAVE:后台异步生成快照,常用。
- 优点:恢复速度比AOF快文件紧凑,适合备份和快速恢复
- 缺点:可能丢失数据(两次快照间隔内的变更会丢失!!!),BGSAVE 需要 fork 进程,内存占用较高
- 适用场景 :
- 定期备份(如每天全量备份)。
- 对少量数据丢失可接受的场景(如缓存)。
RDB的配置文件 redis.config (示范)
- 原理:
bahs
save 900 1 # 900秒内至少1次变更触发快照
save 300 10 # 300秒内至少10次变更
save 60 10000 # 60秒内至少10000次变更 这几个同时生效保证不同场景
dir /var/redis # 快照文件存储路径
dbfilename dump.rdb # 快照文件名
触发快照的意思就是会进行一次更改更新(类似虚拟机的快照,但是这个直接覆盖)
- AOF(Append-Only File)
- 原理
- AOF 记录每次写操作(如 SET、DEL)到日志文件(.aof),每个写命令都会追加到 AOF 文件!!!类似数据库的 WAL(Write-Ahead Log)。
- 重启时,Redis 重放 AOF 文件重建数据
- 同步策略 (appendfsync)
- always:每次写操作同步到磁盘,数据最安全但性能最低。
- everysec:每秒同步,折中方案(最多丢 1 秒数据)。
- no:依赖操作系统同步,性能最高但数据丢失风险大。
- AOF重写
- AOF 文件会随写操作不断增长,占用磁盘空间。
- BGREWRITEAOF 合并冗余命令(如多次 INCR 合并为一个 SET),生成更小的 AOF 文件。
- 优点:数据可靠性高,支持增量记录,适合高一致性场景
- 缺点:文件体积较大恢复速度慢(需重放所有命令)。写频繁的时候性能开销高
- 适用场景 :
- 数据一致性要求高的场景(如订单、会话)。
- 与 RDB 结合使用,兼顾恢复速度和可靠性。
- 原理
AOF配置文件redis.config (示范)
bash
appendonly yes # 启用 AOF
appendfsync everysec # 每秒同步
dir /var/redis # AOF 文件存储路径
appendfilename appendonly.aof # AOF 文件名
auto-aof-rewrite-percentage 100 # AOF 文件增长100%时触发重写
auto-aof-rewrite-min-size 64mb # AOF 文件至少64MB时触发重写
redis-cli中手动输入可以出发RDB和AOF:
bash
BGSAVE # 手动触发快照
BGREWRITEAOF # 手动触发 AOF 重写
下面我们来实际操作一下
关于redis的启动(mac)
我的是mac,如果是homebrew启动的redis即通过指令 brew services start redis
那么即使执行 redis-cli shutdown
也无法关闭,homebrew启动的是类似全局的redis,需要使用 brew services start redis
进行关闭
bash
# 查看当前运行的 Redis 可以用于检查问题
ps aux | grep redi
# 快速检查端口
lsof -i :6379 # 如果没有被占用就没有输出(显示为错误输出但实际没有输出的)
redis-cli config get dir
获取存储对应的目录,如果是homebrew启动的一眼就能看出来
redis-cli config get dbfilename
获取对应的dbfilename
redis.conf的demo:(结合了RDB和AOF)
bash
save 900 1
save 300 10
dir ./redisstorage
dbfilename dump.rdb
appendonly yes
appendfsync everysec
appendfilename appendonly.aof
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
dir是目标存储文件夹,需要新建对应的文件夹
接着我们需要在当前文件夹的终端下执行 redis-server ./redis.conf
根据配置文件启动
运行GO
同样当前文件夹下,运行下面的go文件: (go run xxx.go)
GO
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)
func main() {
// 连接 Redis
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
ctx := context.Background()
// 测试连接
_, err := client.Ping(ctx).Result()
if err != nil {
log.Fatal("连接失败:", err)
}
fmt.Println("连接成功")
// 写入数据(触发 AOF 记录)
err = client.Set(ctx, "persistent_key", "critical_data", 0).Err()
if err != nil {
log.Fatal("设置失败:", err)
}
fmt.Println("设置 persistent_key = critical_data")
// 触发 RDB 快照
err = client.BgSave(ctx).Err()
if err != nil {
log.Fatal("触发 RDB 快照失败:", err)
}
fmt.Println("触发 RDB 快照")
// 触发 AOF 重写(优化 AOF 文件)
err = client.BgRewriteAOF(ctx).Err()
if err != nil {
log.Fatal("触发 AOF 重写失败:", err)
}
fmt.Println("触发 AOF 重写")
// 等待持久化完成
time.Sleep(2 * time.Second)
// 检查持久化状态
info, err := client.Info(ctx, "persistence").Result()
if err != nil {
log.Fatal("获取持久化信息失败:", err)
}
fmt.Println("持久化状态:\n", info)
// 验证数据
value, err := client.Get(ctx, "persistent_key").Result()
if err != nil {
log.Fatal("读取失败:", err)
}
fmt.Printf("读取 persistent_key = %s\n", value)
}
- 运行后可以发现redisstorage中有添加存储数据
redis-cli
进入redis服务get persistent_key
发现可以看到键对应的值结果critical_data- 然后可以通过Ctrl-c退出运行redis的终端,或者
redis-cli shutdown
关闭服务 - 接着再次
redis-server ./redis.conf
启动服务,可以发现get persistent_key
还是可以得到对应的结果,说明成功(如果不配置持久化的话,就是不写配置文件,redis也会有默认配置文件存储在运行redis的文件夹下 )
进阶
GO
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
)
// User 结构体,表示用户信息
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
// 连接 Redis
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
PoolSize: 10, // 连接池
MinIdleConns: 2,
})
ctx := context.Background()
// 测试连接
_, err := client.Ping(ctx).Result()
if err != nil {
log.Fatal("连接失败:", err)
}
fmt.Println("连接成功")
// 模拟用户数据
users := []User{
{ID: 1, Name: "Alice", Age: 25},
{ID: 2, Name: "Bob", Age: 30},
}
// 使用 Pipeline 批量写入缓存
pipe := client.Pipeline()
for _, user := range users {
key := fmt.Sprintf("user:%d", user.ID)
data, err := json.Marshal(user)
if err != nil {
log.Printf("序列化用户 %d 失败: %v", user.ID, err)
continue
}
// 设置缓存,TTL 1 小时
pipe.Set(ctx, key, data, time.Hour)
}
_, err = pipe.Exec(ctx)
if err != nil {
log.Fatal("批量写入失败:", err)
}
fmt.Println("批量写入用户缓存")
// 触发 RDB 快照
err = client.BgSave(ctx).Err()
if err != nil {
log.Fatal("触发 RDB 快照失败:", err)
}
fmt.Println("触发 RDB 快照")
// 检查 AOF 文件大小,决定是否重写
info, err := client.Info(ctx, "persistence").Result()
if err != nil {
log.Fatal("获取持久化信息失败:", err)
}
var aofSize int64
fmt.Sscanf(info, "aof_current_size:%d", &aofSize)
if aofSize > 64*1024*1024 { // 超过 64MB
err = client.BgRewriteAOF(ctx).Err()
if err != nil {
log.Fatal("触发 AOF 重写失败:", err)
}
fmt.Println("触发 AOF 重写")
} else {
fmt.Printf("AOF 文件大小: %d 字节,无需重写\n", aofSize)
}
// 等待持久化完成
time.Sleep(2 * time.Second)
// 读取并验证缓存
for _, user := range users {
key := fmt.Sprintf("user:%d", user.ID)
data, err := client.Get(ctx, key).Result()
if err != nil {
log.Printf("读取用户 %d 失败: %v", user.ID, err)
continue
}
var cachedUser User
err = json.Unmarshal([]byte(data), &cachedUser)
if err != nil {
log.Printf("反序列化用户 %d 失败: %v", user.ID, err)
continue
}
fmt.Printf("读取用户: %+v\n", cachedUser)
}
// 模拟缓存失效后从数据库加载
key := "user:1"
//手动删除缓存
client.Del(ctx, key)
// 现在尝试获取,会触发缓存缺失
_, err = client.Get(ctx, key).Result()
if err == redis.Nil {
fmt.Println("缓存缺失,模拟从数据库加载")
user := User{ID: 1, Name: "Alice", Age: 26}
data, _ := json.Marshal(user)
err = client.Set(ctx, key, data, time.Hour).Err()
if err != nil {
log.Fatal("重新缓存失败:", err)
}
fmt.Println("重新缓存用户 1")
}
}
大家可以新建一个自己拿这个尝试一下,多设置了AOF的自动更新,可以增加插入数据尝试
哨兵模式
哨兵模式(Sentinel)用于 Redis 高可用,监控主从节点,自动故障转移。Go 客户端通过 go-redis 的 FailoverClient 连接哨兵
新建sentinel.conf 文件:
bash
port 26379
sentinel monitor mymaster 127.0.0.1 6379 1
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 15000
- 设置端口为26379
- 设置监控的主节点,以及只用一个哨兵就可以完成选举(多配置哨兵可以实现高可用)
- 主节点响应超过 5000 毫秒(5秒)就认为主观下线
- 障转移超时时间为 15000 毫秒(15秒),时间内没完成转移认为转移失败
准备好GO文件:
GO
package main
import (
"context"
"fmt"
"log"
"github.com/redis/go-redis/v9"
)
func main() {
// 连接哨兵
client := redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: "mymaster", // 哨兵监控的主节点名称
SentinelAddrs: []string{"localhost:26379"}, // 哨兵地址
})
ctx := context.Background()
// 测试连接
_, err := client.Ping(ctx).Result()
if err != nil {
log.Fatal("连接失败:", err)
}
fmt.Println("哨兵模式连接成功")
// 写入数据
err = client.Set(ctx, "sentinel_key", "high_availability", 0).Err()
if err != nil {
log.Fatal("设置失败:", err)
}
fmt.Println("设置 sentinel_key = high_availability")
// 读取数据
value, err := client.Get(ctx, "sentinel_key").Result()
if err != nil {
log.Fatal("读取失败:", err)
}
fmt.Printf("读取 sentinel_key = %s\n", value)
// 模拟主节点故障(手动停止主节点)
fmt.Println("请手动停止主节点(6379),然后再次读取")
// 等待用户操作
// 假设主节点故障,哨兵自动切换到从节点
time.Sleep(10 * time.Second)
// 再次读取,验证故障转移
value, err = client.Get(ctx, "sentinel_key").Result()
if err != nil {
log.Fatal("故障转移后读取失败:", err)
}
fmt.Printf("故障转移后读取 sentinel_key = %s\n", value)
}
需要开启多个终端:
配置主节点redis-server --port 6379
配置从节点:redis-server --port 6380 --replicaof 127.0.0.1 6379
启动哨兵:redis-sentinel ./sentinel.conf
运行GO文件:go run xxx.go
接着自己手动关闭主节点的redis服务,等待后可以发现故障转移后可以读取!
-
通过redis-cli验证
redis-cli -p 26379
SENTINEL get-master-addr-by-name mymaster # 查看当前主节点输出:127.0.0.1 6379
停止主节点(另一个终端)
redis-cli -p 6379 SHUTDOWN
再次检查主节点(哨兵应切换到 6380)
SENTINEL get-master-addr-by-name mymaster
输出:127.0.0.1 6380
还有集群+多哨兵等,就需要启动多个终端,在此先不拓展