ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot 集成 Caffeine 实现本地缓存

SpringBoot 集成 Caffeine 实现本地缓存 目录*1、Caffeine 简介* * 1.1、Caffeine 简介 * 1.2、对比 Guava cache 的性能主要优化项 * 1.3、常见的缓存淘汰算法 * 1.4、SpringBoot 集成 Caffeine 两种方式* 2、SpringBoot 集成 Caffeine 方式一* * 2.1、缓存加载策略 * * 2.1.1、手动加载 * 2.1.2、自动加载【Loading Cache】 * 2.1.3、异步加载【AsyncLoadingCache】 * 2.2、回收策略 * * 2.2.1、基于大小的过期方式 * 2.2.2、基于时间的过期方式 * 2.2.3、基于引用的过期方式 * 2.3、写入外部存储 * 2.4、统计 * 2.5、刷新机制 * 2.6、快速入门 * * 2.6.1、引入依赖 * 2.6.2、创建 Caffeine 缓存配置类 * 2.6.3、定义实体对象 * 2.6.4、定义服务实现类 * 2.6.5、定义控制器调用测试 * 2.7、总结* 3、SpringBoot 集成 Caffeine 方式二* * 3.1、常用的注解 * * 3.1.1、Cacheable 注解 * 3.1.2、CachePut 注解 * 3.1.3、CacheEvict 注解 * 3.1.4、Caching 注解 * 3.1.5、CacheConfig 注解 * 3.2、快速入门 * * 3.2.1、引入依赖 * 3.2.2、开启缓存功能 * 3.2.3、配置缓存类 * 3.2.4、使用缓存 * 3.2.5、添加 Controller * 3.3、配置数据源 * * 3.3.1、不配置(默认) * 3.3.2、配置 Caffeine 缓存 * * 3.3.2.1、CaffeineCacheManager * * 3.3.2.1.1、使用 Java 配置类 * 3.3.2.1.2、使用配置文件 * 3.3.2.2、SimpleCacheManager * 3.3.3、配置 Redis 缓存 * 3.3.4、配置多缓存源 * 3.4、keyGenerator * 3.5、CacheResolver* 5、SPEL* 6、Caffeine 的优劣势和适用场景1、Caffeine 简介-------------### 1.1、Caffeine 简介Caffeine 官网Caffeine 是基于Java 1.8 的高性能本地缓存库同样是 Google 开发的由 Guava 改进而来底层设计思路、功能和使用方式与 Guava 非常类似但是各方面的性能都要远远超过前者可以看做是 Guava cache 的升级版。而且在 Spring5 开始的默认缓存实现就将 Caffeine 代替原来的 Google Guava官方说明指出其缓存命中率已经接近最优值可以通过下图观测到在下面缓存组件中 Caffeine 性能是其中最好的### 1.2、对比 Guava cache 的性能主要优化项Caffeine 底层又做了哪些优化才能让其性能高于 Guava cache 呢主要包含以下三点1.异步策略Guava cache 在读操作中可能会触发淘汰数据的清理操作虽然自身也做了一些优化来减少读的时候的清理操作但是一旦触发就会降低查询效率对缓存性能产生影响。而Caffeine 支持异步操作采用异步处理的策略查询请求在触发淘汰数据的清理操作后会将清理数据的任务添加到独立的线程池中进行异步操作不会阻塞查询请求提高了查询性能2.ConcurrentHashMap 优化Caffeine 底层都是通过ConcurrentHashMap来进行数据的存储因此随着 Java8 中对ConcurrentHashMap的调整数组 链表的结构升级为数组 链表 红黑树的结构以及分段锁升级为 syschronized CAS降低了锁的粒度减少了锁的竞争这两个优化显著提高了 Caffeine 在读多写少场景下的查询性能3.新型淘汰算法 W-TinyLFU传统的淘汰算法如 LRU、LFU、FIFO在实际的缓存场景中都存在一些弊端。因此Caffeine 引入了W-TinyLFU算法由窗口缓存、过滤器、主缓存组成。缓存数据刚进入时会停留在窗口缓存中这个部分只占总缓存的 1%当被挤出窗口缓存时会在过滤器汇总和主缓存中淘汰的数据进行比较如果频率更高则进入主缓存否则就被淘汰主缓存被分为淘汰段和保护段两段都是 LRU 算法第一次被访问的元素会进入淘汰段第二次被访问会进入保护段保护段中被淘汰的元素会进入淘汰段这种算法实现了高命中率和低内存占用### 1.3、常见的缓存淘汰算法常见的缓存淘汰算法1.FIFO先进先出在这种淘汰算法中先进入缓存的会先被淘汰会导致命中率很低 *局限性如果缓存使用的频率较高那么缓存数据会一直处在进进出出的状态间接影响到缓存命中率2.LRU最近最少使用算法每次访问数据都会将其放在我们的队首如果需要淘汰数据就只需要淘汰队尾即可。仍然有个问题如果有个数据在 1 分钟访问了 1000次再后 1 分钟没有访问这个数据但是有其它的数据访问就导致了我们这个热点数据被淘汰 *局限性LRU 可以很好的应对突发流量的情况因为它不需要累计数据频率。但 LRU 通过历史数据来预测未来是局限的它会认为最后到来的数据是最可能被再次访问的从而给与它最高的优先级。3.LFU最近最少频率使用利用额外的空间记录每个数据的使用频率然后选出频率最低进行淘汰 *局限性在 LFU 中只要数据访问模式的概率分布随时间保持不变时其命中率就能变得非常高。比如有部新剧出来了我们使用 LFU 给它缓存下来这部新剧在这几天大概访问了几亿次这个访问频率也在我们的 LFU 中记录了几亿次。但是新剧总会过气的比如一个月之后这个新剧的前几集其实已经过气了但是它的访问量的确是太高了其它的电视剧根本无法淘汰这个新剧所以在这种模式下是有局限性。 上面三种策略各有利弊实现的成本也是一个比一个高同时命中率也是一个比一个好。Guava Cache 虽然有这么多的功能但是本质上还是对 LRU 的封装如果有更优良的算法并且也能提供这么多功能相比之下就相形见绌了。在现有算法的局限性下会导致缓存数据的命中率或多或少的受损而命中略又是缓存的重要指标。HighScalability 网站刊登了一篇文章由前 Google 工程师发明的W-TinyLFU——一种现代的缓存 。Caffine Cache就是基于此算法而研发。Caffeine 因使用 Window TinyLfu 回收策略提供了一个近乎最佳的命中率 当数据的访问模式不随时间变化的时候LFU 的策略能够带来最佳的缓存命中率。然而 LFU 有两个缺点 1. 它需要给每个记录项维护频率信息每次访问都需要更新这是个巨大的开销 2. 如果数据访问模式随时间有变LFU 的频率信息无法随之变化。因此早先频繁访问的记录可能会占据缓存而后期访问较多的记录则无法被命中。 因此大多数的缓存设计都是基于LRU或者其变种来进行的。相比之下LRU 并不需要维护昂贵的缓存记录元信息同时也能够反应随时间变化的数据访问模式。然而在许多负载之下LRU 依然需要更多的空间才能做到跟 LFU一致的缓存命中率。因此一个“现代”的缓存应当能够综合两者的长处。TinyLFU 维护了近期访问记录的频率信息作为一个过滤器当新记录来时只有满足 TinyLFU 要求的记录才可以被插入缓存。如前所述作为现代的缓存它需要解决两个挑战1. 如何避免维护频率信息的高开销2. 如何反应随时间变化的访问模式更详细的解释可以参考论文https://arxiv.org/pdf/1512.00727.pdf### 1.4、SpringBoot 集成 Caffeine 两种方式Caffeine Cache 的 github 地址在 SpringBoot 中有两种使用 Caffeine 作为缓存的方式1. 直接引入Caffeine依赖然后使用Caffeine方法实现缓存2. 引入Caffeine和Spring Cache依赖使用SpringCache注解方法实现缓存2、SpringBoot 集成 Caffeine 方式一----------------------------引入依赖 com.github.ben-manes.caffeine caffeine 3.0.5 Caffeine Cache 提供了三种缓存填充策略手动、同步加载和异步加载。### 2.1、缓存加载策略#### 2.1.1、手动加载最普通的一种缓存无需指定加载方式需要手动调用put()进行加载通过get获取缓存值。如果想要的缓存值不存在并且原子地将值写入缓存则可以调用get(key, k - value)方法该方法将避免写入竞争。在多线程情况下当使用get(key, k - value)时如果有另一个线程同时调用本方法进行竞争则后一线程会被阻塞直到前一线程更新缓存完成而若另一线程调用getIfPresent()方法则会立即返回 null不会被阻塞 CacheString, Object cache Caffeine.newBuilder() // 初始容量 .initialCapacity(100) // 最大缓存数量 .maximumSize(500) // 缓存过期时间写入缓存后经过某个时间缓存失效 .expireAfterWrite(3, TimeUnit.SECONDS) // 缓存失效监听器 .removalListener((key, value, cause) - System.out.println(“key:” key value: value cause: cause)) // 开启统计功能 .recordStats() .build(); cache.put(“name”, “zzc”); // 如果一个 key 不存在那么会进入指定的函数生成 value cache.get(“age”, key - “18”); // 判断是否存在如果不存在则返回 null Object ageValue cache.getIfPresent(“age”); // 移除一个key cache.invalidate(“name”); return cache; #### 2.1.2、自动加载【Loading Cache】LoadingCache是一种自动加载的缓存。其和普通缓存不同的地方在于当缓存不存在/缓存已过期时若调用get()方法则会自动调用CacheLoader.load()方法加载最新值调用getAll()方法将遍历所有的 key 调用get()除非实现了CacheLoader.loadAll()方法。使用LoadingCache时需要指定CacheLoader并实现其中的load()方法供缓存缺失时自动加载。在多线程情况下当两个线程同时调用get()则后一线程将被阻塞直至前一线程更新缓存完成 LoadingCacheString, Object cache Caffeine.newBuilder() // 初始容量 .initialCapacity(100) // 最大缓存数量 .maximumSize(500) // 缓存过期时间写入缓存后经过某个时间缓存失效【仅支持 LoadingCache】 .refreshAfterWrite(3, TimeUnit.SECONDS) // 缓存失效监听器 .removalListener((key, value, cause) - System.out.println(“key:” key value: value cause: cause)) // 开启统计功能 .recordStats() .build(key - “zzc”); return cache; #### 2.1.3、异步加载【AsyncLoadingCache】AsyncLoadingCache是 Cache 的一个变体其响应结果均为CompletableFuture通过这种方式AsyncLoadingCache对异步编程模式进行了适配。默认情况下缓存计算使用ForkJoinPool.commonPool()作为线程池如果想要指定线程池则可以覆盖并实现Caffeine.executor(Executor)方法。synchronous()提供了阻塞直到异步缓存生成完毕的能力它将以 Cache 进行返回。在多线程情况下当两个线程同时调用get(key, k - value)则会返回同一个CompletableFuture对象。由于返回结果本身不进行阻塞可以根据业务设计自行选择阻塞等待或者非阻塞如果要以同步方式调用时应提供CacheLoader要以异步表示时应该提供一个AsyncCacheLoader并返回一个CompletableFutureAsyncLoadingCacheString, Object cache Caffeine.newBuilder() // 初始容量 .initialCapacity(100) // 最大缓存数量 .maximumSize(500) // 缓存过期时间写入缓存后经过某个时间缓存失效 .expireAfterWrite(3, TimeUnit.SECONDS) // 缓存失效监听器 .removalListener((key, value, cause) - System.out.println(“key:” key value: value cause: cause)) // 开启统计功能 .recordStats() .buildAsync(key - “zzc”); //异步缓存返回的是 CompletableFuture CompletableFuture future cache.get(“1”); future.thenAccept(System.out::println); ### 2.2、回收策略Caffeine提供了 3 种回收策略基于大小回收基于时间回收基于引用回收。#### 2.2.1、基于大小的过期方式基于大小的回收策略有两种方式基于缓存大小maximumSize()基于权重maximumWeight()、weigher()maximumWeight与maximumSize不可以同时使用#### 2.2.2、基于时间的过期方式基于固定的到期策略 *expireAfterAccess()*expireAfterWrite()基于不同的到期策略expireAfter(Expiry expiry)*expireAfterCreate()*expireAfterUpdate()*expireAfterRead()#### 2.2.3、基于引用的过期方式Java 中四种引用类型引用类型被垃圾回收时间用途生存时间强引用 Strong Reference从来不会对象的一般状态JVM停止运行时终止软引用 Soft Reference在内存不足时对象缓存内存不足时终止弱引用 Weak Reference在垃圾回收时对象缓存gc运行后终止虚引用 Phantom Reference从来不会可以用虚引用来跟踪对象被垃圾回收器回收的活动当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知JVM停止运行时终止 // 当key和value都没有引用时驱逐缓存 LoadingCacheString, Object cache Caffeine.newBuilder() .weakKeys() .weakValues() .build(key - function(key)); // 当垃圾收集器需要释放内存时驱逐 LoadingCacheString, Object cache1 Caffeine.newBuilder() .softValues() .build(key - function(key)) 【注意】AsyncLoadingCache不支持弱引用和软引用*weakKeys()使用弱引用存储 key。如果没有其它地方对该 key 有强引用那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等因此这会导致整个缓存使用身份 () 相等来比较 key而不是使用 equals()*weakValues()使用弱引用存储value。如果没有其它地方对该 value 有强引用那么该缓存就会被垃圾回收器回收。由于垃圾回收器只依赖于身份(identity)相等因此这会导致整个缓存使用身份 () 相等来比较 key而不是使用 equals()*softValues()使用软引用存储 value。当内存满了过后软引用的对象以将使用最近最少使用(least-recently-used ) 的方式进行垃圾回收。由于使用软引用是需要等到内存满了才进行回收所以我们通常建议给缓存配置一个使用内存的最大值。softValues() 将使用身份相等(identity) () 而不是equals() 来比较值【注意】weakValues()和softValues()不可以一起使用### 2.3、写入外部存储CacheWriter方法可以将缓存中所有的数据写入到第三方 LoadingCacheString, Object cache2 Caffeine.newBuilder() .writer(new CacheWriterString, Object() { Override public void write(String key, Object value) { // 写入到外部存储 } Override public void delete(String key, Object value, RemovalCause cause) { // 删除外部存储 } }) .build(key - function(key)); 如果你有多级缓存的情况下这个方法还是很实用【注意】CacheWriter不能与弱键或AsyncLoadingCache一起使用### 2.4、统计 CacheString, Object cache Caffeine.newBuilder() .maximumSize(10_000) .recordStats() .build(); 通过使用Caffeine.recordStats(), 可以转化成一个统计的集合. 通过Cache.stats()返回一个CacheStats。CacheStats提供以下统计方法 // 返回缓存命中率 hitRate() // 缓存回收数量 evictionCount() // 加载新值的平均时间 averageLoadPenalty() ### 2.5、刷新机制refreshAfterWrite()表示 x 秒后自动刷新缓存的策略可以配合淘汰策略使用 private static int NUM 0; LoadingCacheInteger, Integer cache Caffeine.newBuilder() .refreshAfterWrite(1, TimeUnit.SECONDS) //模拟获取数据每次获取就自增1 .build(integer - NUM); //获取ID1的值由于缓存里还没有所以会自动放入缓存 System.out.println(cache.get(1));// 1 // 延迟2秒后理论上自动刷新缓存后取到的值是2 // 但其实不是值还是1因为refreshAfterWrite并不是设置了n秒后重新获取就会自动刷新 // 而是x秒后第二次调用getIfPresent的时候才会被动刷新 Thread.sleep(2000); System.out.println(cache.getIfPresent(1));// 1 //此时才会刷新缓存而第一次拿到的还是旧值 System.out.println(cache.getIfPresent(1));// 2 【注意】刷新机制只支持LoadingCache和AsyncLoadingCache### 2.6、快速入门#### 2.6.1、引入依赖#### 2.6.2、创建Caffeine缓存配置类 Configuration public class CacheConfig { Bean public CacheString, Object caffeineCache() { CacheString, Object cache Caffeine.newBuilder() // 初始容量 .initialCapacity(100) // 最大缓存数量 .maximumSize(500) // 缓存过期时间写入缓存后经过某个时间缓存失效 .expireAfterWrite(3, TimeUnit.SECONDS) // 缓存失效监听器 .removalListener((key, value, cause) - System.out.println(“key:” key value: value cause: cause)) // 开启统计功能 .recordStats() .build(); cache.put(“name”, “zzc”); // 如果一个 key 不存在那么会进入指定的函数生成 value cache.get(“age”, key - “18”); // 判断是否存在如果不存在则返回 null Object ageValue cache.getIfPresent(“age”); // 移除一个key cache.invalidate(“name”); return cache; } } #### 2.6.3、定义实体对象 Data public class User { private Integer id; private String name; } #### 2.6.4、定义服务实现类 Service public class UserServiceImpl { Autowired private CacheString, Object caffeineCache; // 模拟数据库数据 private MapInteger, User userMap new HashMap(); public void add(User user) { userMap.put(user.getId(), user); // 添加缓存 caffeineCache.put(String.valueOf(user.getId()), user); } public User get(Integer id) { // 从缓存中获取 User user (User)caffeineCache.asMap().get(String.valueOf(id)); if (Objects.nonNull(user)) { return user; } // 缓存没有从数据库获取 user userMap.get(id); if (Objects.nonNull(user)) { // 添加缓存 caffeineCache.put(String.valueOf(id), user); } return user; } public void update(User user) { // 更新数据库 userMap.put(user.getId(), user); // 更新缓存 caffeineCache.put(String.valueOf(user.getId()), user); } public void delete(Integer id) { // 删除数据库 userMap.remove(id); // 删除缓存 caffeineCache.asMap().remove(String.valueOf(id)); } } #### 2.6.5、定义控制器调用测试### 2.7、总结上述一些策略在创建时都可以进行自由组合一般情况下有两种方法* 设置maxSize、refreshAfterWrite不设置expireAfterWrite/expireAfterAccess设置expireAfterWrite当缓存过期时会同步加锁获取缓存所以设置expireAfterWrite时性能较好但是某些时候会取旧数据,适合允许取到旧数据的场景* 设置maxSize、expireAfterWrite/expireAfterAccess不设置refreshAfterWrite数据一致性好不会获取到旧数据但是性能没那么好适合获取数据时不耗时的场景3、SpringBoot 集成 Caffeine 方式二----------------------------### 3.1、常用的注解Spring缓存注解Cacheable、CachePut、CacheEvict【日积月累】SpringBoot 通过注解CacheConfig Cacheable CacheEvict CachePut Caching使用缓存cache 方面的注解主要有以下 5 个1.Cacheable【创建、查询缓存】触发缓存入口一般放在创建和获取的方法上Cacheable注解会先查询是否已经有缓存。如果有则直接从缓存中返回如果没有则会执行方法并返回结果缓存【返回方法返回 NULL则不进行缓存】2.CachePut【更新缓存】更新缓存且不影响方法执行用于修改的方法上该注解下的方法始终会被执行3.CacheEvict【删除缓存】触发缓存的 eviction用于删除的方法上4.Caching【组合缓存配置】将多个缓存组合在一个方法上该注解可以允许一个方法同时设置多个注解5.CacheConfig【类级别共享配置】在类级别设置一些缓存相关的共同配置与其它缓存配合使用避免在每个缓存方法上重复配置相同的缓存属性Cacheable和CachePut的区别*Cacheable它的注解的方法是否被执行取决于Cacheable中的条件方法很多时候都可能不被执行*CachePut这个注解不会影响方法的执行也就是说无论它配置的条件是什么方法都会被执行更多的时候是被用到修改上 如果不想使用注解的方式去操作缓存也可以直接使用SimpleCacheManager获取缓存的 key 进而进行操作#### 3.1.1、Cacheable注解Cacheable注解使方法返回结果被缓存【返回结果为 NULL则不进行缓存】再次通过相同参数调用时会直接从缓存获取而不再执行该方法逻辑。Cacheable注解参数如下*value()/cacheNames()指定缓存/缓存管理器名称用来划分不同的缓存区避免相同 key 值互相影响*key()指定缓存的键值 key。默认情况下Spring 会根据方法的参数生成缓存键也可以使用 Spring EL 表达式来自定义缓存键的生成方式如#id,#user.id等*keyGenerator()指定使用的缓存键生成器的名称可以自定义 key 生成类通过反射方式自己构建 key跟 key() 参数不能同时赋值 * 在 Spring 框架中缓存键生成器KeyGenerator负责为缓存中的每个数据项生成唯一的键用于在检索时查找数据项。默认情况下Spring 使用SimpleKeyGenerator作为缓存键生成器它使用方法的参数作为键 key*cacheManager()指定使用的缓存管理器的名称。不赋值时为默认管理器常用于多缓存源场景*cacheResolver()指定使用的缓存解析器的名称跟keyGenerator()类似*condition()设置匹配条件【针对请求参数】用于判断是否执行缓存操作只有当表达式的结果为 true 时才会执行缓存操作。格式为 spring EL 表达式*unless()设置排除条件【针对返回值】用于判断是否不执行缓存操作只有当表达式的结果为 false 时才会执行缓存操作。格式为 spring EL 表达式*sync()指定是否使用同步模式进行缓存操作。若使用同步模式在多个线程同时对一个 key 进行 load 时其它线程将被阻塞默认值为false表示使用异步模式。在异步模式下如果多个线程同时访问同一个缓存项可能会导致缓存穿透的问题。可以将 sync 设置为 true 来避免这个问题如 Cacheable(cacheNames“users”, key“#user.id”) public User get(User user) { return new User(user.getId()); } Cacheable(cacheNames“users”, key“#user.id”, condition“#user.id 300”, unless“#result.id 500”) public User get(User user) { return new User(user.getId()); } #### 3.1.2、CachePut注解CachePut注解在功能上跟Cacheable基本相同不同之处就是每次都会执行方法逻辑更新缓存参数除不包含sync参数外其它跟Cacheable一致如 CachePut(cacheNames“users”, key“#id”) public User put(int id) { return new User(id); } #### 3.1.3、CacheEvict注解CacheEvict注解对符合参数条件的缓存做删除处理参数除跟Cacheable类似的参数外还包含另外*allEntries删除指定cacheNames区域内所有的缓存*beforeInvocation如果为 true 在执行方法之前做删除缓存处理为 false 时在执行方法之后做删除处理默认为 false如 CacheEvict(cacheNames“users”, key“#id”) public void delete(int id) { System.out.println(“do delete: id); } CacheEvict(cacheNames“users”, allEntriestrue) public void clear() { System.out.println(“do clear”); } #### 3.1.4、Caching注解Caching注解可同时组合、配置多个Cacheable、CachePut、CacheEvict注解 Caching( cacheable { Cacheable(value “userCache”, key “#id”) }, put { CachePut(value “userCache”, key “#result.id”) }, evict { CacheEvict(value “userListCache”, allEntries true) } ) public User getUserById(Long id) { // 从数据库中获取用户信息的逻辑 return user; } 当调用getUserById方法时会先从名为userCache的缓存中查找对应的用户信息如果缓存中不存在则执行方法的逻辑并将返回的用户信息存储到userCache缓存中并且将userListCache缓存中的所有数据移除#### 3.1.5、CacheConfig注解CacheConfig注解类级别的注解类下所有被缓存注解的方法都会继承所配置的参数避免方法上相同参数重复配置如 Service CacheConfig(cacheNames“users”, cacheManager“myCacheManager”) public class UserService2 { Cacheable(key”#id) public User get(int id) { return new User(id); } CachePut(key“#id”) public User put(int id) { return new User(id); } } ### 3.2、快速入门#### 3.2.1、引入依赖 org.springframework.boot spring-boot-starter-cache com.github.ben-manes.caffeine caffeine 2.6.2 #### 3.2.2、开启缓存功能开启缓存功能需要先添加使能注解EnableCaching通常习惯在启动类配置否则缓存注解Cacheable等不起作用 EnableCaching SpringBootApplication public class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } } #### 3.2.3、配置缓存类 Configuration public class CacheConfig { Bean(“caffeineCacheManager”) public CacheManager cacheManager() { CaffeineCacheManager cacheManager new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() .initialCapacity(100) .maximumSize(500) .expireAfterWrite(3, TimeUnit.SECONDS)); return cacheManager; } } #### 3.2.4、使用缓存修改UserServiceImpl Slf4j Service CacheConfig(cacheNames “caffeineCacheManager”) public class UserServiceImpl3 { // 模拟数据库数据 private MapInteger, User userMap new HashMap(); CachePut(key “#user.id”) public User add(User user) { log.info(“add”); userMap.put(user.getId(), user); return user; } Cacheable(key “#id”) public User get(Integer id) { log.info(“get”); return userMap.get(id); } CachePut(key “#user.id”) public User update(User user) { log.info(“update”); userMap.put(user.getId(), user); return user; } CacheEvict(key “#id”) public void delete(Integer id) { log.info(“delete”); userMap.remove(id); } } #### 3.2.5、添加 Controller RestController public class TestController { Autowired private UserServiceImpl3 userServiceImpl3; PostMapping public String add(RequestBody User user) { userServiceImpl3.add(user); return “add”; } GetMapping(“/{id}”) public String get(PathVariable Integer id) { User user userServiceImpl3.get(id); return “get”; } PutMapping public String update(RequestBody User user) { userServiceImpl3.update(user); return “update”; } DeleteMapping(“/{id}”) public String delete(PathVariable Integer id) { userServiceImpl3.delete(id); return “delete”; } } ### 3.3、配置数据源既然是对数据进行缓存就会涉及数据缓存到哪里问题是进程本地内存还是进程外远程存储就需要配置缓存源Spring 提供了丰富的缓存源种类。不同的种类引入的依赖不同 org.springframework.boot spring-boot-starter-cache org.springframework.boot spring-boot-starter-data-redis com.github.ben-manes.caffeine caffeine #### 3.3.1、不配置(默认)如果项目没有第三方缓存源依赖时SpringBoot 会默认配置ConcurrentMapCacheManager缓存管理器其内部由ConcurrentHashMap存储缓存数据如果有第三方缓存依赖例如caffeine、redis 时就会相应的配置CaffeineCacheManager或RedisCacheManager做默认缓存管理器#### 3.3.2、配置 Caffeine 缓存缓存配置有两种*CaffeineCacheManager使用一个全局的 Caffeine 配置来创建所有的缓存。不能为每个方法单独配置缓存过期时间但可以在程序启动时全局的配置缓存方便设置所有方法的缓存过期时间*SimpleCacheManager当应用程序启动时通过配置多个CaffeineCache来创建多个缓存。可以为每个方法单独配置缓存过期时间##### 3.3.2.1、CaffeineCacheManager###### 3.3.2.1.1、使用 Java 配置类Java 配置方式 Configuration public class CacheConfig { Bean(“caffeineCacheManager”) public CacheManager cacheManager() { CaffeineCacheManager cacheManager new CaffeineCacheManager(); cacheManager.setCaffeine(Caffeine.newBuilder() .initialCapacity(100) .maximumSize(500) .expireAfterWrite(3, TimeUnit.SECONDS)); return cacheManager; } } 当然也可以使用配置文件进行配置缓存项信息但是灵活性不够高。###### 3.3.2.1.2、使用配置文件配置文件方式可参考通用配置类CacheProperties定义使用配置文件进行配置缓存项信息 spring: cache: type: caffeine cache-names: - userCache caffeine: spec: maximumSize1024,refreshAfterWrite60s 如果使用refreshAfterWrite配置必须指定一个CacheLoader不用该配置则无需这个 bean。如上所述该CacheLoader将关联被该缓存管理器管理的所有缓存所以必须定义为CacheLoaderObject, Object自动配置将忽略所有泛型类型 Configuration public class CacheConfig { /** * 相当于在构建LoadingCache对象的时候 build()方法中指定过期之后的加载策略方法 * 必须要指定这个BeanrefreshAfterWrite60s属性才生效 * return */ Bean public CacheLoaderString, Object cacheLoader() { CacheLoaderString, Object cacheLoader new CacheLoaderString, Object() { Override public Object load(String key) throws Exception { return null; } // 重写这个方法将oldValue值返回回去进而刷新缓存 Override public Object reload(String key, Object oldValue) throws Exception { return oldValue; } }; return cacheLoader; } } ##### 3.3.2.2、SimpleCacheManager配置多个 CaffeineCache 来创建多个缓存 Bean(name “simpleCacheManager”) public SimpleCacheManager simpleCacheManager() { SimpleCacheManager result new SimpleCacheManager(); CaffeineCache users new CaffeineCache(“users”, Caffeine.newBuilder() .expireAfterWrite(600, TimeUnit.SECONDS) .maximumSize(10000L).build()); CaffeineCache roles new CaffeineCache(“roles”, Caffeine.newBuilder() .expireAfterWrite(600, TimeUnit.SECONDS) .maximumSize(10000L).build()); result.setCaches(Arrays.asList(users, roles)); return result ; } #### 3.3.3、配置 Redis 缓存 Bean(name “redisCacheManager”) public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(600)) .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues() .prefixCacheNameWith(“mtr”); RedisCacheManager redisCacheManager RedisCacheManager.builder(factory) .cacheDefaults(config) .transactionAware() .build(); return redisCacheManager; } #### 3.3.4、配置多缓存源 Configuration public class CacheConfig { Bean(name “simpleCacheManager”) public SimpleCacheManager simpleCacheManager() { SimpleCacheManager result new SimpleCacheManager(); CaffeineCache users new CaffeineCache(“users”, Caffeine.newBuilder() .expireAfterWrite(600, TimeUnit.SECONDS) .maximumSize(10000L).build()); CaffeineCache roles new CaffeineCache(“roles”, Caffeine.newBuilder() .expireAfterWrite(600, TimeUnit.SECONDS) .maximumSize(10000L).build()); result.setCaches(Arrays.asList(users, roles)); return result ; } Bean(name “redisCacheManager”) Primary public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(600)) .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues() .prefixCacheNameWith(“mtr”); RedisCacheManager redisCacheManager RedisCacheManager.builder(factory) .cacheDefaults(config) .transactionAware() .build(); return redisCacheManager; } } 【注意】如果使用了多个 cahce比如redis、caffeine 等必须指定某一个CacheManage为primary在Cacheable注解中没指定cacheManager则使用标记为primary的那个。### 3.4、keyGenerator Component(“keyGenerator”) public class CacheKeyGenerator implements KeyGenerator { public static final int NO_PARAM_KEY 0; public static final int NULL_PARAM_KEY 53; Override public Object generate(Object target, Method method, Object… params) { StringBuilder key new StringBuilder(); key.append(target.getClass().getSimpleName()).append(“.”).append(method.getName()).append(“:”); if (params.length 0) { key.append(NO_PARAM_KEY); } else { int count 0; for (Object param : params) { if (0 ! count) { key.append(‘,’); } if (param null) { key.append(NULL_PARAM_KEY); } else if (ClassUtils.isPrimitiveArray(param.getClass())) { int length Array.getLength(param); for (int i 0; i length; i) { key.append(Array.get(param, i)); key.append(‘,’); } } else if (ClassUtils.isPrimitiveOrWrapper(param.getClass()) || param instanceof String) { key.append(param); } else { //Java一定要重写hashCode和eqauls key.append(param.hashCode()); } count; } } String finalKey key.toString(); System.out.println(“using cache key” finalKey); return finalKey; } } ### 3.5、CacheResolverSimpleCacheResolverNamedCacheResolver是 Spring 内部的CacheResolver接口实现类可根据实际情况参考实现5、SPEL------Spring Cache 提供了一些供我们使用的 SpEL 上下文数据下表名称位置描述示例methodNameroot 对象当前被调用的方法名#root.methodnamemethodroot 对象当前被调用的方法#root.method.nametargetroot 对象当前被调用的目标对象实例#root.targettargetClassroot 对象当前被调用的目标对象的类#root.targetClassargsroot 对象当前被调用的方法的参数列表#root.args[0]cachesroot 对象当前方法调用使用的缓存列表#root.caches[0].nameArgument Name执行上下文当前被调用的方法的参数如findArtisan(Artisan artisan),可以通过#artsian.id获得参数#artsian.idresult执行上下文方法执行后的返回值仅当方法执行后的判断有效如 unless cacheEvict的beforeInvocationfalse#result1. 当我们要使用root对象的属性作为key时我们也可以将“#root”省略因为Spring默认使用的就是root对象的属性。如Cacheable(key targetClass methodName #p0)2. 使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。如Cacheable(valueuserCache, key#id)、Cacheable(valueuserCache, key#p0)SpEL 提供了多种运算符类型运算符关系!ltgtlegeeqne算术- * /%^逻辑条件: (ternary): (elvis)正则表达式matches其他类型.[…]![…]1$[…]6、Caffeine 的优劣势和适用场景--------------------*优势对比 Guava cache 有更高的缓存性能*劣势仍然存在缓存漂移的问题JDK 版本过低无法使用*适用场景读多写少对数据一致性要求不高的场景纯内存缓存JDK8 及更高版本中追求比 Guava cache 更高的性能* * *1. … ??
返回列表