Article

Go(四)Redis还不会使用?

Go(四)Redis还不会使用?

作者:lomtom

个人网站:lomtom.cn 🔗

个人公众号:博思奥园 🔗

你的支持就是我最大的动力。

Go 系列:

  1. Go(一)基础入门
  2. Go(二)结构体
  3. Go(三)Go 配置文件
  4. Go(四)Redis 还不会使用?
  5. Go(五)Go 不知道怎么用 Gorm?
  6. Go(六)来来来,教你怎么远程调用
  7. Go(七)你说你不会并发?
  8. Go(八)还不知道函数式选项模式?

redis

底层为 C 语言 解决 hash 冲突类似于 1.7 的 hashmap

redis 概念

  1. 非关系型的键值对数据库,可以根据键以 O(1)的时间复杂度取出或插入关联值
  2. Reds 的数据是存在内存中的
  3. 键值对中键的类型可以是字符串,整型,浮点型等,且键是唯一的
  4. 键值对中的值类型可以是 string,hash,list,set, sorted set 等
  5. Reds 内置了复制,磁盘持久化,LUA 脚本,事务,SSL,客户端代理等功能
  6. 通过 Reds 哨兵和自动分区提供高可用性

应用场景

  1. 计数器 可以对 Sng 进行自增自减运算,从而实现计数器功能。Reds 这种内存型数据库的读写性能非常高, 很适合存储频繁读写的计数量
  2. 分布式 D 生成 利用自增特性,一次请求一个大一点的步长如 incr2000,缓存在本地使用,用完再请求。
  3. 海量数据统计 位图(btmp):存储是否参过次活动,是否已读某篇文章,用户是否为会员,日活统计。
  4. 会话缓存 可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就 不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
  5. 分布式队列/阻塞队列 List 是一个双向链表,可以通过 push/push 和 rpop/pop 写入和读取消息。可以通过使用 brpop/b|pop 来实现阻塞队列

String(最大存储 512M)

String 是 redis 中使用最多的存储类型

1、数据结构(3.2 之前): sds:simple dynamic string 是一个二进制安全数组 sds:

  • length: 长度
  • free:剩余长度
  • char [] :{1,12,324,123}

2、数据结构(3.2 之后) 根据存储的内容来

3、扩容机制 容量不够时,扩容为原来的两倍,直到 1024k,不再成倍增加,而是以 1024k 的增加。

Go 中使用 Redis

Go 中也有很多比较流行的并且开源 Redis 库,比如go-redis 🔗redigo 🔗,在 github 上,分别 12.3k 和 8.6k 的 star 数量(截止到 2021.09.03)

在这里将以 go-redis 为例。

安装

第一步:安装go-redis

请勿省略版本号
go get github.com/go-redis/redis/v8

配置连接

第二步:连接 Redis 服务器 连接 Redis 服务器有两种方法,第一种使用redis.Options,第二种就是使用redis.ParseURL

1第一种
import "github.com/go-redis/redis/v8"

rdb := redis.NewClient(&redis.Options{
	Addr:	  "localhost:6379",
	Password: "", // no password set
	DB:		  0,  // use default DB
})

2第二种
opt, err := redis.ParseURL("redis://<user>:<pass>@localhost:6379/<db>")
if err != nil {
	panic(err)
}

rdb := redis.NewClient(opt)

那么,我这里使用从我的配置里读取,不会使用 go 读取配置文件可以参考

Go(三)Go 配置文件 🔗


import (
	"context"
	"github.com/go-redis/redis/v8"
	"log"
)

var rdb *redis.Client

// GetRedis 获取连接
func GetRedis() *redis.Client {
	return rdb
}

func InitRedis() {
	rdb =  redis.NewClient(&redis.Options{
		Addr:     Redis.Host + ":" + Redis.Port,
		Password: Redis.Password,
		DB:       Redis.DB,
	})
	_,err := rdb.Ping(context.Background()).Result()
	if err != nil{
		log.Printf("redis connect get failed.%v",err)
		return
	}
	log.Printf("redis init success")
}

Ping() 旧版本是不需要参数的,从 v8 版本开始 需要参数 context.Context

编写存取逻辑

第三步:封装 存/获取值 函数

网上大部分教程都是在 v8 之前的,而在 v8 需要传入context.Context,所以在存取时需要额外增加一个参数

1、存值
var ctx = context.Background()

func SetString(key string,value interface{}) error  {
	if key == "" || value == nil {
		return  errors.New(common.ArgsIsNull)
	}
	rdb := config.GetRedis()
	err := rdb.Set(ctx,key,value,0).Err()
	if err != nil {
		return errors.New(err.Error())
	}
	log.Infof("push key: %v, value: %v",key,value)
	return nil
}


2、取值
func GetString(key string) (interface{},error)  {
	if key == "" {
		return  "",errors.New(common.ArgsIsNull)
	}
	rdb := config.GetRedis()
	res,err := rdb.Get(ctx,key).Result()
	if err == redis.Nil{
		return "",errors.New(fmt.Sprintf(common.ResIsNull,key))
	}
	if err != nil {
		return "",errors.New(err.Error())
	}
	return res,nil
}

说明:

  1. Set 除了context、键、值外,还需要传入一个过期时间
  2. Get 方法,返回错误码可能是 redis 当中不含有该值,所以做一个特殊处理

使用

第四步:使用

func  Test(test *testing.T)  {
	err := SetString("1", "12")
	if err != nil {
		return
	}
}
func  Test1(test *testing.T)  {
	res,err := GetString("12")
	if err != nil {
		log.Debug(err)
	}else {
		log.Debug(res)
	}
}

把值为12 push 到键为1,然后在 redis 中查看

1、redis控制台
Connecting ...

Connected.
redis:0>get 1
"12"

2、代码获取
=== RUN   Test1
2021-09-03 20:30:38	DEBUG	redis/redis_test.go:40	12
--- PASS: Test1 (0.00s)
PASS

Redis 发布/订阅模式

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。

值得注意的是:订阅者接收不到启动之前的消息。

订阅者

func Receive(key ...string) error {
	if len(key) <= 0 {
		return errors.New(common.ArgsIsNull)
	}
	rdb := config.GetRedis()
	sub := rdb.Subscribe(ctx, key...)
	defer time.AfterFunc(time.Second, func() {
		_ = sub.Close()
	})
	_, err := sub.Receive(ctx)
	if err != nil {
		return errors.New(err.Error())
	}
	// 设置channel参数 https://github.com/go-redis/redis/issues/1850
	ch := sub.Channel(
		redis.WithChannelSize(100),
		redis.WithChannelHealthCheckInterval(10*time.Second),
		redis.WithChannelSendTimeout(3000 * time.Second),
	)
	for msg := range ch {
		// todo 执行操作
		fmt.Println(msg.Channel, msg.Payload)
	}
	return nil
}

发布者

func Publish(key string, value interface{}) error {
	if key == "" || value == nil {
		return errors.New(common.ArgsIsNull)
	}
	rdb := config.GetRedis()
	err := rdb.Publish(ctx, key, value).Err()
	if err != nil {
		return errors.New(err.Error())
	}
	log.Infof("Publish key: %v, value: %v", key, value)
	return nil
}

注:如果需要Publish自定义结构体,需要实现MarshalBinary方法。

type Msg struct {
	Title   string `form:"title" json:"title" binding:"required"`     // 标题
	Content string `form:"content" json:"content" binding:"required"` // 内容
}

当我尝试Publish 自定结构体,就会这样的错误提示,这告诉我们需要将我们的结构体转为二进制形式

redis: can't marshal *dto.Msg (implement encoding.BinaryMarshaler)

这里有两种解决方法: 1、我们可以把Msg 结构体实现MarshalBinary方法,Redis 将自动执行所有操作

func (m *Msg) MarshalBinary() (data []byte, err error) {
	return json.Marshal(m)
}

2、或者可以选择在Publish前自己手动转为二进制形式

func  Test3(test *testing.T)  {
	msg := &dto.Msg{Title: "123123",Content: "123123"}
	marshal, err1 := json.Marshal(msg)
	if err1 != nil {
		return
	}
	err := Publish(common.Sms,marshal)
	if err != nil {
		log.Debug(err)
	}
}

Redis 小技巧

1、为什么要使用 mset 或者 mget

减少带带宽和 io,因为 redis 需要封装 resp 协议(需要封装 tcp 协议与 ip 协议,各需要 20byte),如果使用 set 或 get 去设置或获取(k,v)会封装两次,而是用 mset k1 v1 k2 v2,就会减少封装次数,减少带宽和 io。

2、现在系统有千万级的活跃用户,如何实现日活统计,为了增强用户粘性,要上线一个连续打卡发放积分的功能,怎么实现连续打卡用户统计。

使用 setbit 设值、getbit、bitcount 统计 参考:https://blog.csdn.net/hgd613/article/details/54095729 🔗

Copyright

本文为原创内容,欢迎分享与引用,请保留作者与原文链接。

文章标题

Go(四)Redis还不会使用?

作者

lomtom

发布方式

原创发布

原文链接 https://lomtom.cn/399e7555
More Reads