前提

  • Spring Boot项目中在没有缓存管理的情况下,虽然数据表中的数据没有发生变化,但是每执行一次查询操作(本质是执行同样的SQL语句),都会访问一次数据库并执行一次SQL语句 。
  • 我们可以在查询前手动操作reids数据库进行访问,现在提供一种注解的思路。

API及默认实现

Cache接口

  • 主要是缓存的增删查功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package org.springframework.cache;

    public interface Cache {
    String getName(); //缓存的名字

    Object getNativeCache(); //得到底层使用的缓存,如Ehcache

    ValueWrapper get(Object key); //根据key得到一个ValueWrapper, 然后调用其get方法获取值

    <T> T get(Object key, Class<T> type);//根据key,和value的类型直接获取value

    void put(Object key, Object value);//往缓存放数据

    void evict(Object key);//从缓存中移除key对应的缓存

    void clear(); //清空缓存

    interface ValueWrapper { //缓存值的Wrapper
    Object get(); //得到真实的value
    }
    }
  • 由此可见,缓存的对象就是键值对。

  • 默认实现:

    • ConCurrentMapCache:使用java.util.concurrentHashMap实现的Cache。
    • GuavaCache: 对Gguava com.google.common.cache.Cache进行的Wrapper,需要Google Guava 12.0或更高版本。
    • EhCacheCache: 使用Ehcache实现。
    • JCacheCache:对javax.cache.Cache进行的Wrapper。

CacheManager

  • Spring提供的缓存管理器,便于管理程序中的多个cache。

    1
    2
    3
    4
    5
    6
    7
    package org.springframework.cache;
    import java.util.Collection;

    public interface CacheManager {
    Cache getCache(String name); //根据Cache名字获取Cache
    Collection<String> getCacheNames(); //得到所有Cache的名字
    }
  • 默认实现:

    CacheManger 描述
    SimpleCacheManager 使用简单的Collection来存储缓存,主要用于测试
    ConcurrentMapCacheManager 使用ConcurrentMap作为缓存技术(默认),需要显式的删除缓存,无过期机制
    NoOpCacheManager 仅测试用途,不会实际存储缓存
    EhCacheCacheManager 使用EhCache作为缓存技术,以前在hibernate的时候经常用
    GuavaCacheManager 使用google guava的GuavaCache作为缓存技术(1.5版本已不建议使用)
    CaffeineCacheManager 是使用Java8对Guava缓存的重写,spring5(springboot2)开始用Caffeine取代guava
    HazelcastCacheManager 使用Hazelcast作为缓存技术
    JCacheCacheManager 使用JCache标准的实现作为缓存技术,如Apache Commons JCS
    RedisCacheManager 使用Redis作为缓存技术

SpringBoot默认缓存

  • 在前面搭建的Web应用基础上,开启Spring Boot默认支持的缓存,体验Spring Boot默认缓存的使用效果

  • pom.xml中引入cache 基础依赖,情况下使用 ConcurrentMapCache不需要引用任何依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!-- 基础依赖 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <!-- 使用 ehcache -->
    <dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    </dependency>
    <!-- 使用 caffeine https://mvnrepository.com/artifact/com.github.ben-manes.caffeine/caffeine -->
    <dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.6.0</version>
    </dependency>
    <!-- 使用 redis -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  • application配置

    1
    2
    3
    4
    5
    6
    spring.cache.type= #缓存的技术类型,可选 generic,ehcache,hazelcast,infinispan,jcache,redis,guava,simple,none
    spring.cache.cache-names= #应用程序启动创建缓存的名称,必须将所有注释为@Cacheable缓存name(或value)罗列在这里,否者:Cannot find cache named 'xxx' for Builder[xx] caches=[sysItem] | key='' | keyGenerator='' | cacheManager='' | cacheResolver='' | condition='' | unless='' | sync='false'#以下根据不同缓存技术选择配置
    spring.cache.ehcache.config= #EHCache的配置文件位置
    spring.caffeine.spec= #caffeine类型创建缓存的规范。查看CaffeineSpec了解更多关于规格格式的细节spring.cache.infinispan.config= #infinispan的配置文件位置
    spring.cache.jcache.config= #jcache配置文件位置
    spring.cache.jcache.provider= #当多个jcache实现类时,指定选择jcache的实现类
  • 使用@EnableCaching注解开启基于注解的缓存支持

    1
    2
    3
    4
    5
    6
    7
    8
    @EnableCaching  // 开启Spring Boot基于注解的缓存管理支持
    @SpringBootApplication
    public class SpringbootCacheApplication {

    public static void main(String[] args) {
    SpringApplication.run(Springboot04CacheApplication.class, args);
    }
    }
  • 使用@Cacheable注解对数据操作方法进行缓存管理。

    • 将@Cacheable注解标注在Service类的查询方法上,对查询结果进行缓存

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      package tk.fulsun.demo.mapper;

      import java.util.List;
      import org.apache.ibatis.annotations.Param;
      import org.springframework.cache.annotation.Cacheable;
      import tk.fulsun.demo.model.User;
      import tk.fulsun.demo.model.UserExample;

      public interface UserMapper {
      @Cacheable(cacheNames = "users")
      List<User> selectByExample(UserExample example);

      }

    • 上述代码中,在UserMapper类中的selectByExample(UserExample example) 方法上添加了查询缓存注解@Cacheable,该注解的作用是将查询结果 List<User>存放在Spring Boot默认缓存中名称为users的名称空间(namespace)中,对应缓存唯一标识(即缓存数据对应的主键k)默认为方法参数example的值

  • 再次测试访问可以看出,在配置了Spring Boot默认注解后,再次执行selectByExample()方法,重复进行同样的查询操作,数据库只执行了一次SQL查询语句,说明项目开启的默认缓存支持已经生效 。

  • 底层结构:在诸多的缓存自动配置类中, SpringBoot默认装配的是SimpleCacheConfiguration, 他使用的CacheManagerConcurrentMapCacheManager, 使用 ConcurrentMap当底层的数据结构,按照Cache的名字查询出Cache, 每一个Cache中存在多个k-v键值对,缓存值

debug观察

  • 为了可以更好的观察缓存的存储,我们可以在单元测试中注入CacheManager

缓存注解介绍

  • 刚刚通过使用@EnableCaching@Cacheable注解实现了Spring Boot默认的基于注解的缓存管理,除此之外,还有更多的缓存注解及注解属性可以配置优化缓存管理

@EnableCaching

  • @EnableCaching是由spring框架提供的,springboot框架对该注解进行了继承,该注解需要配置在类上(通常配置在项目启动类上),用于开启基于注解的缓存支持
  • 如果没有任何缓存组件,会默认使用最后一个Simple缓存组件进行管理。
  • Simple缓存组件是Spring Boot默认的缓存管理组件,它默认使用内存中的ConcurrentMap进行缓存存储,所以在没有添加任何第三方缓存组件的情况下,可以实现内存中的缓存管理,但是我们不推荐使用这种缓存管理方式 , 对象保存在内存中,任何对对象内属性的修改都可能会改变缓存的值。

@CacheConfig

  • 主要用于配置该类中会用到的一些共用的缓存配置。
  • @CacheConfig(cacheNames = "users"):配置了该数据访问对象中返回的内容将存储于名为users的缓存对象中,我们也可以不使用该注解,直接通过@Cacheable自己配置缓存集的名字来定义。

@Cacheable

  • @Cacheable注解也是由spring框架提供的,可以作用于类或方法(通常用在数据查询方法上),用于对方法结果进行缓存存储。

  • 注解的执行顺序是

    • 先进行缓存查询,如果为空则进行方法查询,并将结果进行缓存;
    • 如果缓存中有数据,不进行方法查询,而是直接使用缓存数据
  • @Cacheable注解提供了多个属性,用于对缓存存储进行相关配置

    属性名 说明
    value/cacheNames cacheNames为Spring 4新增,作为value的别名), 指定缓存空间的名称,必配属性。这两个属性二选一使用
    key 指定缓存数据的key,默认使用方法参数值,可以使用SpEL表达式
    @Cacheable(key = "#p0"):使用函数第一个参数作为缓存的key值
    condition 缓存对象的条件,非必需,也需使用SpEL表达式,只有满足表达式条件的内容才会被缓存
    @Cacheable(key = "#p0", condition = "#p0.length() < 3"),表示只有当第一个参数的长度小于3的时候才会被缓存
    unless 指定在符合某条件下,不进行数据缓存,需使用SpEL表达式
    它不同于condition参数的地方在于它的判断时机,该条件是在函数被调用之后才做判断的,所以它可以通过对result进行判断
    keyGenerator 指定缓存数据的key的生成器,与key属性二选一使用
    自定义的key生成器,我们需要去实现org.springframework.cache.interceptor.KeyGenerator接口,并使用该参数来指定。
    需要注意的是:该参数与key是互斥的
    cacheManager 指定缓存管理器,只有当有多个时才需要使用
    cacheResolver 指定缓存解析器,与cacheManager属性二选一使用
    需通过org.springframework.cache.interceptor.CacheResolver接口来实现自己的缓存解析器,并用该参数指定
    sync 指定是否使用异步缓存。默认false
  • 执行流程&时机

    • 方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取,(CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建;
    • 去Cache中查找缓存的内容,使用一个key,默认就是方法的参数,如果多个参数或者没有参数,是按照某种策略生成的,默认是使用KeyGenerator生成的,使用SimpleKeyGenerator生成key
  • SimpleKeyGenerator生成key的默认策略:

    参数个数 key
    没有参数 new SimpleKey()
    有一个参数 参数值
    多个参数 new SimpleKey(params)

@CachePut

  • 目标方法执行完之后生效, 通常用于数据新增和修改方法上

  • @CachePut被使用于修改操作比较多,哪怕缓存中已经存在目标值了,但是这个注解保证这个方法依然会执行,执行之后的结果被保存在缓存中

  • @CachePut注解也提供了多个属性,这些属性与@Cacheable注解的属性完全相同。

  • 更新操作

    • 前端会把id+实体传递到后端使用,我们就直接指定方法的返回值从新存进缓存时的key="#id"
    • 如果前端只是给了实体,我们就使用key="#实体.id" 获取key.
    • 同时,他的执行时机是目标方法结束后执行, 所以也可以使用 key="#result.id", 拿出返回值的id

@CacheEvic

  • 通常用在删除方法上,用来从缓存中移除相应数据。
  • 它的参数与@Cacheable类似
  • @CacheEvict注解是由Spring框架提供的,可以作用于类或方法(通常用在数据删除方法上),该注解的作用是删除缓存数据。
  • @CacheEvict注解的默认执行顺序是,先进行方法调用,然后将缓存进行清除。
  • 除了同@Cacheable一样的参数之外,它还有下面两个参数:
    • allEntries:非必需,默认为false。当为true时,会移除所有数据
    • beforeInvocation:非必需,默认为false,会在调用方法之后移除数据。当为true时,会在调用方法之前移除数据。

SpEL上下文数据

  • Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档

    名称 位置 描述 示例
    methodName root对象 当前被调用的方法名 #root.methodname
    method root对象 当前被调用的方法 #root.method.name
    target root对象 当前被调用的目标对象实例 #root.target
    targetClass root对象 当前被调用的目标对象的类 #root.targetClass
    args root对象 当前被调用的方法的参数列表 #root.args[0]
    caches root对象 当前方法调用使用的缓存列表 #root.caches[0].name
    Argument Name 执行上下文 当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数 #artsian.id
    result 执行上下文 方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false) #result
  • 注意:

    1. 当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。 如

      1
      @Cacheable(key = "targetClass + methodName +#p0")
    2. 使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。 如:

      1
      2
      @Cacheable(value="users", key="#id")
      @Cacheable(value="users", key="#p0")

SpEL提供了多种运算符

类型 运算符
关系 <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne
算术 +,- ,* ,/,%,^
逻辑 &&,||,!,and,or,not,between,instanceof
条件 ?: (ternary),?: (elvis)
正则表达式 matches
其他类型 ?.,?[…],![…],^[…],$[…]

整合Redis缓存实现

  • 在Spring Boot中,数据的缓存管理存储依赖于Spring框架中cache相关的org.springframework.cache.Cacheorg.springframework.cache.CacheManager缓存管理器接口。

  • 如果程序中没有定义类型为 CacheManager 的Bean组件或者是名为 cacheResolver 的 CacheResolver缓存解析器,Spring Boot将尝试选择并启用以下缓存组件(按照指定的顺序):

    1. Generic
    2. JCache (JSR-107) (EhCache 3、Hazelcast、Infinispan等)
    3. EhCache 2.x
    4. Hazelcast
    5. Infinispan
    6. Couchbase
    7. Redis
    8. Caffeine
    9. Simple
  • 上面按照Spring Boot缓存组件的加载顺序,列举了支持的9种缓存组件,在项目中添加某个缓存管理组件(例如Redis)后,Spring Boot项目会选择并启用对应的缓存管理器。

  • 如果项目中同时添加了多个缓存组件,且没有指定缓存管理器或者缓存解析器(CacheManager或者cacheResolver),那么Spring Boot会按照上述顺序在添加的多个缓存中优先启用指定的缓存组件进行缓存管理。

引入依赖

  1. 添加Spring Data Redis依赖启动器, 在pom.xml文件中添加Spring Data Redis依赖启动器

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. Redis服务连接配置

    1
    2
    3
    4
    5
    6
    # Redis服务地址
    spring.redis.host=127.0.0.1
    # Redis服务器连接端口
    spring.redis.port=6379
    # Redis服务器连接密码(默认为空)
    spring.redis.password=

改造service层

  • 对Service类中的方法进行修改使用@Cacheable@CachePut@CacheEvict三个注解定制缓存管理

  • 分别进行缓存存储、缓存更新和缓存删除的演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    package tk.fulsun.demo.service;

    import java.util.List;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.cache.annotation.CacheEvict;
    import org.springframework.cache.annotation.CachePut;
    import org.springframework.cache.annotation.Cacheable;
    import org.springframework.cache.annotation.Caching;
    import org.springframework.stereotype.Service;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestBody;
    import tk.fulsun.demo.mapper.UserMapper;
    import tk.fulsun.demo.model.User;
    import tk.fulsun.demo.model.UserExample;
    import tk.fulsun.demo.model.UserExample.Criteria;

    /**
    * @author fulsun
    * @description: 用户管理的服务层
    * @date 5/31/2021 3:32 PM
    */
    @Service
    public class UserService {

    @Autowired private UserMapper userMapper;

    @GetMapping("/user/all")
    public List<User> getAllUser() {
    return userMapper.selectByExample(null);
    }

    // 查询,使用缓存 查询结果为空则不缓存
    @Cacheable(cacheNames = "users", key = "#name", unless = "#result==null")
    public User getUserByName(@PathVariable("name") String name) {
    UserExample example = new UserExample();
    Criteria criteria = example.createCriteria();
    criteria.andNameEqualTo(name.trim());
    List<User> users = userMapper.selectByExample(example);
    if (users.size() > 1) {
    return null;
    } else {
    return users.get(0);
    }
    }

    @Cacheable(cacheNames = "users", key = "#name", unless = "#result.size()==0")
    public List<User> getUserlikeName(@PathVariable("name") String name) {
    UserExample example = new UserExample();
    Criteria criteria = example.createCriteria();
    criteria.andNameLike('%' + name + '%');
    return userMapper.selectByExample(example);
    }

    // 查询,使用缓存 查询结果为空则不缓存
    @Cacheable(cacheNames = "users", key = "#id", unless = "#result==null")
    public User getUserById(@PathVariable("id") int id) {
    return userMapper.selectByPrimaryKey(id);
    }

    public int addUser(@RequestBody User user) {
    return userMapper.insertSelective(user);
    }

    // 更新,修改缓存 key = "#root.args[0].id"
    // @CachePut(cacheNames = "users", key = "#result.id")
    @Caching(
    put = {@CachePut(cacheNames = "users", key = "#result.id")
    // 开启后会覆盖格式为User类型,再次读取会转换为list类型,出现ClassCastException
    // @CachePut(cacheNames = "users", key = "#result.name"
    })
    public User updateUser(@RequestBody User user) {
    userMapper.updateByPrimaryKeySelective(user);
    return userMapper.selectByPrimaryKey(user.getId());
    }

    // 删除,删除缓存
    // beforeInvocation=true,防止执行过程中出现异常,不管这个方法执行成功与否,该缓存都将不存在。
    @CacheEvict(beforeInvocation = true, cacheNames = "users", key = "#id")
    public int delUser(@PathVariable("id") int id) {
    return userMapper.deleteByPrimaryKey(id);
    }
    }

设置全局有效时间

  • 上面的代码在测试的时候,能够创建对应的缓存,如更新后的查询方法会走缓存

  • 这里面也有一些在删除后,key为name的值还会保留在redis中

  • 可以在Spring Boot全局配置文件中配置Redis有效期,示例代码如下:

    1
    2
    # 对基于注解的Redis缓存数据统一设置有效期为1分钟,单位毫秒
    spring.cache.redis.time-to-live=60000
  • 通过template的方式,使用可视化工具可以看到json数据,使用注解后value值出现乱码

乱码问题

  • 可以在可视化工具中看到缓存的value是乱码的清空

  • 原因: SpringBoot Data在整合Redis作为Cache的实现方式后,组件之间的依赖关系变为:CacheAutoConfiguration–>RedisCacheConfiguration(autoconfigure.cache)–>RedisCacheManager–>RedisCache–>RedisCacheConfiguration(redis.cache)

  • 解决:https://blog.csdn.net/studying0419/article/details/107466232

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
    RedisSerializer<String> redisSerializer = new StringRedisSerializer();
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    om.activateDefaultTyping(
    LaissezFaireSubTypeValidator.instance,
    ObjectMapper.DefaultTyping.NON_FINAL,
    JsonTypeInfo.As.WRAPPER_ARRAY);
    om.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
    om.registerModule(new JavaTimeModule());
    jackson2JsonRedisSerializer.setObjectMapper(om);
    // 配置序列化
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
    RedisCacheConfiguration redisCacheConfiguration = config
    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));

    RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
    .cacheDefaults(redisCacheConfiguration)
    .build();
    return cacheManager;
    }