📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻一周,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,如需交流,欢迎留言评论。👍
文章目录
- 写在前面的话
- 技术统括
- 技术简介
- 选型比较
- 实战运用
- 快速入门
- Bean和Util
- SpringCache 模式
- 技术拓展
- 配置属性
- 清除/驱逐策略
- 过期/更新策略
- 填充策略
- 移除监听
- 写入/删除监听
- 统计信息
- 总结陈词
写在前面的话
笔者所在公司的框架采用 Redis 作为缓存中间件,在部分场景下,可以借用 Redis 实现增强接口性能、减轻数据库压力、充当持久存储等功能。 但程序访问 Redis 毕竟需要消耗网络带宽,此外,经常由于各种因素导致 Redis 的性能降低,诸如编码不当、键过多、 网络异常等。 鉴于此,公司框架基于 Google 开发的高性能的 Java 缓存库 Caffeine,封装了本地缓存工具,便于业务项目使用。由于目前 Redis 已广泛使用,框架层面并未将本地缓存与 @Cacheable 等注解绑定,而是基于按需使用的原则,以注入 Bean 的形式封装工具方法
技术统括
技术简介
【Caffeine 技术简介】
Caffeine 是一个高性能的 Java 本地缓存库,提供了强大的缓存功能和灵活的配置选项,你可以使用 Caffeine 来实现本地缓存,并根据具体的需求进行配置和使用。
Caffeine 是基于 Java8 的高性能本地缓存库,并且在 Spring5 (SpringBoot 2.x) 后,Spring 官方放弃了 Guava,而使用了性能更优秀的 Caffeine 作为默认缓存组件。
Caffeine 是在 Guava Cache 的基础上做一层封装,性能有明显提高,二者同属于内存级本地缓存,从并发的角度来讲,Caffeine明显优于Guava,原因是使用了Java 8最新的StampedLock锁技术。
据说,比 Guava Cache 优秀,那既然 Guava 还没开始用,那就直接它吧。
Maven依赖都不需要单独引入,SB2.x自动引入了,爽。
【Caffeine 优势特性】
Caffeine提供灵活的结构来创建缓存,并且有以下特性:
自动加载条目到缓存中,可选异步方式
可以基于大小剔除
可以设置过期时间,时间可以从上次访问或上次写入开始计算
异步刷新
keys自动包装在弱引用中
values自动包装在弱引用或软引用中
条目剔除通知
缓存访问统计
【Caffeine 使用场景】
Caffeine 是一个高性能的 Java 本地缓存库,适用于各种日常开发场景。
以下是一些 Caffeine 在日常开发中适合做的事情:
缓存常用数据:使用 Caffeine 缓存常用的数据,例如配置信息、用户信息、系统参数等,可以提高数据的访问速度和系统的性能。
减轻数据库压力:将频繁访问的数据库查询结果缓存到 Caffeine 中,可以减轻数据库的压力,提高系统的并发处理能力。
缓存计算结果:使用 Caffeine 缓存计算结果,例如复杂查询结果、数据聚合结果等,可以避免重复计算,提高系统的响应速度。
实现数据预热:在系统启动时将一些常用的数据预先加载到 Caffeine 缓存中,可以提高系统的初始化速度和响应速度。
实现请求限流:使用 Caffeine 缓存来记录请求次数和频率,实现请求的限流和流量控制,保护系统的稳定性和可用性。
实现本地锁:利用 Caffeine 缓存的原子性操作特性,可以实现分布式锁的简单版本,用于控制并发访问。
缓存控制器结果:在 Spring MVC 或 Spring Boot 应用中,可以使用 Caffeine 缓存控制器方法的返回结果,减少方法的执行次数,提高系统的性能和吞吐量。
总的来说,Caffeine 是一个功能强大、性能优越的本地缓存库,适用于各种日常开发场景,可以提高系统的性能、稳定性和可维护性。
【 补充:Guava LoadingCache 适合存储那些满足以下条件的数据】
频繁访问的数据:LoadingCache 提供了高性能的数据读取和写入,适合缓存那些被频繁访问的数据。这些数据可能是热点数据,经常被应用程序读取,但不经常更新。
计算密集型或者昂贵的数据:LoadingCache 可以在缓存中存储一些计算密集型或者昂贵的数据,例如数据库查询结果、API 调用结果等。这样可以避免重复计算或者昂贵的网络请求,提高系统的性能和响应速度。
临时性的数据:LoadingCache 也适合存储一些临时性的数据,例如会话数据、用户权限信息等。这些数据可能在一段时间内频繁被访问,但是不需要长期保存。
不需要持久化的数据:LoadingCache 是一个本地缓存,不具备持久化存储的能力。因此适合存储一些不需要长期保存的数据,例如缓存数据源的数据、临时计算结果等。
需要自动加载和刷新的数据:LoadingCache 支持自动加载和刷新数据,可以通过设置 refreshAfterWrite 来定期刷新缓存数据,以保持数据的新鲜性。因此适合存储那些需要定期刷新的数据,例如缓存数据源的数据、动态配置信息等。
总的来说,LoadingCache 适合存储那些被频繁访问、不需要持久化存储、临时性的数据,以及那些需要自动加载和刷新的数据。通过合理使用 LoadingCache,可以提高系统的性能、减少资源消耗,同时提供更好的用户体验。
选型比较
【Caffeine 和 GuavaCache 差异】
剔除算法方面,GuavaCache采用的是「LRU」算法,而Caffeine采用的是「Window TinyLFU」算法,这是两者之间最大,也是根本的区别。
立即失效方面,Guava会把立即失效 (例如:expireAfterAccess(0) and expireAfterWrite(0)) 转成设置最大Size为0。这就会导致剔除提醒的原因是SIZE而不是EXPIRED。Caffiene能正确识别这种剔除原因。
取代提醒方面,Guava只要数据被替换,不管什么原因,都会触发剔除监听器。而Caffiene在取代值和先前值的引用完全一样时不会触发监听器。
异步化方方面,Caffiene的很多工作都是交给线程池去做的(默认:ForkJoinPool.commonPool()),例如:剔除监听器,刷新机制,维护工作等。
【Caffeine 和 Redis 区别】
本地缓存与分布式缓存对应,缓存进程和应用进程同属于一个JVM,数据的读、写在一个进程内完成。本地缓存没有网络开销,访问速度很快。
从横向对常用的缓存进行对比,有助于加深对缓存的理解,有助于提高技术选型的合理性。下面对比三种常用缓存:Redis、EhCache、Caffeine。
通常采用 Caffeine/Guava + Redis 实现二级缓存方案,先从本地拿,拿不到再从缓存获取,减少 Redis 的连接消耗。
【Caffeine 有什么用,为什么不直接用Map】
**解答1:**Caffeine 使用了 ConcurrentHashMap 作为其缓存数据结构的基础,但对其进行了一些优化和改进,以提高并发性能、减少内存消耗,并添加了一些额外的功能和特性,使其更适合于高性能的缓存应用场景。
ConcurrentHashMap 是 Java 标准库中提供的线程安全的哈希表实现,它使用了一种分段锁的方式来实现并发访问控制,以提高并发读写性能。Caffeine 利用了 ConcurrentHashMap 的并发性能和线程安全性,并在此基础上进行了优化和改进,以提供更高性能的缓存服务。
总的来说,尽管 Caffeine 的底层实现基于 ConcurrentHashMap,但它对 ConcurrentHashMap 进行了一些改进和扩展,使得它更适合于高性能、低延迟的缓存应用场景。
**解答2:**Caffeine 是一个用于构建内存缓存的 Java 库,它提供了一些高效、可配置的缓存功能,可以帮助开发人员在应用程序中轻松地实现缓存机制。
与直接使用 Java 中的 Map 不同,Caffeine 提供了更多的特性和优势,使得它在某些场景下更为适用:
自动加载和刷新机制:Caffeine 支持自动加载和刷新缓存中的条目,无需手动编写加载逻辑。通过 LoadingCache 接口,可以在缓存中不存在指定键的条目时,自动调用加载逻辑来获取新的值,并将其放入缓存中。
缓存的自动清理和过期处理:Caffeine 提供了可配置的过期策略,可以根据时间、大小或其他条件来自动清理缓存中的条目。这样可以确保缓存不会占用过多的内存,并及时清理过期的条目,保持缓存的有效性和性能。
高性能和低内存占用:Caffeine 的实现经过优化,具有很高的性能和低的内存占用。它使用了一些高效的数据结构和算法,以及基于并发的设计模式,可以在多线程环境下高效地处理并发访问和更新操作。
可扩展性和灵活性:Caffeine 提供了丰富的配置选项和扩展点,可以根据具体的需求和场景来定制缓存的行为。开发人员可以灵活地配置缓存的大小、过期时间、刷新策略等参数,以满足不同应用场景的需求。
总的来说,尽管 Java 中的 Map 提供了基本的键值对存储功能,但在需要更多高级特性和性能优化的场景下,使用 Caffeine 可能会更加合适和方便。Caffeine 提供了更丰富的功能和更高效的实现,可以帮助开发人员构建出更可靠、更高性能的缓存系统。
实战运用
快速入门
Step1、添加Maven依赖
注意,高版本Spring或SB以及依赖了caffeine,直接使用提供的版本即可,不要再额外引入了。
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>3.1.8</version>
</dependency>
Step2、编写工具Bean
@Component
public class CaffeineCache {private final Cache<String, Object> cache;public CaffeineCache() {// 创建一个基于 Caffeine 的本地缓存,设置最大缓存条目数为 10000,过期时间为 10 分钟cache = Caffeine.newBuilder().maximumSize(10000).expireAfterWrite(10, TimeUnit.MINUTES).build();}// 添加缓存项public void put(String key, Object value) {cache.put(key, value);}// 获取缓存项public Object get(String key) {return cache.getIfPresent(key);}// 移除缓存项public void remove(String key) {cache.invalidate(key);}// 清空缓存public void clear() {cache.invalidateAll();}
}
Step3、使用测试
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class MyService {@Autowiredprivate CaffeineCache caffeineCache;public void doSomething() {// 添加缓存项caffeineCache.put("key", "value");// 获取缓存项Object value = caffeineCache.get("key");// 移除缓存项caffeineCache.remove("key");// 清空缓存caffeineCache.clear();}
}
通过以上步骤,你就可以在 Spring Boot 应用中使用 Caffeine 工具类来创建本地缓存,并在需要时进行缓存操作。你可以根据具体的需求调整缓存的配置参数,例如最大缓存条目数、过期时间等。
Bean和Util
将 Caffeine 缓存工具类设计为 Bean 是比较常见的做法,这样可以更好地与 Spring Boot 集成,并通过依赖注入的方式在需要的地方直接使用,同时也方便在配置类中对其进行配置和管理。
将 Caffeine 缓存工具类设计为 Bean 的优势包括:
便于管理和配置:作为 Spring Bean,可以利用 Spring 的依赖注入和配置功能,更方便地管理和配置 Caffeine 缓存的属性,如最大缓存条目数、过期时间等。
与 Spring Boot 集成更紧密:作为 Spring Bean,与 Spring Boot 的集成更加紧密,可以直接在其他 Bean 中通过自动装配进行使用,而不需要额外的配置。
易于测试:将 Caffeine 缓存工具类设计为 Bean 后,可以更方便地在单元测试中进行模拟和替换,提高测试的可维护性和灵活性。
因此,建议将 Caffeine 缓存工具类设计为 Spring Bean,以便更好地利用 Spring Boot 的特性和功能,并方便在应用程序中使用和管理。
SpringCache 模式
与SB整合通常体现在@Cacheable,有需要再扩展。
公司框架Redis使用已久,为减少影响,还是封装工具Bean,按需使用。
技术拓展
配置属性
initialCapacity 初始的缓存空间大小
maximumSize 缓存的最大条数
maximumWeight 缓存的最大权重
expireAfterAccess 最后一次写入或访问后,经过固定时间过期
expireAfterWrite 最后一次写入后,经过固定时间过期
refreshAfterWrite 写入后,经过固定时间过期,下次访问返回旧值并触发刷新
weakKeys 打开 key 的弱引用
weakValues 打开 value 的弱引用
softValues 打开 value 的软引用
recordStats 缓存使用统计
expireAfterWrite 和 expireAfterAccess 同时存在时,以 expireAfterWrite 为准。
weakValues 和 softValues 不可以同时使用。
maximumSize 和 maximumWeight 不可以同时使用。
清除/驱逐策略
缓存的驱逐策略是为了预测哪些数据在短期内最可能被再次用到,从而提升缓存的命中率。LRU策略或许是最流行的驱逐策略。但LRU通过历史数据来预测未来是局限的,它会认为最后到来的数据是最可能被再次访问的,从而给予它最高的优先级。
Caffeine提供三类驱逐策略:基于大小(size-based),基于时间(time-based)和基于引用(reference-based)
1、基于大小(size-based)
基于大小驱逐,有两种方式:一种是基于缓存大小,一种是基于权重。
// 根据缓存的计数进行驱逐
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder().maximumSize(10_000).build(key -> createExpensiveGraph(key));// 根据缓存的权重来进行驱逐(权重只是用于确定缓存大小,不会用于决定该缓存是否被驱逐)
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder().maximumWeight(10_000).weigher((Key key, Graph graph) -> graph.vertices().size()).build(key -> createExpensiveGraph(key));
2、基于时间:设置缓存的有效时间
// 设置缓存有效期为 10 秒,从最后一次写入开始计时
Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(10)) .build();
3、基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。
// 性能较差,不建议使用。
Cache<String, String> cache = Caffeine.newBuilder().weakKeys().weakValues().build();
【弱引用相关补充】
Caffeine.weakKeys() 使用弱引用存储key。如果没有强引用这个key,则GC时允许回收该条目
Caffeine.weakValues() 使用弱引用存储value。如果没有强引用这个value,则GC时允许回收该条目
Caffeine.softValues() 使用软引用存储value, 如果没有强引用这个value,则GC内存不足时允许回收该条目
过期/更新策略
# expireAfterAccess:设置条目在最后一次访问后的过期时间。默认值为不过期。
# expireAfterWrite:设置条目在被创建或最后一次写入后的过期时间。默认值为不过期。
# refreshAfterWrite:这个其实算更新策略,设置条目在被创建或最后一次写入后的自动刷新时间。默认值为不自动刷新。
# expireAfter:
# 在expireAfter中需要自己实现Expiry接口,这个接口支持create,update,access了之后多久过期,
# 这里和前面两个API不同的是,需要你告诉缓存框架,他应该在具体的某个时间过期
# 也就是通过前面的重写create,update,access的方法,获取具体的过期时间。
Cache<String, String> cacheHandle = Caffeine.newBuilder().expireAfter(new Expiry<String, String>() {// 创建后多久过期@Overridepublic long expireAfterCreate(@NonNull String key, @NonNull String value, long currentTime) {return TimeUnit.SECONDS.toNanos(3); //3秒后过期}// 更新后多久过期@Overridepublic long expireAfterUpdate(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {return currentDuration; // 保持不变,即不改变过期时间}// 读取后多久过期@Overridepublic long expireAfterRead(@NonNull String key, @NonNull String value, long currentTime, @NonNegative long currentDuration) {return currentDuration; // 保持不变,即不改变过期时间}}).build();
cacheHandle.put("abc", "123");
System.out.println("abc的值:" + cacheHandle.getIfPresent("abc"));
ThreadUtil.sleep(5000);
System.out.println("abc的值:" + cacheHandle.getIfPresent("abc"));
填充策略
Caffeine 提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载。
很好理解,其实就是build的几个重载模式。
参考:Caffeine Cache 进程缓存利器
移除监听
RemovalListener:如果我们需要在缓存被移除的时候,得到通知产生回调,并做一些额外处理工作。
Cache<String, String> cache = Caffeine.newBuilder().maximumSize(2).removalListener(((key, value, cause) -> {System.out.println("Removed key: " + key + ", value: " + value + ", cause: " + cause);})).build();
cache.put("key1", "value1");
cache.put("key2", "value22");
cache.put("key3", "value333");
ThreadUtil.sleep(1000);
System.out.println(cache.asMap());//下方是输出信息:
Removed key: key1, value: value1, cause: SIZE
{key2=value22, key3=value333}
写入/删除监听
@Autowired
private OnelinkCaffeineCacheHandle caffeineCache;Caffeine<Object, Object> customBuild = caffeineCache.getCustomBuild();
customBuild.writer(new CacheWriter<String, String>() {@Overridepublic void write(String key, String value) {System.out.println("Cache entry written for key: " + key + ", value: " + value);}@Overridepublic void delete(String key, String value, RemovalCause cause) {System.out.println("Cache entry removed for key: " + key + ", value: " + value + ", removal cause: " + cause);}
});
统计信息
【关于 CacheStats】
Guava 的 CacheStats 类是用来表示缓存的统计信息的。它提供了一组方法来获取缓存的命中率、加载次数、加载失败次数等信息。通过这些统计信息,可以帮助你了解缓存的使用情况,优化缓存的配置和性能。
以下是 CacheStats 类的一些常用方法:
hitCount():返回缓存命中的次数。
missCount():返回缓存未命中的次数。
loadSuccessCount():返回缓存加载成功的次数。
loadExceptionCount():返回缓存加载失败的次数。
totalLoadTime():返回缓存加载的总时间,单位为纳秒。
evictionCount():返回缓存中条目被回收的次数。
使用 CacheStats 类可以帮助你监控缓存的性能,并且根据统计信息来调整缓存的配置,以提高系统的性能和稳定性。
【示例代码】
Tips:for循环中大量使用才有统计的意义,正常生产环境也没必要记录。
public static void main(String[] args) {Cache<String, String> cache = Caffeine.newBuilder().maximumSize(2).recordStats().removalListener(((key, value, cause) -> {System.out.println("Removed key: " + key + ", value: " + value + ", cause: " + cause);})).build();// 添加一些条目cache.put("key1", "value1");cache.put("key2", "value22");cache.put("key3", "value333");ThreadUtil.sleep(1000);// 此时缓存大小超过最大权重,将会驱逐一些条目System.out.println(cache.asMap());System.out.println(cache.stats());//输出信息如下:// Removed key: key1, value: value1, cause: SIZE// {key2=value22, key3=value333}// CacheStats{hitCount=0, missCount=0, loadSuccessCount=0, loadFailureCount=0, totalLoadTime=0, evictionCount=1, evictionWeight=1}// hitCount(): 返回命中缓存的总数// evictionCount():缓存逐出的数量
}
总结陈词
上文介绍了本地缓存的用法,仅供参考。
本地缓存具备其独有美丽,可以搭配Redis发挥更多作用。
但切记缓存都是一个双刃剑,用的姿势如果不对,会造成更严重的后果。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。