首页 > 数据库 > 数据库开发技术 > Golang操作Redis指南
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
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
在上面Conn的接口中可以看到有一个Do方法,可以用来执行redis命令操作。
Do方法定义
//使用Conn的Do方法来操作redis命令Do(commandName string, args ...interface{}) (reply interface{}, err error)//示例n, err := conn.Do("APPEND", "key", "value")
简单示例:
$ 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
注意:建议使用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中使用和释放一个链接。正如前面提到的,从线程池中获取的连接会有并发限制。
使用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}}
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 1214127.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 intIdleCount 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连接池中最大的空闲链接为3MaxActive: 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 mgetfmt.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 {// 初始的channelChannel string// 匹配的模式Pattern string// channel的消息体Data []byte}type//订阅和取消订阅func (c PubSubConn) Subscribe(channel ...interface{}) errorfunc (c PubSubConn) Unsubscribe(channel ...interface{}) error
测试示例:
package mainimport ("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}//订阅channelsif 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 <- nreturncase redis.Message:if err := onMessage(n.Channel, n.Data); err != nil {done <- errreturn}case redis.Subscription:switch n.Count {case len(channels):// Notify application when all channels are subscribed.if err := onStart(); err != nil {done <- errreturn}case 0:// Return from the goroutine when all channels are unsubscribed.done <- nilreturn}}}}()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 loopcase 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}}
欢迎关注我的公众号
[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
原文作者:云原生运维技术;如有侵权,请联系管理员删除。