最近上手go,使用go-redis库时因某些原因产生疑惑,感触良多。
特总结此文,希望能加深印象。
此文所述go-redis是github.com/go-redis/redis/v8
,使用之前要自行引入。
redis连接池
redis连接池,顾名思义,一个池子里面放着很多redis连接。
这里就引出了一个问题,为什么在go-redis和redisgo两个库都要引入连接池呢?直接单个连接难道不香吗?
单线程和执行命令都不是redis的性能瓶颈,通信才是其瓶颈。因为redis每秒可执行10万次,假设使用单个连接将100条命令传输给redis服务,命令传输需要50ms时间,命令执行却只需要的时间1ms,剩余的49ms时间就只有闲置等到之后的命令来才继续执行。
所以为了更好利用redis的性能,引入redis连接池也就不难理解了。
go-redis连接池和单个连接使用
该怎么使用go-redis连接池和go-redis的单个连接呢?此处以简单demo为例,千万别搬运,至于原因看完文章你就知道了
使用连接池
opts := redis.Options{
Addr: address, //redis地址
Password: password, //密码
DialTimeout: time.Second * 5, //超时时间 默认5秒
DB: 0, // 默认使用0号库
//闲置连接检查包括IdleTimeout,MaxConnAge
IdleCheckFrequency: 500 * time.Millisecond, //闲置连接检查的周期,默认为1分钟,-1表示不做周期性检查,只在客户端获取连接时对闲置连接进行处理。
IdleTimeout: 500 * time.Millisecond, //闲置超时,默认5分钟,-1表示取消闲置超时检查
MaxConnAge: 0 * time.Second, //连接存活时长,从创建开始计时,超过指定时长则关闭连接,默认为0,即不关闭存活时长较长的连接
//钩子函数
OnConnect: func(ctx context.Context, cn *redis.Conn) error { //仅当客户端执行命令时需要从连接池获取连接时,如果连接池需要新建连接时则会调用此钩子函数
log.Printf("从连接池获取并新建连接conn=%v\n", cn)
return nil
},
}
client = redis.NewClient(&opts) //创建连接
// 关闭连接池
defer client.Close()
// 心跳测试
if err := client.Ping(context.Background()).Err(); err != nil {
log.Println("redis客户端连接失败,err:", err)
return nil
}
使用单个连接
此处为官方文档示例代码
cn := rdb.Conn(ctx)
defer cn.Close()
if err := cn.ClientSetName(ctx, "myclient").Err(); err != nil {
panic(err)
}
name, err := cn.ClientGetName(ctx).Result()
if err != nil {
panic(err)
}
fmt.Println("client name", name)
当然,值得注意的是,go-redis官方文档对单个连接特地说了这样一句话。
Conn represents a single Redis connection rather than a pool of connections. Prefer running commands from Client unless there is a specific need for a continuous single Redis connection.
我的英文不太好,但是配合机器翻译过来大概是这么个意思:Conn 表示单个 Redis 连接,而不是连接池。推荐从Client(也就是连接池)运行命令,除非对连续的单个 Redis 连接有特定需求。
了解完基本使用之后,新的问题又来了。该怎么证明或知道Client实例内部存在连接池呢?该怎么确定Client实例是自动管理连接呢?
进一步了解go-redis连接池
我第一次决定用go-redis时,就特地跑去看了官方文档,官方文档都说连接池是自动管理了,我依旧保持眼不见即不为实的态度,想更加明确和确认官方文档说的是真的。
虽然我上手go时间不长,但是我依旧首选想到是阅读源码,借助ctrl点击Client进去之后大概看了很久,看到pool(连接池)的相关逻辑之后,我眼见为实了。
其实是有大佬对里面的源码进行过很详细的分析的,有兴趣的可以自己搜索引擎解决或留言。
当然,有很多小白看不懂源码的,就连我看其实也不是很轻松。这个时候问题又来了,该怎么知道go-redis连接池什么时候建立和销毁连接呢?
别急,我们先将连接池的使用封装起来测试,出于学习和示例目的简单封装并更改里面的连接池相关参数,让很多东西变得灵敏,但是生产环境极度不建议不懂瞎改。
redis.go简单封装
package redis
import (
"context"
"github.com/go-redis/redis/v8"
"log"
"time"
)
// go-redis 自带连接池 自动管理
var client *redis.Client
func InitClient(address, password string) *redis.Client {
opts := redis.Options{
Addr: address, //redis地址
Password: password, //密码
DialTimeout: time.Second * 5, //超时时间 默认5秒
DB: 0, // 默认使用0号库
//闲置连接检查包括IdleTimeout,MaxConnAge
IdleCheckFrequency: 500 * time.Millisecond, //闲置连接检查的周期,默认为1分钟,-1表示不做周期性检查,只在客户端获取连接时对闲置连接进行处理。
IdleTimeout: 500 * time.Millisecond, //闲置超时,默认5分钟,-1表示取消闲置超时检查
MaxConnAge: 0 * time.Second, //连接存活时长,从创建开始计时,超过指定时长则关闭连接,默认为0,即不关闭存活时长较长的连接
//钩子函数
OnConnect: func(ctx context.Context, cn *redis.Conn) error { //仅当客户端执行命令时需要从连接池获取连接时,如果连接池需要新建连接时则会调用此钩子函数
log.Printf("从连接池获取并新建连接conn=%v\n", cn)
return nil
},
}
client = redis.NewClient(&opts) //创建连接
// 心跳测试
if err := client.Ping(context.Background()).Err(); err != nil {
log.Println("redis客户端连接失败,err:", err)
return nil
}
return client
}
// Close 关闭Redis 连同连接池一起关闭
func Close() error {
// 会将连接池一同关闭
return client.Close()
}
封装完了,我们总需要调用,所以main.go少不了。
main.go
package main
import (
"redis"
"context"
Redis "github.com/go-redis/redis/v8"
"log"
"math/rand"
"os"
"os/signal"
"syscall"
"time"
)
var rdb *Redis.Client
var close bool
func main() {
// 初始化读取配置 伪代码 自行去获取你的配置传入
redisConfig := getMyRedisConfig()
// 测试哟
rdb = redis.InitClient(redisConfig.Address, redisConfig.Password)
defer func(rdb *Redis.Client) {
err := rdb.Close()
log.Println("关闭redis连接池")
if err != nil {
log.Println("关闭redis连接池失败")
}
}(rdb)
go doPing()
go doPing()
go doPing()
go doPing()
go doPing()
go doPing()
go doPing()
go doPing()
// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
quit := make(chan os.Signal)
// 当用户终端输入退出时 捕捉除了kill-9的所有中断信号
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
<-quit
log.Println("优雅退出")
close = true
for i := 0; i < 5; i++ {
log.Println("goroutine关闭后")
// 连接池总数
log.Println("连接池内连接总数:", rdb.PoolStats().TotalConns)
// 连接池空闲数量
log.Println("连接池内空闲连接数量:", rdb.PoolStats().IdleConns)
time.Sleep(time.Millisecond * time.Duration(getRandInt()))
}
}
// 循环ping
func doPing() {
for {
if close {
break
}
ping, err := rdb.Ping(context.Background()).Result()
if err != nil {
log.Println("ping failed", err)
}
log.Println(ping)
// 连接池总数
log.Println("连接池内连接总数:", rdb.PoolStats().TotalConns)
// 连接池空闲数量
log.Println("连接池内空闲连接数量:", rdb.PoolStats().IdleConns)
time.Sleep(time.Millisecond * time.Duration(getRandInt()))
}
log.Println("doPing over")
}
// 获取随机300-500之间的整数
func getRandInt() int {
// 我们一般使用系统时间的不确定性来进行初始化
rand.Seed(time.Now().Unix())
return rand.Intn(500-300) + 300
}
在开始运行程序之前,需要简单介绍go-redis提供的获取连接池内连接总数的方法rdb.PoolStats().TotalConns
,以及获取连接池内空闲连接数量rdb.PoolStats().IdleConns
。
还有当实例化连接池时IdleCheckFrequency,IdleTimeout
这两个参数如果时间设置太长,就很难在短时间观察到连接池内连接的新建和销毁状况。
每次从连接池获取连接执行命令并且连接池需要新建连接时会走钩子OnConnect
,详细可以看上面redis.go的注释。
验证程序输出结果
redis.go封装的是创建redis连接池,main.go主要负责调用连接池实例执行命令,同时我开启多个goroutine(go协程)同时循环去执行redis的ping。随机时间只是为了让连接池变化方便我们观测。
执行程序之后,输出结果如下:
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 1
2022/11/01 12:02:52 连接池内空闲连接数量: 1
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 2
2022/11/01 12:02:52 连接池内空闲连接数量: 1
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 2
2022/11/01 12:02:52 连接池内空闲连接数量: 2
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 6
2022/11/01 12:02:52 连接池内空闲连接数量: 3
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 6
2022/11/01 12:02:52 连接池内空闲连接数量: 6
2022/11/01 12:02:52 连接池内连接总数: 6
2022/11/01 12:02:52 连接池内空闲连接数量: 6
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 7
2022/11/01 12:02:52 连接池内空闲连接数量: 6
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 7
2022/11/01 12:02:52 连接池内空闲连接数量: 7
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 1
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 3
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 3
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 5
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 5
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 7
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 8
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 1
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 4
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 6
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 7
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 7
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 8
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 8
2022/11/01 12:02:52 PONG
2022/11/01 12:02:52 连接池内连接总数: 8
2022/11/01 12:02:52 连接池内空闲连接数量: 8
2022/11/01 12:02:53 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:53 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:53 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:53 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:53 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:53 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:53 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 7
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 4
2022/11/01 12:02:53 连接池内空闲连接数量: 2
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 6
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 6
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 6
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 7
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 7
2022/11/01 12:02:53 从连接池获取并新建连接conn=Redis<redis:6379 db:0>
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 8
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 1
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 8
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 4
2022/11/01 12:02:53 连接池内空闲连接数量: 5
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 8
2022/11/01 12:02:53 PONG
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 8
^C2022/11/01 12:02:53 优雅退出
2022/11/01 12:02:53 goroutine关闭后
2022/11/01 12:02:53 连接池内连接总数: 8
2022/11/01 12:02:53 连接池内空闲连接数量: 8
2022/11/01 12:02:53 doPing over
2022/11/01 12:02:53 doPing over
2022/11/01 12:02:53 doPing over
2022/11/01 12:02:53 doPing over
2022/11/01 12:02:53 doPing over
2022/11/01 12:02:53 doPing over
2022/11/01 12:02:53 doPing over
2022/11/01 12:02:53 doPing over
2022/11/01 12:02:53 goroutine关闭后
2022/11/01 12:02:53 连接池内连接总数: 0
2022/11/01 12:02:53 连接池内空闲连接数量: 0
2022/11/01 12:02:54 goroutine关闭后
2022/11/01 12:02:54 连接池内连接总数: 0
2022/11/01 12:02:54 连接池内空闲连接数量: 0
2022/11/01 12:02:54 goroutine关闭后
2022/11/01 12:02:54 连接池内连接总数: 0
2022/11/01 12:02:54 连接池内空闲连接数量: 0
2022/11/01 12:02:54 goroutine关闭后
2022/11/01 12:02:54 连接池内连接总数: 0
2022/11/01 12:02:54 连接池内空闲连接数量: 0
2022/11/01 12:02:55 关闭redis连接池
从这段运行结果,我们其实就可以很清晰看出其实go-redis确实是自动管理了连接池,而且连接池内部会自动创建和销毁连接。至于销毁规则和时间,可以查看Client实例化的相关参数。
当然,目前这个demo在goroutine关闭后的连接池销毁连接过快,1秒就销毁了8个连接,如果有兴趣的朋友可以对demo进行改进,更清楚去看销毁的连接池内连接销毁的过程。
总结
go-redis创建Client实例,本质上返回是一个连接池实例,只是当每次用此连接池实例执行命令时,它会自动从内部连接池获取管理好的单个连接实例去执行,其内部也会自动创建和销毁连接,go-redis确实是自动管理连接池的。
所以在项目需求为单台不分库(不使用多个db)
redis的情况且没有其他特殊需求的情况下,其实只需要使用一个连接池实例一撸到底就完事了,压根不用自己再去费心手动管理连接池。
当然,如果你有集群需求,用go-redis也照样能比较轻松上手。go-redis的star高是有原因的,因为集群需求它也封装好了。此处就不继续探讨了。
如有错漏之处,欢迎指正。
关于redis的db补充
严格意义上来说,redis的数据库db,多个db只是区分命名空间数据隔离。
一般业务情况下,只使用db0足够。而大型业务需要集群部署时,也支持db0。如果确实想在集群环境支持多db也可以使用如GaussDB(第三方企业级数据库)实现,或寻求其他途径解决。
当然,如果确实有特殊需求需要区分命名空间也完全可以使用,使用多个db性能还会高于单db,但无论使用单db还是多db,每秒处理请求的次数都不会成为瓶颈。
还不快抢沙发