一、背景
社区珍藏业务是一个典型的读多写少的场景,社区各种外围Feeds流都须要依赖用户是否珍藏的数据判断,晚期缓存设计时因为流量不是很大,未体现出显著的问题,近期通过监控平台等相干伎俩发现了相干的一些问题,因而咱们针对这些问题对缓存做了重构设计,以保障珍藏业务的性能和稳定性。
二、问题剖析定位
2.1 接口RT偏大
通过监控平台查看「判断是否珍藏接口」的RT在最高在8ms左右,该接口的次要作用是判断指定单个用户是否已珍藏一批内容,其实如果缓存命中率高的话,接口RT就应该趋近于Redis的RT程度,也就是1-2ms左右。
(图中有单根尖刺,这个具体问题要具体分析优化,咱们这里次要论述整体程度的优化)
2.2 Redis&MySQL拜访QPS偏高
通过监控平台能够看到从上游服务过去的珍藏查问QPS绝对拜访Redis缓存的QPS放大了15倍,并且MySQL查问的最高QPS占上游访问量靠近37%,这阐明缓存并没有很高的命中率,导致回表查问的概率还是很大。
QPS访问量见下图:
Redis访问量
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倍左右,最终也升高了期待连贯次数
五、总结
通过对本次问题的剖析和解决,不难看出一个良好的缓存设计对于服务来说是如许的重要。好的缓存设计不仅可能晋升性能,同时能够升高资源应用,整体晋升了资源利用率。同时上游的流量和上游根本持平,在流量上升时,不会对上游造成很大的压力,这样服务整体的抗并发能力也晋升了很多。
*文/Sky
@得物技术公众号