【深入浅出 Spring 原理及实战】「缓存 Cache 开发系列」带你深入分析 Spring 所提供的缓存 Cache 抽象详解的核心原理探索
缓存的理解
缓存的工作机制是先从缓存中读取数据,如果没有再从慢速设备上读取实际数据,并将数据存入缓存中。通常情况下,我们会将那些经常读取且不经常修改的数据或昂贵(CPU/IO)的且对于相同请求有相同计算结果的数据存储到缓存中。
它能够让数据更加接近于使用者,下图所示。
这个图表示在应用程序和数据源之间添加了一个缓存层。在访问数据时,应用程序首先会尝试从缓存层中获取数据。如果数据已经存在于缓存中,则直接返回。否则,需要从数据源获取数据,并将其存储到缓存中,以便以后能够更快地获取相同的数据。这样做可以显著提高应用程序的性能和响应速度,并减轻数据源的负载。
缓存使用的案例
一个典型的例子是 CPU--L1/L2--内存--磁盘的存储结构。当 CPU 需要数据时,它会先从 L1/L2 缓存中读取数据,如果没有则到内存中查找,最后再到磁盘上找。此外,在使用 Maven 时,我们的依赖通常会先从本地仓库中查找,然后再到本地服务器仓库查找,最后才去远程仓库服务器查找。京东的物流为什么如此快?这是因为他们在各个地方都建立了分仓库,如果货物在该仓库内,则配送速度非常快。
缓存命中率
即从缓存中读取数据的次数 与 总读取次数的比率,命中率越高越好:
命中率 = 从缓存中读取次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])Miss 率 = 没有从缓存中读取的次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])这是一个非常重要的监控指标,如果做缓存一定要健康这个指标来看缓存是否工作良好;
基本概念
在合理应用缓存前,需要了解缓存领域里相关的几个常用术语:
【缓存命中】: 表示数据已经存在于缓存中,可以直接从缓存获取数据,无需重新访问原始数据源。
【缓存未命中】:表示数据不在缓存中,如果缓存空间有剩余,则会将数据添加到缓存中。
【存储成本】:当出现缓存未命中时,需要从原始数据源拉取数据并将其放置到缓存中。这个操作的时间和空间成本称为“存储成本”。
【缓存失效】:当数据源发生更改时,缓存中的数据将变得过时,需要更新或清除缓存以确保下次请求时能够访问最新数据。
【缓存污染】:将不常访问的数据存储到缓存中,可能会占用缓存空间,导致高频访问的数据无法存储到缓存中。
【移除策略】(Eviction policy):即如果缓存满了,从缓存中移除数据的策略;常见的有 LFU、LRU、FIFO 等,需要通过替换策略删除缓存中的一些数据来腾出空间。常见的替换策略包括以下几种:
先进先出(FIFO,First In First Out):先进先出算法,即删除缓存中最早添加的数据。
最近最少使用(LRU,Least Recently Used):最久未使用算法,删除最长时间未被访问的数据,使用时间距离现在最久的那个被移除;
最少使用(LFU,Least Frequently Used):最近最少使用算法,删除最少访问次数的数据,一定时间段内使用次数(频率)最少的那个被移除;
随机替换(RR):随机选择一个数据进行删除。
【存活期】TTL(Time To Live ),即从缓存中创建时间点开始直到它到期的一个时间段(不管在这个时间段内有没有访问都将过期)
【空闲期】TTI(Time To Idle),即一个数据多久没被访问将从缓存中移除的时间。
到此,我们已经基本了解了缓存的概念。在 Java 中,我们通常会对方法调用进行缓存控制,例如对于方法 "getValueByKey(String key)",我们应该首先从缓存中查找是否有对应的数据,如果没有再调用该方法从数据库中加载用户数据,并将其添加到缓存中。这样,在下一次调用相同的方法时,就可以直接从缓存中获取数据,而无需再次访问数据库。
Spring Cache API 及默认提供的实现
自 Spring 3.1 起,提供了 Cache 支持,且提供了 Cache 抽象,在此之前一般通过 AOP 实现,使用 Spring Cache 的好处:提供基本的 Cache 抽象,方便切换各种底层 Cache;通过注解 Cache 可以实现类似于事务一样,缓存逻辑透明的应用到我们的业务代码上,且只需要更少的代码就可以完成;
对于 Spring Cache 抽象,主要从以下几个方面学习:
Spring 提供的核心 Cache 接口
开发者首先可以通过 Cache API 与默认提供的 Cache 实现进行交互,获取并存储数据。而在一些情况下,可以通过使用 Cache 注解来简化对缓存的使用。此外,如果需要实现更复杂的缓存逻辑,也可以自己实现相应的缓存逻辑。
Java 代码
通过上面的源码可以看出来,Spring-Cache 是提供了缓存操作的读取/写入/移除方法。
默认提供了如下实现:
ConcurrentMapCache:使用 java.util.concurrent.ConcurrentHashMap 实现的 Cache;
GuavaCache:对 Guava com.google.common.cache.Cache 进行的 Wrapper,需要 Google Guava 12.0 或更高版本,@since spring 4;
EhCacheCache:使用 Ehcache 实现
JCacheCache:对 javax.cache.Cache 进行的 wrapper,@since spring 3.2;spring4 将此类更新到 JCache 0.11 版本;
另外,因为我们在应用中并不是使用一个 Cache,而是多个,因此 Spring 还提供了 CacheManager 抽象,用于缓存的管理:
Java 代码
默认提供的实现:
ConcurrentMapCacheManager/ConcurrentMapCacheFactoryBean:管理 ConcurrentMapCache;GuavaCacheManager;
EhCacheCacheManager/EhCacheManagerFactoryBean;
JCacheCacheManager/JCacheManagerFactoryBean;
另外还提供了 CompositeCacheManager 用于组合 CacheManager,即可以从多个 CacheManager 中轮询得到相应的 Cache,如
Java 代码
当我们调用 cacheManager.getCache(cacheName) 时,会先从第一个 cacheManager 中查找有没有 cacheName 的 cache,如果没有接着查找第二个,如果最后找不到,因为 fallbackToNoOpCache=true,那么将返回一个 NOP 的 Cache 否则返回 null。
除了 GuavaCacheManager 之外,其他 Cache 都支持 Spring 事务的,即如果事务回滚了,Cache 的数据也会移除掉。
Spring 不进行 Cache 的缓存策略的维护,这些都是由底层 Cache 自己实现,Spring 只是提供了一个 Wrapper,提供一套对外一致的 API。
Spring 缓存的问题
Spring 缓存模糊匹配 Evict 的问题
缓存都是 key-value 风格的,模糊匹配本来就不应该是 Cache 要做的;而是通过自己的缓存代码实现;
Spring cache 的缺陷
假设有一个缓存,其中存储了 list<User>类型的数据。现在我执行了一个 update(user)方法,但并不想清除整个缓存,而只是希望替换掉更新过的元素。
在现有的缓存抽象中,并没有很好的解决方案,可以考虑通过在之前的 Helper 方法中使用 condition 来解决这个问题,但这并不是一种十分优雅的方式。
因此,我认为 Spring Cache 注解还存在一些不完美之处。针对这种情况,我们可以通过对 Cacheable 注解进行增强来实现更加灵活的缓存控制。具体地说,我们可以在 Cacheable 注解中添加 SpEL 表达式,用于定制要缓存的数据、方法执行前后的条件以及缓存成功后的其他逻辑。例如:
当然,对于大多数场景,Spring Cache 注解已经足够使用。如果需要处理更加复杂的场景,则可以考虑使用 AOP。如果需要自定义实现缓存逻辑,则应该使用 Spring Cache API 进行缓存抽象。
版权声明: 本文为 InfoQ 作者【洛神灬殇】的原创文章。
原文链接:【http://xie.infoq.cn/article/52aaa3d89daa0786b5fd81c18】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论 (1 条评论)