欢迎访问我的个人博客,《一个缓存使用的思考:Spring Cache VS Caffeine 原生 API》

最近在学习本地缓存发现,在 Spring 技术栈的开发中,既可以使用 Spring Cache 的注解形式操作缓存,也可用各种缓存方案的原生 API。那么是否 Spring 官方提供的就是最合适的方案呢?那么本文将通过一个案例来为你揭晓。

Spring Cache

Since version 3.1, the Spring Framework provides support for transparently adding caching to an existing Spring application. The caching abstraction allows consistent use of various caching solutions with minimal impact on the code.

Spring Cache 和 slf4j、jdbc 类似,是由 Spring Framwork 提供的一个缓存抽象层,可以接入各种缓存解决方案来进行使用,通过 Spring Cache 的集成,我们只需要通过一组注解来操作缓存就可以了。目前支持的有 GenericJCache (JSR-107)EhCache 2.xHazelcastInfinispanCouchbaseRedisCaffeineSimple,几乎包含了主流的本地缓存方案。

其主要的原理就是向 Spring Context 中注入 Cache 和 CacheManager 这两个 bean,再通过 Spring Boot 的自动装配技术,会根据项目中的配置文件自动注入合适的 Cache 和 CacheManager 实现。

本地缓存方案

Java 技术栈中成熟的本地缓存方案已经有很多了,有大而全的 ehcache,也有后起之秀 Google Guava Cache。下面是常用的三大本地缓存方案的对比,引用自博客 如何优雅的设计和使用缓存?

项目 Ehcache Guava Cache Caffeine
读写性能 好,需要做淘汰操作 很好
淘汰算法 支持多种淘汰算法, LRU,LFU,FIFO LRU,一般 W-TinyLFU, 很好
功能丰富程度 功能很丰富 功能很丰富,支持刷新和虚引用等 功能和 Guava Cache 类似
工具大小 很大,最新版本 1.4MB 是 Guava 工具类中的一个小部分,较小 一般,最新版本 644KB
是否持久化
是否支持集群

目前比较推荐的是 Caffeine,淘汰算法比较先进,并且得到 Spring Cache 的支持(新版的 Spring Cache 不再支持 Guava Cache)。下文的代码也是使用 Caffeine 的原生 API 的。

案例

使用过 Spring Cache 的人应该会发现,通过几个注解就能够轻松实现缓存的 CRUD 操作,并且替换其他的缓存方案不需要对代码进行改动吗,同时也不需要写例如下文的样板代码:

{
// 缓存命中
if(cache.getIfPresent(key) != null){
// todo
}else{
// 缓存未命中,IO 获取数据,结果存入缓存
Object value = repo.getFromDB(key);
cache.put(key,value);
}
}

那学到这里,我就产生了疑惑,既然 Spring 出了缓存的注解化开发,并且大量的博客也都在往 Spring Cache 上引,那还是否需要用原生 API 呢?毕竟在 Spring Data JPA 出现后,我们的确很少关注后端 ORM 框架,也不再直接使用 Hibernate 了。

当我实现了项目中的一个需求,这个问题好像就豁然开朗了。

其实需求很简单,原本在本地 HashMap 中维护的一个映射表,由于后期需要频繁改动而放到了数据库中。但由于数据量并不大且不配置映射表时,数据保持不变,因此既然在学习缓存,就想把它加进去。那么现在需要做的就是:

  1. 一个读取映射表全表的方法 aliasMap()。并缓存数据到 Caffeine。
  2. 一个支持映射记录 CRUD 操作的页面,且修改映射表时,更新缓存。
@Cacheable(value = "default", key = "#root.methodName")
@Override
public Map<String, String> aliasMap() {
return getMapFromDB();
}

由于 Spring Cache 的注解一般是添加在类或者方法上的,换而言之,缓存的是方法返回的对象。显然,通过某个方法来触发另一个缓存中的对象的更新是行不通的。这样是否意味着 Spring Cache 无法实现了呢?仔细去看一下 Spring Cache 的原理,其实还是可行的。

Spring Cache 会向 Spring Context 中注入 Cache 和 CacheManager 这两个 bean,再通过 Spring Boot 的自动装配技术,根据项目中的配置文件自动注入合适的 Cache 和 CacheManager 实现。再看到 CaffeineCacheManager 的源码:

public class CaffeineCacheManager implements CacheManager {
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap(16);
private boolean dynamic = true;
private Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
@Nullable
private CacheLoader<Object, Object> cacheLoader;
private boolean allowNullValues = true;
}

显然,缓存是存在 cacheMap 这样一个 ConcurrentHashMap 中,那只要我们能够手动去获取到这个 bean 的实例去操作它,那么这个需求就可以实现了,代码如下:

@Autowired
private CacheManager cacheManager;
@Cacheable(value = "default", key = "#root.methodName")
@Override
public Map<String, String> aliasMap() {
return getMapFromDB();
} private Map<String, String> getMapFromDB() {
Map<String, String> map = new HashMap<>();
List<PartAlias> list = repository.findAll();
list.forEach(x -> map.put(x.getAlias(), x.getName()));
return map;
} @Override
public PartAlias saveOrUpdateWithCache(PartAlias obj) {
PartAlias partAlias = repository.saveAndFlush(obj);
Cache cache = cacheManager.getCache("default");
cache.clear();
cache.put("aliasMap", getMapFromDB());
return partAlias;
}

经过测试,上面的代码是可行的。显然,遇到一些稍微复杂的需求,仅仅依靠 Spring Cache 的注解是远远不够的,我们需要自己去操作 cache 对象。如果使用原生 API 就非常简单了,能应对不同的需求。

What's More

上面的需求,Spring Cache 尚且还是能够处理的,但是如果要实现数据的自动加载和刷新呢?现在 Spring Cache 并不能够很好的支持。

spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1024
cache-names: cache1,cache2

上面的代码是用来配置 cache 的,结合上文 CaffeineCacheManager 的源码,我们可以知道,Spring Cache 的配置是全局的,也就是说例如最大条数、过期时间等参数是为全体缓存进行设置的,无法单独为某个缓存设置。而在 Caffeine 中用于数据加载和刷新的 CacheLoader 也是 CaffeineCacheManager 这个 bean 共有的,因此也就失去存在的意义,毕竟每个缓存的加载和数据刷新的方式是不可能相同的。

因此,在遇到复杂场景下, 还是得上原生 API 的,Spring Cache 就显得心有余而力不足了。笔者也写个一个工具类,可以全局使用缓存。

@Component
public class CaffeineCacheManager {
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16); /**
* 缓存创建
*
* @param cacheName
* @param cache
*/
public void createCache(String cacheName, Cache cache) {
cacheMap.put(cacheName, cache);
} /**
* 缓存获取
*
* @param name
* @return
*/
public synchronized Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache == null) {
throw new IllegalArgumentException("No this cache.");
}
return cache;
} @Autowired
private static CaffeineCacheManager manager;
public static void main(String[] args) {
manager.createCache("default", Caffeine.newBuilder()
.maximumSize(1024)
.build());
Cache<String, Object> cache = manager.getCache("default");
// TODO
}
}

当然,再来提一提,既然是 Spring 的套路,总是会给开发者留一条后路的,如果愿意折腾的,可以阅读 CacheManager 的代码,再根据自己需求重新实现,从而管理自己的 cache 实例。

总结

本文不是一篇介绍 Spring Cache 和 Caffeine 用法的文章(有需要可以阅读参考文献),而是在探讨 Spring Cache 和 Caffeine 的原生 API 的使用场景。显然,Spring 全家桶有时未必是最优的解决方案(有能力重写的另当别论了)!所以也希望网上有更多的博客可以 focus on 框架本身的使用,而不是千篇一律的各种集成到 Spring xxx。

附录

yaml 配置

initialCapacity: # 初始的缓存空间大小
maximumSize: # 缓存的最大条数
maximumWeight: # 缓存的最大权重
expireAfterAccess: # 最后一次写入或访问后经过固定时间过期
expireAfterWrite: # 最后一次写入后经过固定时间过期
refreshAfterWrite: # 创建缓存或者最近一次更新缓存后经过固定的时间间隔,刷新缓存
weakKeys: # 打开 key 的弱引用
weakValues: # 打开 value 的弱引用
softValues: # 打开 value 的软引用
recordStats: # 开发统计功能

原理篇

合理使用缓存

缓存的目的主要是为了降低主主数据库的压力,服务可以直接从缓存中获取数据,从而提高响应速度,让原本有限的资源可以服务更多的用户。

从工程的角度来说,缓存的引入并不是盲目的,如果主数据库压力不大的情况,并不需要添加缓存。多添加一个数据中间件显然也会增加维护的成本,而且在实际使用过程中还会存在一些,例如缓存击穿、缓存雪崩等问题。

基本概念

  • 命中率。 返回正确结果数 / 请求缓存次数, 命中率越高,表明缓存的使用率越高。
  • 最大元素。缓存中可以存放元素的最大数目, 一旦超过,会通过合适的策略进行清空操作。
  • 清空策略:FIFO、LFU、LRU

缓存类型

缓存根据存储的方式可以分成本地缓存和分布式缓存。

  • 本地缓存:本地缓存一般指的是缓存在应用进程内部的缓存。以 Java 技术栈为例,可是自己实现一个 HashMap 作为数据缓存,也可以直接使用现成的缓存方案,例如 ehcache、caffeine 等。
  • 分布式缓存:缓存和应用环境分离,会单独存放在自己的服务器或集群里,且多个应用可直接的共享缓存。 常见的缓存方案有 MemCache 和 Redis 等。

这一节主要是让大家对缓存有一个基本的认识,缓存不是一种具体的技术,而是一种通用的技术方案,如何选择合适的缓存方案集成到自己的项目中去并且如何解决引入缓存后产生的一些经典问题,不是本文讨论的重点。有关于缓存的详细介绍和选型可以参考:

参考文献

一个缓存使用案例:Spring Cache VS Caffeine 原生 API的更多相关文章

  1. Spring JMSTemplate 与 JMS 原生API比较

    博客分类: JMS Spring 2.x   JMSUtil与Spring JmsTemplate的对比 Author:信仰 Date:2012-4-20 未完待续,截止日期2012-4-20 从以下 ...

  2. Spring MVC 支持的原生API参数

    HttpServletRequest HttpServletResponse HttpSession java.security.Principal Local InputStream OutputS ...

  3. Spring MVC 使用Servlet原生API作为参数

    具体看代码: @RequestMapping("/testServletAPI") public void testServletAPI(HttpServletRequest re ...

  4. Spring Cache缓存框架

    一.序言 Spring Cache是Spring体系下标准化缓存框架.Spring Cache有如下优势: 缓存品种多 支持缓存品种多,常见缓存Redis.EhCache.Caffeine均支持.它们 ...

  5. 「性能提升」扩展 Spring Cache 支持多级缓存

    为什么多级缓存 缓存的引入是现在大部分系统所必须考虑的 redis 作为常用中间件,虽然我们一般业务系统(毕竟业务量有限)不会遇到如下图 在随着 data-size 的增大和数据结构的复杂的造成性能下 ...

  6. 以Spring Cache扩展为例介绍如何进行高效的源码的阅读

    摘要 日常开发中,需要用到各种各样的框架来实现API.系统的构建.作为程序员,除了会使用框架还必须要了解框架工作的原理.这样可以便于我们排查问题,和自定义的扩展.那么如何去学习框架呢.通常我们通过阅读 ...

  7. 如何进行高效的源码阅读:以Spring Cache扩展为例带你搞清楚

    摘要 日常开发中,需要用到各种各样的框架来实现API.系统的构建.作为程序员,除了会使用框架还必须要了解框架工作的原理.这样可以便于我们排查问题,和自定义的扩展.那么如何去学习框架呢.通常我们通过阅读 ...

  8. Spring Cache 带你飞(二)

    接着上一篇讲了 Spring Cache 如何被 Spring Aop 代理加载对应的代码,以及何如注入相关界面逻辑. Spring Cache 带你飞(一) 本篇我们围绕两个要点展开: 一个数据是如 ...

  9. 品味Spring Cache设计之美

    最近负责教育类产品的架构工作,两位研发同学建议:"团队封装的Redis客户端可否适配Spring Cache,这样加缓存就会方便多了" . 于是边查阅文档边实战,收获颇丰,写这篇文 ...

随机推荐

  1. 如何在 Spring/Spring Boot 中做参数校验?你需要了解的都在这里!

    本文为作者原创,如需转载请在文首著名地址,公众号转载请申请开白. springboot-guide : 适合新手入门以及有经验的开发人员查阅的 Spring Boot 教程(业余时间维护中,欢迎一起维 ...

  2. 使用Typescript重构axios(三十二)——写在最后面(总结)

    0. 系列文章 1.使用Typescript重构axios(一)--写在最前面 2.使用Typescript重构axios(二)--项目起手,跑通流程 3.使用Typescript重构axios(三) ...

  3. 前端Vue中常用rules校验规则

    前提 在 vue开发中,难免遇到各种表单校验,这里整理了网络上和自己平时高频率用到的一些校验方法.如果错误欢迎指出,后期不断补充更新. 1.是否合法IP地址 export function valid ...

  4. python中实例方法,类方法,静态方法简单理解

    按照字面名称来理解的话: 实例方法就是实例化对象的方法,绑定在实例对象上 类方法就是类自己的方法,不需要实例化对象,类自己就是对象,直接绑定在类上 静态方法就是普通的函数,函数作为对象,不过是封装在类 ...

  5. 使用requests实现人人网登录,并做cookie维持

    import requests import re,time s = requests.Session() def doLogin(): login_url = 'http://www.renren. ...

  6. 新手小白的Linux学习之路

    大家好,我是一个零基础的新手小白,在此和大家分享一下新手小白的学习之路.欢迎各位大神指教!谢谢 Linux简介: Linux操作系统诞生于1991年10月,由芬兰赫尔辛基大学的在校生Linus Tor ...

  7. 0x8000FFFF 错误的解决方式

    问题描述: 在F盘新建文件夹或文件的时候提示0x8000FFFF灾难性错误: 解决方法: 1.在F盘的位置,右击选择属性 2.在弹出的窗口中选择工具,点击检查 3.根据系统提示进行响应的驱动扫描与修复 ...

  8. 力扣(LeetCode)字符串中的单词数 个人题解

    统计字符串中的单词个数,这里的单词指的是连续的不是空格的字符. 请注意,你可以假定字符串里不包括任何不可打印的字符. 示例: 输入: "Hello, my name is John" ...

  9. 更改input标签的placeholder的样式

    主要是要区别不同浏览器的不同css类 在input框中有时想将输入的字和placeholder设为不同的颜色或其它效果,这时就可以用以下代码来对placeholder进行样式设置了. input::- ...

  10. vue引用组件的两个方法

    <template> <div> <myComponent></myComponent> </div> </template> ...