golang操作redis

chihqiang chihqiang 2025-10-09 约 23 分钟 9084 字

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

亮点满满:

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

安装

go get github.com/redis/go-redis/v9
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

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

## 连接 Redis

~~~yaml
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 常用操作速成 ~~~bash // 写入键值 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)
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
86
87
88
89
90
91

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

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

~~~yaml
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 + 分布式锁防超卖 ~~~javascript 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) } }
1
2
3
4
5
6
7
8

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

**🔐 分布式锁流程图**:

```bash
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 队列应用场景 ~~~yaml 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) } }
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

核心思路

- 利用 **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`:控制连接建立和读写超时,防止请求阻塞。

```go
// 初始化 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:1`、`user:123:2`…… - 避免大量请求集中到同一个 Key,缓解 Redis 单点压力。 2. **结合本地缓存** - 使用应用端缓存(如 `freecache`、`ristretto`)存储热点数据,先读本地缓存再回退到 Redis。 - 减少对 Redis 的请求频率,提高系统吞吐量和响应速度。
标签: redis
分享: Twitter Facebook
chihqiang

chihqiang

代码与生活的点滴记录