重学springboot系列之ehcache缓存,缓存问题(代码片段)

大忽悠爱忽悠 大忽悠爱忽悠     2023-02-20     473

关键词:


EhCache缓存

在Spring框架内我们首选Spring Cache作为缓存框架的门面,之所以说它是门面,是因为它只提供接口层的定义以及AOP注解等,不提供缓存的具体存取操作。缓存的具体存储还需要具体的缓存存储,比如EhCache 、Redis等。Spring Cache与缓存框架的关系有点像SLF4j与logback、log4j的关系。

  • EhCache 适用于单体应用的缓存,当应用进行分布式部署的时候,各应用的副本之间缓存是不同步的。EhCache 由于没有独立的部署服务,所以它的缓存和应用的内存是耦合在一起的,当缓存数据量比较大的时候要注意系统资源能不能满足应用内存的要求。
  • redis由于是可以独立部署的内存数据库服务,所以它能够满足应用分布式部署的缓存集中存储的要求,也就是分布式部署的应用使用一份缓存,从而缓存自然是同步的。但是对于一些小规模的应用,额外引入了redis服务,增加了运维的成本。

所以,比如我们自己开发一个小博客,自己的服务器又没有很多的资源独立部署redis服务,用EHCache作为缓存是比较好的选择。如果是企业级用户量,使用redis独立部署的服务作为缓存是更好的选择。


整合Spring Cache 与Ehcache

通过上一小节的学习,可以使用Spring cache通过注解的方式来操作缓存,一定程度上减少了程序员缓存操作代码编写量。注解添加和移除都很方便,不与业务代码耦合,容易维护。 这一部分内容是没有变化的,所以我们仍然使用Spring Cache

第一步:pom.xml 添加 Spring cache 和 Ehcache的 jar 依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>net.sf.ehcache</groupId>
        <artifactId>ehcache</artifactId>
    </dependency>

第二步:添加入口启动类 @EnableCaching 注解开启 Caching,实例如下。

@EnableCaching

在Spring Boot中通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者,也就是说Spring Cache支持下面的这些缓存框架:

  • Generic
  • JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
  • EhCache 2.x(发现ehcache的bean,就使用ehcache作为缓存)
  • Hazelcast
  • Infinispan Couchbase
  • Redis
  • Caffeine
  • Simple

第三步:添加ehcache配置

yml配置

需要说明的是config:classpath:/ehcache.xml可以不用写,因为默认就是这个路径。但ehcache.xml必须有。

spring:
  cache:
    type: ehcache
    ehcache:
      config: classpath:/ehcache.xml

在 resources 目录下,添加 ehcache 的配置文件 ehcache.xml ,文件内容如下:

<ehcache>
    <diskStore path="java.io.tmpdir/cache_dongbb"/>
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
    />
    <cache name="user_detail"
            maxElementsInMemory="10000"
            eternal="true"
            overflowToDisk="true"
            diskPersistent="true"
            diskExpiryThreadIntervalSeconds="600"/>
</ehcache>

配置含义:

  • name:缓存名称,与缓存注解的value(cacheNames)属性值相同。
  • maxElementsInMemory:缓存最大个数。
  • eternal:缓存对象是否永久有效,一但设置了,timeout将不起作用。
  • timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
  • timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
  • overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
  • diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
  • maxElementsOnDisk:硬盘最大缓存个数。
  • diskPersistent:是否缓存虚拟机重启期数据。
  • diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
  • memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
  • clearOnFlush:内存数量最大时是否清除。
  • diskStore 则表示临时缓存的硬盘目录。

“java.io.tmpdir”操作系统缓存的临时目录,不同操作系统的缓存临时目录不一样,在Windows的缓存目录为C:\\\\Users\\\\登录用户~1\\\\AppData\\\\Local\\\\Temp\\\\ ; Linux目录为/tmp


缓存的使用方法

缓存的使用方法仍然是Spring Cache的注解,使用方法是一样的,参考上一小节学习


缓存使用中的坑

注意:@Cacheable 注解在对象内部调用不会生效。这个坑不是单独针对EhCache的,只要使用Spring Cache都会有这个问题。

@Component
public class ClassA   
    @Override
    public void MethodA(String username)  
        MethodA1(username);  //缓存失效,@Cacheable 注解在对象内部调用不会生效
    

    @Cacheable(value = USER_DETAIL,key = "#username")
    public void MethodA1(String username) 
       //执行方法体
    

原因: Spring 缓存注解是基于Spring AOP切面,必须走代理才能生效,同类调用或者子类调用父类带有缓存注解的方法时属于内部调用,没有走代理,所以注解不生效。

解决办法: 将缓存方法,放在一个单独的类中

@Component
public class ClassA   
    @Resource
    ClassB classB;

    @Override
    public void methodA(String username)  
        classB.methodA1(username);  //缓存失效,@Cacheable 注解在对象内部调用不会生效
    

@Component
public class ClassB   

    @Cacheable(value = USER_DETAIL,key = "#username")
    public void methodA1(String username) 
       //执行方法体
    


缓存雪崩穿透等解决方案

缓存使用的若干问题

缓存穿透

正常情况下,我们去查询数据大部分都是存在的。如果请求去查询一条压根儿数据库中根本就不存在的数据,也就是缓存和数据库都查询不到这条数据,但是请求每次都会打到数据库上面去,造成对后端数据库的强大压力。这种查询不存在数据的现象我们称为缓存穿透。(有可能会是某些不法份子的恶意行为,多线程打满去向服务查询不存在的数据)

解决办法

  • 做好查询请求的数据校验,治标不治本
  • 缓存空值,之所以会穿透缓存给压力到数据库,就是因为缓存层没有缓存null值。后文会说明在Spring Boot环境下如何配置
  • 使用redis BloomFilter(这个已经脱离了Spring Boot课程范围,了解即可或自行学习)

缓存击穿

在平常高并发的系统中,大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们称为缓存击穿

比如:鹿晗宣布恋情,导致微博瘫痪。就有可能是缓存击穿导致的,大家都去看这一个热点新闻,热点新闻的缓存如果超时失效了,就造成后端服务压力增大,服务器瘫痪。(当然这只是我猜的,举例而已)

解决办法

  • 可以通过准确的监控热点流量,及时的针对热点服务及缓存组件进行自动化的扩容。
  • 通过Hystrix或sentinel等服务限流工具,保证系统的可用性,拒绝掉一部分流量的访问。
  • 第三种方法就是加锁,SpringCache采用sync属性,只有一个线程去维护缓存,其他线程会被阻塞,直到缓存中更新该条目为止。也就是第一次查询只允许一个线程,等数据被缓存之后,才支持并发。
@Cacheable(value = CACHE_OBJECT,key = "#id",sync=true)   
public ArticleVO getArticle(Long id) 

缓存雪崩

同一时刻大量缓存失效,导致请求集中的全部打到数据库。比如:双十一零点搞活动,为了支撑这次活动,事先已经缓存好大量的数据。如果所有的数据全是缓存24小时,那24小时之后这些数据缓存将集中失效,最终结果就是11.12号服务崩溃。

解决办法

可以通过准确的监控热点流量,及时的针对热点服务及缓存组件进行自动化的扩容。

不同缓存的失效时间不能一致,同一种缓存的失效时间也尽量随机(最小值–>最大值)


读写加锁

引入中间件Canal,感知到mysql的更新去更新

读多写多的,直接去数据库查询


redis 缓存配置

在 application.yml指定 spring.cache.type=redis。

spring:
  cache:
    type: redis
    redis:
      cache-null-values: true   # 缓存null,防止缓存穿透
      use-key-prefix: true  # 是否使用缓存前缀
      key-prefix: boot-launch  # 缓存前缀,缓存按应用分类
      time-to-live:  3600  # 缓存到期时间,默认不主动删除永远不到期

其中值得注意的一点是,Spring Cache默认只支持全局对所有的缓存配置生效时间,不支持对缓存的生效时间分类配置,容易造成缓存雪崩。


自定义缓存到期时间

由于redis缓存设置的到期时间是统一的,没有办法根据缓存名称(value属性)分别设置缓存到期的时间,容易造成缓存雪崩。所以我们进行一个简单的改造。在改造之前我们先来看一下RedisCacheManager源码


RedisCacheManager构造函数包含三个参数

  • RedisCacheWriter这个在之前的章节我们就配置过
  • RedisCacheConfiguration defaultCacheConfiguration这个是默认的全局配置,针对所有缓存
  • Map<String, RedisCacheConfiguration> initialCacheConfigurations这个是针对某一种缓存的个性化配置,泛型String是缓存名称,泛型RedisCacheConfiguration是该缓存的个性化配置

理解了上面的源码,下面的改造代码就不难理解了。

@Data
@Configuration
@ConfigurationProperties(prefix = "caching")  //application.yml配置前缀
public class RedisConfig 

 
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) 
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        //序列化重点在这四行代码
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    


     //从这里开始改造
    //自定义redisCacheManager
    @Bean
    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) 
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());

        RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter,
                this.buildRedisCacheConfigurationWithTTL(redisTemplate,RedisCacheConfiguration.defaultCacheConfig().getTtl().getSeconds()),  //默认的redis缓存配置
                this.getRedisCacheConfigurationMap(redisTemplate)); //针对每一个cache做个性化缓存配置

        return  redisCacheManager;
    

    //配置注入,key是缓存名称,value是缓存有效期
    private Map<String,Long> ttlmap;  //lombok提供getset方法

    //根据ttlmap的属性装配结果,个性化RedisCacheConfiguration
    private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap(RedisTemplate redisTemplate) 
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();

        for(Map.Entry<String, Long> entry : ttlmap.entrySet())
            String cacheName = entry.getKey();
            Long ttl = entry.getValue();
            redisCacheConfigurationMap.put(cacheName,this.buildRedisCacheConfigurationWithTTL(redisTemplate,ttl));
        

        return redisCacheConfigurationMap;
    

    //根据传参构建缓存配置
    private RedisCacheConfiguration buildRedisCacheConfigurationWithTTL(RedisTemplate redisTemplate,Long ttl)
        return  RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()))
                .entryTtl(Duration.ofSeconds(ttl));
    



自定义配置实现缓存失效时间个性化

在 application.yml指定 缓存名称对应的缓存生效时间,单位为秒

caching:
  ttlmap:
    article: 10
    xxx: 20
    yyy: 50

重学springboot系列之springcache详解(代码片段)

重学SpringBoot系列之Springcache详解为什么使用缓存为什么使用SpringCache如何使用SpringCache加依赖开启缓存加缓存注解测试常用注解@Cacheable缓存中spel表达式可取值@CachePut@CacheEvict@Caching@CacheConfig自定义缓存注解完整应用... 查看详情

shiro整合springboot缓存之ehcache实现(代码片段)

Shiro整合Springboot缓存之EhCache实现Cache作用引入shiro和ehcache的整合依赖开启缓存Cache作用1.Cache缓存:计算机内存中一段数据2.作用:用来减轻DB的访问压力,从而提高系统的查询效率3.流程:引入shiro和ehcache的整合依... 查看详情

重学springboot系列之基础知识回顾(代码片段)

重学SpringBoot系列之基础知识回顾SpringBoot项目结构SpringBoot、SpringMVC、Spring对比SpringBoot自动配置什么是SpringBootStarter?什么是SpringBootStarterParent嵌入式web容器SpringDataspringboot2.x新特性基础环境升级依赖组件升级默认软件替换新技... 查看详情

重学springboot系列之异步任务与定时任务(代码片段)

重学SpringBoot系列之异步任务与定时任务实现Async异步任务环境准备同步调用异步调用异步回调为异步任务规划线程池SpringBoot任务线程池自定义线程池优雅地关闭线程池通过@Scheduled实现定时任务开启定时任务方法不同定时方式... 查看详情

重学springboot系列之整合静态资源与模板引擎(代码片段)

重学SpringBoot系列之整合静态资源与模板引擎webjars与静态资源springboot静态资源favicon.ico图标欢迎页面使用WebJars管理css&js1.pom中引入依赖2.访问引入的js文件自动检测依赖的版本测试模板引擎选型与未来趋势javaweb开发经历的几个... 查看详情

重学springboot系列之restful接口及常用注解(代码片段)

重学SpringBoot系列之RestFul接口RESTful接口与http协议状态表述RestFul风格的好处RESTfulAPI的设计风格RESTful是面向资源的(名词)用HTTP方法体现对资源的操作(动词)HTTP状态码Get方法和查询参数不应该改变数据使用复数... 查看详情

重学springboot系列之嵌入式容器的配置与应用(代码片段)

重学SpringBoot系列之嵌入式容器的配置与应用嵌入式容器的运行参数配置调整SpringBoot应用容器的参数两种配置方法配置文件方式常用配置参数tomcat性能优化核心参数自定义配置类方式为Web容器配置HTTPS如何生成自签名证书将SSL应... 查看详情

重学springboot系列之整合数据库开发框架---中(代码片段)

重学Springboot系列之整合数据库开发框架---中javabean的赋值转换为什么要做javabean赋值转换BeanUtils和Dozer?引入Dozer(6.2.0)自定义类型转换(非对称类型转换)映射localDateTime的问题整合MybatisGenerator操作数据整合M... 查看详情

重学springboot系列之统一全局异常处理(代码片段)

重学SpringBoot系列之统一全局异常处理设计一个优秀的异常处理机制异常处理的乱象例举该如何设计异常处理开发规范自定义异常和相关数据结构该如何设计数据结构枚举异常的类型自定义异常请求接口统一响应数据结构使用示... 查看详情

重学springboot系列之mockito测试(代码片段)

重学SpringBoot系列之Mockito测试mock中文文档使用Mockito编码完成接口测试编码实现接口测试为什么要写代码做测试?使用接口测试工具Postman很方便啊junit测试框架Mockito测试框架真实servlet容器环境下的测试@SpringBootTest注解@E... 查看详情

重学springboot系列之整合数据库开发框架---下(代码片段)

重学Springboot系列之整合数据库开发框架---下mybatis+atomikos实现分布式事务整合jta-atomikos配置多数据源统一事务管理器service层测试mybatisplus+atomikos实现分布式事务遗留问题整合jta-atomikos配置多数据源(调整)Spring事务... 查看详情

重学springboot系列之整合数据库开发框架---上(代码片段)

重学Springboot系列之整合数据库开发框架整合SpringJDBC操作数据jdbc简介使用jdbc操作数据库的步骤将SpringJDBC集成到Springboot项目springbootjdbc基础代码SpringJDBC多数据源的实现配置多个数据源通过JavaConfig将数据源注入到Spring上下文。Artic... 查看详情

重学springboot系列之服务器推送技术(代码片段)

重学Springboot系列之服务器推送技术主流服务器推送技术说明需求与背景服务端推送常用技术全双工通信:WebSocket服务端主动推送:SSE(ServerSendEvent)websocket与SSE比较服务端推送事件SSE模拟网络支付场景应用场景sse规范模拟实现服务端... 查看详情

重学springboot系列之整合分布式文件系统(代码片段)

重学SpringBoot系列之整合分布式文件系统文件本地上传与提供访问服务复习文件上传目录自定义配置文件上传的Controller实现写一个模拟的文件上传页面,进行测试MinIO简介与选型介绍为什么使用MInIO替换了FastDFS理由一:安... 查看详情

重学springboot系列之json处理工具类(代码片段)

重学springboot系列之JSON处理工具类FastJSON、Gson和Jackson对比在Spring中注解方法使用Jackson常用注解手动数据转换BugJackson全局配置FastJSON、Gson和Jackson对比开源的Jackson:SpringBoot默认是使用Jackson作为JSON数据格式处理的类库,Jack... 查看详情

重学springboot系列之邮件发送的整合与使用(代码片段)

重学Springboot系列之邮件发送的整合与使用基础协议及邮件配置整合名词概念解释整合邮件发送功能引入依赖邮箱配置发送简单邮件附录:QQ邮箱发邮件设置发送html和基于模板的邮件发送html邮件服务基于freemarker模板的邮件发... 查看详情

重学springboot系列之生命周期内的拦截过滤与监听(代码片段)

重学SpringBoot系列之生命周期内的拦截过滤与监听Servlet域对象与属性变化监听监听器定义与实现使用场景监听器的实现全局Servlet组件扫描注解监听器测试session创建时机Servlet过滤器的实现过滤器过滤器的实现servletspring拦截器及请... 查看详情

重学springboot系列之日志框架与全局日志管理(代码片段)

重学SpringBoot系列之日志框架与全局日志管理日志框架的体系结构五花八门的日志工具包日志框架日志门面日志门面存在的意义日志框架选型日志级别常见术语概念解析logback日志框架配置application配置文件实现日志配置日志格式... 查看详情