性能优化之redis大项目整合实践 | 青训营

200 阅读6分钟

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上浪费过多的时间。

三,应用思路

  1. 通过对抖音的业务逻辑进行分析我画了如下思维导图

    user1转存失败,建议直接上传图片文件

    可以看出对于video的点赞,用户之间的关注,在实际业务场景其实是应用的非常多的,但是同时这也给数据库造成了比较大的读写压力,如果同时太多人进行业务访问会造成非常严重的卡顿,这是数据库的性能瓶颈

  2. 但是如果在业务层和底层数据层加一层缓存,专门处理这样的频繁的交互逻辑,进行更快的数据返回,就可以很大程度上缓解数据库的压力,如图

    逻辑转存失败,建议直接上传图片文件

    于是我进行了如下调整,

四,代码实现

首先配置redis:

  1. 在config.yaml文件中配置参数

    #redis配置
    #maxidle连接池最大空闲数
    #maxactive连接池最大连接数为零不设上限
    redis:
      ipaddress: ***.*.*.*
      port: ****
      maxidle: *
      maxactive: *
    
    #.......其他配置
    
  2. 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
    }
    
  3. 缓存层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
    }
    
  4. 建立用户关系记录(例)

    // 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
    }
    
  5. 判断函数(例)

    // 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
    }
    
  6. 删除关系函数(例)

    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
    }
    
  7. 对于单一用户的各字段计数函数(例)

    // 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
    }
    
  8. 关于获取用户计数(例)

    // getUserCountKey 关于user计数的key
    func getUserCountKey(userid int64) string {
    	return fmt.Sprintf("%s_%d", "UserCountKey", userid)
    }
    

总结

​ 以上是个例中对于redis的应用场景,在业务层调用时就可以在redis对应连接中生成数据,在一些类似点赞,关注等数据交互较多的场景中可以有效减少数据库的读写压力