真实案例-⾼并发系统的缓存设计思路
前⾔
今天的分享来⾃我职业⽣涯中的⼀个真实项⽬,在本⽂中就称之为R项⽬吧。对于⼀些不⽅便直接透露的东西,⽤⽐较通⽤的词汇来代替,尽可能完整地还原当时在业务场景与技术⽅案选择上的思考。
R项⽬服务了国内半数以上的安卓⼿机⽤户。对⽤户来说,R提供的是⽤户⽐较感兴趣的⼀些便捷服务。对企业来说,巨⼤的流量是商业化的最佳战场,意味着不菲的收⼊,关系着企业以及员⼯的钱袋⼦。
背景&问题
这是⼀个⾼并发读的服务场景,没有写⼊。
对响应时间要求苛刻,否则将严重影响⽤户体验。后来内⽹压测时的平均响应时间在10ms以下。
根据客户端参数,进⾏⼀些复杂的规则匹配与计算,然后将匹配到的数据返回。
客户端参数包括:组、版本、分类、关键词、编号、特征数组
后台配置的规则由以上参数排列组合⽽成,编号需要⽀持范围匹配,特征数组需要求交集等等。
运营后台对规则的增删改需要在五分钟内⽣效。
参数的组合太多,规则的组合也很多,这看似是⼀个时间复杂度O(n^2)的查询场景。10ms响应时间的限制由不得我们笛卡尔积似地慢慢匹配。
业界通⽤⽅案:缓存
缓存在很多互联⽹系统中都是很正常的存在,虽然具体⽤法会因为业务场景五花⼋门,但疏通同归,⽬标只有⼀个:尽最⼤可能把每⼀次请求变成时间复杂度为O(1)的查询。
在设计⽅案时,整体上我想到了以下⼏点:
1. MVVM架构思想:运营库与线上库隔离解耦,配置数据库为Model层,配置平台输出给线上库的数据属于View层,线上接⼝输出给客户端的数
据属于View-Model层。View-Model层在数据结构上⾃然体现客户端展⽰逻辑。
2. 缓存应放在离⽤户最近的地⽅:对响应时间苛刻的服务端来说可采⽤进程内缓存。带来的是分布式数据⼀致性问题,当前场景下采⽤最终⼀致
性策略,时间控制在五分钟内。access被淘汰了吗
3. 如何避免穿透、击穿、雪崩问题:多级缓存策略。整体分为进程内⼀级缓存与redis⼆级缓存,所有请求到⼀级缓存为⽌。通过异步刷新、检查
的⽅式同步⼆级缓存数据到进程内。运营平台可以随时向redis写⼊数据。
序列图如下:
将⼀直不过期。
2. expireAfterWrite(long,TimeUnit):在最后⼀次写⼊缓存后开始计时,在指定的时间后过期。
3. expireAfter(Expiry):⾃定义策略,过期时间由Expiry实现⾃定义计算。
Caffeine的驱逐会阻塞查询操作(使⽤了锁),因此要尽量可能少地发⽣驱逐,因此⼀级预计算缓存选择expireAfterAccess的驱逐策略,⼀级响应缓存采⽤expireAfterWrite策略,确保热点数据不会被提前淘汰。
对于已经过期的数据,Caffeine并不会在到期后⽴即删除,⽽是再次请求时异步检查数据是否已过期,过期则删除。
当缓存的数据达到设置的m a xsize后,如何进⾏数据淘汰?
当缓存的数据达到设置的ma
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进⾏了介绍。如果你有⼀些疑问,欢迎留⾔讨论。/知乎可搜索码神⼿记同名账号,分享关注,共同进步。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论