作者:盛磊,单位:中国移动智慧家庭运营中心
缓存技术在高流量、大并发的应用服务中是一把利器,使用缓存可以降低数据库访问压力、提高接口响应速度。缓存技术分为本地缓存和分布式缓存,二者各有利弊。本地缓存无法在集群中进行共享,存在应用服务重启数据丢失、需要重新预热加载的问题,而分布式缓存如redis、Memcached可以解决此类问题。但是由于本地缓存没有分布式缓存的网络io耗时和集中化依赖问题,依然在很多业务场景中有着独到的应用。本文主要介绍现有的主流本地缓存技术以及挑战,并提出一种自研本地缓存技术。
Part 01● 本地缓存使用场景 ●
在程序中,有些表数据,数据量有限,但是程序启动就会马上访问,并且访问的很频繁,比如(例如配置参数,区域信息)。针对这种情况,可以将数据放到程序的本地缓存中即内存中,从而提高系统的访问效率、减少数据库访问。此外,相比本地缓存,数据库访问、分布式缓存会占用连接,存在网络消耗,本地缓存只需要考虑缓存占用的内存空间、缓存的失效策略。数据库、本地缓存及分布式缓存的区别如下表所示:
Part 02● 现状和挑战 ●
2.1 Map
Map是一种k-v数据结构,非常方便于自己实现本地缓存,比如使用HashMap全局变量,主要需要考虑启动或调用时加载数据、线程安全、内存泄漏、内存溢出等问题。常用的ConcurrentHashMap通过Node数组、链表、红黑树等数据结构,实现数据分段和锁保护,兼顾了访问性能和安全性。
此外,自己实现本地缓存好处在于,可以灵活控制缓存写入时机,可以结合业务应用的启动时机、通知机制或调用,自己控制未命中时查询并写入还是服务启动时就全量预热。
但是,通过Map自研实现本地缓存,需要显式删除才能将数据从缓存中清理;如果不考虑内存大小限制,一旦候选缓存数据量很大,容易出现内存溢出问题,造成服务崩溃造成重大线上问题,而自研实现缓存限制策略又增加了复杂性和维护风险。因此,要不要自研实现本地缓存,需要综合考虑待缓存数据量与技术难度风险。
2.2 Guava Cache
Guava Cache是google实现的开源本地缓存技术,开箱即用,解决了实际应用中遇到的大多问题,是常用的本地缓存技术。构建一个本地缓存实例示例代码如下:
Guava cache类似于concurrentHashMap,但是与之不同的是,concurrentHashMap需要显式地删除缓存,同时难以控制对本地内存的使用量。在缓存失效策略和本地内存占用控制方面,GuavaCache都有灵活的可选择控制策略。
2.3 缓存失效策略
2.3.1基于大小的失效策略
2.3.2基于时间的失效策略
2.3.3基于引用的失效
此外,GuavaCache支持本地缓存对象删除的监听机制,在实际应用中,可以通过分析key删除的原因,综合评价本地缓存大小和失效策略设计的合理性,方便进一步优化本地缓存设置。
2.4 Caffeine
Caffeine cache与Guava cache非常类似,比如上面讲到的guava cache三大类过期策略,caffeine都有。但是由于Guava cache基于LRU淘汰算法,而Caffeine 因为使用了 Window-TinyLFU 缓存淘汰策略,提供了一个近乎最佳的命中率,综合了 LRU 和 LFU 算法的长处,使其成为本地缓存之王。
Window-TinyLFU算法原理如上图所示,基本原理是将Cache分成了几个区,新数据放到Window Cache,满了之后使用LRU进行晋升Probation Cache,然后再根据TinyLYU算法决定是否再次晋级,或者淘汰,详细的晋升和淘汰机制可以百度学习。看到这里,是不是觉得这套算法逻辑颇像JVM的分代回收算法,果然分区而治才是王道。
Caffeine封神除了淘汰算法无敌之外,还提供了AsyncLoadingCache,可以自定义线程池,多线程异步的好处在于调用方可以针对某些获取源数据耗时选择阻塞等待或非阻塞,防止因为某些少量超时导致load阻塞问题。
Caffeine还内置了统计功能,通过Caffeine.recordStats()打开数据收集,然后Cache.stats()方法将会返回当前缓存的一些统计指标,例如:
hitRate():查询缓存的命中率
evictionCount():被驱逐的缓存数量
averageLoadPenalty():新值被载入的平均耗时
不过开启统计功能会有一些性能损耗,这个需要具体评估。
Part 03● 自研本地缓存技术 ●
上述介绍的中间件技术,基本可以满足绝大多数开发场景,但是在实际应用中,经常遇到需要全量缓存表数据的场景,比如规则引擎中配置的通用规则,告警过滤规则等,这种基于全量预热数据的本地缓存需求,主要要求缓存更新的实时性与数据完整性,与上述基于key-value命中的缓存机制不太相符,因此这里介绍一种自研的缓存全量数据的本地缓存技术。
该技术是要缓存表中全量有效数据,基于三个关键的表设计字段:update_time(更新时间)、status(数据状态)、unique_key(表征数据唯一或通过计算保证唯一的字段),通过内置的单线程定时任务,实现本地缓存的增量更新和全量更新,同时兼顾了缓存更新的时效性和准确性,由于不需要依赖其他中间件,可以有效应用在所有的业务系统中。基本原理如下图所示:
1.服务启动时执行一次全量数据加载,开启定时任务,并记录当前时间t0为全量更新时间t1和增量更新时间t2。
2.每次定时任务执行时,如果(当前时间t-上次全量更新时间t1)>全量更新时间阈值max1,则执行一次全量数据对齐,并刷新全量更新时间t1=t。当然这个策略看实际需求是否有必要。
3.如果未触发全量更新,会增量查询 (当前时间t-上次增量更新时间t2)时间区间内表中更新数据,并更新t2=t。
4.如果第3步查询到有更新数据,则增量更新到本地缓存(要求表设计为逻辑删除)。
5.定时任务不断重复2~4步骤即可。
Part 04● 总结 ●
本地缓存是服务开发者做性能优化的重要技术手段之一,本篇介绍了常用的几种本地缓存技术方案。本着拿来主义的原则,通过需求场景评估后,如果这些成熟的本地缓存中间件能够满足需求,则优先选用,如guava cache, caffeine。自己实现的好处在于能够灵活控制本地缓存读、写、失效、监听等各种时机,可以更加深入融合实际需求,但也存在内存控制、内存泄漏、并发问题等挑战,所以需要综合评判。