spring cache

概述

Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。
Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的专业缓存例如 EHCache 集成。
其特点总结如下:

  • 通过少量的配置 annotation 注释即可使得既有代码支持缓存
  • 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存
  • 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
  • 支持 AspectJ,并通过其实现任何方法的缓存支持
  • 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性

实现

  1. maven配置

1 <dependency> 2 <groupId>aspectj</groupId> 3 <artifactId>aspectjrt</artifactId> 4 <version>1.5.4</version> 5 </dependency> 6 <dependency> 7 <groupId>org.aspectj</groupId> 8 <artifactId>aspectjweaver</artifactId> 9 <version>1.8.10</version> 10 </dependency> 11 <dependency> 12 <groupId>org.springframework</groupId> 13 <artifactId>spring-context</artifactId> 14 <version>${spring.version}</version> 15 </dependency> 16
  1. 本项目采用纯java的配置,以下是配置spring cache的代码,缓存管理采用spring自带SimpleCacheManager,

1 2@Configuration 3@EnableCaching //开启缓存注解 4public class SpringCacheConfig { 5 6 /** 7 * 使用SimpleCacheManager管理缓存 8 * @return SimpleCacheManager 9 */ 10 @Bean 11 public SimpleCacheManager cacheManager() { 12 SimpleCacheManager cacheManager = new SimpleCacheManager(); 13 cacheManager.setCaches(Arrays.asList( 14 //新建2个缓存实例 15 new ConcurrentMapCache[]{ 16 new ConcurrentMapCache("default"), 17 new ConcurrentMapCache("test"), 18 new ConcurrentMapCache("users")})); 19 return cacheManager; 20 } 21

@Cacheable、@CachePut、@CacheEvict 注释介绍
通过上面的例子,我们可以看到 spring cache 主要使用两个注释标签,即 @Cacheable、@CachePut 和 @CacheEvict,我们总结一下其作用和配置方法。
*表 1. @Cacheable 作用和配置方法
@Cacheable 的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存*
@Cacheable 主要的参数

value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:@Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”} key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:@Cacheable(value=”testcache”,key=”#userName”) condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @Cacheable(value=”testcache”,condition=”#userName.length()>2”)

@CachePut 作用和配置方法

value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:@Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”} key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:@Cacheable(value=”testcache”,key=”#userName”) condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @Cacheable(value=”testcache”,condition=”#userName.length()>2”)

表 3. @CacheEvict 作用和配置方法

value 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 例如:@Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”} key 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 例如:@Cacheable(value=”testcache”,key=”#userName”) condition 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @Cacheable(value=”testcache”,condition=”#userName.length()>2”) allEntries 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 例如:@CachEvict(value=”testcache”,allEntries=true) beforeInvocation 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 @CachEvict(value=”testcache”,beforeInvocation=true)

1@Service("userService") 2public class UserService { 3 4 @Autowired 5 private UserRepository userRepository; 6 7 @Autowired 8 private CacheManager cacheManager; 9 10 private Logger logger = LoggerFactory.getLogger(UserService.class); 11 12 @Cacheable(cacheNames = "users") 13 public List<User> getUsers(){ 14 logger.info("getUsers for userService"); 15 Map<String, Object> param = new HashMap(); 16 return userRepository.getUserList(param); 17 } 18 19 @CachePut(cacheNames = "users") 20 public User saveUser(User user){ 21 logger.info("save user at userRepository !"); 22 return userRepository.save(user); 23 } 24 25

基本原理

和 spring 的事务管理类似,spring cache 的关键原理就是 spring AOP,通过 spring AOP,其实现了在方法调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。我们来看一下下面这个图:

这里写图片描述

上图显示,当客户端“Calling code”调用一个普通类 Plain Object 的 foo() 方法的时候,是直接作用在 pojo 类自身对象上的,客户端拥有的是被调用者的直接的引用。
而 Spring cache 利用了 Spring AOP 的动态代理技术,即当客户端尝试调用 pojo 的 foo()方法的时候,给他的不是 pojo 自身的引用,而是一个动态生成的代理类

这里写图片描述

如上图所示,这个时候,实际客户端拥有的是一个代理的引用,那么在调用 foo() 方法的时候,会首先调用 proxy 的 foo() 方法,这个时候 proxy 可以整体控制实际的 pojo.foo() 方法的入参和返回值,比如缓存结果,比如直接略过执行实际的 foo() 方法等,都是可以轻松做到的。

扩展性

直到现在,我们已经学会了如何使用开箱即用的 spring cache,这基本能够满足一般应用对缓存的需求,但现实总是很复杂,当你的用户量上去或者性能跟不上,总需要进行扩展,这个时候你或许对其提供的内存缓存不满意了,因为其不支持高可用性,也不具备持久化数据能力,这个时候,你就需要自定义你的缓存方案了,还好,spring 也想到了这一点。
我们先不考虑如何持久化缓存,毕竟这种第三方的实现方案很多,我们要考虑的是,怎么利用 spring 提供的扩展点实现我们自己的缓存,且在不改原来已有代码的情况下进行扩展。
首先,我们需要提供一个 CacheManager 接口的实现,这个接口告诉 spring 有哪些 cache 实例,spring 会根据 cache 的名字查找 cache 的实例。另外还需要自己实现 Cache 接口,Cache 接口负责实际的缓存逻辑,例如增加键值对、存储、查询和清空等。利用 Cache 接口,我们可以对接任何第三方的缓存系统,例如 EHCache、OSCache,甚至一些内存数据库例如 memcache 或者 h2db 等。下面我举一个简单的例子说明如何做。

1package cacheOfAnno; 2 3 import java.util.Collection; 4 5 import org.springframework.cache.support.AbstractCacheManager; 6 7 public class MyCacheManager extends AbstractCacheManager { 8 private Collection<? extends MyCache> caches; 9 10 /** 11 * Specify the collection of Cache instances to use for this CacheManager. 12 */ 13 public void setCaches(Collection<? extends MyCache> caches) { 14 this.caches = caches; 15 } 16 17 @Override 18 protected Collection<? extends MyCache> loadCaches() { 19 return this.caches; 20 } 21 22 } 23

上面的自定义的 CacheManager 实际继承了 spring 内置的 AbstractCacheManager,实际上仅仅管理 MyCache 类的实例。

1package cacheOfAnno; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 import org.springframework.cache.Cache; 7 import org.springframework.cache.support.SimpleValueWrapper; 8 9 public class MyCache implements Cache { 10 private String name; 11 private Map<String,Account> store = new HashMap<String,Account>();; 12 13 public MyCache() { 14 } 15 16 public MyCache(String name) { 17 this.name = name; 18 } 19 20 @Override 21 public String getName() { 22 return name; 23 } 24 25 public void setName(String name) { 26 this.name = name; 27 } 28 29 @Override 30 public Object getNativeCache() { 31 return store; 32 } 33 34 @Override 35 public ValueWrapper get(Object key) { 36 ValueWrapper result = null; 37 Account thevalue = store.get(key); 38 if(thevalue!=null) { 39 thevalue.setPassword("from mycache:"+name); 40 result = new SimpleValueWrapper(thevalue); 41 } 42 return result; 43 } 44 45 @Override 46 public void put(Object key, Object value) { 47 Account thevalue = (Account)value; 48 store.put((String)key, thevalue); 49 } 50 51 @Override 52 public void evict(Object key) { 53 } 54 55 @Override 56 public void clear() { 57 } 58 } 59

上面的自定义缓存只实现了很简单的逻辑,但这是我们自己做的,也很令人激动是不是,主要看 get 和 put 方法,其中的 get 方法留了一个后门,即所有的从缓存查询返回的对象都将其 password 字段设置为一个特殊的值,这样我们等下就能演示“我们的缓存确实在起作用!”了。

注意和限制

基于 proxy 的 spring aop 带来的内部调用问题

上面介绍过 spring cache 的原理,即它是基于动态生成的 proxy 代理机制来对方法的调用进行切面,这里关键点是对象的引用问题,如果对象的方法是内部调用(即 this 引用)而不是外部引用,则会导致 proxy 失效,那么我们的切面就失效,也就是说上面定义的各种注释包括 @Cacheable、@CachePut 和 @CacheEvict 都会失效,我们来演示一下。

可见,结果是每次都查询数据库,缓存没起作用。要避免这个问题,就是要避免对缓存方法的内部调用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式来解决这个问题。
@CacheEvict 的可靠性问题
我们看到,@CacheEvict 注释有一个属性 beforeInvocation,缺省为 false,即缺省情况下,都是在实际的方法执行完成后,才对缓存进行清空操作。期间如果执行方法出现异常,则会导致缓存清空不被执行。我们演示一下

非 public 方法问题
和内部调用问题类似,非 public 方法如果想实现基于注释的缓存,必须采用基于 AspectJ 的 AOP 机制,这里限于篇幅不再细述。

代码交流 2021