Go语言安装并操作redis的go-redis库


Posted in Golang onApril 14, 2022

Redis介绍

Redis是一个开源的内存数据库,Redis提供了多种不同类型的数据结构,很多业务场景下的问题都可以很自然地映射到这些数据结构上。除此之外,通过复制、持久化和客户端分片等特性,我们可以很方便地将Redis扩展成一个能够包含数百GB数据、每秒处理上百万次请求的系统。

Redis支持的数据结构

Redis支持诸如字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、带范围查询的排序集合(sorted sets)、位图(bitmaps)、hyperloglogs、带半径查询和流的地理空间索引等数据结构(geospatial indexes)。

Redis应用场景

  • 缓存系统,减轻主数据库(MySQL)的压力。
  • 计数场景,比如微博、抖音中的关注数和粉丝数。
  • 热门排行榜,需要排序的场景特别适合使用ZSET。
  • 利用LIST可以实现队列的功能。

准备Redis环境

这里直接使用Docker启动一个redis环境,方便学习使用。

docker启动一个名为redis507的5.0.7版本的redis server示例:

docker run --name redis507 -p 6379:6379 -d redis:5.0.7

注意:此处的版本、容器名和端口号请根据自己需要设置。

启动一个redis-cli连接上面的redis server:

docker run -it --network host --rm redis:5.0.7 redis-cli

go-redis库

安装

区别于另一个比较常用的Go语言redis client库:redigo,我们这里采用https://github.com/go-redis/redis连接Redis数据库并进行操作,因为go-redis支持连接哨兵及集群模式的Redis。

使用以下命令下载并安装:

go get -u github.com/go-redis/redis

连接

普通连接

// 声明一个全局的rdb变量
var rdb *redis.Client
// 初始化连接
func initClient() (err error) {
	rdb = redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "", // no password set
		DB:       0,  // use default DB
	})
	_, err = rdb.Ping().Result()
	if err != nil {
		return err
	}
	return nil
}

V8新版本相关

最新版本的go-redis库的相关命令都需要传递context.Context参数,例如:

package main
import (
	"context"
	"fmt"
	"time"
	"github.com/go-redis/redis/v8" // 注意导入的是新版本
)
var (
	rdb *redis.Client
)
// 初始化连接
func initClient() (err error) {
	rdb = redis.NewClient(&redis.Options{
		Addr:     "localhost:16379",
		Password: "",  // no password set
		DB:       0,   // use default DB
		PoolSize: 100, // 连接池大小
	})
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	_, err = rdb.Ping(ctx).Result()
	return err
}
func V8Example() {
	ctx := context.Background()
	if err := initClient(); err != nil {
		return
	}

	err := rdb.Set(ctx, "key", "value", 0).Err()
	if err != nil {
		panic(err)
	}

	val, err := rdb.Get(ctx, "key").Result()
	if err != nil {
		panic(err)
	}
	fmt.Println("key", val)
	val2, err := rdb.Get(ctx, "key2").Result()
	if err == redis.Nil {
		fmt.Println("key2 does not exist")
	} else if err != nil {
		panic(err)
	} else {
		fmt.Println("key2", val2)
	}
	// Output: key value
	// key2 does not exist
}

连接Redis哨兵模式

func initClient()(err error){
	rdb := redis.NewFailoverClient(&redis.FailoverOptions{
		MasterName:    "master",
		SentinelAddrs: []string{"x.x.x.x:26379", "xx.xx.xx.xx:26379", "xxx.xxx.xxx.xxx:26379"},
	})
	_, err = rdb.Ping().Result()
	if err != nil {
		return err
	}
	return nil
}

连接Redis集群

func initClient()(err error){
	rdb := redis.NewClusterClient(&redis.ClusterOptions{
		Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},
	})
	_, err = rdb.Ping().Result()
	if err != nil {
		return err
	}
	return nil
}

基本使用

HVals

package main
import (
	"fmt"
	"github.com/go-redis/redis"
	"reflect"
)
var rdb *redis.Client
// 初始化连接
func initClient() (err error) {
	rdb = redis.NewClient(&redis.Options{
		Addr:     "10.0.3.100:6379",
		Password: "EfcHGSzKqg6cfzWq", // no password set
		DB:       8,  // use default DB
	})

	_, err = rdb.Ping().Result()
	if err != nil {
		return err
	}
	return nil
}
func main() {
	err:=initClient()
	if err != nil {
		fmt.Println(err)
	}
	value,err2 := rdb.HVals("toutiao_web_gt100").Result()
	if err2 != nil {
		fmt.Println(err2)
	}
	fmt.Println(reflect.TypeOf(value))
	fmt.Println(len(value))
	for i,j:=0,len(value);i<j;i++{
		fmt.Println(value[i])
	}
}

set/get示例

func redisExample() {
	err := rdb.Set("score", 100, 0).Err()
	if err != nil {
		fmt.Printf("set score failed, err:%v\n", err)
		return
	}
	val, err := rdb.Get("score").Result()
	if err != nil {
		fmt.Printf("get score failed, err:%v\n", err)
		return
	}
	fmt.Println("score", val)
	val2, err := rdb.Get("name").Result()
	if err == redis.Nil {
		fmt.Println("name does not exist")
	} else if err != nil {
		fmt.Printf("get name failed, err:%v\n", err)
		return
	} else {
		fmt.Println("name", val2)
	}
}

zset示例

func redisExample2() {
	zsetKey := "language_rank"
	languages := []redis.Z{
		redis.Z{Score: 90.0, Member: "Golang"},
		redis.Z{Score: 98.0, Member: "Java"},
		redis.Z{Score: 95.0, Member: "Python"},
		redis.Z{Score: 97.0, Member: "JavaScript"},
		redis.Z{Score: 99.0, Member: "C/C++"},
	}
	// ZADD
	num, err := rdb.ZAdd(zsetKey, languages...).Result()
	if err != nil {
		fmt.Printf("zadd failed, err:%v\n", err)
		return
	}
	fmt.Printf("zadd %d succ.\n", num)

	// 把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, err := rdb.ZRevRangeWithScores(zsetKey, 0, 2).Result()
	if err != nil {
		fmt.Printf("zrevrange failed, err:%v\n", err)
		return
	}
	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)
	}
}

输出结果如下:

$ ./06redis_demo 
zadd 0 succ.
Golang's score is 100.000000 now.
Golang 100
C/C++ 99
Java 98
JavaScript 97
Java 98
C/C++ 99
Golang 100

根据前缀获取Key

vals, err := rdb.Keys(ctx, "prefix*").Result()

执行自定义命令

res, err := rdb.Do(ctx, "set", "key", "value").Result()

按通配符删除key

当通配符匹配的key的数量不多时,可以使用Keys()得到所有的key在使用Del命令删除。 如果key的数量非常多的时候,我们可以搭配使用Scan命令和Del命令完成删除。

ctx := context.Background()
iter := rdb.Scan(ctx, 0, "prefix*", 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)
}

Pipeline

Pipeline 主要是一种网络优化。它本质上意味着客户端缓冲一堆命令并一次性将它们发送到服务器。这些命令不能保证在事务中执行。这样做的好处是节省了每个命令的网络往返时间(RTT)。

Pipeline 基本示例如下:

pipe := rdb.Pipeline()
incr := pipe.Incr("pipeline_counter")
pipe.Expire("pipeline_counter", time.Hour)
_, err := pipe.Exec()
fmt.Println(incr.Val(), err)

上面的代码相当于将以下两个命令一次发给redis server端执行,与不使用Pipeline相比能减少一次RTT。

INCR pipeline_counter
EXPIRE pipeline_counts 3600

也可以使用Pipelined

var incr *redis.IntCmd
_, err := rdb.Pipelined(func(pipe redis.Pipeliner) error {
	incr = pipe.Incr("pipelined_counter")
	pipe.Expire("pipelined_counter", time.Hour)
	return nil
})
fmt.Println(incr.Val(), err)

在某些场景下,当我们有多条命令要执行时,就可以考虑使用pipeline来优化。

事务

Redis是单线程的,因此单个命令始终是原子的,但是来自不同客户端的两个给定命令可以依次执行,例如在它们之间交替执行。但是,Multi/exec能够确保在multi/exec两个语句之间的命令之间没有其他客户端正在执行命令。

在这种场景我们需要使用TxPipelineTxPipeline总体上类似于上面的Pipeline,但是它内部会使用MULTI/EXEC包裹排队的命令。例如:

pipe := rdb.TxPipeline()
incr := pipe.Incr("tx_pipeline_counter")
pipe.Expire("tx_pipeline_counter", time.Hour)
_, err := pipe.Exec()
fmt.Println(incr.Val(), err)

上面代码相当于在一个RTT下执行了下面的redis命令:

MULTI
INCR pipeline_counter
EXPIRE pipeline_counts 3600
EXEC

还有一个与上文类似的TxPipelined方法,使用方法如下:

var incr *redis.IntCmd
_, err := rdb.TxPipelined(func(pipe redis.Pipeliner) error {
	incr = pipe.Incr("tx_pipelined_counter")
	pipe.Expire("tx_pipelined_counter", time.Hour)
	return nil
})
fmt.Println(incr.Val(), err)

Watch

在某些场景下,我们除了要使用MULTI/EXEC命令外,还需要配合使用WATCH命令。在用户使用WATCH命令监视某个键之后,直到该用户执行EXEC命令的这段时间里,如果有其他用户抢先对被监视的键进行了替换、更新、删除等操作,那么当用户尝试执行EXEC的时候,事务将失败并返回一个错误,用户可以根据这个错误选择重试事务或者放弃事务。

Watch(fn func(*Tx) error, keys ...string) error

Watch方法接收一个函数和一个或多个key作为参数。基本使用示例如下:

// 监视watch_count的值,并在值不变的前提下将其值+1
key := "watch_count"
err = client.Watch(func(tx *redis.Tx) error {
	n, err := tx.Get(key).Int()
	if err != nil && err != redis.Nil {
		return err
	}
	_, err = tx.Pipelined(func(pipe redis.Pipeliner) error {
		pipe.Set(key, n+1, 0)
		return nil
	})
	return err
}, key)

最后看一个V8版本官方文档中使用GET和SET命令以事务方式递增Key的值的示例,仅当Key的值不发生变化时提交一个事务。

func transactionDemo() {
	var (
		maxRetries   = 1000
		routineCount = 10
	)
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	// Increment 使用GET和SET命令以事务方式递增Key的值
	increment := func(key string) error {
		// 事务函数
		txf := func(tx *redis.Tx) error {
			// 获得key的当前值或零值
			n, err := tx.Get(ctx, key).Int()
			if err != nil && err != redis.Nil {
				return err
			}
			// 实际的操作代码(乐观锁定中的本地操作)
			n++
			// 操作仅在 Watch 的 Key 没发生变化的情况下提交
			_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
				pipe.Set(ctx, key, n, 0)
				return nil
			})
			return err
		}
		// 最多重试 maxRetries 次
		for i := 0; i < maxRetries; i++ {
			err := rdb.Watch(ctx, txf, key)
			if err == nil {
				// 成功
				return nil
			}
			if err == redis.TxFailedErr {
				// 乐观锁丢失 重试
				continue
			}
			// 返回其他的错误
			return err
		}
		return errors.New("increment reached maximum number of retries")
	}
	// 模拟 routineCount 个并发同时去修改 counter3 的值
	var wg sync.WaitGroup
	wg.Add(routineCount)
	for i := 0; i < routineCount; i++ {
		go func() {
			defer wg.Done()
			if err := increment("counter3"); err != nil {
				fmt.Println("increment error:", err)
			}
		}()
	}
	wg.Wait()
	n, err := rdb.Get(context.TODO(), "counter3").Int()
	fmt.Println("ended with", n, err)
}

以上就是golang连接redis库及基本操作示例过程的详细内容!

Golang 相关文章推荐
golang http使用踩过的坑与填坑指南
Apr 27 Golang
golang在GRPC中设置client的超时时间
Apr 27 Golang
goland 清除所有的默认设置操作
Apr 28 Golang
golang 实现对Map进行键值自定义排序
Apr 28 Golang
golang 如何用反射reflect操作结构体
Apr 28 Golang
golang协程池模拟实现群发邮件功能
May 02 Golang
Golang: 内建容器的用法
May 05 Golang
Golang Gob编码(gob包的使用详解)
May 07 Golang
Golang 实现获取当前函数名称和文件行号等操作
May 08 Golang
go goroutine 怎样进行错误处理
Jul 16 Golang
Golang数据类型和相互转换
Apr 12 Golang
Golang 入门 之url 包
May 04 Golang
golang操作redis的客户端包有多个比如redigo、go-redis
Apr 14 #Golang
Go语言grpc和protobuf
Golang流模式之grpc的四种数据流
Apr 13 #Golang
Golang数据类型和相互转换
Apr 12 #Golang
Go语言的协程上下文的几个方法和用法
Apr 11 #Golang
Golang 1.18 多模块Multi-Module工作区模式的新特性
Apr 11 #Golang
golang三种设计模式之简单工厂、方法工厂和抽象工厂
You might like
探讨:使用XMLSerialize 序列化与反序列化
2013/06/08 PHP
php中count获取多维数组长度的方法
2014/11/03 PHP
PHP面向对象程序设计(OOP)之方法重写(override)操作示例
2018/12/21 PHP
php设计模式之迭代器模式实例分析【星际争霸游戏案例】
2020/04/07 PHP
jQuery示例收集
2010/11/05 Javascript
javascript标签在页面中的位置探讨
2013/04/11 Javascript
js导出table到excel同时兼容FF和IE示例
2013/09/03 Javascript
单元选择合并变色示例代码
2014/05/26 Javascript
完美兼容各大浏览器的jQuery仿新浪图文淡入淡出间歇滚动特效
2014/11/12 Javascript
Labelauty?jQuery单选框/复选框美化插件分享
2015/09/26 Javascript
JavaScript对象数组如何按指定属性和排序方向进行排序
2016/06/15 Javascript
js鼠标单击和双击事件冲突问题的快速解决方法
2016/07/11 Javascript
微信小程序 wx.request(object) API详解及实例代码
2016/09/30 Javascript
ActiveX控件的使用-js实现打印超市小票功能代码详解
2017/11/22 Javascript
vue 弹框产生的滚动穿透问题的解决
2018/09/21 Javascript
使用微信SDK自定义分享的方法
2019/07/03 Javascript
js实现漂亮的星空背景
2019/11/01 Javascript
json_decode 索引为数字时自动排序问题解决方法
2020/03/28 Javascript
浅谈vue中$bus的使用和涉及到的问题
2020/07/28 Javascript
PyQt5固定窗口大小的方法
2019/06/18 Python
pyqt5 禁止窗口最大化和禁止窗口拉伸的方法
2019/06/18 Python
python 含子图的gif生成时内存溢出的方法
2019/07/07 Python
tensorflow模型文件(ckpt)转pb文件的方法(不知道输出节点名)
2020/04/22 Python
Python如何实现FTP功能
2020/05/28 Python
django项目中使用云片网发送短信验证码的实现
2021/01/19 Python
美国最灵活的移动提供商:Tello
2017/07/18 全球购物
凯伦·米莲女装网上商店:Karen Millen
2017/11/07 全球购物
接口的多继承会带来哪些问题
2015/08/17 面试题
Linux管理员面试经常问道的相关命令
2014/12/12 面试题
项目计划书范文
2014/01/09 职场文书
宣传保护环境的公益广告词
2014/03/13 职场文书
大学生社会实践方案
2014/05/11 职场文书
2014年质量工作总结
2014/11/22 职场文书
先进个人总结范文
2015/02/15 职场文书
高中运动会广播稿
2015/08/19 职场文书
python中Tkinter 窗口之输入框和文本框的实现
2021/04/12 Python