“第六届字节跳动青训营-点赞性能优化实践记录| 青训营”

397 阅读6分钟

今天记录一下在青训营大项目中,实现点赞模块性能优化的思路。

1.需求分析

  • 场景:当用户在刷视频Feed流时,每一次下滑,都需要对数十篇内容进行登录用户是否点赞状态的判断。同时要准备好当前视频的点赞数据。对性能要求最高的是“ 判断用户是否点赞内容 ”和“ 视频点赞数 ”场景。点赞业务吞吐量高,但是点赞数量可以接受一定的数据不一致(比如两个用户对于某一视频点赞数量在同一时刻有些许出入,并不影响业务)

  • 目标:高性能。在高并发状态下,保证高QPS时的响应速度。

  • 优化服务:用户点赞判断 、用户点赞/取消点赞行为、用户点赞计数。

2.点赞性能优化

2.1 服务架构

使用 mysql+redis+定时任务

  • 单独架一个计数服务,将计数与其他业务逻辑解耦。
  • 不能用数据库抗实时读写流量,用 MySQL 来做固化存储,Redis 做缓存,读写操作都落缓存,新开一个协程设定定时任务将 redis 中的信息持久化到 DB 。

whiteboard_exported_image.png

2.2 redis存储结构

redis 采用 hash 类型储存点赞记录缓存、string 类型存储点赞数计数缓存。

  • 点赞判断【key: "videoId", field:"userid", value:"0/1" 】判断某个用户是否点赞作品;
  • 点赞计数【key: "videoId", value: "33"】表示某个视频获得了多少点赞;

UML 图.jpg

2.3 点赞优化
(1)是否点赞判断
  • 问题描述: 是否点赞本质上就是对点赞记录存在性的判断。当用户在刷视频Feed流时,每一次下滑都需要对数十篇内容进行登录用户是否点赞状态的判断,同时每次用户对视频的点赞操作都会进行点赞判断,该判断的QPS比较高且需要快速的响应。

    • 在基础的设计中,每次对于点赞的判断直接从 mysql 的 favorite 表中进行查询。而对每个用户来说点赞与否的判断都是高频使用的场景,从服务端来说,直接查询 mysql 会出现请求量过大导致数据库压力过大、处理能力下降甚至宕机;从用户端来说,直接访问数据库会导致用户请求响应时间过长,影响用户体验。
  • 优化方案: 使用 redis 做缓存维护用户对视频点赞的记录,mysql 作为点赞信息的固化存储。 用户每次点赞存在判断都直接请求 redis,同时 redis 设置定时任务定期将记录持久化到 mysql 。但缓存也面临消耗资源量大的优化问题,进一步优化要考虑如何降低 redis 中的存储空间。

    • 方案细节如下:

      • 在 redis 中以 videoId:userId 为 key,value 为 0 代表未点赞,为 1 代表已点赞;
      • 是否点赞的判断请求直接访问 redis 得到判断结果;
      • (TODO)使用布隆过滤器降低 redis 中的内存占用。
(2)点赞计数
  • 问题描述: 当展示视频信息时,需要获取视频的点赞数;同时每次点赞/取消点赞的操作都需要对点赞计数进行修改。因此点赞计数也是一个高QPS的操作,需要快速的响应。

    • 在基础的设计中,对于点赞数的统计需要查询 mysql 遍历 favorite 表进行统计,完成后才会将计数信息返回给业务逻辑。计数服务每次都涉及到对数据库的遍历读取,随着数据量的增大该操作会导致用户请求响应时间过长,严重影响用户体验。同时,高频操作每次直接访问数据库进行遍历读取会导致数据库压力过大、处理能力下降甚至宕机。
  • 优化方案: 使用 redis 做缓存维护每条视频的点赞数量,用户直接访问 redis 获取结果。当发生点赞数量的变化时,直接使用 redis 内置的方法高效对点赞数量进行自增1/自减1的操作。

    • 方案细节如下:

      • 在 redis 中以 videoId 为 key,value 存储点赞数量;
      • 点赞计数的请求直接访问 redis 得到计数结果;
      • 当点赞/取消点赞时,使用 redis.InscrCtx 或 redis.DecrCtx 方法对 redis 中点赞计数进行自增1/自减1。
(3)点赞操作
  • 问题描述: 用户对视频的点赞是一个高频使用场景,需要很高的QPS进行数据存取操作并及时返回更新的点赞计数。对于点赞计数,遍历表项统计点赞会数非常慢,并且会随着数据量的增加和高频次的访问下使得数据库压力过大。

    • 在基础的设计中,点赞的行为会直接在 mysql 新增 favorite 表项,并根据视频 id 遍历查询点赞过的用户 id 进行点赞数统计。频繁修改使得数据库压力过大响应时间过长,同时每次都要从数据库中查询统计点赞数,响应时间非常慢。
  • 优化方案: 根据存在性判断的结果,如果为点赞行为则修改对应的点赞记录 redis 表项,并更新点赞计数的redis 表项。由于点赞服务的场景下 redis 和 mysql 之间允许存在一定的不一致性,可以定时将redis数据持久化到mysql。

    • 方案细节如下:

      • 在 redis 的 hash 存储中添加点赞记录【key: "videoId", field:"userid", value:"1" 】代表当前视频用户已点赞;
      • 2)在 redis 的 string 存储中增加视频点赞计数,使用redis.InscrCtx(ctx, key)方法进行自增1;
      • 3)开启一个定时任务将 redis 中的点赞信息记录、点赞计数记录每隔 30min 持久化到 mysql 中。

流程图.jpg

(4)取消点赞操作
  • 问题描述: 用户取消点赞与点赞的问题基本相同,频繁的修改、访问数据库不可取。

    • 在基础的设计中,取消点赞的行为会直接在 mysql 修改 favorite 表项将逻辑删除值置为 1,并根据视频 id 遍历查询点赞过的用户 id 进行点赞数统计。频繁的数据库修改、访问导致响应慢,数据库压力大。
  • 优化方案: 根据存在性判断的结果,如果为取消点赞行为则修改对应的点赞记录 redis 表项,并更新点赞计数的redis 表项,并定时将 redis 数据持久化到 mysql。

    • 方案细节如下:

      • 在 redis 的 hash 存储中添加修改记录【key: "videoId", field:"userid", value:"0" 】代表当前视频用户取消点赞;
      • 2)在 redis 的 string 存储中减少视频点赞计数,使用redis.DecrCtx(ctx, key)方法进行自减1;
      • 3)由于点赞服务外可以允许一定的不一致性,开启一个定时任务将 redis 中的点赞信息记录、点赞计数记录每隔 30min 持久化到 mysql 中。

流程图-2.jpg