学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两个语句之间没有其他客户端正在执行命令。

在这种场景下我们需要使用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命令监视某个键之后,直到该用户执行了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模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JFMB8fAF-1657248846808)(day11 课上笔记.assets/image-20220327144410788.png)]

.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错误,返回错误有两种情况

  1. 查询本身出错了
  2. 查的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

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐