SpringBoot系列—缓存及其源码分析(十六)

个人博客:haichenyi.com。感谢关注

  缓存是每个项目都用到的为了提高接口响应效率,降低数据库的查询压力,从而提高系统性能。所以,缓存对于一个项目来说是至关重要的。

简介

  spring 3+之后,就定义了CacheManager和Cache接口来统一不同的缓存技术。

  • CacheManager:缓存管理器,用于管理各种Cache缓存组件
  • Cahce:定义了各种操作,Spring在Cache接口下提供了各种xxCache的实现;比如:ConcurrentMapCache,RedisCache,JCacheCache等等

源码解析

  这里我以默认缓存为例:ConcurrentMapCache

  上面说了,缓存只用配置就可以直接使用,所以,配置,一说到配置,我们就会想到之前说的自动配置类AutoConfigure。如下图:

SpringBoot系列—缓存及其源码分析(十六)_第1张图片
缓存结构图.png

  我们看到,自动配置类目录下面有一个cache包,这就是缓存自动配置的包,我们熟悉的类就有CacheProperties这个类,我们点进去看:

SpringBoot系列—缓存及其源码分析(十六)_第2张图片
缓存配置类 .png

  我们熟悉的prefix就在这里了,也就是我们在全局配置类里面的键。

  这里,配置的一般都是这个类里面的全局变量,我把这个CacheType给框出来了(下面的cacheNames这个list变量也很重要),上面的注释的意思是说,缓存类型,默认情况下,是根据环境自动检测的。

  我们上面说到CacheManager和Cache接口是用来同意管理不同的缓存技术。不同的,也就是指的我们这里的缓存类型。所以,我们这里的缓存类型肯定有好几种,然后,这里又讲是根据环境自动检测的。也就是我们配置好的。也就是我们在全局配置类里面配置好的

spring.cache.xxx=xxx

  既然是配置,肯定是定义了之后才能配置的,没定义,怎么可能配置,spring又不是神。那,定义了那些种类的缓存技术呢?我们看一下CacheType类

SpringBoot系列—缓存及其源码分析(十六)_第3张图片
缓存枚举类 .png

  定义的,就这10种:GENERIC,JCACHE,EHCACHE,HAZELCAST,INFINISPAN,COUCHBASE,REDIS,CAFFEINE,SIMPLE,NONE。作为一个初学者,我想一眼看过去,我们能看到熟悉的Redis,学后天,Redis框架肯定要学。

  我们现在,什么都没有配置,默认的缓存类型就是这个Simple,可以在Cache目录下面的CacheAutoConfiguration类中看到selectImports方法,在这里打断点,我们debug模式运行,我们就能看到它导入的缓存类型,然后,每个进行检测,看匹配哪一个。

SpringBoot系列—缓存及其源码分析(十六)_第4张图片
默认导入的缓存 .png

  我们就看一下SimpleCacheConfiguration是怎么实现的

SpringBoot系列—缓存及其源码分析(十六)_第5张图片
SimpleCacheConfiguration图 .png

  我们看到了,SimpleCacheConfiguration默认使用的是:ConcurrentMapCacheManager,我们,看一下这个Manager是怎么实现的

SpringBoot系列—缓存及其源码分析(十六)_第6张图片
ConcurrentMapCacheManager图1 .png
SpringBoot系列—缓存及其源码分析(十六)_第7张图片
ConcurrentMapCacheManager图2.png

  这类,实现的就是CacheManager接口,而CacheManager接口就只有两个方法,就是上图中的两个方法setCacheNamesgetCache

  先说一下数据是怎么缓存的,缓存是一个容器,这个容器怎么获取的呢?就是通过这里的name,name是获取这个容器的key,然后里面的数据存放形式,都是key-value的形式存放的。这个key也是我们定义的,value就是数据库查询的数据。如下图。

SpringBoot系列—缓存及其源码分析(十六)_第8张图片
缓存图.png

  这个setCacheNames,就是,我们配置的cacheNames的值,它会获取好之后,将这些值封装成list,通过setCacheNames方法赋值给这里的变量cacheMap。我们可以看一下这个变量:

private final ConcurrentMap cacheMap = new ConcurrentHashMap<>(16);

  然后就是这个getCache方法,上面的setCacheNames以cacheName为键去存这个Cache,这个就是刚好相反,获取方法,通过cacheName去获取这个Cache。

  然后,我们现在是获取到了这个缓存容器,那么,我们要怎么从这个缓存容器中去获取我们对应的数据呢?

  我们看到上面那个变量是Map是以String为键,以Cache为值,我们最开始说过了,Cache和CacheManager是用来管理不同缓存技术的接口,所以,这里的值不可能是一个接口对象,肯定是它的实现类,我们再仔细看上面两个方法的实现类,我们会看到

//setCacheNames方法
this.cacheMap.put(name, createConcurrentMapCache(name));

//getCache方法
cache = createConcurrentMapCache(name);

  很明显,这里就是Cache对象是怎么创建的,我们点到这个方法里面去看:

/**
     * Create a new ConcurrentMapCache instance for the specified cache name.
     * @param name the name of the cache
     * @return the ConcurrentMapCache (or a decorator thereof)
     */
    protected Cache createConcurrentMapCache(String name) {
        SerializationDelegate actualSerialization = (isStoreByValue() ? this.serialization : null);
        return new ConcurrentMapCache(name, new ConcurrentHashMap<>(256),
                isAllowNullValues(), actualSerialization);

    }

  所以,它这里是new的ConcurrentMapCache肯定是Cache的实现类。我们看到这个构造方法,第二个参数是一个hashMap,而我们的缓存容器里面也是以键值对的方式存储数据的。我们再看这个ConcurrentMapCache

SpringBoot系列—缓存及其源码分析(十六)_第9张图片
ConcurrentMapCache图.png

  一共就只有三个全局变量,第一个name,是我们前面传过来的cacheName,第二个是Map类型,第三个是SerializationDelegate类型的变量,序列化的一个什么东西。排除法判断,只可能这个Map就是用来存放我们的缓存数据的。我们搜索这个变量,我们会看到如下几个方法:

    //获取缓存数据
    @Override
    @Nullable
    protected Object lookup(Object key) {
        return this.store.get(key);
    }

    //存放
    @Override
    public void put(Object key, @Nullable Object value) {
        this.store.put(key, toStoreValue(value));
    }
    
    //通过key移除数据
    @Override
    public void evict(Object key) {
        this.store.remove(key);
    }
    
    //清空所有数据
    @Override
    public void clear() {
        this.store.clear();
    }

    //清空所有数据
    @Override
    public boolean invalidate() {
        boolean notEmpty = !this.store.isEmpty();
        this.store.clear();
        return notEmpty;
    }

  至此,缓存怎么存放,怎么获取都说完了。

用法

  与前面差不多,都是在启动类上面开启,在方法上面标记注解就行了

  1. @EnableCaching:在启动类上,开启基于注解的缓存
  2. @Cacheable:标在方法上,返回的结果会进行缓存(先查缓存中的结果,没有则调用方法并将结果放到缓存中)
  3. @CachePut:保证方法被调用后,又将对应缓存中的数据更新(先调用方法,调完方法再将结果放到缓存)
  4. @CacheEvict:清除缓存

  @Cacheable,@CachePut,@CacheEvict三个注解都有几个重要的属性:

  • cacheNames:缓存的名字。
  • key: 作为缓存中的Key值,可以使用SpEL表达式指定(不指定,key就是参数值),缓存结果是方法返回值

  上面两个属性是前面我们一直都在强调的比较重要的属性,然后,清除缓存的注解中还有两个属性需要了解:

  • allEntries =true : 指定清除这个缓存中所有数据。
  • beforeInvocation = true : true在方法之前执行;默认false在方法之后执行,出现一场则不会清除缓存

  我这里值贴出来缓存相关的类:

@Service
public class UserService {
    @Autowired
    UserMapper userMapper;

    @Cacheable(cacheNames = "user", key = "#id")
    public User getUser(Integer id) {
        return userMapper.getUserById(id);
    }

    @CachePut(cacheNames = "user", key = "#result.id")
    public User updateUser(User user) {
        userMapper.updateUser(user);
        return user;
    }

    @CacheEvict(cacheNames = "user", key = "#result")
    public Integer deleteUser(Integer id) {
        userMapper.deleteUserById(id);
        return id;
    }

}

总结

  第一步:默认采用的是SimpleCacheConfiguration 使用 ConcurrentMapCacheManager

  第二步:getCache 获取的是 ConcurrentMapCache 缓存对象进行存取数据,它使用ConcurrentMap对象进行缓存数据。

@Cacheable(cacheNames = "user", key = "#id")

第一次请求时:

  第三步:当发送第一次请求时,会从getCache(name)中获取,看有没有ConcurrentMapCache缓存对象,如果没有 则创建出来, 并且创建出来的key就是通过

@Cacheable(cacheNames = "user")标识的name值

  第四步:接着会从ConcurrentMapCache里面调用lookup获取缓存数据,通过key值获取的,

默认采用的是service方法中的参数值,如果缓存中没有获取到,则调用目标方法进行获取数据(即从数据库中查询),获取之后则再将它 放到缓存中(key=参数值,value=返回值)

第二次请求时:

  第五步:如果再次调用 则还是先ConcurrentMapCacheManager.getCache()获取缓存对象,如果有则直接返回, 如果没有则创建

  第六步:然后再调用 ConcurrentMapCache.lookup方法从缓存中获取数据, 如果缓存有数据则直接响应回去,不 会再去调用目标方法

第三次请求与第二次一样

如果缓存中没有缓存管理器,则与第一次请求一致

你可能感兴趣的:(SpringBoot系列—缓存及其源码分析(十六))