go-redis使用
# 1. go-redis
go-redis使用入门 - 掘金 (opens new window)
# 安装
Go 社区中目前有很多成熟的 redis client 库,比如[https://github.com/gomodule/redigo (opens new window) 和https://github.com/go-redis/redis (opens new window),读者可以自行选择适合自己的库。本书使用 go-redis 这个库来操作 Redis 数据库。
使用以下命令下安装 go-redis 库。
go get github.com/go-redis/redis
# 连接
# 普通连接模式
go-redis 库中使用 redis.NewClient 函数连接 Redis 服务器。
package main
import (
"fmt"
"github.com/go-redis/redis"
)
func ConnRedis() (rds *redis.Client, err error) {
rds = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // 密码
DB: 0, // 数据库
PoolSize: 20, // 连接池大小
})
_, err = rds.Ping().Result()
if err != nil {
fmt.Println("连接失败")
}
return rds, nil
}
func main() {
connRedis, err := ConnRedis()
if err != nil {
return
}
defer connRedis.Close()
fmt.Println(connRedis)
}
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
除此之外,还可以使用 redis.ParseURL 函数从表示数据源的字符串中解析得到 Redis 服务器的配置信息。
opt, err := redis.ParseURL("redis://<user>:<pass>@localhost:6379/<db>")
if err != nil {
panic(err)
}
rdb := redis.NewClient(opt)
2
3
4
5
# TLS连接模式
如果使用的是 TLS 连接方式,则需要使用 tls.Config 配置。
rdb := redis.NewClient(&redis.Options{
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
// Certificates: []tls.Certificate{cert},
// ServerName: "your.domain.com",
},
})
2
3
4
5
6
7
# Redis Sentinel模式
使用下面的命令连接到由 Redis Sentinel 管理的 Redis 服务器。
rdb := redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: "master-name",
SentinelAddrs: []string{":9126", ":9127", ":9128"},
})
2
3
4
# Redis Cluster模式
使用下面的命令连接到 Redis Cluster,go-redis 支持按延迟或随机路由命令。
rdb := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},
// 若要根据延迟或随机路由命令,请启用以下命令之一
// RouteByLatency: true,
// RouteRandomly: true,
})
2
3
4
5
6
7
# 基本使用
# 执行命令
下面的示例代码演示了 go-redis 库的基本使用。
// doCommand go-redis基本使用示例
func doCommand(rdb *redis.Client) {
//ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
//defer cancel()
// 执行命令获取结果
val, err := rdb.Get("key").Result()
fmt.Println(val, err)
// 先获取到命令对象
cmder := rdb.Get("key")
fmt.Println(cmder.Val()) // 获取值
fmt.Println(cmder.Err()) // 获取错误
// 直接执行命令获取错误
err = rdb.Set("key", 10, time.Hour).Err()
// 直接执行命令获取值
value := rdb.Get("key").Val()
fmt.Println(value)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 执行任意命令
go-redis 还提供了一个执行任意命令或自定义命令的 Do 方法,特别是一些 go-redis 库暂时不支持的命令都可以使用该方法执行。具体使用方法如下。
// doDemo rdb.Do 方法使用示例
func doDemo(rdb *redis.Client) {
//ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
//defer cancel()
// 直接执行命令获取错误
err := rdb.Do("set", "key", 10, "EX", 3600).Err()
fmt.Println(err)
// 执行命令获取结果
val, err := rdb.Do("get", "key").Result()
fmt.Println(val, err)
}
2
3
4
5
6
7
8
9
10
11
12
13
# redis.Nil
go-redis 库提供了一个 redis.Nil 错误来表示 Key 不存在的错误。因此在使用 go-redis 时需要注意对返回错误的判断。在某些场景下我们应该区别处理 redis.Nil 和其他不为 nil 的错误。
// getValueFromRedis redis.Nil判断
func getValueFromRedis(key, defaultValue string, rdb *redis.Client) (string, error) {
//ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
//defer cancel()
val, err := rdb.Get(key).Result()
if err != nil {
// 如果返回的错误是key不存在
if errors.Is(err, redis.Nil) {
return defaultValue, nil
}
// 出其他错了
return "", err
}
return val, nil
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 其他示例
# zset示例
下面的示例代码演示了如何使用 go-redis 库操作 zset。
// zsetDemo 操作zset示例
func zsetDemo(rdb *redis.Client) {
// key
zsetKey := "language_rank"
// value
languages := []redis.Z{
{Score: 90.0, Member: "Golang"},
{Score: 98.0, Member: "Java"},
{Score: 95.0, Member: "Python"},
{Score: 97.0, Member: "JavaScript"},
{Score: 99.0, Member: "C/C++"},
}
//ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
//defer cancel()
// ZADD
err := rdb.ZAdd(zsetKey, languages...).Err()
if err != nil {
fmt.Printf("zadd failed, err:%v\n", err)
return
}
fmt.Println("zadd success")
// 把Golang的分数加10
newScore, err := rdb.ZIncrBy(zsetKey, 10.0, "Golang").Result()
if err != nil {
fmt.Printf("zincrby failed, err:%v\n", err)
return
}
fmt.Printf("Golang's score is %f now.\n", newScore)
// 取分数最高的3个
ret := rdb.ZRevRangeWithScores(zsetKey, 0, 2).Val()
for _, z := range ret {
fmt.Println(z.Member, z.Score)
}
// 取95~100分的
op := &redis.ZRangeBy{
Min: "95",
Max: "100",
}
ret, err = rdb.ZRangeByScoreWithScores(zsetKey, *op).Result()
if err != nil {
fmt.Printf("zrangebyscore failed, err:%v\n", err)
return
}
for _, z := range ret {
fmt.Println(z.Member, z.Score)
}
}
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
执行上面的函数将得到如下输出结果。
zadd success
Golang's score is 100.000000 now.
Golang 100
C/C++ 99
Java 98
Python 95
JavaScript 97
Java 98
C/C++ 99
Golang 100
2
3
4
5
6
7
8
9
10
# 扫描或遍历所有key
你可以使用KEYS prefix:
命令按前缀获取所有 key。
vals, err := rdb.Keys(ctx, "prefix*").Result()
2
但是如果需要扫描数百万的 key ,那速度就会比较慢。这种场景下你可以使用Scan 命令来遍历所有符合要求的 key。
// scanKeysDemo1 按前缀查找所有key示例
func scanKeysDemo1(rdb *redis.Client) {
//ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
//defer cancel()
var cursor uint64
for {
var keys []string
var err error
// 按前缀扫描key
keys, cursor, err = rdb.Scan(cursor, "prefix:*", 0).Result()
if err != nil {
panic(err)
}
for _, key := range keys {
fmt.Println("key", key)
}
if cursor == 0 { // no more keys
break
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Go-redis 允许将上面的代码简化为如下示例。
// scanKeysDemo2 按前缀扫描key示例
func scanKeysDemo2() {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
// 按前缀扫描key
iter := rdb.Scan(ctx, 0, "prefix:*", 0).Iterator()
for iter.Next(ctx) {
fmt.Println("keys", iter.Val())
}
if err := iter.Err(); err != nil {
panic(err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
例如,我们可以写出一个将所有匹配指定模式的 key 删除的示例。
// delKeysByMatch 按match格式扫描所有key并删除
func delKeysByMatch(match string, timeout time.Duration) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
iter := rdb.Scan(ctx, 0, match, 0).Iterator()
for iter.Next(ctx) {
err := rdb.Del(ctx, iter.Val()).Err()
if err != nil {
panic(err)
}
}
if err := iter.Err(); err != nil {
panic(err)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
此外,对于 Redis 中的 set、hash、zset 数据类型,go-redis 也支持类似的遍历方法。
iter := rdb.SScan(ctx, "set-key", 0, "prefix:*", 0).Iterator()
iter := rdb.HScan(ctx, "hash-key", 0, "prefix:*", 0).Iterator()
iter := rdb.ZScan(ctx, "sorted-hash-key", 0, "prefix:*", 0).Iterator(
2
3
4
# Pipeline
Redis Pipeline 允许通过使用单个 client-server-client 往返执行多个命令来提高性能。区别于一个接一个地执行100个命令,你可以将这些命令放入 pipeline 中,然后使用1次读写操作像执行单个命令一样执行它们。这样做的好处是节省了执行命令的网络往返时间(RTT)。
y在下面的示例代码中演示了使用 pipeline 通过一个 write + read 操作来执行多个命令。
pipe := rdb.Pipeline()
incr := pipe.Incr(ctx, "pipeline_counter")
pipe.Expire(ctx, "pipeline_counter", time.Hour)
cmds, err := pipe.Exec(ctx)
if err != nil {
panic(err)
}
// 在执行pipe.Exec之后才能获取到结果
fmt.Println(incr.Val())
2
3
4
5
6
7
8
9
10
11
12
13
上面的代码相当于将以下两个命令一次发给 Redis Server 端执行,与不使用 Pipeline 相比能减少一次RTT。
INCR pipeline_counter
EXPIRE pipeline_counts 3600
2
3
或者,你也可以使用Pipelined
方法,它会在函数退出时调用 Exec。
var incr *redis.IntCmd
cmds, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
incr = pipe.Incr(ctx, "pipelined_counter")
pipe.Expire(ctx, "pipelined_counter", time.Hour)
return nil
})
if err != nil {
panic(err)
}
// 在pipeline执行后获取到结果
fmt.Println(incr.Val())
2
3
4
5
6
7
8
9
10
11
12
13
14
我们可以遍历 pipeline 命令的返回值依次获取每个命令的结果。下方的示例代码中使用pipiline一次执行了100个 Get 命令,在pipeline 执行后遍历取出100个命令的执行结果。
cmds, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
for i := 0; i < 100; i++ {
pipe.Get(ctx, fmt.Sprintf("key%d", i))
}
return nil
})
if err != nil {
panic(err)
}
for _, cmd := range cmds {
fmt.Println(cmd.(*redis.StringCmd).Val())
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在那些我们需要一次性执行多个命令的场景下,就可以考虑使用 pipeline 来优化。
# 事务
Redis 是单线程执行命令的,因此单个命令始终是原子的,但是来自不同客户端的两个给定命令可以依次执行,例如在它们之间交替执行。但是,Multi/exec
能够确保在multi/exec
两个语句之间的命令之间没有其他客户端正在执行命令。