一个缓存使用案例:Spring Cache VS Caffeine 原生 API
欢迎访问我的个人博客,《一个缓存使用的思考: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 的集成,我们只需要通过一组注解来操作缓存就可以了。目前支持的有 Generic、JCache (JSR-107) 、EhCache 2.x、Hazelcast、Infinispan、Couchbase、Redis、Caffeine、Simple,几乎包含了主流的本地缓存方案。
其主要的原理就是向 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 中维护的一个映射表,由于后期需要频繁改动而放到了数据库中。但由于数据量并不大且不配置映射表时,数据保持不变,因此既然在学习缓存,就想把它加进去。那么现在需要做的就是:
- 一个读取映射表全表的方法
aliasMap()。并缓存数据到 Caffeine。 - 一个支持映射记录 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-framework-cache
- spring-boot-cache
- caffeine/wiki
- 如何优雅的设计和使用缓存?
- 你应该知道的缓存进化史
- Caffeine 缓存
- Spring Boot 缓存实战 Caffeine
- Spring Boot 2.X(七):Spring Cache 使用
一个缓存使用案例:Spring Cache VS Caffeine 原生 API的更多相关文章
- Spring JMSTemplate 与 JMS 原生API比较
博客分类: JMS Spring 2.x JMSUtil与Spring JmsTemplate的对比 Author:信仰 Date:2012-4-20 未完待续,截止日期2012-4-20 从以下 ...
- Spring MVC 支持的原生API参数
HttpServletRequest HttpServletResponse HttpSession java.security.Principal Local InputStream OutputS ...
- Spring MVC 使用Servlet原生API作为参数
具体看代码: @RequestMapping("/testServletAPI") public void testServletAPI(HttpServletRequest re ...
- Spring Cache缓存框架
一.序言 Spring Cache是Spring体系下标准化缓存框架.Spring Cache有如下优势: 缓存品种多 支持缓存品种多,常见缓存Redis.EhCache.Caffeine均支持.它们 ...
- 「性能提升」扩展 Spring Cache 支持多级缓存
为什么多级缓存 缓存的引入是现在大部分系统所必须考虑的 redis 作为常用中间件,虽然我们一般业务系统(毕竟业务量有限)不会遇到如下图 在随着 data-size 的增大和数据结构的复杂的造成性能下 ...
- 以Spring Cache扩展为例介绍如何进行高效的源码的阅读
摘要 日常开发中,需要用到各种各样的框架来实现API.系统的构建.作为程序员,除了会使用框架还必须要了解框架工作的原理.这样可以便于我们排查问题,和自定义的扩展.那么如何去学习框架呢.通常我们通过阅读 ...
- 如何进行高效的源码阅读:以Spring Cache扩展为例带你搞清楚
摘要 日常开发中,需要用到各种各样的框架来实现API.系统的构建.作为程序员,除了会使用框架还必须要了解框架工作的原理.这样可以便于我们排查问题,和自定义的扩展.那么如何去学习框架呢.通常我们通过阅读 ...
- Spring Cache 带你飞(二)
接着上一篇讲了 Spring Cache 如何被 Spring Aop 代理加载对应的代码,以及何如注入相关界面逻辑. Spring Cache 带你飞(一) 本篇我们围绕两个要点展开: 一个数据是如 ...
- 品味Spring Cache设计之美
最近负责教育类产品的架构工作,两位研发同学建议:"团队封装的Redis客户端可否适配Spring Cache,这样加缓存就会方便多了" . 于是边查阅文档边实战,收获颇丰,写这篇文 ...
随机推荐
- CSPS模拟 93
恰饭的时候lsc说我颓颓废废是要ak的前兆 所以我rp掉光了=.= T1 思维一片混乱 T2 只会n^3 发现决策单调性,但没想全 只知道$determin(l,r)>=determin(l,r ...
- CSPS模拟 75
我身边 mikufun:矩阵树学学学 Lrefrain:矩阵题刷刷刷 Dybala:神仙定理康康康 skyh:讨论讨论讨论(most mei face) DeepinC:我过样例了! Mouding: ...
- Laravel用户认证
前期准备 Laravel的权限配置文件位于 config/auth.php,Laravel的认证组件由"guards"和"providers"组成, Guard ...
- UiPath之文件操作
今天给大家介绍一下,在UiPath中如何操作文件,比如需要在某个文件夹中自动创建一个当天日期的文本. 主要使用的activity有: l Assign l Path Exists l If l ...
- OpenCV的Mat构造函数
1.函数说明 构造函数:public Mat(int rows, int cols, MatType type, IntPtr data, long step = 0) 可以通过数据指针构造Mat对象 ...
- tslib1.1移植
安装步骤: 1.准备工作确保以下软件已安装 # apt-get install autoconf(或autoconf2.13)# apt-get install automake# apt-get i ...
- 大宇java面试系列(三):Redis常见面试题
1. Redis 是什么?都有哪些使用场景? 我们先来理解经典的CAP理论: 一致性:是指从数据层面来看的一致性. 可用性:是指从系统层面的可用性. 容错性:是指从网络层面的的容错性. 数据库逐渐从关 ...
- 【原创】使用批处理脚本生成包并自动上传到nuget
Hello 大家好,我是TANZAME,我们又见面了. NuGet 是什么这里就不再重复啰嗦,园子里一搜一大把.今天要跟大家分享的是,在日常开发过程中如何统一管理我们的包,如何通过批处理脚本生成包并自 ...
- 微擎框架商业版 V2.1.2 去后门一键安装版+去除云平台+无附带模块
下载地址:http://dd.ma/AdVvoDu5 关注微信公众号codervip,点击公众号菜单,获取提取码! 这个是一键安装版本,所以微擎安装比较简单,不用大家手动去改数据库了,而且修复上个2. ...
- PHP 提交复选框数据
PHP 提交复选框数据 前台,name要加 []: <input type="checkbox" name="cate[]" value="ca ...