当前位置: 首页 > news >正文

案例速成GO+redis 个人笔记

更多个人笔记:(仅供参考,非盈利)
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 (示范)
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 (示范)

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:

BGSAVE # 手动触发快照
BGREWRITEAOF # 手动触发 AOF 重写

下面我们来实际操作一下

关于redis的启动(mac)

我的是mac,如果是homebrew启动的redis即通过指令 brew services start redis 那么即使执行 redis-cli shutdown 也无法关闭,homebrew启动的是类似全局的redis,需要使用 brew services start redis 进行关闭

# 查看当前运行的 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)

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)

package mainimport ("context""fmt""log""time""github.com/redis/go-redis/v9"
)func main() {// 连接 Redisclient := 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的文件夹下 )

进阶

package mainimport ("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() {// 连接 Redisclient := 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 int64fmt.Sscanf(info, "aof_current_size:%d", &aofSize)if aofSize > 64*1024*1024 { // 超过 64MBerr = 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 Usererr = 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 文件:

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文件:

package mainimport ("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

还有集群+多哨兵等,就需要启动多个终端,在此先不拓展

相关文章:

  • Llama factory如何全参数微调 Qwen2.5-7B-Instruct 模型并导入Ollama推理(详细版)
  • spark总结
  • uniapp开发04-scroll-view组件的简单案例
  • 启动命令汇总(Redis / Kafka / Flume / Spark)
  • DIFY 浅尝 - Dify + Ollama 抓取BBC新闻
  • Java学习手册:常用的内置工具类包
  • 云原生--核心组件-容器篇-3-Docker三大核心之--镜像
  • elk中kibana一直处于可用和降级之间且es群集状态并没有问题的解决方法
  • 从 Vue 到 React:React 合成事件
  • 使用 AFL++ 对 IoT 二进制文件进行模糊测试 - 第一部分
  • Linux之netlink(2)libnl使用介绍(1)
  • Redis 数据类型全览:特性、场景与操作实例
  • 【Hive入门】Hive动态分区与静态分区:使用场景与性能对比完全指南
  • 游戏引擎学习第245天:wglChoosePixelFormatARB
  • 写入cache时数据格式错误产生的ERRO导致整个测试框架无法运行
  • PID程序实现
  • php一些命名规范 和 css命名规范
  • AIGC在自动化测试领域的创新应用:智能生成测试用例与缺陷预测
  • SpringCloud原理和机制
  • 产销协同的作用是什么?又如何对各部门发挥作用?
  • 吕国范任河南省人民政府副省长
  • 马上评丨学生举报食堂饭菜有蛆,教育局应该护谁的犊子
  • “富卫保险冠军赛马日”创双纪录,打造赛马旅游盛宴,印证香港联通国际优势
  • 榆林市委常委王华胜已任榆林市政协党组书记
  • 广西给出最后期限:6月30日之前主动交代问题可从宽处理
  • 三亚一景区发生游客溺亡事件,官方通报:排除他杀