当前位置: 首页 > 后端技术 > Java

Caffeine

时间:2023-04-01 18:39:05 Java

,SpringBoot集成缓存性能之王,利用缓存提升性能。今天码哥就带大家练习使用spring-boot-starter-cache这个抽象缓存组件来集成本地缓存性能之王Caffeine。需要注意的是:内存缓存只适合单体应用,不适合分布式环境。在分布式环境下,需要将缓存修改同步到各个节点,需要同步机制来保证各个节点的缓存数据最终是一致的。SpringCache有什么用不到SpringCache的抽象缓存接口,我们需要根据不同的缓存框架来实现缓存,需要在相应的代码中对应缓存的加载、删除、更新等。例如,我们对查询采用绕过缓存的策略:先从缓存中查询数据,如果找不到,再从数据库中查询,写入缓存。伪代码如下:publicUsergetUser(longuserId){//从缓存中查询用户user=cache.get(userId);如果(用户!=null){返回用户;}//从数据库加载用户dbUser=loadDataFromDB(userId);if(dbUser!=null){//设置到缓存cache.put(userId,dbUser)}returndbUser;}我们需要写很多这样繁琐的代码,SpringCache对缓存进行了抽象,并提供了如下注解实现缓存management:@Cacheable:触发缓存读取操作,在查询方法中使用。如果在缓存中找到,直接取出缓存返回,否则执行目标方法,缓存结果。@CachePut:在触发缓存更新的方法上,与Cacheable相比,该注解的方法会一直执行,方法返回的结果用于更新缓存,适用于插入更新行为的方法.@CacheEvict:触发缓存失效,删除缓存项或清除缓存,适用于delete方法。另外,抽象的CacheManager既可以集成基于本地内存的单体应用,也可以集成EhCache、Redis等缓存服务器。最方便的是不需要修改任何代码,通过一些简单的配置和注解就可以访问不同的缓存框架。集成Caffeine码哥带你使用注解完成缓存操作来集成。完整代码请访问github:https://github.com/MageByte-Z...,在pom.xml文件中添加如下依赖:org.springframework.bootspring-boot-starter-cachecom.github.ben-manes.caffeinecaffeine使用JavaConfig配置CacheManager:@Slf4j@EnableCaching@ConfigurationpublicclassCacheConfig{@Autowired@Qualifier("cacheExecutor")privateExecutorcacheExecutor;@BeanpublicCaffeinecaffeineCache(){returnCaffeine.newBuilder()//设置一个固定时间在最后一次写入或访问后过期。expireAfterAccess(7,TimeUnit.DAYS)//初始缓存空间大小。initialCapacity(500)//使用自定义线程pool.executor(cacheExecutor).removalListener(((key,value,cause)->log.info("key:{}removed,removalCause:{}.",key,cause.name())))//缓存中的最大项目数.maximumSize(1000);}@BeanpublicCacheManagercacheManager(){CaffeineCacheManagercaffeineCacheManager=newCaffeineCacheManager();caffeineCacheManager.setCaffeine(caffeineCache());//不缓存空值caffeineCacheManager.setAllowNullValues(false);下一步是准备;返回ManagerCache}}如何使用@Slf4j@ServicepublicclassAddressService{publicstaticfinalStringCACHE_NAME="caffeine:address";privatestaticfinalAtomicLongID_CREATOR=newAtomicLong(0);privateMapaddressMap;publicAddressService(){addressMap=newConcurrentHashMap<>();addressMap.put(ID_CREATOR.incrementAndGet(),AddressDTO.builder().customerId(ID_CREATOR.get()).address("地址1").build());addressMap.put(ID_CREATOR.incrementAndGet(),AddressDTO.builder().customerId(ID_CREATOR.get()).address("地址2").build());addressMap.put(ID_CREATOR.incrementAndGet(),AddressDTO.builder().customerId(ID_CREATOR.get()).address("地址3").build());}@Cacheable(cacheNames={CACHE_NAME},key="#customerId")publicAddressDTOgetAddress(longcustomerId){log.info("customerId:{}没有行走存储,从数据库查询开始",customerId);返回addressMap.get(cust标识符);}@CachePut(cacheNames={CACHE_NAME},key="#result.customerId")publicAddressDTOcreate(Stringaddress){longcustomerId=ID_CREATOR.incrementAndGet();AddressDTOaddressDTO=AddressDTO.builder().customerId(customerId).address(地址).build();addressMap.put(customerId,addressDTO);返回地址DTO;}@CachePut(cacheNames={CACHE_NAME},key="#result.customerId")publicAddressDTOupdate(LongcustomerId,Stringaddress){AddressDTOaddressDTO=addressMap.get(customerId);if(addressDTO==null){thrownewRuntimeException("没有customerId="+customerId+"的地址");}addressDTO.setAddress(地址);返回地址DTO;}@CacheEvict(cacheNames={CACHE_NAME},key="#customerId")publicbooleandelete(longcustomerId){log.info("缓存{}被删除",customerId);返回真;}}使用CacheName隔离不同业务场景的缓存,每个Cache内部持有一个map结构来存储数据,key可以使用Spring的Spel表达式单元测试开始:@RunWith(SpringRunner.class)@SpringBootTest(classes=CaffeineApplication.class)@Slf4jpublicclassCaffeineApplicationTests{@AutowiredprivateAddressServiceaddressService;@AutowiredprivateCacheManager缓存管理器;@TestpublicvoidtestCache(){//插入缓存和数据库AddressDTOnewInsert=addressService.create("南山大道");//去缓存AddressDTOaddress=addressService.getAddress(newInsert.getCustomerId());长客户ID=2;//首先misscache,printcustomerId:{}没有去缓存,开始从数据库中查询AddressDTOaddress2=addressService.getAddress(customerId);//命中缓存AddressDTOcacheAddress2=addressService.getAddress(customerId);//更新数据库和缓存addressService.update(customerId,"Address2Modified");//更新后查询,仍然命中缓存AddressDTOhitCache2=addressService.getAddress(customerId);Assert.assertEquals(hitCache2.getAddress(),"地址2已被修改");//删除缓存addressService.delete(customerId);//未命中缓存,从数据库中读取AddressDTOhit=addressService.getAddress(customerId);System.out.println(hit.getCustomerId());}}有没有发现,只需要给相应的方法加上注解,就可以愉快的使用缓存了。需要注意的是,设置的cacheNames必须对应,每个业务场景使用对应的cacheNames。另外,key可以使用spel表达式。可以关注@CachePut(cacheNames={CACHE_NAME},key="#result.customerId"),result表示接口返回的结果。Spring提供了几个元数据供直接使用。名称位置描述示例methodName根对象被调用方法的名称#root.methodNamemethod根对象被调用方法#root.method.nametarget根对象被调用目标对象#root.targettargetClass根对象被调用目标类#root.targetClassargs根对象用于调用目标的参数(作为数组)#root.args[0]caches根对象运行当前方法的缓存集合#root.caches[0].nameParameterName任何方法参数的名称评价语境。如果名称不可用(可能是由于没有调试信息),参数名称也可以在#a<#arg>下使用,其中#arg表示参数索引(从0开始)。#iban或#a0(您也可以使用#p0或#p<#arg>符号作为别名)。result评估上下文方法调用的结果(要缓存的值)。仅在unless表达式、cacheput表达式(计算键)或cacheevict表达式(当beforeInvocation为false时)可用。对于支持的包装器(例如Optional),#result指的是实际对象,而不是包装器。#result核心原理JavaCaching定义了五个核心接口,分别是CachingProvider、CacheManager、Cache、Entry和Expiry。核心类图:Cache:抽象缓存操作,如get()、put();CacheManager:管理Cache,可以理解为Cache的集合管理。之所以有多个Cache,是为了可以根据不同的场景使用。缓存失效时间和数量限制。CacheInterceptor,CacheAspectSupport,AbstractCacheInvoker:CacheInterceptor是一个AOP方法拦截器,在方法前后做额外的逻辑,比如查询操作,先检查缓存,找不到数据就执行方法,写入方法的结果入缓存等,它继承了CacheAspectSupport(缓存操作的主要逻辑),AbstractCacheInvoker(封装了Cache的读写)。CacheOperation、AnnotationCacheOperationSource、SpringCacheAnnotationParser:CacheOperation定义了缓存操作的缓存名称、缓存键、缓存条件、CacheManager等。AnnotationCacheOperationSource是获取CacheOperation对应的缓存注解的类,SpringCacheAnnotationParser是解析注解的类。解析后会封装到CacheOperationCollection中,供AnnotationCacheOperationSource查找。CacheAspectSupport:缓存切面支持类,是CacheInterceptor的父类,封装了所有缓存操作的主要逻辑。主要流程如下:通过CacheOperationSource获取所有的CacheOperation列表。如果有@CacheEvict注解,并且在调用前被标记为已执行,则删除/清除缓存。如果有@Cacheable注解,如果缓存不命中(查询结果为null),则查询缓存,则加入cachePutRequests,原方法执行后,写入缓存。当缓存命中时,缓存值作为结果;当缓存未命中或者有@CachePut注解时,需要调用原方法,使用原方法的返回值作为结果,如果有@CachePut注解,则添加到cachePutRequests中。如果缓存未命中,则将查询结果值写入缓存;如果有@CachePut注解,方法执行结果也会写入缓存。如果有@CacheEvict注解,调用后标记为Execute,则删除/清除缓存。今天就到此为止。我将与您分享一些工作技巧。后面码哥会分享如何接入Redis,带大家实现一个基于SpingBoot的Caffeine作为二级缓存,Redis作为二级缓存的分布式二级缓存框架。下期再见,能在评论区夸我帅吗?不叫也没关系,也鼓励点赞分享。参考资料[1]https://segmentfault.com/a/11...[2]https://docs.spring.io/spring...