真实案例-高并发系统的缓存设计思路

作者 : 开心源码 本文共2731个字,预计阅读时间需要7分钟 发布时间: 2022-05-14 共158人阅读

前言

今天的分享来自我职业生涯中的一个真实项目,在本文中就称之为R项目吧。对于少量不方便直接透露的东西,用比较通用的词汇来代替,尽可能完整地复原当时在业务场景与技术方案选择上的思考。
R项目服务了国内半数以上的安卓手机客户。对客户来说,R提供的是客户比较感兴趣的少量便捷服务。对企业来说,巨大的流量是商业化的最佳战场,意味着不菲的收入,关系着企业以及员工的钱袋子。

背景&问题

  • 这是一个高并发读的服务场景,没有写入。
  • 对响应时间要求苛刻,否则将严重影响客户体验。后来内网压测时的平均响应时间在10ms以下。
  • 根据用户端参数,进行少量复杂的规则匹配与计算,而后将匹配到的数据返回。
  • 用户端参数包括:群组、版本、分类、关键词、编号、特征数组
  • 后端配置的规则由以上参数排列组合而成,编号需要支持范围匹配,特征数组需要求交集等等。
  • 经营后端对规则的增删改需要在五分钟内生效。

参数的组合太多,规则的组合也很多,这看似是一个时间复杂度O(n^2)的查询场景。10ms响应时间的限制由不得我们笛卡尔积似地慢慢匹配。

业界通用方案:缓存

缓存在很多互联网系统中都是很正常的存在,尽管具体用法会由于业务场景五花八门,但疏通同归,目标只有一个:尽最大可能把每一次请求变成时间复杂度为O(1)的查询。

在设计方案时,整体上我想到了以下几点:

  1. MVVM架构思想:经营库与线上库隔离解耦,配置数据库为Model层,配置平台输出给线上库的数据属于View层,线上接口输出给用户端的数据属于View-Model层。View-Model层在数据结构上自然表现用户端展现逻辑。
  2. 缓存应放在离客户最近的地方:对响应时间苛刻的服务端来说可采用进程内缓存。带来的是分布式数据一致性问题,当前场景下采用最终一致性策略,时间控制在五分钟内。
  3. 如何避免穿透、击穿、雪崩问题:多级缓存策略。整体分为进程内一级缓存与redis二级缓存,所有请求到一级缓存为止。通过异步刷新、检查的方式同步二级缓存数据到进程内。经营平台可以随时向redis写入数据。

序列图如下:

R项目多级缓存策略.png

缓存key的生成策略

准则:key的粒度要掌握好,必需能覆盖大部分请求,在内存成本与响应速度之间做好权衡(trade-off)

补充业务背景信息:因为群组、版本、编号、分类、关键词的个数是有限的,一个大的人群特征通常也相同。

群组+版本+编号+分类+关键词+特征组合起来后生成MD5值,该值作为缓存key。

过期&刷新时间策略

准则:根据数据生效时间的限制,使各个部分的时间满足一个或者多个不等式。

  • 一级响应缓存过期时间:T1(1分钟)
  • 一级估计算缓存刷新时间:T2(1分钟,决定了数据生效的最小时间)
  • 一级估计算缓存过期时间:T3(4分钟,决定了数据生效的最大时间),达到key数量上限时LRU淘汰。
  • 定时同步一级估计算缓存时间:每T4分钟(4分钟),从redis检查一次新上线的数据,增加到一级估计算缓存中。

为了保证数据的升级在5分钟内生效,几个时间的设置应满足以下关系:
T1+T4<=5
T1+T3<=5
T1+T2<5,对于持续高频命中一级估计算缓存的key(每个T2时间区间内都有一次命中),数据升级后的生效时间是会提前的,所需时间是:T1+T2。T1+T2<5。

避免没有必要的计算

准则:通过一套规则来预判能否需要进行下一步计算。注定没有结果的计算就不需要缓存任何数据,避免内存白费。

补充业务背景信息:关键词的总个数有限,但至少数十万。经营配置能覆盖的是少数头部关键词。其它的关键词不会有任何结果。

因而,将配置库中的所有关键词放到内存中,形成一套索引。以O(1)时间就可判断一个关键词能否需要有后续计算。

进程内缓存开发库:Caffeine

现在,我们已经确认了一个适用于实际场景的多级缓存方案。尽可能地降低了响应时间,避免了CPU和内存资源的白费。接下来就要看看有没有现成的技术组件可以帮助我们快速实现方案,毕竟造轮子是有成本的,找一个优秀的轮子有助于快速实现商业价值。

Redis是业界比较通用的选择,数据量相对较多的二级估计算缓存放在Redis里。既能满足容量要求,又能满足加载速度的要求,让新的配置更快生效。在这个项目中对Redis的要求并不高,一个单点足以。但系统对Redis的qps达到单点上限时,进行水平扩容就可。

进程内缓存使用的是Caffeine。

Caffeine的数据驱逐策略

它支持基于大小、时间、引用三种驱逐策略,我们选择基于时间的策略。该策略下又分为三种定时驱逐策略:

  1. expireAfterAccess(long,TimeUnit):在最后一次访问或者者写入后开始计时,在指定的时间后过期。如果一直有请求访问该key,那么该缓存将一直不过期。
  2. expireAfterWrite(long,TimeUnit):在最后一次写入缓存后开始计时,在指定的时间后过期。
  3. expireAfter(Expiry):自己设置策略,过期时间由Expiry实现自己设置计算。

Caffeine的驱逐会阻塞查询操作(使用了锁),因而要尽量可能少地发生驱逐,因而一级估计算缓存选择expireAfterAccess的驱逐策略,一级响应缓存采用expireAfterWrite策略,确保热点数据不会被提前淘汰。
对于已经过期的数据,Caffeine并不会在到期后立即删除,而是再次请求时异步检查数据能否已过期,过期则删除。

当缓存的数据达到设置的maxsize后,如何进行数据淘汰?

Caffeine将缓存的数据分为三部分(三个队列):

  1. Eden 新生队列,容量为size的1%,记录的是新生的数据,防止突发流量时,因为新数据没有访问频率而被淘汰。
  2. Probation 缓刑队列,真正进行数据淘汰的地方,容量为size的79.2%。Eden满了之后,Eden中访问频率较低的数据会进入Probation队列,Probation满了之后会淘汰访问频率低的数据。
  3. Protected 保护队列,容量为size的19.8%。当Probation中的某个数据被访问后,这个数据将会进入Protected队列。当Protected满了或者Probation中没有数据了,仍然会将访问频率低的数据放入Probation队列。
    Probation队列在进行数据淘汰时,是先比较头部和尾部数据使用频率,频率低则淘汰。当尾部数据频率小于5时,直接淘汰尾部数据。Caffeine的作者认为设置这样一个预热值,更有利于提升命中率。

结语

以上内容从业务背景出发,引出缓存设计的少量方法论(准则),最后对进程内缓存库Caffeine进行了详情。假如你有少量疑问,欢迎留言探讨。微信/知乎可搜索码神手记同名账号,分享关注,共同进步。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 真实案例-高并发系统的缓存设计思路

发表回复