[toc]

1. 介绍

SpringCache提供基本的Cache抽象,并没有具体的缓存能力,需要配合具体的缓存实现来完成,目前SpringCache支持redis、ehcache、simple(基于内存,使用ConcurrentHashMap)等方式来实现缓存。

因为spring的缓存机制会引入一些问题, 一些大型项目可能不适合, 比如并发场景下出现异常等等, 但大部分的项目还是可以适用的(毕竟中国没有多少具备高并发的公司)

2. 常用注解

2.1 @EnableCaching

开启缓存功能,一般使用在springboot的启动类或配置类上

2.2 @Cacheable

使用缓存。在方法执行前Spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;没有则调用方法并将方法返回值放进缓存。

**注解参数解释: **

@Cacheable属性名用途备注
cacheNames/value指定缓存空间的名称,不同缓存空间的数据是彼此隔离的
key同一个cacheNames中通过key区别不同的缓存。如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合,如:@CachePut(value = “demo”, key = “‘user’+#user.id”),字符串中spring表达式意外的字符串部分需要用单引号SpringCache提供了与缓存相关的专用元数据,如target、methodMame、result、方法参数等,如:@CachePut(value = “demo”, key = “#result==null”)
当方法的参数和值一样时, 会导致重复;
如果是基本参数: 参数名+值
如果是对象参数: 参数的类名+对象的成员变量+值 (类似于toString的结果)
keyGenratorkey的生成策略,SpringCache默认使用SimpleKeyGenerator,默认情况下将参数值作为键,但是可能会导致key重复出现,因此一般需要自定义key的生成策略和key参数二选一
cacheManager指定缓存管理器(例如ConcurrentHashMap、Redis等)。
cacheResolver和cacheManager作用一样,使用时二选一。
condition(指定缓存的条件(对参数判断,满足什么条件时才缓存))
condition是在调用方法之前判断条件,满足条件才缓存, 支持spel语法
@Cacheable(cacheNames=“book”, condition="#name.length() < 32")
unless(否定缓存的条件(对结果判断,满足什么条件时不缓存))
unless是在调用方法之后判断条件,如果SpEL条件成立,则不缓存【条件满足不缓存】,支持spel语法
@Cacheable(cacheNames = “hello”,unless="#result.id.contains(‘1’)" )
sync缓存过期之后,如果多个线程同时请求对某个数据的访问,会同时去到数据库,导致数据库瞬间负荷增高。Spring4.3为@Cacheable注解提供了一个新的参数“sync”(默认为false)。当设置它为true时,只有一个线程的请求会去到数据库,其他线程都会等待直到缓存可用。这个设置可以减少对数据库的瞬间并发访问。

2.3 @CachePut

更新缓存,将方法的返回值放到缓存中

2.4 @CacheEvict

清空缓存

2.5 @Caching

@Caching是一个组注解,可以为一个方法定义提供基于@Cacheable@CacheEvict或者@CachePut注解的数组。

例如下面这个例子, 在方法上加 @caching并标注需要的逻辑, 这样调用这个方法时就能同时触发多个操作

@Caching(
        put = {
            @CachePut(value = "myCache", key = "#result.id")
        },
        evict = {
            @CacheEvict(value = "otherCache", key = "#id")
        },
        cacheable = {
            @Cacheable(value = "myFifthCache", key = "#result.id")
        }
    )
    public MyEntity saveData(Long id, String name) {
        MyEntity entity = new MyEntity(id, name);
        return entity;
    }

2.6 @CacheConfig:

一个类中可能会有多个缓存操作,而这些缓存操作可能是重复的。这个时候可以使用 @CacheConfig是一个类级别的注解.

它可以存放该类中所有缓存的公有属性,比如设置缓存空间的名字cacheNames、key生成策略keyGenerator、缓存管理器cacheManager

3. 缓存具体实现

支持多种缓存提供程序,包括 CaffeineHazelcastRedisConcurrentHashMap(默认值)。

也就是说支持本地缓存和第三方缓存框架, 如果是第三方的缓存就需要额外的pom坐标支持

推荐caffeine这个本地缓存框架, 性能好,功能全, 只是需要额外引入caffeine这个框架,就有点小成本了

其次推荐使用redis, 功能全,性能好(没有caffeine这种本地缓存快), 项目中反正都要引入redis的, 刚好顺便能用

这里以redis为例

4. 使用

4.1 引入依赖

 <dependencies>
        <!--引入缓存-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
            <version>2.3.0.RELEASE</version>
        </dependency>
     	<!--引入redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.3.0.RELEASE</version>
        </dependency>
    </dependencies>

4.2 配置类

@Configuration
public class RedisConfiguration {

    // ${cache} 获取配置文件的配置信息   #{}是spring表达式,获取Bean对象的属性
    @Value("#{${cache}}")
    private Map<String, Long> ttlParams;

    /**
     * @param redisConnectionFactory
     * @功能描述 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()
                // 设置key采用String的序列化方式
              .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer.UTF_8))
                //设置value序列化方式采用jackson方式序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer()))
                //当value为null时不进行缓存
                .disableCachingNullValues()
                // 配置缓存空间名称的前缀 (这里会覆盖 @cacheable注解的vaule字段)
                .prefixCacheNameWith("demo:")
                //全局配置缓存过期时间【可以不配置】
                .entryTtl(Duration.ofMinutes(30L));
        //专门指定某些缓存空间的配置,如果过期时间【主要这里的key为缓存空间名称】
        Map<String, RedisCacheConfiguration> map = new HashMap<>();
        Set<Map.Entry<String, Long>> entries = ttlParams.entrySet();
        for (Map.Entry<String, Long> entry : entries) {
            //指定特定缓存空间对应的过期时间
            map.put("user", redisCacheConfiguration.entryTtl(Duration.ofSeconds(40)));
            map.put(entry.getKey(), redisCacheConfiguration.entryTtl(Duration.ofSeconds(entry.getValue())));
        }
        return RedisCacheManager
                .builder(redisConnectionFactory)
                .cacheDefaults(redisCacheConfiguration)  //默认配置
                .withInitialCacheConfigurations(map)  //某些缓存空间的特定配置
                .build();
    }


    /**
     * 自定义缓存的redis的KeyGenerator【key生成策略】
     * 注意: 该方法只是声明了key的生成策略,需在@Cacheable注解中通过keyGenerator属性指定具体的key生成策略
     * 可以根据业务情况,配置多个生成策略
     * 如: @Cacheable(value = "key", keyGenerator = "cacheKeyGenerator")
     */
    @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);
            return sb.append(key);
        };
    }


    /**
     * @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();
    }
}

4.3 开启并使用

@EnableCaching   //开启缓存的主键
@SpringBootApplication
public class RedisDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(RedisDemoApplication.class, args);
    }
}
@Service
@CacheConfig(cacheNames = "user",keyGenerator = "keyGenerator")
public class RedisServiceImpl implements RedisService {

    @Cacheable(value = "user", key = "'list'")
    @Override
    public List<User> list() {
        System.out.println("=========list");
        List<User> users = new ArrayList<>();
        return users;
    }

    @CacheEvict(value = "user", key = "'list'")
    @Override
    public void del(Integer id) {
        System.out.println("************************************+id");
    }

    @CachePut(value = "demo", key = "#result==null")
    @Override
    public void select(Integer id) {
        System.out.println("===============dddd================");
    }
}

4.4 配置文件

spring:
	cache:
        type: redis   # 指定使用的缓存类型,不传会自动识别
        redis:    当自定义ChacheManager时,就这里的配置不需要配置,配置了也不起作用
            use-key-prefix: true  
            key-prefix: "demo:"
            time-to-live: 60000  #缓存超时时间 单位:ms
            cache-null-values: false #是否缓存空值
            cache-names: user # 初始化一些缓存名
            ttl: '{"user":60,"dept":30}'  #自定义某些缓存空间的过期时间

网上有些文章会使用配置文件的方式, 其实这是 spring-boot-autoconfigure 自动配置的能力, 它会根据你引入的包(redis/caffeine), 自动注入对应的缓存机制 , 比如redis的 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration

如果需要自定义一些配置, 就需要按4.1~4.3节那种配置

5. 注意事项

  1. @Cache注解的方法必须为 public

  2. 默认情况下,@CacheEvict标注的方法执行期间抛出异常,则不会清空缓存。

  3. 基于 proxyspring aop 带来的内部调用问题(aop的通用问题)

  4. 使用内存时一定要设置过期时间和允许使用内存大小 等等, 不然在极端情况下会把内存吃满

    默认的 ConcurrentHashMap不支持设置过期时间和允许使用的内存大小

    redis不支持允许使用的内存大小

    caffeine都能支持(这也是推荐使用它的原因之一)

Spring Cache 整合 Redis 做缓存使用~ 快速上手~ - 掘金 (juejin.cn)

SpringBoot实现Redis缓存(SpringCache+Redis的整合)_springboot redis cache_user2025的博客-CSDN博客

Spring Cache,key重复问题

SpringBoot项目中使用缓存Cache的正确姿势!!! - 掘金 (juejin.cn)