Spring Boot二级缓存配置

Java
210
0
0
2024-04-10
标签   SpringBoot

前言

之前说了使用JdbcTemplate、MyBatis、Spring Data JPA三种连接数据库的方式。

对于一些不常变更,或者是数据库数据太多,需要优化请求速度的,我们可以使用二级缓存解决。

二级缓存分两种

  • 本地缓存 比如 Ehcache
  • 远程缓存 比如 Redis

我们可以根据实际情况选择对应的缓存。

Ehcache

在使用 Spring Data JPA 进行数据访问时,可以使用二级缓存来提高程序的性能。

注意

这里使用的不是基于Hibernate 的Ehcache实现。

使用注意

二级缓存也存在一些潜在的问题,如缓存的数据可能不是最新的(缓存不一致)、缓存数据的内存占用等。 因此,在使用二级缓存时,需要根据具体的业务场景和需求来决定是否使用以及如何配置和管理缓存。

以下演示了如何在 Spring Boot 应用程序中配置 Ehcache 作为二级缓存。

添加依赖

pom.xml 文件中添加 Ehcache 依赖

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

添加Ehcache 配置

创建 Ehcache 配置文件 ehcache.xml

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
         updateCheck="false">
    <diskStore path="java.io.tmpdir"/>
    <defaultCache
            maxElementsInMemory="10000"
            maxElementsOnDisk="0"
            eternal="false"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="3"
            timeToLiveSeconds="3"
            diskSpoolBufferSizeMB="50"
            diskExpiryThreadIntervalSeconds="10"
            memoryStoreEvictionPolicy="LFU"/>
    <cache name="userService.getUserByName"
           maxEntriesLocalHeap="10000"
           eternal="false"
           overflowToDisk="false"
           diskPersistent="false"
           timeToIdleSeconds="3"
           timeToLiveSeconds="3">
    </cache>
</ehcache>

在上述示例代码中,创建了名为 userService.getUserByName 的缓存区域,设置了最大缓存数、缓存时间等属性。

参数解释

各个熟悉的含义

  • name 缓存空间名称(非缓存key)
  • maxElementsInMemory:设置了缓存的上限,最多存储多少个记录对象
  • maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大
  • eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false
  • overflowToDisk:true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。
  • diskSpoolBufferSizeMB:磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。
  • diskPersistent:是否缓存虚拟机重启期数据,设置成true表示缓存虚拟机重启期数据
  • diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认为120秒
  • timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地处于空闲状态
  • timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位。 当对象自从被存放到缓存中后,如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。 如果该属性值为0,则表示对象可以无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有意义
  • memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)

添加配置

application.properties 文件中启用二级缓存

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

添加配置类

package cn.psvmc.zapijpa.cache;

import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Objects;

@Configuration
@EnableConfigurationProperties(CacheProperties.class) // 这样写的好处就是与配置文件的配置信息进行绑定
public class EhCacheConfiguration {

    private final CacheProperties cacheProperties;

    public EhCacheConfiguration(CacheProperties cacheProperties) {
        this.cacheProperties = cacheProperties;
    }

    @Bean
    public EhCacheManagerFactoryBean ehCacheManagerFactory() {
        EhCacheManagerFactoryBean ehCacheManagerFactory = new EhCacheManagerFactoryBean();
        ehCacheManagerFactory.setConfigLocation(cacheProperties.resolveConfigLocation(cacheProperties.getEhcache().getConfig()));
        ehCacheManagerFactory.setShared(true);
        return ehCacheManagerFactory;
    }

    @Bean
    public EhCacheCacheManager ehCacheCacheManager(EhCacheManagerFactoryBean ehCacheManagerFactory) {
        return new EhCacheCacheManager(Objects.requireNonNull(ehCacheManagerFactory.getObject()));
    }
}

代码配置

在Application上添加@EnableCaching,开启缓存。

设置缓存

import cn.psvmc.zapijpa.entity.UserEntity;
import cn.psvmc.zapijpa.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.List;

@Service
@Transactional
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    // 根据名称获取用户列表
    @Cacheable(cacheNames = "userService.getUserByName",key = "#name")
    public List<UserEntity> getUserByName(String name) {
        return userRepository.findByName(name);
    }
    
    // 批量添加用户
    @Transactional
    @CacheEvict(cacheNames = "userService.getUserByName",allEntries = true)
    public List<UserEntity> addAll(List<UserEntity> users) {
        return userRepository.saveAll(users);
    }
}

Controller

import cn.psvmc.zapijpa.entity.UserEntity;
import cn.psvmc.zapijpa.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/get_user")
    public List<UserEntity> getUser() {
        return  userService.getUserByName("张");
    }

    @RequestMapping("/get_user2")
    public List<UserEntity> getUser2() {
        return  userService.getUserByName("王");
    }
    
    @RequestMapping("/add_users")
    public List<UserEntity> addUsers() {
        UserEntity u1 = new UserEntity("张三",10);
        UserEntity u2 = new UserEntity("李四",18);
        UserEntity u3 = new UserEntity("王五",22);
        UserEntity u4 = new UserEntity("赵六",16);
        List<UserEntity> userList = Arrays.asList(u1, u2, u3, u4);
        return  userService.addAll(userList);
    }
}

这样上面的两个请求都会在第一次的时候查询数据库,后面的请求都用的缓存。

超时时间未生效

关键的问题在于没有指定缓存类型为ehcacheehcache.xml文件压根就没有生效。

springboot使用默认的SimpleCacheConfiguration,不是用的ehcache

解决方法如下:

application.properties添加如下

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

Redis

添加依赖

<!--redis jar包-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

在Application上添加@EnableCaching,开启缓存。

添加配置

spring.cache.type=redis
#redis配置
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=cachetest.xhkjedu.com
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=xhkjedud07
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-idle=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=10000

配置类

注意:

配置缓存管理器CacheManager有两种方式:

  • 方式1:通过RedisCacheConfiguration.defaultCacheConfig()获取到默认的RedisCacheConfiguration对象,修改RedisCacheConfiguration对象的序列化方式等参数。
  • 方式2:通过继承CachingConfigurerSupport类自定义缓存管理器,覆写各方法。

两种方式任选其一即可。

方式1

默认序列化在redis中保存的类似于这样,不太好排查

image-20240117103951826

我们可以自定义配置类设置序列化方式

package cn.psvmc.zapijpa.cache;

import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.interceptor.SimpleKeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class RedisConfiguration {
    /**
     * @param redisConnectionFactory redis连接工厂
     * @功能描述 redis作为缓存时配置缓存管理器CacheManager,主要配置序列化方式、自定义
     * <p>
     * 注意:配置缓存管理器CacheManager有两种方式:
     * 方式1:通过RedisCacheConfiguration.defaultCacheConfig()获取到默认的RedisCacheConfiguration对象,
     * 修改RedisCacheConfiguration对象的序列化方式等参数【这里就采用的这种方式】
     * 方式2:通过继承CachingConfigurerSupport类自定义缓存管理器,覆写各方法,参考:
     * https://blog.csdn.net/echizao1839/article/details/102660649
     * <p>
     * 切记:在缓存配置类中配置以后,yaml配置文件中关于缓存的redis配置就不会生效,如果需要相关配置需要通过@value去读取
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        redisCacheConfiguration = redisCacheConfiguration
                // 设置key采用String的序列化方式
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer.UTF_8))
                //设置value序列化方式采用jackson方式序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer()))
                //当value为null时不进行缓存
                .disableCachingNullValues()
                // 配置缓存空间名称的前缀
                .prefixCacheNameWith("spring-cache:")
                //全局配置缓存过期时间【可以不配置】
                .entryTtl(Duration.ofMinutes(30L));
        //专门指定某些缓存空间的配置,如果过期时间【主要这里的key为缓存空间名称】
        Map<String, RedisCacheConfiguration> map = new HashMap<>();

        return RedisCacheManager
                .builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration)  //默认配置
                .withInitialCacheConfigurations(map)  //某些缓存空间的特定配置
                .build();
    }

    /**
     * 自定义缓存的redis的KeyGenerator【key生成策略】
     * 注意: 该方法只是声明了key的生成策略,需在@Cacheable注解中通过keyGenerator属性指定具体的key生成策略
     * 可以根据业务情况,配置多个生成策略
     * 如: @Cacheable(value = "key", keyGenerator = "keyGenerator")
     */
    @Bean
    public KeyGenerator keyGenerator() {
        /*
          target: 类
          method: 方法
          params: 方法参数
         */
        return (target, method, params) -> {
            //获取代理对象的最终目标对象
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getSimpleName()).append(":");
            sb.append(method.getName()).append(":");
            //调用SimpleKey的key生成器
            Object key = SimpleKeyGenerator.generateKey(params);
            sb.append(key);
            return sb;
        };
    }
    
    /**
     * @param redisConnectionFactory:配置不同的客户端,这里注入的redis连接工厂不同: JedisConnectionFactory、LettuceConnectionFactory
     * @功能描述 :配置Redis序列化,原因如下:
     * (1) StringRedisTemplate的序列化方式为字符串序列化,
     * RedisTemplate的序列化方式默为jdk序列化(实现Serializable接口)
     * (2) RedisTemplate的jdk序列化方式在Redis的客户端中为乱码,不方便查看,
     * 因此一般修改RedisTemplate的序列化为方式为JSON方式【建议使用GenericJackson2JsonRedisSerializer】
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = serializer();
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // key采用String的序列化方式
        redisTemplate.setKeySerializer(StringRedisSerializer.UTF_8);
        // value序列化方式采用jackson
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(StringRedisSerializer.UTF_8);
        //hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
    /**
     * 此方法不能用@Ben注解,避免替换Spring容器中的同类型对象
     */
    public GenericJackson2JsonRedisSerializer serializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

这样序列化的结果就方便查看了。

image-20240117104620012

方式2

继承CachingConfigurerSupport的方式。

package cn.psvmc.zapijpa.cache;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.lang.Nullable;

import java.lang.reflect.Method;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Value("${cache.expireTime}")
    private Integer cacheExpireTime;
    private final static Logger log = LoggerFactory.getLogger(RedisConfig.class);

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        //设置redis序列化方式
        StringRedisTemplate template = new StringRedisTemplate(connectionFactory);
        template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        template.afterPropertiesSet();
        //初始化CacheManager,使用redis作为二级缓存
        return new RedisCacheManager(
                RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory),
                getDefaultTtlRedisCacheConfiguration(cacheExpireTime), getCustomizeTtlRedisCacheConfigurationMap()
        );
    }

    /**
     * 设置缓存默认配置
     */
    private RedisCacheConfiguration getDefaultTtlRedisCacheConfiguration(Integer seconds) {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        //设置序列化方式以及失效时间
        redisCacheConfiguration = redisCacheConfiguration
                .serializeValuesWith(
                        RedisSerializationContext
                                .SerializationPair
                                .fromSerializer(jackson2JsonRedisSerializer)
                )
                // 配置缓存空间名称的前缀
                .prefixCacheNameWith("spring-cache:")
                .entryTtl(Duration.ofSeconds(seconds));
        return redisCacheConfiguration;
    }

    /**
     * 自定义缓存失效时间
     */
    private Map<String, RedisCacheConfiguration> getCustomizeTtlRedisCacheConfigurationMap() {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        //dictionary就是我们自定义的key,使用@Cacheable等注解时,将其value属性设置为dictionary,那么这个dictionary的缓存失效时间就是这里我们自定义的失效时间(cacheExpireTime)
        redisCacheConfigurationMap.put("dictionary", this.getDefaultTtlRedisCacheConfiguration(cacheExpireTime));
        return redisCacheConfigurationMap;
    }

    /**
     * 设置缓存默认key生成方式,使用@Cacheable注解时,如果不指明key值,则默认按下面方式生成key
     */
    @Nullable
    @Override
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append("_");
                sb.append(method.getName());
                for (Object obj : params) {
                    sb.append("_");
                    sb.append(obj.toString());
                }
                log.info("缓存自动生成key:" + sb);
                return sb.toString();
            }
        };
    }
}

配置文件中添加

cache.expireTime=600

代码配置

代码配置和上面一样就行

注意一点实体类必须实现序列化,如果自定义了配置类中大的序列化方式则不用实现。

public class UserEntity implements Serializable {
}

缓存失效时间

上面设置了全局的缓存失效时间

我们可以设置通过代码设置缓存的失效时间

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisCacheConfig {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void setExpireTime(String cacheKey, long expireSeconds) {
        redisTemplate.expire(cacheKey,expireSeconds, TimeUnit.SECONDS);
    }
}

但是因为最终生成的的key是拼接的,目前没找到好的设置的方法。

缓存注解

启用缓存

需要缓存的地方在类上添加@EnableCaching

添加缓存

在方法上添加@Cacheable(cacheNames = "userService.getUserByName",key = "#name")

其中

  • cacheNames 时缓存的名称也可以使用value,使用Ehcache的时候,如果和XML配置中的对应,可以生效对应的规则,如果不对应会使用默认规则。
  • key 如果方法有参数,可以放在key上。这样参数不同都可以产生新的缓存。

缓存名称规则

缓存最终生成的key的规则是

prefix+cacheNames+”::”+key

其中

  • cacheNames是必须要设置的
  • key可以不设置,默认是根据方法的参数的值拼接的。也可以自定义默认的生成规则,或者指定生成器。

删除缓存

删除缓存@CacheEvict(cacheNames = "userService.getUserByName",allEntries = true)

当执行addAll的时候,因为用户改变了,所以我们清除缓存。

allEntries:是否清除这个缓存(cacheNames)中的所有数据。默认false。

无论我们缓存的时候是否设置了key,都要设置allEntries = true,否则无法删除缓存。

相关注解或概念

说明

@EnableCaching

开启基于注解的缓存

@Cacheable

主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,缓存后再次调用方法则直接返回缓存内容。

@CachePut

保证方法被调用,并缓存结果。常用于更新数据。

@CacheEvict

清空缓存

@CacheConfig

所有的@Cacheable()里面都有一个value="xxx"的属性,这显然如果方法多了,写起来也是挺累的,我们可以在类上声明@CacheConfig

声明后在方法上只需要添加@Cacheable就可以了。

@CacheConfig(cacheNames = "user")
public class UserService 
{
    @Cacheable
    public List<UserEntity> getUserByName(String name) {
        return userRepository.findByName(name);
    }
}

注意注解引用的类

import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.EnableCaching;

条件缓存

//@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存;
@Cacheable(value = "user", key = "#id", condition = "#id lt 10")
public User conditionFindById(final Long id)
 
 
//@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存;
@CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'")
public User conditionSave(final User user)
 
 
//@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反)
@CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'")
public User conditionSave2(final User user)
 
 
//@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存;
@CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'")
public User conditionDelete(final User user)