周末,跟阿里的一个朋友(去年晋升为P9了)聊了很久,聊的内容几乎全是技术,当然了,两个技术男聊得最多的话题当然就是技术了。从基础到架构,从算法到AI,无所不谈。中间又穿插着不少天马行空的想象,虽然现在看起来不太实际,但是随着技术的进步,相信五年、十年之后都会实现的。
不知道是谁提起了在高并发环境下如何构建缓存服务,结果一路停不下来了!!
缓存特征(1)命中率:命中数/(命中数+没有命中数)
(2)最大元素(空间):代表缓存中可以存放的最大元素的数量,一旦缓存中元素的数量超过这个值,或者缓存数据所占的空间超过了最大支持的空间,将会触发缓存清空策略。根据不同的场景,合理设置最大元素(空间)的值,在一定程度上可以提高缓存的命中率,从而更有效的使用缓存。
(3)清空策略:FINO(先进先出)、LFU(最少使用)、LRU(最近最少使用)、过期时间、随机等。
FINO(先进先出):最先进入缓存的数据,在缓存空间不够或超出最大元素限制的情况下,会优先被清除掉,以腾出新的空间来接收新的数据。这种策略的算法主要是比较缓存元素的创建时间,在数据实时性较高的场景下,可以选择这种策略,优先保证最新策略可用。
LFU(最少使用):无论元素是否过期,根据元素的被使用次数来判断,清除使用次数最少的元素来释放空间。算法主要是比较元素的命中次数,在保证高频数据有效的场景下,可以选择这种策略。
LRU(最近最少使用):无论元素是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素,释放空间。算法主要是比较元素最近一次被获取的时间,在热点数据场景下,可以选择这种策略。
过期时间:根据过期时间判断,清理过期时间最长的元素,或者清理最近要过期的元素。
(1)业务场景和业务需求
缓存往往适合读多写少的场景。业务需求对实时性的要求,直接会影响到缓存的过期时间和更新策略。实时性要求越低,就越适合缓存。在相同Key和相同请求数的情况下,缓存的时间越长,命中率就会越高。
(2)缓存的设计(粒度和策略)
通常情况下,缓存的粒度越小,命中率越高。缓存的更新和命中策略也会影响缓存的命中率,当数据发生变化时,直接更新缓存的值会比移除缓存或使缓存过期的命中率更高。
(3)缓存容量和基础设施
缓存的容量有限,则容易引起缓存失效和被淘汰(目前多数的缓存框架或中间件都采用了LRU算法)。同时,缓存的技术选型也是至关重要的,比如采用应用内置的本地缓存就比较容易出现单机瓶颈,而采用分布式缓存则毕竟容易扩展。所以需要做好系统容量规划,并考虑是否可扩展。此外,不同的缓存框架或中间件,其效率和稳定性也是存在差异的。
(4)其他因素
当缓存节点发生故障时,需要避免缓存失效并最大程度降低影响,这种特殊情况也是架构师需要考虑的。业内比较典型的做法就是通过一致性Hash算法,或者通过节点冗余的方式。
有些朋友可能会有这样的理解误区:既然业务需求对数据时效性要求很高,而缓存时间又会影响到缓存命中率,那么系统就别使用缓存了。其实这忽略了一个重要因素--并发。通常来讲,在相同缓存时间和key的情况下,并发越高,缓存的收益会越高,即便缓存时间很短。
提高缓存命中率的方法从架构师的角度,需要应用尽可能的通过缓存直接获取数据,并避免缓存失效。这也是比较考验架构师能力的,需要在业务需求,缓存粒度,缓存策略,技术选型等各个方面去通盘考虑并做权衡。尽可能的聚焦在高频访问且时效性要求不高的热点业务上,通过缓存预加载(预热)、增加存储容量、调整缓存粒度、更新缓存等手段来提高命中率。
对于时效性很高(或缓存空间有限),内容跨度很大(或访问很随机),并且访问量不高的应用来说缓存命中率可能长期很低,可能预热后的缓存还没来得被访问就已经过期了。
缓存的分类和应用场景(1)本地缓存:编程实现(成员变量、局部变量、静态变量)、Guava Cache
(2)分布式缓存:Memcached、Redis
(1)缓存的一致性
更新数据库成功——更新缓存失败
更新缓存成功——更新数据库失败
更新数据库成功——淘汰缓存失败
淘汰缓存成功——更新数据库失败
(2)缓存并发
并发时请求缓存时已过期或者没有命中或者更新的情况下有大量的请求访问数据库。
解决办法:在缓存更新或者过期的情况下,先尝试获取到lock,当更新完成后,尝试释放锁,其他的请求只需要牺牲一定的等待时间