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

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

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

简介

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

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

源码解析

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

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

缓存结构图.png

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

缓存配置类 .png

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

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

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

spring.cache.xxx=xxx

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

缓存枚举类 .png

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

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

默认导入的缓存 .png

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

SimpleCacheConfiguration图 .png

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

ConcurrentMapCacheManager图1 .png
ConcurrentMapCacheManager图2.png

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

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

缓存图.png

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

private final ConcurrentMap<String, Cache> 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

ConcurrentMapCache图.png

  一共就只有三个全局变量,第一个name,是我们前面传过来的cacheName,第二个是Map<object,object>类型,第三个是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<Object,Object>对象进行缓存数据。

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

第一次请求时:

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

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

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

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

第二次请求时:

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

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

第三次请求与第二次一样

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 理论总结 它要解决什么样的问题? 数据的访问、存取、计算太慢、太不稳定、太消耗资源,同时,这样的操作存在重复性。因...
    jiangmo阅读 2,773评论 0 11
  • ---------------------------------------------------------...
    LAMYMAY阅读 694评论 0 3
  • JSR107 Java Caching定义了5个核心接口,分别是CachingProvider, CacheMan...
    匆匆岁月阅读 1,405评论 0 1
  • 1、Spring缓存 1.1、缓存依赖 1.2、缓存注解 @EnableCaching:在主入口类上添加该注解,用...
    木石前盟Caychen阅读 799评论 0 6
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,036评论 1 32