redis在字节青训营大项目实践中的应用
一,简介
听了字节青训营的关于redis的应用的课程之后,自己对于redis有了更深刻的理解,于是准备利用redis内存读取速度快的特性,来缓解大项目中点赞,关注等需要对数据操作频繁的动作对于数据库的读写压力
二,redis基本原理
-
Redis是一个key-value存储系统,它支持的value类型相对较多,包括string、list、set和zset,这些数据都支持push/pop/add/remove及交并补等操作,而且这些操作都是原子性的,在此基础上,redis支持各种不同方式的排序。为了保证效率,数据是缓存在内存中的,Redis会周期性的把数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave同步
-
Redis支持丰富的数据类型,最为常用的数据类型主要有五种:String、Hash、List、Set和Sort Set,Redis通常将数据存储到内存中,或被配置为使用虚拟内存,Redis有一个很重要的特点就是它可以实现持久化数据,通过两种方式可以实现数据持久化,一是RDB快照方式,将内存中的数据不断写入磁盘, 二是使用类似MySql的AOF日志方式,记录每次更新的日志,前者性能较高,但是可能会引起一定程度的数据丢失,后者相反,Redis支持即将数据到多台子数据库上,这种特性提高读取数据性能非常有益
-
工作方式
- 多样的数据模型
- 持久化
- 主从同步
-
分布式可扩展性
- 刚开始的版本可以在客户端实现,也可以使用代理;后来Redis Cluster是一个实现了分布式且允许单点故障的Redis高级版本,它没有中心节点,各个节点地位一致,具有线性可伸缩的功能。Redis Cluster的分布式存储结构,其中节点与节点之间通过二进制协议进行通讯,节点与客户端之间通过ascii协议进行通信,在数据的放置策略上,Redis Cluster将整个key的数值域分成16384个哈希槽。每个节点上可以存储一个或多个哈希槽,也就是说当前Redis Cluster支持的最大节点就是16384
-
为什么快
主要是以下几点点
-
一)、纯内存操作
数据存放在内存中,内存的响应时间大约是 100纳秒 ,这是Redis每秒万亿级别访问的重要基础。
-
二)、单线程操作,避免了频繁的上下文切换
虽然是采用单线程,但是单线程避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU;虽然作者认为CPU不是瓶颈,内存与网络带宽才是。但实际测试时并非如此,见上。
-
三)、采用了非阻塞I/O多路复用机制
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。加上Redis自身的事件处理模型将epoll中的连接,读写,关闭都转换为了事件,不在I/O上浪费过多的时间。
-
三,应用思路
-
通过对抖音的业务逻辑进行分析我画了如下思维导图
可以看出对于video的点赞,用户之间的关注,在实际业务场景其实是应用的非常多的,但是同时这也给数据库造成了比较大的读写压力,如果同时太多人进行业务访问会造成非常严重的卡顿,这是数据库的性能瓶颈
-
但是如果在业务层和底层数据层加一层缓存,专门处理这样的频繁的交互逻辑,进行更快的数据返回,就可以很大程度上缓解数据库的压力,如图
于是我进行了如下调整,
四,代码实现
首先配置redis:
-
在config.yaml文件中配置参数
#redis配置 #maxidle连接池最大空闲数 #maxactive连接池最大连接数为零不设上限 redis: ipaddress: ***.*.*.* port: **** maxidle: * maxactive: * #.......其他配置 -
config.go中对redis进行配置
type Redis struct { Ipaddress string `yaml:"ipaddress"` Port string `yaml:"port"` Maxidle int `yaml:"maxidle"` Maxactive int `yaml:"maxactive"` } //........其他配置 var C Config func ConfInit() error { yamlFile, err := os.ReadFile("./config/config.yaml") if err != nil { fmt.Println(err.Error()) return err } // 将读取的yaml文件解析为响应的 struct err = yaml.Unmarshal(yamlFile, &C) if err != nil { fmt.Println(err.Error()) return err } return nil } -
缓存层redis.go编写初始化函数
// RedisPool 数据库连接池 var RedisPool *redis.Pool // RedisPoolInit 初始化数据库redis连接池 func RedisPoolInit() error { RedisPool = &redis.Pool{ MaxIdle: config.C.Redis.Maxidle, //最大空闲数 MaxActive: config.C.Redis.Maxactive, //最大连接数,0不设上 Wait: true, IdleTimeout: time.Duration(1) * time.Second, //空闲等待时间 Dial: func() (redis.Conn, error) { c, err := redis.Dial("tcp", config.C.Redis.Ipaddress+":"+config.C.Redis.Port) //redis IP地址 if err != nil { fmt.Println(err) return nil, err } redis.DialDatabase(0) return c, err }, } return nil } -
建立用户关系记录(例)
// SetUserRelation 建立用户和用户的关系集合 func SetUserRelation(userid, touserId int64) error { conn := RedisPool.Get() defer func(conn redis.Conn) { err := conn.Close() if err != nil { } }(conn) key := getUserRelationKey(userid) //往集合中加关注的人 _, err := conn.Do("SADD", key, touserId) if err != nil { log.Println(err) } return nil } -
判断函数(例)
// IsUserRelation 判断是否在集合中 func IsUserRelation(userid, touserId int64) bool { conn := RedisPool.Get() defer func(conn redis.Conn) { err := conn.Close() if err != nil { } }(conn) key := getUserRelationKey(userid) res, err := redis.Int64(conn.Do("SISMEMBER", key, touserId)) if err != nil { log.Println(err.Error()) return false } if res == 0 { fmt.Printf("%#v", res) return false } return true } -
删除关系函数(例)
func DeleteUserRelation(userid, touserId int64) error { conn := RedisPool.Get() defer func(conn redis.Conn) { err := conn.Close() if err != nil { } }(conn) key := getUserRelationKey(userid) _, err := conn.Do("SREM", key, touserId) if err != nil { return err } return nil } -
对于单一用户的各字段计数函数(例)
// SetUserCount 设置user计数 func SetUserCount(userid int64) error { conn := RedisPool.Get() defer func(conn redis.Conn) { err := conn.Close() if err != nil { } }(conn) key := getUserCountKey(userid) _, err := conn.Do("hmset", redis.Args{key}.AddFlat(map[string]int64{ "followCount": 0, "followerCount": 0, "workCount": 0, "favoriteCount": 0, "totalFavorite": 0, })...) if err != nil { return err } return nil } -
关于获取用户计数(例)
// getUserCountKey 关于user计数的key func getUserCountKey(userid int64) string { return fmt.Sprintf("%s_%d", "UserCountKey", userid) }
总结
以上是个例中对于redis的应用场景,在业务层调用时就可以在redis对应连接中生成数据,在一些类似点赞,关注等数据交互较多的场景中可以有效减少数据库的读写压力