Go(四)Redis还不会使用?
- November 24, 2021
作者:lomtom
个人网站:lomtom.cn 🔗
个人公众号:博思奥园 🔗
你的支持就是我最大的动力。
Go系列:
- Go(一)基础入门
- Go(二)结构体
- Go(三)Go配置文件
- Go(四)Redis还不会使用?
- Go(五)Go不知道怎么用Gorm?
- Go(六)来来来,教你怎么远程调用
- Go(七)你说你不会并发?
- Go(八)还不知道函数式选项模式?
redis
底层为C语言 解决hash冲突类似于1.7的hashmap
redis概念
- 非关系型的键值对数据库,可以根据键以O(1)的时间复杂度取出或插入关联值
- Reds的数据是存在内存中的
- 键值对中键的类型可以是字符串,整型,浮点型等,且键是唯一的
- 键值对中的值类型可以是 string,hash,list,set, sorted set等
- Reds内置了复制,磁盘持久化,LUA脚本,事务,SSL,客户端代理等功能
- 通过Reds哨兵和自动分区提供高可用性
应用场景
- 计数器 可以对Sng进行自增自减运算,从而实现计数器功能。Reds这种内存型数据库的读写性能非常高, 很适合存储频繁读写的计数量
- 分布式D生成 利用自增特性,一次请求一个大一点的步长如incr2000,缓存在本地使用,用完再请求。
- 海量数据统计 位图(btmp):存储是否参过次活动,是否已读某篇文章,用户是否为会员,日活统计。
- 会话缓存 可以使用 Redis来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就 不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
- 分布式队列/阻塞队列 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读取配置文件可以参考
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
}
说明:
- Set除了
context
、键、值外,还需要传入一个过期时间 - 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 🔗