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 🔗

lomtom

标题:Go(四)Redis还不会使用?

作者:lomtom

链接:https://lomtom.cn/399e7555