springcache使用笔记003_注释驱动的 Spring cache 基本原理,注意和限制,@CacheEvict 的可靠性问题

基本原理

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

图 2. 原始方法调用图
springcache使用笔记003_注释驱动的 Spring cache 基本原理,注意和限制,@CacheEvict 的可靠性问题_第1张图片

上图显示,当客户端“Calling code”调用一个普通类 Plain Object 的 foo() 方法的时候,是直接作用在 pojo 类自身对象上的,客户端拥有的是被调用者的直接的引用。

而 Spring cache 利用了 Spring AOP 的动态代理技术,即当客户端尝试调用 pojo 的 foo()方法的时候,给他的不是 pojo 自身的引用,而是一个动态生成的代理类

图 3. 动态代理调用图
springcache使用笔记003_注释驱动的 Spring cache 基本原理,注意和限制,@CacheEvict 的可靠性问题_第2张图片

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

扩展性

直到现在,我们已经学会了如何使用开箱即用的 spring cache,这基本能够满足一般应用对缓存的需求,但现实总是很复杂,当你的用户量上去或者性能跟不上,总需要进行扩展,这个时候你或许对其提供的内存缓存不满意了,因为其不支持高可用性,也不具备持久化数据能力,这个时候,你就需要自定义你的缓存方案了,还好,spring 也想到了这一点。

我们先不考虑如何持久化缓存,毕竟这种第三方的实现方案很多,我们要考虑的是,怎么利用 spring 提供的扩展点实现我们自己的缓存,且在不改原来已有代码的情况下进行扩展。

首先,我们需要提供一个 CacheManager 接口的实现,这个接口告诉 spring 有哪些 cache 实例,spring 会根据 cache 的名字查找 cache 的实例。另外还需要自己实现 Cache 接口,Cache 接口负责实际的缓存逻辑,例如增加键值对、存储、查询和清空等。利用 Cache 接口,我们可以对接任何第三方的缓存系统,例如 EHCache、OSCache,甚至一些内存数据库例如 memcache 或者 h2db 等。下面我举一个简单的例子说明如何做。

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

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

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

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

这还不够,spring 还不知道我们写了这些东西,需要通过 spring*.xml 配置文件告诉它

清单 25. Spring-cache-anno.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
< beans xmlns = "http://www.springframework.org/schema/beans"
  xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
   xmlns:cache = "http://www.springframework.org/schema/cache"
   xmlns:p = "http://www.springframework.org/schema/p" 
   xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/cache
     http://www.springframework.org/schema/cache/spring-cache.xsd">
    
   < cache:annotation-driven />
 
   < bean id = "accountServiceBean" class = "cacheOfAnno.AccountService" />
 
   
   < bean id = "cacheManager" class = "cacheOfAnno.MyCacheManager" >
     < property name = "caches" >
       < set >
         < bean
           class = "cacheOfAnno.MyCache"
           p:name = "accountCache" />
       set >
     property >
   bean >
  
beans >

注意上面配置文件的黑体字,这些配置说明了我们的 cacheManager 和我们自己的 cache 实例。

好,什么都不说,测试!

清单 26. Main.java
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
   ApplicationContext context = new ClassPathXmlApplicationContext(
      "spring-cache-anno.xml");// 加载 spring 配置文件
  
   AccountService s = (AccountService) context.getBean("accountServiceBean");
  
   Account account = s.getAccountByName("someone");
   System.out.println("passwd="+account.getPassword());
   account = s.getAccountByName("someone");
   System.out.println("passwd="+account.getPassword());
}

上面的测试代码主要是先调用 getAccountByName 进行一次查询,这会调用数据库查询,然后缓存到 mycache 中,然后我打印密码,应该是空的;下面我再次查询 someone 的账号,这个时候会从 mycache 中返回缓存的实例,记得上面的后门么?我们修改了密码,所以这个时候打印的密码应该是一个特殊的值

清单 27. 运行结果
1
2
3
real querying db...someone
passwd=null
passwd=from mycache:accountCache

结果符合预期,即第一次查询数据库,且密码为空,第二次打印了一个特殊的密码。说明我们的 myCache 起作用了。

注意和限制

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

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

清单 28. AccountService.java
1
2
3
4
5
6
7
8
9
public Account getAccountByName2(String userName) {
   return this.getAccountByName(userName);
}
 
@Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache
public Account getAccountByName(String userName) {
   // 方法内部实现不考虑缓存逻辑,直接实现业务
   return getFromDB(userName);
}

上面我们定义了一个新的方法 getAccountByName2,其自身调用了 getAccountByName 方法,这个时候,发生的是内部调用(this),所以没有走 proxy,导致 spring cache 失效

清单 29. Main.java
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
   ApplicationContext context = new ClassPathXmlApplicationContext(
      "spring-cache-anno.xml");// 加载 spring 配置文件
  
   AccountService s = (AccountService) context.getBean("accountServiceBean");
  
   s.getAccountByName2("someone");
   s.getAccountByName2("someone");
   s.getAccountByName2("someone");
}
清单 30. 运行结果
1
2
3
real querying db...someone
real querying db...someone
real querying db...someone

可见,结果是每次都查询数据库,缓存没起作用。要避免这个问题,就是要避免对缓存方法的内部调用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式来解决这个问题。

@CacheEvict 的可靠性问题

我们看到,@CacheEvict 注释有一个属性 beforeInvocation,缺省为 false,即缺省情况下,都是在实际的方法执行完成后,才对缓存进行清空操作。期间如果执行方法出现异常,则会导致缓存清空不被执行。我们演示一下

清单 31. AccountService.java
1
2
3
4
@CacheEvict(value="accountCache",allEntries=true)// 清空 accountCache 缓存
public void reload() {
   throw new RuntimeException();
}

注意上面的代码,我们在 reload 的时候抛出了运行期异常,这会导致清空缓存失败。

清单 32. Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
   ApplicationContext context = new ClassPathXmlApplicationContext(
      "spring-cache-anno.xml");// 加载 spring 配置文件
  
   AccountService s = (AccountService) context.getBean("accountServiceBean");
  
   s.getAccountByName("someone");
   s.getAccountByName("someone");
   try {
     s.reload();
   } catch (Exception e) {
   }
   s.getAccountByName("someone");
}

上面的测试代码先查询了两次,然后 reload,然后再查询一次,结果应该是只有第一次查询走了数据库,其他两次查询都从缓存,第三次也走缓存因为 reload 失败了。

清单 33. 运行结果
1
real querying db...someone

和预期一样。那么我们如何避免这个问题呢?我们可以用 @CacheEvict 注释提供的 beforeInvocation 属性,将其设置为 true,这样,在方法执行前我们的缓存就被清空了。可以确保缓存被清空。

清单 34. AccountService.java
1
2
3
4
5
@CacheEvict(value="accountCache",allEntries=true,beforeInvocation=true)
// 清空 accountCache 缓存
public void reload() {
   throw new RuntimeException();
}

注意上面的代码,我们在 @CacheEvict 注释中加了 beforeInvocation 属性,确保缓存被清空。

执行相同的测试代码

清单 35. 运行结果
1
2
real querying db...someone
real querying db...someone

这样,第一次和第三次都从数据库取数据了,缓存清空有效。

这样,第一次和第三次都从数据库取数据了,缓存清空有效。

非 public 方法问题

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

其他技巧

Dummy CacheManager 的配置和作用

有的时候,我们在代码迁移、调试或者部署的时候,恰好没有 cache 容器,比如 memcache 还不具备条件,h2db 还没有装好等,如果这个时候你想调试代码,岂不是要疯掉?这里有一个办法,在不具备缓存条件的时候,在不改代码的情况下,禁用缓存。

方法就是修改 spring*.xml 配置文件,设置一个找不到缓存就不做任何操作的标志位,如下

清单 36. Spring-cache-anno.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
< beans xmlns = "http://www.springframework.org/schema/beans"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
   xmlns:cache = "http://www.springframework.org/schema/cache"
   xmlns:p = "http://www.springframework.org/schema/p" 
   xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/cache
     http://www.springframework.org/schema/cache/spring-cache.xsd">
    
   < cache:annotation-driven />
 
   < bean id = "accountServiceBean" class = "cacheOfAnno.AccountService" />
 
   
   < bean id = "simpleCacheManager"
   class = "org.springframework.cache.support.SimpleCacheManager" >
     < property name = "caches" >
       < set >
         < bean
           class = "org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
           p:name = "default" />
       set >
     property >
   bean >
  
  
   < bean id = "cacheManager"
   class = "org.springframework.cache.support.CompositeCacheManager" >
     < property name = "cacheManagers" >
       < list >
         < ref bean = "simpleCacheManager" />
       list >
     property >
     < property name = "fallbackToNoOpCache" value = "true" />
   bean >
  
beans >

注意以前的 cacheManager 变为了 simpleCacheManager,且没有配置 accountCache 实例,后面的 cacheManager 的实例是一个 CompositeCacheManager,他利用了前面的 simpleCacheManager 进行查询,如果查询不到,则根据标志位 fallbackToNoOpCache 来判断是否不做任何缓存操作。

清单 37. 运行结果
1
2
3
real querying db...someone
real querying db...someone
real querying db...someone

可以看出,缓存失效。每次都查询数据库。因为我们没有配置它需要的 accountCache 实例。

如果将上面 xml 配置文件的 fallbackToNoOpCache 设置为 false,再次运行,则会得到

清单 38. 运行结果
1
2
3
4
5
Exception in thread "main" java.lang.IllegalArgumentException:
   Cannot find cache named [accountCache] for CacheableOperation
     [public cacheOfAnno.Account
     cacheOfAnno.AccountService.getAccountByName(java.lang.String)]
     caches=[accountCache] | condition='' | key=''

可见,在找不到 accountCache,且没有将 fallbackToNoOpCache 设置为 true 的情况下,系统会抛出异常。

小结

总之,注释驱动的 spring cache 能够极大的减少我们编写常见缓存的代码量,通过少量的注释标签和配置文件,即可达到使代码具备缓存的能力。且具备很好的灵活性和扩展性。但是我们也应该看到,spring cache 由于急于 spring AOP 技术,尤其是动态的 proxy 技术,导致其不能很好的支持方法的内部调用或者非 public 方法的缓存设置,当然这都是可以解决的问题,通过学习这个技术,我们能够认识到,AOP 技术的应用还是很广泛的,如果有兴趣,我相信你也能基于 AOP 实现自己的缓存方案。


你可能感兴趣的:(Spring,Cloud)