![cover](https://img-blog.csdnimg.cn/ce74da0ecccd4be58661671e2add7463.png)
24-Go操作redis
学redis主要是学习常用数据结构的命令:https://www.redis.com.cn/commands.html。推荐使用docker快速起一个环境go-redis库驱动包目前社区里用的比较多的是 redigo 和 go-redis我推荐使用,目前最新的是v8版本v8新版本相关最新版本的库相关命令都需要传递参数,例如:连接redis哨兵模式连接redis集群基本使用https://redis
学redis主要是学习常用数据结构的命令:https://www.redis.com.cn/commands.html。
Redis应用场景
- 缓存系统,减轻主数据库(MySQL)的压力。
- 计数场景,比如微博、抖音中的关注数和粉丝数。
- 热门排行榜,需要排序的场景特别适合使用ZSET。
- 利用LIST可以实现队列的功能。
准备redis环境
推荐使用docker快速起一个环境
docker run --name redis507 -p 6379:6379 -d redis:5.0.7
go-redis库
驱动包
目前社区里用的比较多的是 redigo 和 go-redis
我推荐使用go-redis
,目前最新的是v8版本
go get github.com/go-redis/redis/v8
v8新版本相关
最新版本的go-redis
库相关命令都需要传递context.Context
参数,例如:
普通连接
//声明一个全局的rbd变量
var (
rbd *redis.Client
)
// 初始化连接
func initClient() (err error) {
rbd = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "", // no password set
DB: 0, //use default db
PoolSize: 100, //连接池大小
})
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err = rbd.Ping(ctx).Result()
return
}
连接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
}
基本使用
https://redis.uptrace.dev/guide/hll.html
set/get示例
/ set/get示例
func redisDemo1() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// set 示例
err := rbd.Set(ctx, "name", "wanglei", time.Second*5).Err() //set命令一般要跟Err
if err != nil {
panic(err)
}
// get 示例
result, err := rbd.Get(ctx, "name").Result()
if err != nil {
panic(err)
}
fmt.Println(result)
}
zset示例
/ zset示例
func redisDemo2() {
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++"},
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
// ZADD
num, err := rbd.ZAdd(ctx, zsetKey, languages...).Result()
if err != nil {
fmt.Printf("zadd failed, err:%v\n", err)
return
}
fmt.Printf("zadd success. num:%v\n", num)
fmt.Println("-------------------------------")
// 把golang的分数加10
newScore, err := rbd.ZIncrBy(ctx, zsetKey, 10.0, "Golang").Result()
if err != nil {
fmt.Printf("zincrby failed, err:%v\n", err)
return
}
fmt.Printf("Golang scire is %v\n", newScore)
fmt.Println("-------------------------------")
// 取分数最高的3个
ret, err := rbd.ZRevRangeWithScores(ctx, 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)
}
fmt.Println("-------------------------------")
// 取95~100分的
op := &redis.ZRangeBy{
Min: "95",
Max: "100",
}
ret, err = rbd.ZRangeByScoreWithScores(ctx, 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)
}
fmt.Println("-------------------------------")
}
//运行结果:
zadd success. num:0
-------------------------------
Golang scire is 100
-------------------------------
Golang 100
C/C++ 99
Java 98
-------------------------------
Python 95
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的数量不多时,可以使用Key()
得到所有的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怎么取值
// pipelineDemo 获取pipeline结果的第一种方式
// 适合命令不多,命令返回的结果格式不太一样
func pipelineDemo() {
pipe := rdb.Pipeline()
c1 := pipe.Get(context.Background(), "wangwenjian")
c2 := pipe.HMGet(context.Background(), "wangwenjian", "age", "weight")
incr := pipe.Incr(context.Background(), "pipeline_counter")
pipe.Expire(context.Background(), "pipeline_counter", time.Hour)
// 执行命令
_, err := pipe.Exec(context.Background())
// 取结果
// 方式1
fmt.Println(c1.Int())
fmt.Println(c2.Val())
fmt.Println(incr.Val(), err)
}
// pipelineDemo2 获取pipeline结果的第二种方式
// 适合批量执行相同类型的命令(返回值类型一致)
func pipelineDemo2() {
pipe := rdb.Pipeline()
// 输入命令
for i := 1; i < 10; i++ {
pipe.Get(context.Background(), fmt.Sprintf("key%d", i))
}
// 执行命令
cmders, err := pipe.Exec(context.Background())
if err != nil && err != redis.Nil {
fmt.Println(err)
return
}
// 取结果
for _, cmder := range cmders {
v := cmder.String()
// 因为拿到的cmder是一个接口类型
// 需要自己根据上面的命令的返回值进行类型断言
switch cmder.(type) {
case *redis.StringCmd:
case *redis.IntCmd:
// case *redis.StringSliceCmd:
// // ...
}
// cmder.(*redis.StringCmd).Result()
// cmder.(*redis.StringSliceCmd).Result()
fmt.Println(v)
}
事务
Redis是单线程的,因此单个命令始终是原子的,但是来自不同的客户端的两个给定命令可以依次执行,例如在他们之间交替执行。但是,Multi/exec
能够确保在multi/exec
两个语句之间没有其他客户端正在执行命令。
在这种场景下我们需要使用TxPipeline
。TxPipeline
总体来说类似于上面的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
命令监视某个键之后,直到该用户执行了EXEC
命令的这段时间里,如果有其他用户抢先对被监视的键进行替换,更新,删除等操作,那么当用户尝试执行EXEC
的时候,事务将失败并返回一个错误。用户可以根据这个错误选择重试事务或者放弃事务。
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)
TLS模式
.Err() .Result() .Val()
func demo1() {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
// 设置值 类的命令一般用 Err()
err := rdb.Set(ctx, "name", "wuyong", 10*time.Second).Err()
fmt.Println(err)
// 获取值 类的命令后面一般用 Result()
v, err := rdb.Get(context.Background(), "name").Result()
if err != nil {
// 排除掉key不存在的场景
if err == redis.Nil {
// 返回的err是key不存在时...
}
fmt.Println(err)
return
}
fmt.Println(v, err)
// 我只想用value,如果出错了就用默认值
fmt.Println("------")
fmt.Printf("Err()==redis.Nil:%#v\n", rdb.Get(context.Background(), "namexxxxx").Err() == redis.Nil)
fmt.Printf("Err()==nil:%#v\n", rdb.Get(context.Background(), "namexxxxx").Err() == nil)
fmt.Printf("Val():%#v\n", rdb.Get(context.Background(), "namexxxxx").Val())
nv, nerr := rdb.Get(context.Background(), "namexxxxx").Result()
fmt.Printf("Result():%#v %#v\n", nv, nerr)
}
万能do命令
val, err := rdb.Do(ctx, "get", "key").Result()
if err != nil {
if err == redis.Nil {
fmt.Println("key does not exists")
return
}
panic(err)
}
fmt.Println(val.(string))
redis.Nil
查询redis操作会返回err错误,返回错误有两种情况
- 查询本身出错了
- 查的key不存在,redis.Nil 就是redis包定义的 key不存在的err
v, err := rdb.Get(context.Background(), "name").Result()
if err != nil {
// 排除掉key不存在的场景
if err == redis.Nil {
// 返回的err是key不存在时...
}
fmt.Println(err)
return
}
fmt.Println(v, err)
练习
把redis的zeset简单实现一个类似微博热搜排行榜的例子。
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
//把redis的zeset简单实现一个类似微博热搜排行榜的例子。
// 声明全局rdb对象
var (
rdb *redis.Client
)
// 定义新闻member常量,便于后面调用
const (
HotNews1 string = "向东航飞行事故遇难同胞默哀"
HotNews2 string = "**被拿下"
HotNews3 string = "长春就买菜难问题致歉"
HotNews4 string = "妻子实名举报医生丈夫收回扣养小三"
HotNews5 string = "完美世界董事长去世"
)
// 初始化连接
func initClient() (err error) {
rdb = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "",
DB: 0,
PoolSize: 100, //连接池大小
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
_, err = rdb.Ping(ctx).Result()
if err != nil {
panic(err)
}
return
}
// 用zset实现排行榜
func weibo(click int) {
zsetKey := "weibo_ranking"
news := []*redis.Z{
&redis.Z{Score: 4913039.0, Member: HotNews1},
&redis.Z{Score: 4901509.0, Member: HotNews2},
&redis.Z{Score: 4799345.0, Member: HotNews3},
&redis.Z{Score: 4382951.0, Member: HotNews4},
&redis.Z{Score: 4008611.0, Member: HotNews5},
}
// ZADD 将news存入redis
num, err := rdb.ZAdd(context.Background(), zsetKey, news...).Result()
if err != nil {
fmt.Printf("news加入redis失败,err:%v\n", err)
return
}
fmt.Printf("news加入redis成功,加入的新闻news个数:%v\n", num)
// 点击后score加1
switch click {
case 1:
newScore, err := rdb.ZIncrBy(context.Background(), zsetKey, 1.0, HotNews1).Result()
if err != nil {
fmt.Printf("HotNews1的分数加1失败,err:%v\n", err)
return
}
fmt.Printf("HotNews1的分数加1成功,新的分数:%v\n", int64(newScore))
case 2:
newScore, err := rdb.ZIncrBy(context.Background(), zsetKey, 1.0, HotNews2).Result()
if err != nil {
fmt.Printf("HotNews2的分数加1失败,err:%v\n", err)
return
}
fmt.Printf("HotNews2的分数加1成功,新的分数:%v\n", int64(newScore))
case 3:
newScore, err := rdb.ZIncrBy(context.Background(), zsetKey, 1.0, HotNews3).Result()
if err != nil {
fmt.Printf("HotNews3的分数加1失败,err:%v\n", err)
return
}
fmt.Printf("HotNews3的分数加1成功,新的分数:%v\n", int64(newScore))
case 4:
newScore, err := rdb.ZIncrBy(context.Background(), zsetKey, 1.0, HotNews4).Result()
if err != nil {
fmt.Printf("HotNews4的分数加1失败,err:%v\n", err)
return
}
fmt.Printf("HotNews4的分数加1成功,新的分数:%v\n", int64(newScore))
case 5:
newScore, err := rdb.ZIncrBy(context.Background(), zsetKey, 1.0, HotNews5).Result()
if err != nil {
fmt.Printf("HotNews5的分数加1失败,err:%v\n", err)
return
}
fmt.Printf("HotNews5的分数加1成功,新的分数:%v\n", int64(newScore))
default:
fmt.Println("无效输入,请重新输入")
}
// 取热搜Top3
ret, err := rdb.ZRevRangeWithScores(context.Background(), zsetKey, 0, 2).Result()
if err != nil {
fmt.Printf("查询热搜Top3失败,err:%v\n", err)
return
}
fmt.Println("获取热搜top3:")
for _, z := range ret {
fmt.Printf("新闻是: %v 点击量:%v\n", z.Member, int64(z.Score))
}
}
func main() {
if err := initClient(); err != nil {
panic(err)
}
// 通过获取终端输入,模拟实现点击
var click int
for {
fmt.Print("输入点击的新闻:")
fmt.Scan(&click)
weibo(click)
}
// weibo(1)
}
![Logo](https://devpress.csdnimg.cn/79de2bf0b7994defa4242ef90d5513fa.jpg)
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)