1、golang语言Redis客户端简介
通常我们在选择某一组件的客户端包时,优先选择官方提供的包。redis 本身虽然并没有提供 go 语言的 client 包,但是提供了一份 client 包列表,并对部分包做了推荐标识,具体参考:https://redis.io/clients#go
接下来的全部使用示例,都是在 "github.com/gomodule/redigo/redis" 包的基础上实现。选择这个包的原因在于,这个包只有一个 Do 函数执行 Redis 命令,使用方法更接近Redis的原生命令,这无疑会降低我们的学习成本,同时该包对Print-alike API, Pipelining (including transactions), Pub/Sub, Connection pooling, scripting 等我们常用的功能也有良好的支持。
在开始之前,首先下载该第三方包
go get "github.com/gomodule/redigo/redis"
2、Redis连接池对象的构建和关闭
通常在初始化阶段创建 Redis 客户端连接池
var RedisClientPool *redis.Pool
func InitJimDb() {
RedisClientPool = &redis.Pool{
MaxIdle: 100,
MaxActive: 12000,
IdleTimeout: time.Duration(180),
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", "127.0.0.1:6379", redis.DialPassword("xxxxxx"),redis.DialReadTimeout(time.Second),redis.DialWriteTimeout(time.Second))
if err != nil {
logger.Errorf("redisClient dial host: %s, auth: %s err: %s", "127.0.0.1:6379", "xxxxxx", err.Error())
return nil, err
}
return c, nil
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if time.Since(t) < time.Minute {
return nil
}
_, err := c.Do("PING")
if err != nil {
logger.Errorf("redisClient ping err: %s", err.Error())
}
return err
},
}
logger.Infof("init jimDb ok")
}
func CloseJimDb() {
if RedisClientPool != nil {
err := RedisClientPool.Close()
if err != nil {
logger.Errorf("do CloseJimDb error:%s", err.Error())
}
}
}
客户端初始化参数含义:
- MaxIdle:连接池中最大的空闲连接数
- MaxActive:允许的最大连接 Redis 的连接数,设置为0则没有限制
- IdleTimeout:空闲超时时间,超过此时间后,则会关闭连接。若此值设置为0,则不会关闭连接,应用应设置一个小于服务超时的值
- Wait:若为 true,则当连接数达到 MaxActive 时,使用 Get() 获取新的连接时将会等待,直到有连接释放连接
- MaxConnLifetime:最大连接生命时长,当连接存活时间超过改值,则会被关闭,若设置为0,则不会因为存活时间关闭连接
- Dial:用于创建和配置连接的支持方法,通常用于 DB 选择/连接超时时间/读超时时间/写超时时间/密码认证等的初始化
在执行 Redis 数据库操作时,需要首先获取连接,同时记得在操作结束后释放连接
rc := g.RedisClientPool.Get()
defer func() {
rcErr := rc.Close()
if rcErr != nil {
logger.Errorf("Cache init redis client close err: %s", rcErr.Error())
}
}()
注:如果不执行 rc.Close() 释放连接,服务将很快打满内存,通过 pprof 将发现连接数非常大。
系统退出时,记得手动 Close() 客户端连接。
3、Redis客户端基本用法
(1)、redis基本数据类型
数据类型 | 可以存储的值 | 操作 |
---|---|---|
STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作 对整数和浮点数执行自增或者自减操作 |
LIST | 列表 | 从两端压入或者弹出元素 对单个或者多个元素进行修剪, 只保留一个范围内的元素 |
SET | 无序集合 | 添加、获取、移除单个元素 检查一个元素是否存在于集合中 计算交集、并集、差集 从集合里面随机获取元素 |
HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对 获取所有键值对 检查某个键是否存在 |
ZSET | 有序集合 | 添加、获取、删除元素 根据分值范围或者成员来获取元素 计算一个键的排名 |
各类型的基本操作预发可以参考Redis,本文不做过多介绍。
注:对于 Redis 键值对的 Key 值来说,key 只有唯一的类型 String。
示例:
127.0.0.1:6379> set 1 "value1"
OK
127.0.0.1:6379> set "1" "value2"
OK
127.0.0.1:6379> get 1
"value2"
127.0.0.1:6379> get "1"
"value2"
(2)、Redigo客户端支持的操作函数
type Conn interface {
// Close closes the connection.
Close() error
// Err returns a non-nil value when the connection is not usable.
Err() error
// Do sends a command to the server and returns the received reply.
Do(commandName string, args ...interface{}) (reply interface{}, err error)
// Send writes the command to the client's output buffer.
Send(commandName string, args ...interface{}) error
// Flush flushes the output buffer to the Redis server.
Flush() error
// Receive receives a single reply from the Redis server
Receive() (reply interface{}, err error)
}
Redigo 客户端支持的与数据操作相关的方法主要有4个:Do()、Send()、Flush() 和 Receive()。
其中,最多用到的方法是 Do(),其功能是单次执行某一命令,并返回执行结果。Send()、Flush() 和 Receive() 是一组操作,通常同时出现,主要实现 pipline 方式的数据写入,也可以实现消息的订阅通知功能。
(3)、Do()方法示例
Do(commandName string, args ...interface{}) (reply interface{}, err error)
根据源码可知,Do() 的入参,第一个为 commandName,也就是Redis 本身支持指令的大写字符串,arg 根据命令本身的参数按顺序填入即可。由于该客户端支持的 commandName 与 Redis cli 本身的指令名一致,因此学习成本较低。具体我们以 SET 命令为例,其余的操作一依次类推,只要命令和所需参数一致即可:
rc := RedisClientPool.Get()
defer func() {
rcErr := rc.Close()
if rcErr != nil {
t.Errorf("Cache init redis client close err: %s", rcErr.Error())
}
}()
reply, err := rc.Do("SET", "1", "value1")
if err != nil {
t.Errorf("do set %s,%s error:%s", "1", "valuea", err.Error())
return
}
reply, err := rc.Do("MSET", "1", "value1","2","value2")
if err != nil {
t.Errorf("do set %s,%s error:%s", "1", "valuea", err.Error())
return
}
上面示例在操作参数很少时还是十分方便的,但是当我们想批量写入大量数据时,就会显得异常繁琐,那么有没有简单的方式执行数据的批量写入呢?
我们可以通过自己构建 []interface{} 类型的参数数组实现,具体入如下所示:
rc := RedisClientPool.Get()
defer func() {
rcErr := rc.Close()
if rcErr != nil {
t.Errorf("Cache init redis client close err: %s", rcErr.Error())
}
}()
var values = []string{"1", "2", "3", "4", "5", "6", "7", "8", "8", "10"}
args := []interface{}{"redis-list"}
for _, v := range values {
args = append(args, v)
}
reply, err := rc.Do("RPUSH", args...)
if err != nil {
t.Errorf("do set %s,%s error:%s", GetKey("test"), "valuea", err.Error())
return
}
此外,redigo 客户端其实已经给我们提供了一个参数转化的方法,而无需我们自己去手动将参数转化为 []interface{}
具体的源码如下:
// Args is a helper for constructing command arguments from structured values.
type Args []interface{}
// Add returns the result of appending value to args.
func (args Args) Add(value ...interface{}) Args {
return append(args, value...)
}
// AddFlat returns the result of appending the flattened value of v to args.
//
// Maps are flattened by appending the alternating keys and map values to args.
//
// Slices are flattened by appending the slice elements to args.
//
// Structs are flattened by appending the alternating names and values of
// exported fields to args. If v is a nil struct pointer, then nothing is
// appended. The 'redis' field tag overrides struct field names. See ScanStruct
// for more information on the use of the 'redis' field tag.
//
// Other types are appended to args as is.
func (args Args) AddFlat(v interface{}) Args {
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Struct:
args = flattenStruct(args, rv)
case reflect.Slice:
for i := 0; i < rv.Len(); i++ {
args = append(args, rv.Index(i).Interface())
}
case reflect.Map:
for _, k := range rv.MapKeys() {
args = append(args, k.Interface(), rv.MapIndex(k).Interface())
}
case reflect.Ptr:
if rv.Type().Elem().Kind() == reflect.Struct {
if !rv.IsNil() {
args = flattenStruct(args, rv.Elem())
}
} else {
args = append(args, v)
}
default:
args = append(args, v)
}
return args
}
Add() 方法用于添加基本类型参数,例如 string、integer、float、interface 等。
AddFlat() 用于添加复杂类型参数,例如 Struce、slice、map、str 等。
根据源码可知,其基本原理都是帮助我们构建一个 []interface{}类型的参数组。只是简化了我们的代码量。具体实例如下:
rc := RedisClientPool.Get()
defer func() {
rcErr := rc.Close()
if rcErr != nil {
fmt.Printf("Cache init redis client close err: %s", rcErr.Error())
}
}()
var tempMap = map[int64][]byte{
1:bytes,
2:bytes,
3:bytes,
4:bytes,
5:bytes,
6:bytes,
}
_,err := rc.Do("HMSET",redis.Args{}.Add("redis-hash").AddFlat(tempMap)...)
if err != nil{
fmt.Println("hmset error:",err.Error())
}
(4)、pipline数据写入方式
pipline 的数据写入方式与 Do() 这种单次写入方式最大的区别在于批量数据处理的写入速度。由于 Do() 这种单次写入需要等返回结果之后才能进行第二次操作,而 pipline 可以在第一次操作结果返回之前,继续发送后续的 Request,直到把所有的 Request 都发送完毕。因此,当操作很多时,pipeline 更高效。
Redigo客户端的pipline处理方式依赖于三个方法:Send()、Flush()和Receive()。
Send() 发送命令到输出缓冲区。
Flush() 写入命令并刷新输出缓冲区。
Receive()接收服务器的返回值。
调用 Receive() 的次数必须对应使用 Send() 发送命令的次数。具体示例如大下:
rc := RedisClientPool.Get()
defer func() {
rcErr := rc.Close()
if rcErr != nil {
t.Errorf("Cache init redis client close err: %s", rcErr.Error())
}
}()
rc.Send("SET", "1","1")
rc.Send("SET", "2","2")
rc.Flush()
res,_ :=rc.Receive()
fmt.Println(res)
res,_ = rc.Receive()
fmt.Println(res)
注:虽然多个 Send() 的指令是在执行 Flush() 时才真正将输出缓冲区数据刷新到 Redis 服务器,但是 send()中的多个指令并不是事务。如果需要保证多个操作的原子性,还是需要使用事务来实现。
(5)、redis 客户端的并发
redigo 支持 Receive 方法的一个并发调用者和 Send 和 Flush 方法的一个并发调用者,但是不支持 Do() 方法的并发调用。若需要完全并发访问 redis,需要参考第2节内容,创建redis 客户端连接池,通过多个连接实现真正意义上的并发。Redis-Doc
理解起来也很容易,Do()、Send()、Flush() 和 Receive(),都必须依赖某一个连接实现,具体的:
rc := RedisClientPool.Get()
defer func() {
rcErr := rc.Close()
if rcErr != nil {
t.Errorf("Cache init redis client close err: %s", rcErr.Error())
}
}()
reply, err := rc.Do("SET", "1", "value1")
无论拥有几个并发协程,由于所有操作都基于同一个连接,那么 Do() 操作一定需要执行完一个完整的 Request-Response才可以执行下一个请求,因此 Do() 无法支持并发调用。而 Send()、Flush() 和 Receive() 方法是异步操作,无需等待Rsponse返回,因此可以支持并发调用。具体的:一个协程执行 Send() 和 Flush() 方法用于写入操作,另一个协程执行 Receive() 方法用于异步接收操作结果。具体如下:
rc := RedisClientPool.Get()
defer func() {
rcErr := rc.Close()
if rcErr != nil {
t.Errorf("Cache init redis client close err: %s", rcErr.Error())
}
}()
var wg sync.WaitGroup
wg.Add(1)
go func(){
defer wg.Done()
for i:=0;i<4;i++{
rc.Send("SET", i+1,i+1)
}
rc.Flush()
}()
wg.Add(1)
go func(){
defer wg.Done()
for i:=0;i<4;i++{
res,_ := rc.Receive()
fmt.Println(res)
}
}()
wg.Wait()
fmt.Println("exec success")
如果要实现真正意义上对 Redis 的并发访问,只能依赖连接池实现。
(6)、事务操作
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
Redis 事务
命令 | 描述 |
---|---|
MULTI | 标记一个事务块的开始 |
EXEC | 执行所有事务块内的命令 |
DISCARD | 取消事务,放弃执行事务块内的所有命令 |
WATCH | 监视一个(或多个)key,如果在事务执行之前这个(或多个)key被其他命令所改动,那么事务将被打断 |
UNWATCH | 取消 WATCH 命令对所有 keys 的监视 |
其中我们常用的命令是前三个,例如,我们需要对 userCount 和 productCount同时进行自增处理,为保证两个字段一致,基于事务来实现,具体示例如下:
rc := RedisClientPool.Get()
defer func() {
rcErr := rc.Close()
if rcErr != nil {
t.Errorf("Cache init redis client close err: %s", rcErr.Error())
}
}()
rc.Send("MULTI")
rc.Send("INCR", "userCount")
rc.Send("INCR", "produceCount")
rc.Send("EXEC")
rc.Flush()
r,_ := rc.Receive()
fmt.Println(r)
r,_ = rc.Receive()
fmt.Println(r)
r,_ = rc.Receive()
fmt.Println(r)
r,_ = rc.Receive()
fmt.Println(r)
对一个的结果如下:
OK //事务开启的返回
QUEUED //操作入队列
QUEUED //操作入队列
[5 5] //执行操作并返回结果
实际上,大多数情况下,我们并不关心中间过程的返回结果,只关注最终的结果,这种情况下,我们只需将最后一条 Send() 指令替换成 Do()即可。
Do方法结合了Send,Flush和Receive方法的功能。
Do方法首先写入命令并刷新输出缓冲区。接下来,
Do方法接收所有待处理的回复,包括Do执行的命令的回复。
如果收到的任何回复都是错误,则Do返回错误。 如果没有错误,则Do返回最后一个返回值。
如果Do方法的命令参数是“”,则Do方法将刷新输出缓冲区并接收挂起的回复而不发送命令。
具体示例如下:
rc := RedisClientPool.Get()
defer func() {
rcErr := rc.Close()
if rcErr != nil {
t.Errorf("Cache init redis client close err: %s", rcErr.Error())
}
}()
rc.Send("MULTI")
rc.Send("INCR", "userCount")
rc.Send("INCR", "produceCount")
r, _ := rc.Do("EXEC")
fmt.Println(r) // [1,1]
返回结果为:
[6 6]
通过 Do() 命令完成最终事务的执行,将为我们忽略中间结果,更好的关注事务整体的运行结果。
4、相关内容