唐山专业网站建设公司,黑龙江省住房和城乡建设信息网,在线咨询妇科医生免费,wordpress安装不能选择数据库一、Caffeine介绍
1、缓存介绍
缓存(Cache)在代码世界中无处不在。从底层的CPU多级缓存#xff0c;到客户端的页面缓存#xff0c;处处都存在着缓存的身影。缓存从本质上来说#xff0c;是一种空间换时间的手段#xff0c;通过对数据进行一定的空间安排#xff0c;使得下… 一、Caffeine介绍
1、缓存介绍
缓存(Cache)在代码世界中无处不在。从底层的CPU多级缓存到客户端的页面缓存处处都存在着缓存的身影。缓存从本质上来说是一种空间换时间的手段通过对数据进行一定的空间安排使得下次进行数据访问时起到加速的效果。
就Java而言其常用的缓存解决方案有很多例如数据库缓存框架EhCache分布式缓存Memcached等这些缓存方案实际上都是为了提升吞吐效率避免持久层压力过大。
对于常见缓存类型而言可以分为本地缓存以及分布式缓存两种Caffeine就是一种优秀的本地缓存而Redis可以用来做分布式缓存
2、Caffeine介绍
Caffeine官方 https://github.com/ben-manes/caffeine Caffeine是基于Java 1.8的高性能本地缓存库由Guava改进而来而且在Spring5开始的默认缓存实现就将Caffeine代替原来的Google Guava官方说明指出其缓存命中率已经接近最优值。实际上Caffeine这样的本地缓存和ConcurrentMap很像即支持并发并且支持O(1)时间复杂度的数据存取。二者的主要区别在于 ConcurrentMap将存储所有存入的数据直到你显式将其移除 Caffeine将通过给定的配置自动移除“不常用”的数据以保持内存的合理占用。
因此一种更好的理解方式是Cache是一种带有存储和移除策略的Map。 基于 Spring Boot MyBatis Plus Vue Element 实现的后台管理系统 用户小程序支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能 项目地址https://github.com/YunaiV/ruoyi-vue-pro 视频教程https://doc.iocoder.cn/video/ 二、Caffeine基础
使用Caffeine需要在工程中引入如下依赖
dependencygroupIdcom.github.ben-manes.caffeine/groupIdartifactIdcaffeine/artifactId!--https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeinez找最新版--version3.0.5/version
/dependency1、缓存加载策略
1.1 Cache手动创建
最普通的一种缓存无需指定加载方式需要手动调用put()进行加载。需要注意的是put()方法对于已存在的key将进行覆盖这点和Map的表现是一致的。在获取缓存值时如果想要在缓存值不存在时原子地将值写入缓存则可以调用get(key, k - value)方法该方法将避免写入竞争。调用invalidate()方法将手动移除缓存。
在多线程情况下当使用get(key, k - value)时如果有另一个线程同时调用本方法进行竞争则后一线程会被阻塞直到前一线程更新缓存完成而若另一线程调用getIfPresent()方法则会立即返回null不会被阻塞。
CacheObject, Object cache Caffeine.newBuilder()//初始数量.initialCapacity(10)//最大条数.maximumSize(10)//expireAfterWrite和expireAfterAccess同时存在时以expireAfterWrite为准//最后一次写操作后经过指定时间过期.expireAfterWrite(1, TimeUnit.SECONDS)//最后一次读或写操作后经过指定时间过期.expireAfterAccess(1, TimeUnit.SECONDS)//监听缓存被移除.removalListener((key, val, removalCause) - { })//记录命中.recordStats().build();cache.put(1,张三);//张三System.out.println(cache.getIfPresent(1));//存储的是默认值System.out.println(cache.get(2,o - 默认值));1.2 Loading Cache自动创建
LoadingCache是一种自动加载的缓存。其和普通缓存不同的地方在于当缓存不存在/缓存已过期时若调用get()方法则会自动调用CacheLoader.load()方法加载最新值。调用getAll()方法将遍历所有的key调用get()除非实现了CacheLoader.loadAll()方法。使用LoadingCache时需要指定CacheLoader并实现其中的load()方法供缓存缺失时自动加载。
在多线程情况下当两个线程同时调用get()则后一线程将被阻塞直至前一线程更新缓存完成。
LoadingCacheString, String loadingCache Caffeine.newBuilder()//创建缓存或者最近一次更新缓存后经过指定时间间隔刷新缓存refreshAfterWrite仅支持LoadingCache.refreshAfterWrite(10, TimeUnit.SECONDS).expireAfterWrite(10, TimeUnit.SECONDS).expireAfterAccess(10, TimeUnit.SECONDS).maximumSize(10)//根据key查询数据库里面的值这里是个lamba表达式.build(key - new Date().toString());1.3 Async Cache异步获取
AsyncCache是Cache的一个变体其响应结果均为CompletableFuture通过这种方式AsyncCache对异步编程模式进行了适配。默认情况下缓存计算使用ForkJoinPool.commonPool()作为线程池如果想要指定线程池则可以覆盖并实现Caffeine.executor(Executor)方法。synchronous()提供了阻塞直到异步缓存生成完毕的能力它将以Cache进行返回。
在多线程情况下当两个线程同时调用get(key, k - value)则会返回同一个CompletableFuture对象。由于返回结果本身不进行阻塞可以根据业务设计自行选择阻塞等待或者非阻塞。
AsyncLoadingCacheString, String asyncLoadingCache Caffeine.newBuilder()//创建缓存或者最近一次更新缓存后经过指定时间间隔刷新缓存仅支持LoadingCache.refreshAfterWrite(1, TimeUnit.SECONDS).expireAfterWrite(1, TimeUnit.SECONDS).expireAfterAccess(1, TimeUnit.SECONDS).maximumSize(10)//根据key查询数据库里面的值.buildAsync(key - {Thread.sleep(1000);return new Date().toString();});//异步缓存返回的是CompletableFuture
CompletableFutureString future asyncLoadingCache.get(1);
future.thenAccept(System.out::println);2、驱逐策略
驱逐策略在创建缓存的时候进行指定。常用的有基于容量的驱逐和基于时间的驱逐。
基于容量的驱逐需要指定缓存容量的最大值当缓存容量达到最大时Caffeine将使用LRU策略对缓存进行淘汰基于时间的驱逐策略如字面意思可以设置在最后访问/写入一个缓存经过指定时间后自动进行淘汰。
驱逐策略可以组合使用任意驱逐策略生效后该缓存条目即被驱逐。 LRU 最近最少使用淘汰最长时间没有被使用的页面。 LFU 最不经常使用淘汰一段时间内使用次数最少的页面 FIFO 先进先出
Caffeine有4种缓存淘汰设置 大小 LFU算法进行淘汰 权重 大小与权重 只能二选一 时间 引用 不常用本文不介绍
Slf4j
public class CacheTest {/*** 缓存大小淘汰*/Testpublic void maximumSizeTest() throws InterruptedException {CacheInteger, Integer cache Caffeine.newBuilder()//超过10个后会使用W-TinyLFU算法进行淘汰.maximumSize(10).evictionListener((key, val, removalCause) - {log.info(淘汰缓存key:{} val:{}, key, val);}).build();for (int i 1; i 20; i) {cache.put(i, i);}Thread.sleep(500);//缓存淘汰是异步的// 打印还没被淘汰的缓存System.out.println(cache.asMap());}/*** 权重淘汰*/Testpublic void maximumWeightTest() throws InterruptedException {CacheInteger, Integer cache Caffeine.newBuilder()//限制总权重若所有缓存的权重加起来总权重就会淘汰权重小的缓存.maximumWeight(100).weigher((WeigherInteger, Integer) (key, value) - key).evictionListener((key, val, removalCause) - {log.info(淘汰缓存key:{} val:{}, key, val);}).build();//总权重其实是所有缓存的权重加起来int maximumWeight 0;for (int i 1; i 20; i) {cache.put(i, i);maximumWeight i;}System.out.println(总权重 maximumWeight);Thread.sleep(500);//缓存淘汰是异步的// 打印还没被淘汰的缓存System.out.println(cache.asMap());}/*** 访问后到期每次访问都会重置时间也就是说如果一直被访问就不会被淘汰*/Testpublic void expireAfterAccessTest() throws InterruptedException {CacheInteger, Integer cache Caffeine.newBuilder().expireAfterAccess(1, TimeUnit.SECONDS)//可以指定调度程序来及时删除过期缓存项而不是等待Caffeine触发定期维护//若不设置scheduler则缓存会在下一次调用get的时候才会被动删除.scheduler(Scheduler.systemScheduler()).evictionListener((key, val, removalCause) - {log.info(淘汰缓存key:{} val:{}, key, val);}).build();cache.put(1, 2);System.out.println(cache.getIfPresent(1));Thread.sleep(3000);System.out.println(cache.getIfPresent(1));//null}/*** 写入后到期*/Testpublic void expireAfterWriteTest() throws InterruptedException {CacheInteger, Integer cache Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.SECONDS)//可以指定调度程序来及时删除过期缓存项而不是等待Caffeine触发定期维护//若不设置scheduler则缓存会在下一次调用get的时候才会被动删除.scheduler(Scheduler.systemScheduler()).evictionListener((key, val, removalCause) - {log.info(淘汰缓存key:{} val:{}, key, val);}).build();cache.put(1, 2);Thread.sleep(3000);System.out.println(cache.getIfPresent(1));//null}
}3、刷新机制
refreshAfterWrite()表示x秒后自动刷新缓存的策略可以配合淘汰策略使用注意的是刷新机制只支持LoadingCache和AsyncLoadingCache
private static int NUM 0;Test
public void refreshAfterWriteTest() throws InterruptedException {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
}4、统计
LoadingCacheString, String cache Caffeine.newBuilder()//创建缓存或者最近一次更新缓存后经过指定时间间隔刷新缓存refreshAfterWrite仅支持LoadingCache.refreshAfterWrite(1, TimeUnit.SECONDS).expireAfterWrite(1, TimeUnit.SECONDS).expireAfterAccess(1, TimeUnit.SECONDS).maximumSize(10)//开启记录缓存命中率等信息.recordStats()//根据key查询数据库里面的值.build(key - {Thread.sleep(1000);return new Date().toString();});cache.put(1, shawn);
cache.get(1);/** hitCount :命中的次数* missCount:未命中次数* requestCount:请求次数* hitRate:命中率* missRate:丢失率* loadSuccessCount:成功加载新值的次数* loadExceptionCount:失败加载新值的次数* totalLoadCount:总条数* loadExceptionRate:失败加载新值的比率* totalLoadTime:全部加载时间* evictionCount:丢失的条数*/
System.out.println(cache.stats());5、总结
上述一些策略在创建时都可以进行自由组合一般情况下有两种方法 设置 maxSize、refreshAfterWrite不设置 expireAfterWrite/expireAfterAccess设置expireAfterWrite当缓存过期时会同步加锁获取缓存所以设置expireAfterWrite时性能较好但是某些时候会取旧数据,适合允许取到旧数据的场景 设置 maxSize、expireAfterWrite/expireAfterAccess不设置 refreshAfterWrite 数据一致性好不会获取到旧数据但是性能没那么好对比起来适合获取数据时不耗时的场景 基于 Spring Cloud Alibaba Gateway Nacos RocketMQ Vue Element 实现的后台管理系统 用户小程序支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能 项目地址https://github.com/YunaiV/yudao-cloud 视频教程https://doc.iocoder.cn/video/ 三、SpringBoot整合Caffeine
1、Cacheable相关注解
1.1 相关依赖
如果要使用Cacheable注解需要引入相关依赖并在任一配置类文件上添加EnableCaching注解
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-cache/artifactId
/dependency1.2 常用注解 Cacheable 表示该方法支持缓存。当调用被注解的方法时如果对应的键已经存在缓存则不再执行方法体而从缓存中直接返回。当方法返回null时将不进行缓存操作。 CachePut 表示执行该方法后其值将作为最新结果更新到缓存中每次都会执行该方法。 CacheEvict 表示执行该方法后将触发缓存清除操作。 Caching 用于组合前三个注解例如
Caching(cacheable Cacheable(CacheConstants.GET_USER),evict {CacheEvict(CacheConstants.GET_DYNAMIC,allEntries true)}
public User find(Integer id) {return null;
}1.3 常用注解属性 cacheNames/value 缓存组件的名字即cacheManager中缓存的名称。 key 缓存数据时使用的key。默认使用方法参数值也可以使用SpEL表达式进行编写。 keyGenerator 和key二选一使用。 cacheManager 指定使用的缓存管理器。 condition 在方法执行开始前检查在符合condition的情况下进行缓存 unless 在方法执行完成后检查在符合unless的情况下不进行缓存 sync 是否使用同步模式。若使用同步模式在多个线程同时对一个key进行load时其他线程将被阻塞。
1.4 缓存同步模式
sync开启或关闭在Cache和LoadingCache中的表现是不一致的 Cache中sync表示是否需要所有线程同步等待 LoadingCache中sync表示在读取不存在/已驱逐的key时是否执行被注解方法
2、实战
2.1 引入依赖
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-cache/artifactId
/dependencydependencygroupIdcom.github.ben-manes.caffeine/groupIdartifactIdcaffeine/artifactId
/dependency2.2 缓存常量CacheConstants
创建缓存常量类把公共的常量提取一层复用这里也可以通过配置文件加载这些数据例如ConfigurationProperties和Value
public class CacheConstants {/*** 默认过期时间配置类中我使用的时间单位是秒所以这里如 3*60 为3分钟*/public static final int DEFAULT_EXPIRES 3 * 60;public static final int EXPIRES_5_MIN 5 * 60;public static final int EXPIRES_10_MIN 10 * 60;public static final String GET_USER GET:USER;public static final String GET_DYNAMIC GET:DYNAMIC;}2.3 缓存配置类CacheConfig
Configuration
EnableCaching
public class CacheConfig {/*** Caffeine配置说明* initialCapacity[integer]: 初始的缓存空间大小* maximumSize[long]: 缓存的最大条数* maximumWeight[long]: 缓存的最大权重* expireAfterAccess[duration]: 最后一次写入或访问后经过固定时间过期* expireAfterWrite[duration]: 最后一次写入后经过固定时间过期* refreshAfterWrite[duration]: 创建缓存或者最近一次更新缓存后经过固定的时间间隔刷新缓存* weakKeys: 打开key的弱引用* weakValues打开value的弱引用* softValues打开value的软引用* recordStats开发统计功能* 注意* expireAfterWrite和expireAfterAccess同事存在时以expireAfterWrite为准。* maximumSize和maximumWeight不可以同时使用* weakValues和softValues不可以同时使用*/Beanpublic CacheManager cacheManager() {SimpleCacheManager cacheManager new SimpleCacheManager();ListCaffeineCache list new ArrayList();//循环添加枚举类中自定义的缓存可以自定义for (CacheEnum cacheEnum : CacheEnum.values()) {list.add(new CaffeineCache(cacheEnum.getName(),Caffeine.newBuilder().initialCapacity(50).maximumSize(1000).expireAfterAccess(cacheEnum.getExpires(), TimeUnit.SECONDS).build()));}cacheManager.setCaches(list);return cacheManager;}
}2.4 调用缓存
这里要注意的是Cache和Transactional一样也使用了代理类内调用将失效
/*** value缓存key的前缀。* key缓存key的后缀。* sync设置如果缓存过期是不是只放一个请求去请求数据库其他请求阻塞默认是false根据个人需求。* unless不缓存空值,这里不使用会报错* 查询用户信息类* 如果需要加自定义字符串需要用单引号* 如果查询为null也会被缓存*/
Cacheable(value CacheConstants.GET_USER,key user#userId,sync true)
CacheEvict
public UserEntity getUserByUserId(Integer userId){UserEntity userEntity userMapper.findById(userId);System.out.println(查询了数据库);return userEntity;
}