社区收藏缓存设计重构实战

待兔
• 阅读 252

一、背景

社区收藏业务是一个典型的读多写少的场景,社区各种核心Feeds流都需要依赖用户是否收藏的数据判断,早期缓存设计时由于流量不是很大,未体现出明显的问题,近期通过监控平台等相关手段发现了相关的一些问题,因此我们针对这些问题对缓存做了重构设计,以保障收藏业务的性能和稳定性。

二、问题分析定位

2.1 接口RT偏大 通过监控平台查看「判断是否收藏接口」的RT在最高在8ms左右,该接口的主要作用是判断指定单个用户是否已收藏一批内容,其实如果缓存命中率高的话,接口RT就应该趋近于Redis的RT水平,也就是1-2ms左右。

社区收藏缓存设计重构实战

社区收藏缓存设计重构实战

2.2 Redis&MySQL访问QPS偏高

通过监控平台可以看到从上游服务过来的收藏查询QPS相对访问Redis缓存的QPS放大了15倍,并且MySQL查询的最高QPS占上游访问量接近37%,这说明缓存并没有很高的命中率,导致回表查询的概率还是很大。

QPS访问量见下图:

社区收藏缓存设计重构实战

MySQL访问量

社区收藏缓存设计重构实战

基于以上分析我们现在有了明确的优化切入点,接下来我们来看下具体的找下原因是什么。

接下来我们来看一下伪代码的实现:

//判断用户是否对指定的动态收藏
func IsLightContent(userId uint64,contentIds []uint64){
    index := userId%20
    cacheKey := key + "_" + fmt.Sprintf("%d", index)
    pipe := redis.GetClient().Pipeline()
    for _, item := range contentIds {
        InitCache(userId, contentId)
        pipe.SisMember(cacheKey, userId)
    }
    pipe.Exec()
    //......
}

//缓存初始化判断,不存在则初始化数据缓存
func InitCache(userId uint64,contentId uint64){
    index := userId%20
    cacheKey := key + "_" + fmt.Sprintf("%d", index)
    ttl,_ := redis.GetClient().TTL(cacheKey)
    if ttl <= 0{//key不存在或者未设置过期时间
        // query from db
        // sql := "select userId from trendFav where userId%20 = index and content_id = contentId"
        // save to redis
    }else{
       redis.GetClient().Expire(cacheKey,time.Hour()*48)
    }
}

从上面的伪代码中,我们能够很清晰的看到,该方法会遍历内容id集合,然后对每个内容去查询缓存下来的用户集合,判断该当前用户是否收藏。也就是说缓存设计是按照内容维度和用户1:N来设计的,将单个动态下所有收藏过内容的用户id查出来缓存起来。并且基于大Key的考虑,代码又将用户集合分片成20组。这无疑又再次放大了Redis缓存Key的数量。并且每个Key都使用TTL命令来判断是否过期。这样一来Redis的QPS和缓存Key就会被放大很多倍。

正是由于分片策略+缓存时效短,导致了MySQL查询的QPS居高不下。

三、解决方案

基于以上对问题的分析定位,我们思考的解决思路就是一次接口请求降低Redis查询操作,尽可能减少放大的情况,初步判断有如下两个实现路径:

  • 去掉遍历内容查询,改为一次性查询
  • 去掉用户集分片存储,改为单Key存储

上游的调用参数用户和内容是一对多的关系,因此要实现的Redis查询也是要满足一对多的关系,那么显而易见我们的缓存应该是按照用户的维度来存储已经收藏过的内容集合。

用户收藏的内容比较少的话,我们很简单的就可以从数据库全部查询出来放在缓存,但如果用户收藏的内容比较多呢,那也会可能造成大Key问题,如果继续分片存储的话又会回到了原来的方案。我们讨论出以下两种方案:

  • 方案1. 处理大数据大部分常规思路就是要么分片,要么冷热分离

因为业务逻辑的特点,推荐流下用户看到的内容绝大部份基本都是一年以内的,我们可以缓存用户一年以内的收藏内容,这样就限制了用户收藏的极端数量。如果看到的内容发布超过一年时间,可以用MySQL直接查询,这种场景的case概率是很小的。但仔细考虑了下实现,这个需要依赖业务方,我们需要去查询内容的发布时间,以此来判断是否在我们的缓存内,这样会加重整个接口的逻辑,反而得不偿失,因此该思路很快就被否定了。

  • 方案2. 既然不能依赖第三方,就是要从自身拥有的信息上,来能够缓存一部分最热的数据,使得查询能够大范围落到这些数据

我们目前只有内容id,而内容id都是纯数字,数字本身的话可以按照大小来排列。业务查询本身都是最近一段时间的内容,所以查询的内容id都是近期较大的id。那我们可以按照内容id降序排列,取用户收藏过的若干条数据来缓存。只要查询的id都比缓存最小的id大,那么我们就可以只通过缓存来判断出用户是否收藏这些内容了。

示例

初始化缓存时我们按照内容id降序排列,拿到前5000个内容id:

如果查询结果不满5000,那么这个用户缓存了全部收藏记录,此时小缓存的内容id为0

如果大于等于5000,说明还有部分未缓存的记录,此时最小缓存的内容id为第5000个内容ID

等到查询判断时,将查询的内容id数组和缓存的最小内容id对比,如果全部大于,则说明都在缓存范围内,如果有小于,则是超过缓存范围,届时单独去数据库判断,当然这种概率在业务上的发生几率是比较小的。

这里缓存的数量的抉择显得尤为重要,如果太小,那缓存的命中率不高,导致MySQL回表查询概率变大,如果太大,则初始化时比较耗费时间,或产生大Key问题。经过分析线上数据,目前以5000这个数字能够比较好的权衡。

下面是查询缓存判断流程图:

社区收藏缓存设计重构实战

缓存方式由原来的set结构,改为Hash结构,TTL延长到7 * 24 hour。

社区收藏缓存设计重构实战

这样一来,原来的独立调用的TTL和sismember命令,可以合并成一个Hmget命令,减少了一半的Redis访问次数,这个改进收益是相当可观的。

四、优化成果

截止本文撰写时,我们对收藏的功能进行了优化改造并上线,取得了很不错的进展。所有数据为最近7天的数据4.14 - 4.20,优化效果在4.15号17点左右开始。

4.1 RPC接口响应RT降低

1 IsCollectionContent

RPC接口,判断动态是否缓存。平均RT提高了接近3倍。并且RT比较稳定

社区收藏缓存设计重构实战

4.2 Redis负载降低

1 TTL 查询

查询Key有效期,用来判断延长Key有效期。QPS直接降到0

社区收藏缓存设计重构实战

2 SISMEMBER查询

原来旧的收藏缓存查询,已经改为HMGET查询QPS降低到0

社区收藏缓存设计重构实战

3 HMGET查询

新的收藏缓存查询QPS数量和上游过来查询的QPS正好能对应上

社区收藏缓存设计重构实战

4 Redis 内存降低

新的缓存较旧缓存在占用内存和Key数量这2个指标均降低了3倍左右

4.3 MySQL负载降低

1 content_collection表select查询降低

QPS降低了24倍左右并且保持在一个比较稳定的水位

社区收藏缓存设计重构实战

2 MySQL连接并发数降低

查询QPS的减少也降低了并发连接数,大概降低了3倍左右,最终也降低了等待连接次数

社区收藏缓存设计重构实战

社区收藏缓存设计重构实战

五、总结

经过对本次问题的分析和解决,不难看出一个良好的缓存设计对于服务来说是多么的重要。好的缓存设计不仅能够提升性能,同时可以降低资源使用,整体提升了资源利用率。同时下游的流量和上游基本持平,在流量上升时,不会对下游造成很大的压力,这样服务整体的抗并发能力也提升了很多。

点赞
收藏
评论区
推荐文章
待兔 待兔
3年前
一个免费的开源的html转markdown语法的工具
一个免费的开源的html转markdown语法的工具大家好,我是待兔,今天为大家分享一个由www.helloworld.net网站开发并开源的一个非常好用的工具html2md现在好的技术文章确实多,每天各种技术群里,各种技术社区,有很多质量非常好的技术文章,于是我们就收藏了,可是问题来了,我们收藏到哪呢?怎么收藏呢?1.微信群里发的文
Souleigh ✨ Souleigh ✨
2年前
程序员博客发文利器-html2md 更新指南
背景介绍html2md 是由 helloworld开发者社区 开源的一款轻量级功能强大的html转md工具,纯前端开发,不需要后端接口( NodeJS赋能),支持多平台,一键将文章链接转换为md,方便大家收藏和保存文章。界面如下:,欢迎Star相关介绍:技术实现1.技术栈vue 前端三剑客之一,主张最少,具有高度灵活性的渐进式框架nu
想天浏览器 想天浏览器
1年前
想天浏览器功能【超级收藏夹】功能分析
收藏夹是浏览器提供的网址收藏功能,也是浏览器的核心功能之一。用户可以利用收藏夹添加、删除、编辑收藏的网址。传统的浏览器收藏夹保存在本地,以供用户使用保存有收藏夹数据的浏览器时使用。在本地收藏夹的基础上,现有技术中还提供了网络收藏夹,用户在注册网络收藏夹服务后,用户可随时随地登录并使用同一类浏览器的收藏夹数据。实现了收藏夹数据的远程备份和随时恢复功能。超级收藏
Stella981 Stella981
2年前
Guava的两种本地缓存策略
Guava的两种缓存策略缓存在很多场景下都需要使用,如果电商网站的商品类别的查询,订单查询,用户基本信息的查询等等,针对这种读多写少的业务,都可以考虑使用到缓存。在一般的缓存系统中,除了分布式缓存,还会有多级缓存,在提升一定性能的前提下,可以在一定程度上避免缓存击穿或缓存雪崩,也能降低分布式缓存的负载。Guav
Wesley13 Wesley13
2年前
UC手机浏览器js加入收藏夹
概述对于某些网站来说,让用户一键把网页加入收藏夹的设计是非常棒的,它能提醒用户把网页加入收藏夹,从而增加用户的回访率,使网站获得更多的流量。在PC端,只有ie和ff支持用js把网页加入收藏夹的操作,在移动端目前都不支持把网页加入收藏夹,除了uc手机浏览器,因为uc手机浏览器用的U4内核,经过了一些处理。由于U
Wesley13 Wesley13
2年前
mysql5.7.26 基于GTID的主从复制环境搭建
mysql5.7.26基于GTID的主从复制环境搭建时间:2019090616:10:21    阅读:20    评论:0    收藏:0    \点我收藏\标签:connect(https://www.oschina.net/action/GoToLink?
Stella981 Stella981
2年前
Redis缓存穿透问题及解决方案
上周在工作中遇到了一个问题场景,即查询商品的配件信息时(商品:配件为1:N的关系),如若商品并未配置配件信息,则查数据库为空,且不会加入缓存,这就会导致,下次在查询同样商品的配件时,由于缓存未命中,则仍旧会查底层数据库,所以缓存就一直未起到应有的作用,当并发流量大时,会很容易把DB打垮。缓存穿透问题缓存穿透是指查询一个根本不存在的数
Stella981 Stella981
2年前
Redis 缓存问题(13)
缓存使用场景针对读多写少的高并发场景,我们可以使用缓存来提升查询速度。当我们使用Redis作为缓存的时候,一般流程是这样的:!(https://oscimg.oschina.net/oscnet/upeaebdc309cb8e78cbf34403e980ce4ef402.png)因为这些数据是很少修改的,所以在绝大部分的情况下可
凿壁偷光 凿壁偷光
1年前
音乐收藏优化软件-illustrate PerfectTUNES for mac
收藏的专辑或者是歌曲是否损坏?如何修复损坏的音乐收藏?macw推荐这款illustratePerfectTUNES破解版,PerfectTUNES从缺少插图的专辑、重复的曲目到损坏的曲目。纠正这些问题可能是一项耗时的任务。
京东云开发者 京东云开发者
2星期前
对号入座,快看看你的应用系统用了哪些高并发技术?
一系统简介百舸流量运营平台承接着京东金融APP核心资源位和京东APP部分重要资源位,大促单接口QPS达到10w,压测单接口到20w,典型的c端读链路高并发场景。接下来,聊聊我们的系统都有哪些应对高并发的“武功秘籍”。二“武功秘籍”1缓存(redis缓存
待兔
待兔
Lv1
男 · helloworld公司 · CTO - helloworld开发者社区站长
helloworld开发者社区网站站长
文章
89
粉丝
44
获赞
77