golang操作redis

2025-10-09 golang redis

目前 Go 生态里,最炙手可热的 Redis 客户端非 go-redis 莫属。

亮点满满:

  • 原生 Go 实现,性能杠杠的,跑得飞快
  • 内置连接池,高并发也不怵
  • 全面特性支持:集群、哨兵、管道、事务,应有尽有

安装

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

快速上手:Go 与 Redis 来一场高效约会

连接 Redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)

var ctx = context.Background()

func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password
DB: 0, // use default DB
})
_, err := rdb.Ping(ctx).Result()
if err != nil {
panic(err)
}
fmt.Println("Redis 连接成功!")
}

手到擒来:Redis 常用操作速成

1
2
3
4
5
6
7
8
9
// 写入键值
rdb.Set(ctx, "username", "chihqiang", 0)
// 读取键值
val, _ := rdb.Get(ctx, "username").Result()
fmt.Println("username:", val)
// 自增计数器
rdb.Incr(ctx, "counter")
// 设置过期时间
rdb.Set(ctx, "token:chihqiang", "123456", time.Minute)

实战演练:Redis 典型业务场景全揭秘

缓存掌控术:Cache Aside 模式详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
package main

import (
"context"
"log"
"time"

"github.com/redis/go-redis/v9"
)

var ctx = context.Background()

// GetUserUnified 一个方法搞定单个/批量获取和缓存刷新
// ids: 用户 ID 列表
// refresh: 是否强制刷新缓存
// dbQuery: 数据库查询函数,外部传入
func GetUserUnified(rdb *redis.Client, ids []string, refresh bool, dbQuery func(id string) string) (map[string]string, error) {
result := make(map[string]string)

for _, id := range ids {
key := "user:" + id
var user string
var err error

if !refresh {
// 尝试从缓存获取
user, err = rdb.Get(ctx, key).Result()
if err == nil {
log.Println("缓存命中,直接返回:", key)
result[id] = user
continue
}
if err != redis.Nil {
log.Println("Redis 出错:", err)
continue
}
log.Println("缓存未命中,开始从数据库查询:", key)
} else {
log.Println("强制刷新缓存:", key)
}

// 从数据库查询(调用外部传入的函数)
user = dbQuery(id)

// 回写缓存
if err := rdb.Set(ctx, key, user, 10*time.Minute).Err(); err != nil {
log.Println("缓存回写失败:", key, err)
} else {
log.Println("缓存已回写 Redis:", key)
}

result[id] = user
}

return result, nil
}

func main() {
// 初始化 Redis 客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})

// 自定义数据库查询函数
dbQuery := func(id string) string {
// 模拟查询数据库
return "db_user_" + id
}

// 单个用户获取
userMap, _ := GetUserUnified(rdb, []string{"1001"}, false, dbQuery)
log.Println("用户 1001:", userMap["1001"])

// 批量用户获取
ids := []string{"1001", "1002", "1003"}
users, _ := GetUserUnified(rdb, ids, false, dbQuery)
log.Println("批量用户:", users)

// 强制刷新缓存
usersRefresh, _ := GetUserUnified(rdb, []string{"1002"}, true, dbQuery)
log.Println("刷新缓存后的用户 1002:", usersRefresh["1002"])
}

核心思路

Cache Aside(旁路缓存)模式

  1. 先尝试从缓存获取数据。
  2. 缓存未命中 → 回源数据库查询。
  3. 查询到数据库数据后回写缓存(设置过期时间)。
  4. 返回最终结果。

锁定风险:Cache Aside + 分布式锁防超卖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import (
"context"
"log"
"time"

"github.com/redis/go-redis/v9"
)

var ctx = context.Background()
var rdb *redis.Client // 假设已初始化 Redis 客户端

// AcquireLock 尝试获取分布式锁
func AcquireLock(key, value string, ttl time.Duration) bool {
ok, err := rdb.SetNX(ctx, key, value, ttl).Result()
if err != nil {
log.Println("获取锁出错:", err)
return false
}
if ok {
log.Println("锁获取成功:", key)
} else {
log.Println("锁获取失败,已被占用:", key)
}
return ok
}

// ReleaseLock 释放分布式锁
func ReleaseLock(key, value string) {
luaScript := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
res, err := rdb.Eval(ctx, luaScript, []string{key}, value).Result()
if err != nil {
log.Println("释放锁出错:", err)
return
}
if res.(int64) > 0 {
log.Println("锁已释放:", key)
} else {
log.Println("未释放锁(可能不是自己的锁):", key)
}
}

👉 高并发护盾:通过 SETNX + EXPIRE 或 Redlock 算法,轻松防止多个服务同时操作同一资源。

🔐 分布式锁流程图

1
2
Client A 尝试获取锁 → SETNX 成功 → 执行业务 → 释放锁
Client B 尝试获取锁 → SETNX 失败 → 等待或重试

分数风云榜:Redis 排行榜典型业务场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"context"
"fmt"
"log"

"github.com/redis/go-redis/v9"
)

var ctx = context.Background()

func main() {
// 初始化 Redis 客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
DB: 0,
})

// 🌟 添加玩家分数到排行榜(ZSet)
players := []struct {
name string
score float64
}{
{"Alice", 100},
{"Bob", 200},
{"Charlie", 150},
}

for _, p := range players {
if err := rdb.ZAdd(ctx, "rank", redis.Z{Score: p.score, Member: p.name}).Err(); err != nil {
log.Println("添加分数失败:", p.name, err)
} else {
log.Printf("已添加分数: %s -> %f\n", p.name, p.score)
}
}

// 🏆 获取排行榜前 10
res, err := rdb.ZRevRangeWithScores(ctx, "rank", 0, 9).Result()
if err != nil {
log.Println("获取排行榜失败:", err)
return
}

fmt.Println("🏅 当前排行榜前 10 名:")
for i, z := range res {
fmt.Printf("第%d名: %s - %.0f 分\n", i+1, z.Member, z.Score)
}
}

核心思路

  • 使用 Redis 有序集合(ZSet) 来存储玩家和分数。
  • Score 字段表示分数,Member 字段表示玩家名称。
  • 利用 ZSet 的 自动排序 特性,可以快速获取排行榜前 N 名。

小巧消息队列:Redis 队列应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
"context"
"fmt"
"log"

"github.com/redis/go-redis/v9"
)

var ctx = context.Background()

func main() {
// 初始化 Redis 客户端
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
DB: 0,
})

// 🌟 生产者:往队列中推送任务
tasks := []string{"task1", "task2", "task3"}
for _, t := range tasks {
if err := rdb.LPush(ctx, "queue", t).Err(); err != nil {
log.Println("推送任务失败:", t, err)
} else {
log.Println("已推送任务到队列:", t)
}
}

// 🏃 消费者(阻塞式)
log.Println("开始消费队列任务...")
for {
// BRPop 阻塞式弹出队列任务,队列为空时等待
result, err := rdb.BRPop(ctx, 0, "queue").Result()
if err != nil {
log.Println("消费任务失败:", err)
continue
}
// result[0] 是队列名,result[1] 是任务内容
task := result[1]
fmt.Println("消费任务:", task)
}
}

核心思路

  • 利用 Redis 列表(List) 实现消息队列,支持生产者-消费者模型。
  • LPUSH:生产者向队列头部添加任务。
  • BRPOP:消费者从队列尾部阻塞式获取任务,队列为空时自动等待。

性能飞跃:Redis 优化技巧与最佳实践

合理设置过期时间,避免缓存雪崩

  1. 使用随机过期时间(错峰过期)
    • 给缓存设置略微不同的 TTL,避免大量 key 同时过期导致瞬间流量打到数据库。
    • 示例:10min + rand(0~5min)
  2. 结合本地缓存
    • 在应用端使用本地缓存(如 Go map、LRU)保存热点数据,减少对 Redis 的瞬时压力。
    • 热点数据先从本地缓存读取,再回退到 Redis,最后查数据库。

使用连接池,提升 Redis 性能

  • Go-Redis 默认内置连接池
    • 自动管理连接复用,支持高并发请求。
  • 常见配置项
    1. PoolSize:连接池最大连接数,默认根据 CPU 核心数调整。
    2. MinIdleConns:最小空闲连接数,保证请求高峰时有可用连接。
    3. IdleTimeout:空闲连接超时时间,超过则关闭。
    4. DialTimeout / ReadTimeout / WriteTimeout:控制连接建立和读写超时,防止请求阻塞。
1
2
3
4
5
6
7
8
9
10
11
// 初始化 Redis 客户端(带连接池配置)
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 地址
Password: "", // Redis 密码(如有)
DB: 0, // 使用的数据库
PoolSize: 50, // 最大连接数,根据并发量调整
MinIdleConns: 10, // 最小空闲连接,保证高峰期有可用连接
DialTimeout: 5 * time.Second, // 建立连接超时
ReadTimeout: 3 * time.Second, // 读操作超时
WriteTimeout: 3 * time.Second, // 写操作超时
})

批量操作:使用 Pipeline 或 MGet 减少 RTT

  1. Pipeline(管道)
    • 可以一次性发送多条命令,Redis 批量执行,减少网络往返次数(RTT)。
    • 适合批量写入或更新操作。
  2. MGet / MSet
    • 批量获取或设置多个 key,一次请求返回多条结果,避免多次单独请求。
    • 适合批量读取缓存数据,提高吞吐量。
1
2
3
4
5
pipe := rdb.Pipeline()
for i := 0; i < 1000; i++ {
pipe.Set(ctx, fmt.Sprintf("key%d", i), i, 0)
}
_, err := pipe.Exec(ctx)

热点 Key 处理技巧

  1. 给 Key 加随机后缀分片
    • 将高频访问的热点 Key 拆分为多个分片,例如 user:123:1user:123:2……
    • 避免大量请求集中到同一个 Key,缓解 Redis 单点压力。
  2. 结合本地缓存
    • 使用应用端缓存(如 freecacheristretto)存储热点数据,先读本地缓存再回退到 Redis。
    • 减少对 Redis 的请求频率,提高系统吞吐量和响应速度。