ITPub博客

首页 > 数据库 > 数据库开发技术 > Golang操作Redis指南

Golang操作Redis指南

原创 数据库开发技术 作者:云原生运维技术 时间:2019-08-23 08:36:02 0 删除 编辑

相关模块以及安装方式

redigo模块[1]
redis-cluster客户端实现[2] go-redis模块[3]

注意:如下操作使用redigo模块进行redis的操作

安装使用

$ go get -v github.com/gomodule/redigo$ cat redis-conn.go/** * @File Name: redis-conn.go * @Author: xxbandy @http://xxbandy.github.io * @Email: * @Create Date: 2018-04-04 07:04:57 * @Last Modified: 2018-04-08 14:04:38 * @Description: */package mainimport (    "fmt"    "os"    redis "github.com/gomodule/redigo/redis")//自定义的redis实例的端口为32771func main() {    //使用redis封装的Dial进行tcp连接    c,err := redis.Dial("tcp","localhost:32771")    errCheck(err)
defer c.Close()
//对本次连接进行set操作 _,setErr := c.Do("set","url","xxbandy.github.io") errCheck(setErr)
//使用redis的string类型获取set的k/v信息 r,getErr := redis.String(c.Do("get","url")) errCheck(getErr) fmt.Println(r)
}
func errCheck(err error) { if err != nil { fmt.Println("sorry,has some error:",err) os.Exit(-1) } }
# 执行redis的set和get的操作$ go run redis-conn.goxxbandy.github.io

redigo模块的详细使用

连接

Conn接口是redis操作过程中比较重要的接口。应用一般通过调用Dial、DialWithTimeout、NewConn函数来创建一个链接。
注意:应用必须在操作redis结束之后去调用该连接的Close方法来关闭连接,以防止资源消耗以及其他问题。

Conn接口下的相关方法

type Conn interface {    // 关闭链接    Close() error
// 当链接不可用时返回非空值 Err() error
//Do方法向redis服务端发送命令并返回接收到响应 Do(commandName string, args ...interface{}) (reply interface{}, err error)
//Send 将相关的命令写入客户端的buffer中 Send(commandName string, args ...interface{}) error
// Flush 将客户端的输出缓冲内容刷新到redis服务器端 Flush() error
// Receive 从redis服务端接收单个回复 Receive() (reply interface{}, err error)}

常用的创建Conn的方式

# 使用Dial函数# func Dial(network, address string, options ...DialOption) (Conn, error)/*可选的参数func DialConnectTimeout(d time.Duration) DialOptionfunc DialDatabase(db int) DialOptionfunc DialKeepAlive(d time.Duration) DialOptionfunc DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOptionfunc DialPassword(password string) DialOptionfunc DialReadTimeout(d time.Duration) DialOptionfunc DialWriteTimeout(d time.Duration) DialOption*/c,err := redis.Dial("tcp","localhost:32771")defer c.Close()


# 使用DialTimeout函数(默认增加了连接、读写超时时间)// func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error)

# 使用NewConn函数(将一个网络连接转换成redis连接)// func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn

redis命令的执行

在上面Conn的接口中可以看到有一个Do方法,可以用来执行redis命令操作。

Do方法定义

//使用Conn的Do方法来操作redis命令Do(commandName string, args ...interface{}) (reply interface{}, err error)
//示例n, err := conn.Do("APPEND", "key", "value")

redis连接和命令执行的简单示例

简单示例:

$ cat redis-conn.gopackage mainimport (    "fmt"    "os"    "time"    redis "github.com/gomodule/redigo/redis")func main() {    //使用redis封装的Dial进行tcp连接.设置长连接时间为1s,连接超时时间为5s,读写超时均为1s,并设置密码进行连接    c,err := redis.Dial("tcp","localhost:32771",redis.DialKeepAlive(1*time.Second),redis.DialPassword("123qweasd"),redis.DialConnectTimeout(5*time.Second),redis.DialReadTimeout(1*time.Second),redis.DialWriteTimeout(1*time.Second))    errCheck(err)
defer c.Close()
//使用redis的string类型获取set的k/v信息 r,getErr := redis.String(c.Do("get","url")) errCheck(getErr) fmt.Println(r)
}
func errCheck(err error) { if err != nil { fmt.Println("sorry,has some error:",err) os.Exit(-1) } }
$ go run redis-conn.goxxbandy.github.io
# 由于redis服务器设置了密码,如果密码错误会报如下异常$ go run redis-conn.gosorry,has some error: ERR invalid passwordexit status 255

使用Pipelining操作redis

注意:建议使用redis自身包含的命令进行批量操作而不是使用pipelining,比如mset、mget、hmset、hmget等等。原子性可能会更好一些

Conn会使用Send、Flush、Receive来支持pipeline操作。

Send(commandName string, args ...interface{}) errorFlush() errorReceive() (reply interface{}, err error)

Send会写入命令到连接的输出缓冲区里。Flush会将输出缓冲区中的数据刷新到服务端。Receive回去荣服务端读取单个响应。

c.Send("SET", "foo", "bar")c.Send("GET", "foo")c.Flush()c.Receive()v, err = c.Receive()

其实,在Do方法中会包含Send/Flush/Receive等方法。

使用Send和Do方法来实现pipeline。

c.Send("MULTI")c.Send("INCR", "foo")c.Send("INCR", "bar")r, err := c.Do("EXEC")fmt.Println(r) //[1,1]

并发

一般并发访问redis,建议使用安全线程池去获取连接,从一个goroutine中使用和释放一个链接。正如前面提到的,从线程池中获取的连接会有并发限制。

发布/订阅模式(Publish and Subscribe)

使用Send/Flush/Receive方法去实现Pub/Sub订阅。

c.Send("SUBSCRIBE","ANSIBLE-KEY")c.Flush()for {    reply,err := c.Receive()    if err != nil {        return err    }    //process pushed message}

PubSubConn类型使用convenience方法包装了一个Conn来实现了订阅。Subscribe/PSubscribe/Unsubscribe/PUnsubscribe方法会send并且flush一个订阅管理命令。接收方法会将push的消息转换成一个可以进行switch的合适类型。

psc := redis.PubSubConn{Conn: c}
psc.Subscribe("example")for { switch v := psc.Receive().(type) { case redis.Message: fmt.Printf("%s: message: %s\n", v.Channel, v.Data) case redis.Subscription: fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count) case error: return v }}

Redigo操作redis的几种场景示例

使用Args接口数组操作处理批量操作

相关的函数和方法:

//func Values(reply interface{}, err error) ([]interface{}, error)//type Args []interface{}
# 类似于hgetall和config get的输出格式//func StringMap(result interface{}, err error) (map[string]string, error)
# 类似于hmget 类型//func Strings(reply interface{}, err error) ([]string, error)
# 从src中扫描k/v到一个结构体中。一般使用较多的就是hgetall和config get命令//func ScanStruct(src []interface{}, dest interface{}) error

hmset和hgetall命令的使用:

$  cat redis-hash.go/** * @File Name: redis-hash.go * @Author: xxbandy @http://xxbandy.github.io * @Email: * @Create Date: 2018-04-04 08:04:55 * @Last Modified: 2018-04-08 22:04:47 * @Description: */package mainimport (    "fmt"    "time"    "os"    "github.com/gomodule/redigo/redis")//构造一个链接函数,如果没有密码,passwd为空字符串func redisConn(ip,port,passwd string) (redis.Conn, error) {    c,err := redis.Dial("tcp",        ip+":"+port,        redis.DialConnectTimeout(5*time.Second),        redis.DialReadTimeout(1*time.Second),        redis.DialWriteTimeout(1*time.Second),        redis.DialPassword(passwd),        redis.DialKeepAlive(1*time.Second),        )    return c,err}
//构造一个错误检查函数func errCheck(tp string,err error) { if err != nil { fmt.Printf("sorry,has some error for %s.\r\n",tp,err) os.Exit(-1) }}
//构造实际场景的hash结构体var p1,p2 struct { Description string `redis:"description"` Url string `redis:"url"` Author string `redis:"author"`}


//主函数func main() { c,cErr := redisConn("localhost","32771","123qweasd") errCheck("Conn",cErr)
defer c.Close() p1.Description = "my blog" p1.Url = "http://xxbandy.github.io" p1.Author = "bgbiao"
_,hmsetErr := c.Do("hmset",redis.Args{}.Add("hao123").AddFlat(&p1)...) errCheck("hmset",hmsetErr)
m := map[string]string{ "description": "oschina", "url": "http://my.oschina.net/myblog", "author": "xxbandy", }
_,hmset1Err := c.Do("hmset",redis.Args{}.Add("hao").AddFlat(m)...) errCheck("hmset1",hmset1Err)
for _,key := range []string{"hao123","hao"} { v, err := redis.Values(c.Do("hgetall",key)) errCheck("hmgetV",err) //等同于hgetall的输出类型,输出字符串为k/v类型 //hashV,_ := redis.StringMap(c.Do("hgetall",key)) //fmt.Println(hashV) //等同于hmget 的输出类型,输出字符串到一个字符串列表 hashV2,_ := redis.Strings(c.Do("hmget",key,"description","url","author")) for _,hashv := range hashV2 { fmt.Println(hashv) } if err := redis.ScanStruct(v,&p2);err != nil { fmt.Println(err) return } fmt.Printf("%+v\n",p2)

}
}$ tcp-http go run redis-hash.go{Description:my blog Url:http://xxbandy.github.io Author:bgbiao}{Description:oschina Url:http://my.oschina.net/myblog Author:xxbandy}
bash-4.1# redis-cli -a 123qweasd hgetall hao1) "description"2) "oschina"3) "url"4) "http://my.oschina.net/myblog"5) "author"6) "xxbandy"bash-4.1# redis-cli -a 123qweasd hgetall hao1231) "description"2) "my blog"3) "url"4) "http://xxbandy.github.io"5) "author"6) "bgbiao"

hset和hget的使用:

// core functions_,err := c.Do("hset","books","name","golang")if r,err := redis.String("hget","books","name");err == nil {    fmt.Println("book name:",r)}

使用String和Int获取字符串类型的输出

也就是set、get、mset、mget之类的命令。
string相关命令[4]

注意:当获取数字类型的key时需要使用redis.Int()获取数字类型的输出结果

$ cat redis-string.go....func main() {    c,connErr := redisConn("localhost","32771","123qweasd")    errCheck("connErr",connErr)
getV,_ := redis.String(c.Do("get","name")) getV2,_ := redis.Int(c.Do("get","id")) fmt.Println(getV,getV2)}$ go run redis-string.goxxb 1214
127.0.0.1:6379> mget name id1) "xxb"2) "1214"

mset、mget、expire之类的使用:

// core functions_,err = c.Do("mset","id",100,"fn",200)
_,expireErr = c.Do("expire","id",10)
if r,err := redis.Ints(c.Do("mget","id","fn")); err == nil { for _,v := range { fmt.Println("value:",v) }}

lpush、lpop、rpush、rpop等相关命令操作:

// core functions_,err = c.Do("lpush","updateid","0407","0408","0409")
r,err := redis.String(c.Do("lpop","updateid"))

使用redis的安全链连接池

使用连接池可以高效的管理redis的连接,并可以方便地控制redis的并发性能。

相关的结构体和方法:

type Pool struct {    //创建一个tcp链接的匿名函数    Dial func() (Conn, error)    //可选的函数,用来对之前使用过的空闲链接进行安全检查    TestOnBorrow func(c Conn, t time.Time) error    //最大可用链接数    MaxIdle int    //在给定的时间,最大可分配的连接数。为0则不限制    MaxActive int    //空闲链接的关闭时间,如果为空,空闲链接不会被关闭    IdleTimeout time.Duration    //如果Wait为true并且进行MaxActive限制了,Get()将会等待链接被返回    Wait bool    //关闭老链接的时间区间。如果为空则不会在生命周期内关闭链接    MaxConnLifetime time.Duration}
//创建一个Pool结构体func NewPool(newFn func() (Conn, error), maxIdle int) *Pool
// 获取pool的连接数,包含空闲链接和使用连接 func (p *Pool) ActiveCount() int

//关闭并释放pool中的资源func (p *Pool) Close() error
//从pool中获取一个连接func (p *Pool) Get() Conn
// 获取空闲链接数func (p *Pool) IdleCount() int
// 获取连接池的状态信息func (p *Pool) Stats() PoolStatstype PoolStats struct { ActiveCount int IdleCount int}


示例:

$ cat redis-pool.gopackage mainimport (    "fmt"    "time"    "os"    "github.com/gomodule/redigo/redis")//构造一个链接函数,如果没有密码,passwd为空字符串func redisConn(ip,port,passwd string) (redis.Conn, error) {    c,err := redis.Dial("tcp",        ip+":"+port,        redis.DialConnectTimeout(5*time.Second),        redis.DialReadTimeout(1*time.Second),        redis.DialWriteTimeout(1*time.Second),        redis.DialPassword(passwd),        redis.DialKeepAlive(1*time.Second),        )    return c,err}
//构造一个错误检查函数func errCheck(tp string,err error) { if err != nil { fmt.Printf("sorry,has some error for %s.\r\n",tp,err) os.Exit(-1) }}
//构造一个连接池//url为包装了redis的连接参数ip,port,passwdfunc newPool(ip,port,passwd string) *redis.Pool { return &redis.Pool{ MaxIdle: 5, //定义redis连接池中最大的空闲链接为3 MaxActive: 18, //在给定时间已分配的最大连接数(限制并发数) IdleTimeout: 240 * time.Second, MaxConnLifetime: 300 * time.Second, Dial: func() (redis.Conn,error) { return redisConn(ip,port,passwd) }, }}

func main() { //使用newPool构建一个redis连接池 pool := newPool("localhost","32771","123qweasd") defer pool.Close() for i := 0;i <= 4;i++ { go func() { //从pool里面获取一个可用的redis连接 c := pool.Get() defer c.Close() //mset mget fmt.Printf("ActiveCount:%d IdleCount:%d\r\n",pool.Stats().ActiveCount,pool.Stats().IdleCount) _,setErr := c.Do("mset","name","biaoge","url","http://xxbandy.github.io") errCheck("setErr",setErr) if r,mgetErr := redis.Strings(c.Do("mget","name","url")); mgetErr == nil { for _,v := range r { fmt.Println("mget ",v) } } }() }
time.Sleep(1*time.Second)}
# 使用连接池进行获取连接并处理redis操作$ go run redis-pool.goActiveCount:5 IdleCount:0ActiveCount:5 IdleCount:0ActiveCount:5 IdleCount:0ActiveCount:5 IdleCount:0ActiveCount:5 IdleCount:0mget biaogemget http://xxbandy.github.iomget biaogemget http://xxbandy.github.iomget biaogemget http://xxbandy.github.iomget biaogemget http://xxbandy.github.iomget biaogemget http://xxbandy.github.io

使用redis的发布订阅模式

相关的结构体以及方法:

//PubSubConn结构体type PubSubConn struct {    Conn    Conn}
//使用redis链接初始化一个pubsub链接psc := redis.PubSubConn{Conn:redis.conn}
//pubsub链接的相关方法//关闭pubsub链接func (c PubSubConn) Close() error//批量订阅(按模式订阅)func (c PubSubConn) PSubscribe(channel ...interface{}) error
//取消订阅func (c PubSubConn) PUnsubscribe(channel ...interface{}) error
// Receive返回消费的信息作为Subscription, Message, Pong or error.返回值建议直接在switch中使用func (c PubSubConn) Receive() interface{}type Subscription struct { Kind string //订阅,取消订阅 Channel string //被改变的channel //订阅的链接数量 Count int}type Message struct { // 初始的channel Channel string // 匹配的模式 Pattern string // channel的消息体 Data []byte}type


//订阅和取消订阅func (c PubSubConn) Subscribe(channel ...interface{}) errorfunc (c PubSubConn) Unsubscribe(channel ...interface{}) error

测试示例:


package main
import ( "context" "fmt" "time"
"github.com/gomodule/redigo/redis")
//func listenPubSubChannels(ctx context.Context, redisServerAddr string, onStart func() error, onMessage func(channel string, data []byte) error, channels ...string) error {
//一分钟的心跳检测 const healthCheckPeriod = time.Minute
//构建一个redis链接 c, err := redis.Dial("tcp", redisServerAddr, redis.DialReadTimeout(healthCheckPeriod+10*time.Second), redis.DialWriteTimeout(10*time.Second)) if err != nil { return err } defer c.Close()
//构建一个pubsub链接 psc := redis.PubSubConn{Conn: c}
//订阅channels if err := psc.Subscribe(redis.Args{}.AddFlat(channels)...); err != nil { return err } //构造chan来检测通知状态 done := make(chan error, 1)
//启动一个goroutine来接受来自server端的通知 go func() { for { //使用interface{}.(type) 来获取对应的类型,并借助switch和case进行interface{}的类型判断 switch n := psc.Receive().(type) { case error: done <- n return case redis.Message: if err := onMessage(n.Channel, n.Data); err != nil { done <- err return } case redis.Subscription: switch n.Count { case len(channels): // Notify application when all channels are subscribed. if err := onStart(); err != nil { done <- err return } case 0: // Return from the goroutine when all channels are unsubscribed. done <- nil return } } } }()
ticker := time.NewTicker(healthCheckPeriod) defer ticker.Stop()loop: for err == nil { select { case <-ticker.C: // Send ping to test health of connection and server. If // corresponding pong is not received, then receive on the // connection will timeout and the receive goroutine will exit. if err = psc.Ping(""); err != nil { break loop } case <-ctx.Done(): break loop case err := <-done: // Return error from the receive goroutine. return err } }
// Signal the receiving goroutine to exit by unsubscribing from all channels. psc.Unsubscribe()
// Wait for goroutine to complete. return <-done}
func publish(redisServerAddr string) { c, err := redis.Dial("tcp",redisServerAddr) if err != nil { fmt.Println(err) return } defer c.Close()
c.Do("PUBLISH", "c1", "hello")}
// This example shows how receive pubsub notifications with cancelation and// health checks.func main() { redisServerAddr := "localhost:32771"
ctx, cancel := context.WithCancel(context.Background())
//ctx和start callback很好的解决了丢失的消息的填充(使用goroutine来占住message类型的消息) listenErr := listenPubSubChannels(ctx, redisServerAddr, func() error { // The start callback is a good place to backfill missed // notifications. For the purpose of this example, a goroutine is // started to send notifications. go publish(redisServerAddr) return nil }, func(channel string, message []byte) error { fmt.Printf("channel: %s, message: %s\n", channel, message)
// For the purpose of this example, cancel the listener's context // after receiving last message sent by publish(). if string(message) == "goodbye" { cancel() } return nil }, "ansible-key")
if listenErr != nil { fmt.Println(listenErr) return }
}

欢迎关注我的公众号

References

[1] redigo模块: https://github.com/gomodule/redigo
[2] redis-cluster客户端实现: https://github.com/mna/redisc/
[3] go-redis模块: https://github.com/go-redis/redis/
[4] string相关命令: http://redisdoc.com/string/index.html


原文作者:云原生运维技术;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2017-01-18

  • 博文量
    53
  • 访问量
    20337