从腾讯会议列表优化看《缓存设计与中间件协调》

原文主要内容

原文:https://mp.weixin.qq.com/s/DQ6juZBexn3IY_ZaI1x0DQ

主要优化的就是录制的列表的查询。

接口主要的挑战就是

  • 数据的字段多

  • 多数据源

  • 数据的修改的同步的实时性

主菜单-录制列表

总结

深分页解决方案

  1. 延迟join
1
2
select * from t_records inner join 
( select id from t_records where uid = '{my_uid}' limit X,30; ) as t2 using (id)

就是直接分页的时候只扫描出id

  1. seek method
1
select id from t_records where uid = '{my_uid}' and id> {last_id} limit 30;

记录上次的索引id,选择大于这个id的记录

弊端:

  1. 无法支持跳页

这个在c端来说,不是很受到影响

排序有问题

在文章中,如果需要排序,例如根据创建时间排序,那么就不用id作为上次的划分,而使用创建时间大于上一次查询中的最后一条记录的创建时间,不知道为什么不在上诉2.seek method 中再加入order by的条件

文章的解决方案:

    1. 例如创建时间:需要保证id越大的创建时间越大。(雪花算法)
    2. 改成大于等于,小于等于(创建时间可能出现重复)
      1. 另外出来的问题就是,有重复数据,需要去重

    还不如在查出来后在内存中进行排序

缓存问题—重头戏

原文: 解决完深分页的问题,不意味着我们的列表就能经得起高并发的冲击,它最多意味着不会随着翻页的进行而性能断崖式下降。但是,一旦请求量很大的话,很可能第一页的请求不一定扛得住。

为了提升整个列表的性能,肯定要做一定的缓存设计。

如何进行缓存设计也是一个大问题。作为这个我第一个进行博客输出的架构设计文章,我以前设计的缓存真是简陋,或者说基本没有缓存。

大公司好像都是有单独的缓存中间件,文章中也写了腾讯有自己的缓存中间件。以前不能理解为什么会再包装一层缓存中间件,是大厂的数据和业务逻辑都很复杂,所以自己又抽了一层出来。

缓存的设计确实得好好考虑,毕竟缓存中间件一般都是和消息中间件一起出来了。

文章中缓存的设计不是根据我以前的思想:根据用户id进行缓存,而是进行了类似两层的缓存。

把mysql中用户的录制列表的id存进redis中,然后再根据这些id再去redis缓存

缓存设计相关

三种方案:

  1. 按照用户id查出来的列表的所有数据进行缓存
  2. ID 查询+元素缓存
    1. 另外一个可行的方案是先查询出这一页的 ID 数据,然后再针对 ID 去查询对应页面所需要的其他详情数据。
  3. ID 列表缓存+元素缓存

方案1问题:

  1. 一致性维护的困难。例如新增、删除视频的时候,固然需要维护这个 List 的一致性。你能想到可能是新增、删除的时候,直接对 list 进行遍历,找到对应的视频进行新增或者删除操作即可。但实际上在读、写并发场景下,动态维护缓存是很容易导致不一致的。如果为了更好的一致性考虑,可以考虑有变更的时候便删除掉整个 list 缓存。
  2. 过于频繁的维护缓存。无论走修改策略还是删除策略,其维护的时机就是缓存的内容发生变动的时候。缓存整个结果意味着结果变动的内容可能性非常大。例如录制的状态是经常发生变更的:新建->录制中、转码中、转码完成、完成等等。录制的标题也是可能发生变动的,录制的打击状态也是随时可能变的,权限也是可能被管理员修改的。这些单一录制的任意字段都可能需要对整个 list 缓存进行维护(修改/删除),如果采取的是删除策略,那么频繁的维护动作会导致缓存经常失效而性能提升有限。如果采取更新策略则又维护困难且有一致性的问题。
  3. 缓存扩散维护的困难。这个在“我的录制”里不存在这样的问题,但是假设我们把“我的录制”的功能范围扩充为:属于我的录制以及我看过的录制。这种情况下用首页结果缓存就会有巨大挑战。因为一个视频 X 可能被 N 个人看到。但是每个人都有自己的首页缓存(一个人一条 list 的 redis 结构),当 X 的某个字段(例如标题)发生变动的时候,我们需要找到这 N 个人的 list 结构。你可能会说,可以采取 keys 的操作找出来这些 list 去维护即可。但是 keys 操作是 O(N) 时间复杂度的操作,性能极差。哪怕我们采取 scan 去替换 keys,在 N 极大的情况下,这里的损耗也是非常巨大的。

方案2问题

需要多次查询缓存。对于没查到缓存的数据,还得去数据库中查询出数据后放进缓存中

方案3

方案一和方案二的结合。

这里的集合结构体可以采取 ZSet,也可以采取 List。采取 Zset 的场景大概率是动态更新策略的场景,而采取 List 的场景则更多是动态删除策略的场景。

多数据源问题

这个我没遇过,但是提前剧透:直接再套一层

  1. 复杂查询:需要union all
  2. 跨库分页&排序问题
    1. 比如文章中我发的朋友圈+别人给你看的朋友圈

好句:

这也是优秀后台研发工程师的价值所在——同样一模一样的功能,要以一个高性能、高可用的标准去实现它,这两者无论从设计还是实现上根本就不是同一个东西。

解决方案

问题分析

  1. 性能查询负担,每个数据库的数据量都是千万-亿级别的,本身查询的耗时客观摆在这里。做了重度工作之后还需要做各种集合运算,成本很高。最后出来的效果就是接口的耗时很高,反映到录制面板体验上就是用户需要等待较高的时间才能看到数据,这对于用户体验追求极致的团队而言是无法接受的。
  2. 稳定性挑战。每次查询亿级的数据源本身就是一个“高危动作”,如果一个亿级数据库查询成功率是99%,那么四个数据库都成功的概率就会下降到96%。
  3. 成本。为了实现困难且耗时高的查询,我可以选择购买足够高配置的数据库、优化数据库的部分配置参数去抗住查询数据的压力,提升查询成功率。同时在服务器中,通过代码的一些优化,并发多协程地去并行化四个查询的动作。最后部署足够多的机器,应该也能使得整体的稳定性提升,但是这样需要非常高的设备成本,在“降本增效”的大背景下是无法承受的。这是一种用战术勤奋去掩盖战略勤奋的做法。
  4. 分页挑战。如果说通过内存聚合还能勉强做到功能的可用。但是引入分页后这个问题变得几乎无解,因为在一个分布式系统中,要聚合第 N 页的数据需要合并所有系统的前 N 页数据才能计算得出,注意是计算前 N 页不是第 N 页,相当于做一个多路归并排序!也就是翻页越深,查找量和计算量越大。而且我们这个场景更复杂,分页后还需要剔除一部分删除记录,其挑战如下图所示:

可以看的记录+自己手动删除的记录

最后使用了MongoDB

那还有什么问题吗?大V问题

使用用户id+文件id进行分库分表

数据一致性解决方案

一般这种情况都是使用了中间件mq进行解决

可靠消息+离线对账

mq的问题

消息失败/消息丢失:kafka的at-least-once ,再加上人工告警功能

消息幂等:

文章解决方案:自研的一套幂等方案

字段的一致性问题

实体的字段可能会进行修改,所以只缓存索引,不缓存整个实体

最后:技术道路还是好远啊,慢慢学习吧


从腾讯会议列表优化看《缓存设计与中间件协调》
https://hexo-blog-five-swart.vercel.app/2024/10/20/读腾讯会议优化设计有感-缓存设计/
作者
方立
发布于
2024年10月20日
许可协议