spring缓存管理

声明:感谢作者 云中之歌的文章https://www.cnblogs.com/fysola/p/6378400.html和作者

congyh 的文章http://blog.youkuaiyun.com/congyihao/article/details/69748108

下面的文章精华:

结合俩篇文章,我对spring的缓存机制的使用尤其是注解实现缓存有了更明确的认识。

Spring缓存底层也是需要借助其他缓存工具来实现,例如EhCache(Hibernate缓存工具),上层则以统一API编程。

要使用Spring缓存,需要以下三步

  • 1.向Spring配置文件导入context:命名空间
  • 2.在Spring配置文件启用缓存,具体是添加 <cache:annotation-driven cache-manager="缓存管理器ID" />
  • 3.配置缓存管理器,不同的缓存实现配置不同,如果是EhCache,需要先配置一个ehcache.xml

例如

复制代码
 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <ehcache>
 3     <diskStore path="java.io.tmpdir" />
 4     <!-- 配置默认的缓存区 -->
 5     <defaultCache
 6         maxElementsInMemory="10000"
 7         eternal="false"
 8         timeToIdleSeconds="120"
 9         timeToLiveSeconds="120"
10         maxElementsOnDisk="10000000"
11         diskExpiryThreadIntervalSeconds="120"
12         memoryStoreEvictionPolicy="LRU"/>
13     <!-- 配置名为users的缓存区 -->
14     <cache name="users"
15         maxElementsInMemory="10000"
16         eternal="false"
17         overflowToDisk="true"
18         timeToIdleSeconds="300"
19         timeToLiveSeconds="600" />
20 </ehcache>
复制代码

上面的ehcache.xml配置了两个缓存区,Spring中的Bean将会缓存在这些缓存区中,一般的,Spring容器中有多少个Bean,就会在ehcache中定义多少个缓存区。

接着在Spring配置文件中配置缓存管理器如下,其中第一个Bean是一个工厂Bean,用来配置EhCache的CacheManager, 第二个Bean才是为Spring缓存配置的缓存管理器,所以将第一个Bean注入第二个Bean。

复制代码
 1     <cache:annotation-driven cache-manager="cacheManager" />
 2 
 3     <!-- 配置EhCache的CacheManager
 4     通过configLocation指定ehcache.xml文件的位置 -->
 5     <bean id="ehCacheManager"
 6         class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
 7         p:configLocation="classpath:ehcache.xml"
 8         p:shared="false" />
 9     <!-- 配置基于EhCache的缓存管理器
10     并将EhCache的CacheManager注入该缓存管理器Bean -->
11     <bean id="cacheManager"
12         class="org.springframework.cache.ehcache.EhCacheCacheManager"
13         p:cacheManager-ref="ehCacheManager" > 
14     </bean>
复制代码

 

下面是一个完整的Spring配置,

复制代码
 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4     xmlns:p="http://www.springframework.org/schema/p"
 5     xmlns:cache="http://www.springframework.org/schema/cache"
 6     xmlns:context="http://www.springframework.org/schema/context"
 7     xsi:schemaLocation="http://www.springframework.org/schema/beans 
 8     http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
 9     http://www.springframework.org/schema/cache
10     http://www.springframework.org/schema/cache/spring-cache-4.0.xsd
11     http://www.springframework.org/schema/context
12     http://www.springframework.org/schema/context/spring-context-4.0.xsd">
13 
14     <context:component-scan 
15         base-package="com.service"/>
16         
17     <cache:annotation-driven cache-manager="cacheManager" />
18 
19     <!-- 配置EhCache的CacheManager
20     通过configLocation指定ehcache.xml文件的位置 -->
21     <bean id="ehCacheManager"
22         class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
23         p:configLocation="classpath:ehcache.xml"
24         p:shared="false" />
25     <!-- 配置基于EhCache的缓存管理器
26     并将EhCache的CacheManager注入该缓存管理器Bean -->
27     <bean id="cacheManager"
28         class="org.springframework.cache.ehcache.EhCacheCacheManager"
29         p:cacheManager-ref="ehCacheManager" > 
30     </bean>
31     
32 </beans>
复制代码

 

下面将以@Cacheable为例,演示Spring基于EhCache缓存的用法。@Cacheable用于修饰类或者方法,如果修饰类,则类中所有方法都会被缓存。

类级别的缓存

例如有如下Bean类,

复制代码
 1 @Service("userService")
 2 @Cacheable(value="users")
 3 public class UserServiceImpl implements UserService {
 4 
 5     @Override
 6     public User getUsersByNameAndAge(String name, int age) {
 7         System.out.println("正在执行getUsersByNameAndAge()..");
 8         return new User(name,age);
 9     }
10 
11     @Override
12     public User getAnotherUser(String name, int age) {
13         System.out.println("正在执行getAnotherUser()..");
14         return new User(name,age);
15     }
16 }
复制代码

 

基于类的缓存,将会缓存类中的所有方法,缓存之后,程序调用该类实例的任何方法,只要传入的参数相同,Spring将不会真正执行该方法,而是直接根据传入的参数去查找缓存中的数据!

比如像下面这样使用缓存数据,

复制代码
1     public static void test2() {
2         ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
3         UserService us = ctx.getBean("userService", UserService.class);
4         User u1 = us.getUsersByNameAndAge("张三", 50);
5         //由于第二次调用userService方法时,使用了相同参数,那么真正的方法将不会执行,
6         //Spring将直接从缓存按参数查找数据
7         User u2 = us.getAnotherUser("张三", 50);
8         System.out.println(u1==u2);
9     }
复制代码

 

输出结果,

1 正在执行getUsersByNameAndAge()..
2 true

 

可以看到,上面的getAnotherUser()并没有真正执行,因为传入的参数与之前的方法传入的参数相同,于是Spring直接从缓存区数据了。

上面的Bean类中的注解@Cacheable除了必选属性value之外,还有key, condition,, unless属性,后面三个都是用来设置Spring存储策略,对于基于类的缓存来说,Spring默认以方法传入的参数作为key去缓存中查找结果。

当然我们也可以修改key的策略,让Spring按照其他标准,比如按照第一个参数是否相同来作为key,在缓存中查找结果。

将上面的Bean类修改如下,

复制代码
1 @Service("userService")
2 @Cacheable(value="users", key="#name")
3 public class UserServiceImpl implements UserService {
4 
5     @Override
6     public User getUsersByNameAndAge(String name, int age) {
复制代码

 

意味着我们传入相同的name,Spring就不会真正执行方法。只有name不同的时候,方法才会真正执行,例如下面,

复制代码
1     public static void test2() {
2         ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
3         UserService us = ctx.getBean("userService", UserService.class);
4         User u1 = us.getUsersByNameAndAge("张三", 50);
5         //将@Cacheable的key参数改为key="#name"之后,下面的方法将可以执行。
6         User u2 = us.getAnotherUser("李四", 50);
7         System.out.println(u1==u2);
8     }
复制代码

 

可以看到这回getAnotherUser()方法得到执行了,

1 正在执行getUsersByNameAndAge()..
2 正在执行getAnotherUser()..
3 false

 

我们也可以设置condition属性,例如,

复制代码
1 @Service("userService")
2 @Cacheable(value="users", condition="#age<100")
3 public class UserServiceImpl implements UserService {
4 
5     @Override
6     public User getUsersByNameAndAge(String name, int age) {
复制代码

 

那么对于下面的代码来说,两个方法都不会被缓存,Spring每次都是执行真正的方法取结果,

复制代码
1     public static void test2() {
2         ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
3         UserService us = ctx.getBean("userService", UserService.class);
4         User u1 = us.getUsersByNameAndAge("张三", 500);
5         User u2 = us.getAnotherUser("李四", 500);
6         System.out.println(u1==u2);
7     }
复制代码

 

执行结果,

1 正在执行getUsersByNameAndAge()..
2 正在执行getAnotherUser()..
3 false

方法级别的缓存

方法级别的缓存则只会对方法起作用了,不同的方法可以设置不用的缓存区,例如下面这样,

复制代码
 1 @Service("userService")
 2 public class UserServiceImpl implements UserService {
 3 
 4     @Cacheable("users1")
 5     @Override
 6     public User getUsersByNameAndAge(String name, int age) {
 7         System.out.println("正在执行getUsersByNameAndAge()..");
 8         return new User(name,age);
 9     }
10 
11     @Cacheable("users2")
12     @Override
13     public User getAnotherUser(String name, int age) {
14         System.out.println("正在执行getAnotherUser()..");
15         return new User(name,age);
16     }
17 }
复制代码

 

使用下面的测试代码,

复制代码
 1     public static void test2() {
 2         ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
 3         UserService us = ctx.getBean("userService", UserService.class);
 4         //第一次执行方法,方法将会真正执行并缓存
 5         User u1 = us.getUsersByNameAndAge("张三", 500);
 6         //虽然下面方法传入相同参数,但是因为这两个方法在不同的缓存区,所以无法使用缓存数据
 7         User u2 = us.getAnotherUser("张三", 500);
 8         System.out.println(u1==u2);
 9         //上面已经缓存过,这里不会真正执行,直接使用缓存
10         User u3 = us.getAnotherUser("张三", 500);
11         System.out.println(u3==u2);
12     }
复制代码

执行结果,

1 正在执行getUsersByNameAndAge()..
2 正在执行getAnotherUser()..
3 false
4 true

使用@CacheEvict清除缓存

被@CacheEvict修饰的方法可以用来清除缓存,使用@CacheEvict可以指定如下属性。

allEntries, 是否清空整个缓存区

beforeInvocation: 是否在执行方法之前清除缓存。默认是方法执行成功之后才清除。

condiition以及key, 与@Cacheable中一样的含义。

下面示范简单用啊,

复制代码
 1 @Service("userService")
 2 @Cacheable("users")
 3 public class UserServiceImpl implements UserService {
 4 
 5     @Override
 6     public User getUsersByNameAndAge(String name, int age) {
 7         System.out.println("正在执行getUsersByNameAndAge()..");
 8         return new User(name,age);
 9     }
10 
11     @Override
12     public User getAnotherUser(String name, int age) {
13         System.out.println("正在执行getAnotherUser()..");
14         return new User(name,age);
15     }
16     //指定根据name,age参数清楚缓存
17     @CacheEvict(value="users")
18     public void evictUser(String name, int age) {
19         System.out.println("--正在清空"+name+","+age+"对应的缓存--");
20     }
21     
22     //指定清除user缓存区所有缓存的数据
23     @CacheEvict(value="users", allEntries=true)
24     public void evictAll() {
25         System.out.println("--正在清空整个缓存--");
26     }
27 }
复制代码

 

下面是测试类,

复制代码
 1     public static void test2() {
 2         ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
 3         UserService us = ctx.getBean("userService", UserService.class);
 4         //系统会缓存两个方法
 5         User u1 = us.getUsersByNameAndAge("张三", 500);
 6         User u2 = us.getAnotherUser("李四",400);
 7         //调用evictUser()方法清除缓冲区指定的数据
 8         us.evictUser("李四", 400);
 9         //前面清除了  李四, 400 的缓存,下面的方法返回的数据将会再次被缓存
10         User u3 = us.getAnotherUser("李四", 400);
11         System.out.println(us == u3);    //false
12         //前面已经缓存了 张三, 500的数据,下面方法不会重新执行,直接取缓存中的数据
13         User u4 = us.getAnotherUser("张三", 500);
14         System.out.println(u1==u4); //输出true
15         //清空整个缓存
16         us.evictAll();
17         //由于整个缓存都已经被清空,下面的代码都会被重新执行
18         User u5 = us.getAnotherUser("张三", 500);
19         User u6 = us.getAnotherUser("李四", 400);
20         System.out.println(u1==u5); //输出false
21         System.out.println(u3==u6); //输出false
22     }
复制代码

 

执行结果,

复制代码
 1 正在执行getUsersByNameAndAge()..
 2 正在执行getAnotherUser()..
 3 --正在清空李四,400对应的缓存--
 4 正在执行getAnotherUser()..
 5 false
 6 true
 7 --正在清空整个缓存--
 8 正在执行getAnotherUser()..
 9 正在执行getAnotherUser()..
10 false
11 false
复制代码

上面是对缓存的简单使用和测试,下面是对注解的理解

congyh

生效原理

抽象而不是实现

Spring框架中, 缓存服务是一组抽象, 也就是一组API, 由

  • org.springframework.cache.Cache
  • org.springframework.cache.CacheManager

抽象. 抽象中并不包含多线程或者多进程处理逻辑, 这些逻辑应该由缓存实现方提供. Spring data工程默认提供了一些实现, 例如ConcurrentHashMap, GemFire, Ehcache等.

通过在方法级别上加@Cacheable等注解, 每当方法被调用的时候, Spring通过AOP的方式拦截方法的执行, 执行缓存的添加, 更新, 删除等操作(取决于注解属性的配置和注解类型).

声明式缓存

在Spring中使用缓存的方式是声明式缓存, 只需要三步配置即可启用:

  • 缓存声明 
    在需要缓存的方法上以注解的形式标识.
  • 缓存配置 
    显式或隐式配置缓存数据存储后端(Redis, ConcurrentHashMap等).
  • @Configuration配置类中加上@EnableCaching, 在应用范围内启用注解.
声明式缓存的类型

共有以下类型:

  • @Cacheable 查询缓存
  • @CacheEvict 删除缓存条目
  • @CachePut 更新缓存条目
  • @Caching
  • @CacheConfig

三级缓存策略定制

从上到下, 依次可以进行三次缓存策略的设定, 每一层都会覆盖上层的默认设定:

  • 全局范围的定制: 配置在CacheManagerKeyGenerator中.
  • 类级别的定制: 使用@CacheConfig注解;
  • 方法级别的定制.

Key生成策略

说到底最终还是要以key-value的形式写到后端存储中, 那么Spring的Key生成策略就是值得考虑的. Spring默认的KeyGenerator使用以下算法:

  • 如果没有参数, 返回SimpleKey.EMPTY
  • 如果只有一个参数, 直接返回参数实例.
  • 如果有多个参数, 那么返回一个包含所有参数的SimpleKey.

那么也就是说, 对于cacheNames相同的方法, 如果他们调用时的参数类型和值也相同, 那么就会认为使用的是同一条缓存条目:

@Cacheable(cacheNames = "girl")
public Girl findOne(Integer id) {
    logger.error("方法findOne被调用!");
    return girlRepository.findOne(id);
}

@Cacheable(cacheNames = "girl")
public Girl findOne1(Integer id) {
    logger.error("方法findOne1被调用!");
    return girlRepository.findOne(id);
}

// Test类中
@Test
public void cacheTest() {
    girlService.findOne(14);
    girlService.findOne1(14); // 经测试, 第二条调用不会触发数据库操作
}

缓存添加–@Cacheable的使用

通常用来标识查询缓存, 也就是数据库的select一类的不会改变目标对象状态的情景.

以下是一个使用的例子:

@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

我们可以通过指定自定义类型的key, 来显式设定缓存的key, 默认是包含方法的所有参数构建的.

同步式缓存

默认缓存策略是不会加锁的, 也就是多线程执行同一个方法的时候, 对于同样参数的调用, 可能仍会被计算多次. 我们可以通过在声明式缓存中配置sync属性, 显式告知CacheManager, 使用同步(阻塞式)策略更新缓存, 也就是多线程下对同一方法的同一参数的调用会被阻塞, 只有一个运行, 其他的线程在锁释放后使用缓存.

@Cacheable(cacheNames="foos", sync="true")
public Foo executeExpensiveOperation(String id) {...}

条件缓存

通过在声明式缓存中添加conditionunless属性, 可以指明缓存只有在符合特定条件下才生效, 并且在执行完后显式排除掉部分结果.

@Cacheable(cacheNames="book", condition="#name.length < 32", unless="#result.hardback")
public Book findBook(String name)

注意:

  • condition是在方法执行前评估, unless是在方法执行后评估.
  • 关于声明式缓存中能够使用的SpEL类型, 详见文章开头的链接.

缓存更新–@CachePut的使用

通常用来表明该方法的执行应引起缓存条目的更新, 类比于sql的update操作.

@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)

缓存清除–@CacheEvict的使用

通常用来表明方法的执行应引起相应缓存条目(或相应缓存主题整体)的清除.

@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)

上面allEntries指明是对相应缓存主题整体或者key对应条目的清除.

多缓存策略–@Caching的使用

允许多个不冲突的缓存策略作为一个缓存策略组同时生效.

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

统一声明缓存–@CacheConfig的使用

类级别的注解, 用于描述类内方法共享的缓存主题名, 缓存key, CacheManagerCacheResolver等.

@CacheConfig("books")
public class BookRepositoryImpl implements BookRepository {

    @Cacheable
    public Book findBook(ISBN isbn) {...}
}

Spring Boot中对缓存的增强支持

对缓存后端的适配选择更多了, 通常使用的是Redis.

可以通过spring.cache.type显式指定要使用的后端类型, 或者Spring Boot会在路径中自动搜寻缓存后端提供者:

  • 如果找到了, 那么自动创建相应类型的CacheManager实现类, 例如RedisCacheManager.
  • 如果没有找到, 那么默认会使用ConcurrentHashMap作为缓存后端.
  • 如果想要临时关闭缓存, 可以显式指定spring.cache.type=none来关闭缓存操作. 通常用在测试场景中.

定制CacheManager–添加缓存过期时间等

我们也可以通过Java Bean的方式来对Spring Boot自动配置的缓存后端进一步定制, 如设定缓存主题名:

@Bean
public CacheManagerCustomizer<ConcurrentMapCacheManager> cacheManagerCustomizer() {
    return new CacheManagerCustomizer<ConcurrentMapCacheManager>() {
        @Override
        public void customize(ConcurrentMapCacheManager cacheManager) {
            cacheManager.setCacheNames(Arrays.asList("one", "two"));
        }
    };
}

我们可以通过为@Bean添加额外的@Order注解来多次增强配置同一个CacheManager.

一些需要特别说明的点

  • Spring推荐只在具体类的public方法上使用@Cache*注解 
    因为Java中接口的注解是不会被继承到类上的, 所以如果是通过cglib技术进行的动态代理不会触发缓存效果, 此外, 通过字节码织入实现的切面技术也不会触发缓存效果. 所以最好是在具体类的public方法上声明@Cache*.

参考链接:

  1. Spring框架官方文档-Cache部分
  2. Spring Boot Cache章节
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值