Spring的缓存机制

Spring的缓存机制

Spring的缓存不是一种具体的缓存实现方案,它底层需要依赖EhCache、Guava等具体的缓存工具。应用程序只要面向Spring缓存API编程,应用底层的缓存实现可以在不同的缓存之间自由切换,应用程序无须任何改变,只需要对配置文件略作修改即可。

启用Spring缓存

为了启用Spring缓存,需要在配置文件中导入cache:命名空间。
导入cache:命名空间之后,启用Spring缓存还需要两步:

  • 在Spring配置文件中添加<cache:annotation-driven cache-mangager="缓存管理器ID"/>,该元素指定Spring根据注解来启用Bean级别或方法的缓存
  • 针对不同的缓存实现配置对应的缓存管理器

Spring内置缓存实现的配置

Spring内置的缓存实现只是一种内存中的缓存,并非真正的缓存实现,因此通常指能用于简单的测试环境,不建议在实际项目中使用Spring内置的缓存实现。
Spring内置的缓存实现使用SimpleCacheManager作为缓存管理器,使用SimpleCacheManager配置缓存非常简单,直接在Spring容器中配置该Bean,然后通过<property.../>驱动该缓存管理器执行setCaches()放来来设置缓存区即可。
SimpleCacheManager是一种内存中的缓存区,底层直接使用了JDK的ConcurrentMap来实现缓存,SimpleCacheManager使用了ConcurrentMapCacheFactoryBean作为缓存区,每个ConcurrentMapCacheFactoryBean配置一个缓存区

<!-- 使用SimpleCacheManager配置Spring内置的缓存管理器 -->
<bean id="cacheManager" class="org.springframework.cache.suppot.SimpleCacheManager">
    <!-- 配置缓存区 -->
    <property name="caches">
        <set>
            <!-- 使用ConcurrentMapCacheFactoryBean配置缓存区
                下面列出多个缓存区,p:name用于为缓存区指定名字 -->
            <bean class="org.springframeword.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
            <bean class="org.springframeword.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="users"/>
        </set>
    </property>

上面配置文件使用SimpleCacheManager配置了Spring内置的缓存管理器,并为该缓存管理器配置列两个缓存区:default和users————这些缓存区的名字很重要,后面使用注解驱动缓存时需要根据缓存区的名字来讲缓存数据放入指定缓存区中。

在实际应用中,开发者可以根据自己的需要,配置更多的缓存区,一般来说,应用有多少个组件需要缓存,程序就应该配置多少个缓存区

EhCache缓存实现的配置

为了使用EhCache,需要在应用的类加载路径下添加一个ehcache.xml配置文件。例如,使用如下echcache.xml文件

<?xml version="1.0" encoding="gbk"?>
<ehcache>
    <diskStore path="java.io.tmpdir" />
    <!-- 配置默认的缓存区 -->
    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        maxElementsOnDisk="10000000"
        diskExpiryThreadIntervalSeconds="120"
        memoryStoreEvictionPolicy="LRU"/>
    <!-- 配置名为users的缓存区 -->
    <cache name="users"
        maxElementsInMemory="10000"
        eternal="false"
        overflowToDisk="true"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600" />
</ehcache>

配置文件解析:

  • diskStore : ehcache支持内存和磁盘两种存储
    • path :指定磁盘存储的位置
  • defaultCache : 默认的缓存
  • cache :自定的缓存,当自定的配置不满足实际情况时可以通过自定义(可以包含多个cache节点)
    • cache :自定的缓存,当自定的配置不满足实际情况时可以通过自定义(可以包含多个cache节点)
    • maxElementsInMemory :内存中允许存储的最大的元素个数,0代表无限个
    • clearOnFlush:内存数量最大时是否清除。
    • eternal :设置缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。根据存储数据的不同,例如一些静态不变的数据如省市区等可以设置为永不过时
    • timeToIdleSeconds : 设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
    • timeToLiveSeconds :缓存数据的生存时间(TTL),也就是一个元素从构建到消亡的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值是0就意味着元素可以停顿无穷长的时间。
    • overflowToDisk :内存不足时,是否启用磁盘缓存。
    • maxEntriesLocalDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。
    • maxElementsOnDisk:硬盘最大缓存个数。
    • diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
    • diskPersistent:是否在VM重启时存储硬盘的缓存数据。默认值是false。
    • diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。

上面的配置文件中同样配置列两个缓存区,其中第一个是用于配置匿名的、默认的缓存区,第二个才是配置了名为users的缓存区。如果需要,完全可以复制多个<cache... style="box-sizing: border-box;">元素,用于配置多个有名字的缓存区。这些缓存区的名字很重要,后面使用注解驱动缓存时需要根据缓存区的名字来讲缓存数据放入指定缓存区中。</cache...>

Spring使用EhCacheCacheManager作为EhCache缓存实现的缓存管理器,因此只要该对象配置Spring容器中,它就可以作为缓存管理器使用,但EhCacheCacheManager底层需要依赖一个net.sf.ehcache.CacheManager作为实际的缓存管理器。
为了将net.sf.ehcache.CacheManager纳入Spring容器的管理之下,Spring提供了EhCacheMangagerFactoryBean工厂Bean,该工厂实现了FactoryBean<CacheManage>接口。当程序吧EhCacheMangagerFactoryBean部署在Spring容器中,并通过Spring容器请求该工厂Bean时,实际返回的是它的产品————也就是CacheManage对象

<beans>
    <context:component-scan 
        base-package="org.crazyit.app.service"/>

    <cache:annotation-driven cache-manager="cacheManager" />

    <!-- 配置EhCache的CacheManager
    通过configLocation指定ehcache.xml文件的位置 -->
    <bean id="ehCacheManager"
        class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
        p:configLocation="classpath:ehcache.xml"
        p:shared="false" />
    <!-- 配置基于EhCache的缓存管理器
    并将EhCache的CacheManager注入该缓存管理器Bean -->
    <bean id="cacheManager"
        class="org.springframework.cache.ehcache.EhCacheCacheManager"
        p:cacheManager-ref="ehCacheManager" > 
    </bean>

</beans>

使用@Cacheable执行缓存

@Cacheable可用于修饰类或修饰方法,当使用@Cacheable修饰类时,用于告诉Spring在类级别上进行缓存————程序调用该类的实例的任何方法时都需要缓存,而且共享同一个缓存区;当使用@Cacheable修饰方法时,用于告诉Spring在方法级别上进行缓存————只有当程序调用该方法时才需要缓存

类级别的缓存

使用@Cacheable修饰类时,就可控制Spring在类级别进行缓存,这样当程序调用该类的任意方法时,只要传入的参数相同,Spring就会使用缓存

@Service("userService")
// 指定将数据放入users缓存区
@Cacheable(value = "users")
public class UserServiceImpl implements UserService
{
    public User getUsersByNameAndAge(String name, int age)
    {
        System.out.println("--正在执行findUsersByNameAndAge()查询方法--");
        return new User(name, age);
    }
    public User getAnotherUser(String name, int age)
    {
        System.out.println("--正在执行findAnotherUser()查询方法--");
        return new User(name, age);
    }
}

当程序第一次调用该类的实例的某个方法时,Spring缓存机制会将该方法返回的数据放入指定缓存区。以后程序调用该类的实例的任何方法时,只要传入的参数相同,Spring将不会真正执行该方法,而是直接利用缓存区中的数据

使用@Cacheable是可指定如下属性:

  • value:必须属性。该属性可指定多个缓存区的名字,用于指定将方法返回值放入指定的缓存区内
  • key:通过SpEL表达式显式指定缓存的key
  • condition:该属性指定一个返回boolean值的SpEL表达式,只有当该表达式返回true时,Spring才会缓存方法返回值
  • unless:该属性指定一个返回boolean值的SpEL表达式,当该表达式返回true时,Spring就不缓存方法返回值

与@Cache注解功能类似的还有一个@Cacheput注解,与@Cacheable不同的是,@Cacheput修饰的方法不会读取缓存区中的数据————这以为着不管缓存区是否已有数据,@Cacheput总会告诉Spring要重新执行这些方法,并在此将方法返回值放入缓存区

方法级别的缓存

使用@Cacheable修饰方法时,就可控制Spring在方法界别进行缓存,这样当程序调用该方法时,只要传入的参数相同,Spring就会使用缓存

@Service("userService")
public class UserServiceImpl implements UserService
{
    @Cacheable(value = "users1")
    public User getUsersByNameAndAge(String name, int age)
    {
        System.out.println("--正在执行findUsersByNameAndAge()查询方法--");
        return new User(name, age);
    }
    @Cacheable(value = "users2")
    public User getAnotherUser(String name, int age)
    {
        System.out.println("--正在执行findAnotherUser()查询方法--");
        return new User(name, age);
    }
}

上面代码中指定getUsersByNameAndAge()和getAnotherUser()方法分别使用不同的缓存区,这意味着两个方法都会缓存,但由于它们使用了不同的缓存区,因此它们不能共享缓存区数据

使用@CacheEvict清除缓存

被@CacheEvict注解修饰的方法可用于清除缓存,使用@CacheEvict注解时可指定如下属性:

  • value:必需属性。用于指定该方法用于清除哪个缓存区的数据
  • allEntries:该属性指定是否清空整个缓存区
  • beforeInvocation:该属性指定是否在执行方法之前清除缓存。默认实在fang
  • candion:该属性指定一个SpEL变道时,只有当该表达式为true时才清除缓存
  • key:通过SpEL表达式显式指定缓存的key
@Service("userService")
@Cacheable(value = "users")
public class UserServiceImpl implements UserService
{
    public User getUsersByNameAndAge(String name, int age)
    {
        System.out.println("--正在执行findUsersByNameAndAge()查询方法--");
        return new User(name, age);
    }
    public User getAnotherUser(String name, int age)
    {
        System.out.println("--正在执行findAnotherUser()查询方法--");
        return new User(name, age);
    }
    // 指定根据name、age参数清除缓存
    @CacheEvict(value = "users")
    public void evictUser(String name, int age)
    {
        System.out.println("--正在清空"+ name
            + " , " + age + "对应的缓存--");
    }
    // 指定清除user缓存区所有缓存数据
    @CacheEvict(value = "users" , allEntries=true)
    public void evictAll()
    {
        System.out.println("--正在清空整个缓存--");
    }
}

    public static void main(String[] args)
    {
        ApplicationContext ctx =
            new ClassPathXmlApplicationContext("beans.xml");
        UserService us = ctx.getBean("userService" , UserService.class);
        // 调用us对象的2个带缓存的方法,系统会缓存两个方法返回的数据
        User u1 = us.getUsersByNameAndAge("孙悟空", 500);
        User u2 = us.getAnotherUser("猪八戒", 400);
        //调用evictUser()方法清除缓存中指定的数据
        us.evictUser("猪八戒", 400);
        // 由于前面根据"猪八戒", 400缓存的数据已经被清除了,
        // 因此下面代码会重新执行,方法返回的数据将被再次缓存。
        User u3 = us.getAnotherUser("猪八戒", 400);  // ①
        System.out.println(u2 == u3); // 输出false
        // 由于前面前面已经缓存了参数为"孙悟空", 500的数据,
        // 因此下面代码不会重新执行,直接利用缓存中的数据。
        User u4 = us.getAnotherUser("孙悟空", 500);   // ②
        System.out.println(u1 == u4); // 输出true
        // 清空整个缓存。
        us.evictAll();
        // 由于整个缓存都已经被清空,因此下面两行代码都会重新执行
        User u5 = us.getAnotherUser("孙悟空", 500);
        User u6 = us.getAnotherUser("猪八戒", 400);
        System.out.println(u1 == u5); // 输出false
        System.out.println(u3 == u6); // 输出false
    }

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

推荐阅读更多精彩内容