待缓存的POJO
/**
* Created by hgf on 16/6/22.
*/
public class Account {
private int id;
private String username;
public Account() {
}
public Account(int id, String username) {
this.id = id;
this.username = username;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", username='" + username + '\'' +
'}';
}
}
自己实现的简单cache
cache的功能:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* Created by hgf on 16/6/22.
*/
public class MyCache<T> {
private ConcurrentMap<String, T> cache = new ConcurrentHashMap<String, T>();
public T getValue(String key) {
return cache.get(key);
}
public void putOrUpdate(String key, T value) {
cache.put(key, value);
}
public void evictCache(String key) {
cache.remove(key);
}
public void evictAllInCache() {
cache.clear();
}
}
在CacheService中使用cahce:
/**
* Created by hgf on 16/6/22.
*/
public class MyCacheService {
private MyCache<Account> cache;
public MyCacheService() {
this.cache = new MyCache<Account>();
}
public Account getAccount(String cacheId){
Account account = cache.getValue(cacheId);
if(account!=null){
return account;
}
//get from DB or file
return null;
}
}
使用基于注解的spring cahce
使用基于注解的spring cache实现上面的cache
/**
* Created by hgf on 16/6/22.
*/
@Service
public class AccountService {
//假定此处是从数据库获取数据
public Account getFromDB(int id){
System.out.println("get "+ id + " from DB");
return new Account(id,"someone");
}
//如果命中cache,是不会执行getFromDB方法的
@Cacheable(value = "accountCache")
public String getById(int id){
return getFromDB(id);
}
}
spring xml配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
<cache:annotation-driven />
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"></bean>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="accountCache"></bean>
</set>
</property>
</bean>
<context:component-scan base-package="com.meituan"></context:component-scan>
</beans>
配置说明:
<cache:annotation-driven />
:用来添加spring的cache驱动,使得spring框架支持cache。这个配置项缺省使用了一个名字叫 cacheManager 的缓存管理器。所以下文中使用名为cacheManager的缓存管理器。- 使用
org.springframework.cache.support.SimpleCacheManager
实现混存管理器,并让spring加载。 - 配置
SimpleCacheManager
属性caches
。该属性是set类型。使用spring 已经实现的org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean
来保存(该类的实现也是使用java.util.concurrent.ConcurrentHashMap
类似于之前自定义的cache)。 p:name="accountCache"
设置缓存的名称,在注解中可以指定缓存名称,不知定就缓存在默认的缓存中。
默认的缓存管理器的名字只能是cacheManager,改为其他的名称不会被spring加载成功,导致找不到cacheManager缓存管理器错误。
启动测试程序:
public class App
{
public static void main( String[] args ){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:application.xml");
AccountService accountService = applicationContext.getBean(AccountService.class);
for(int i = 0; i < 10; i++){
accountService.getById(2);
}
}
}
输出:
get 2 from DB
只输出一行,其余的均命中cache,直接从cahce中读。
修改缓存
更新缓存
想更改缓存中的数据,需要更新缓存
在AccountService中添加方法:
//修改
@CachePut(value = "accountCache", key = "#account.getId()")
public Account updateCache(Account account){
System.out.println("do put account");
return account;
}
使用@CachePut
注解,可以确保方法被执行,同时方法的返回值也被记录到缓存中。 参数key
表示缓存(Cache)的健(key)与给定参数中的key的值相等才会将返回结果缓存到cache中。 假设此处key设置为key="1"
, 那么只有account=1的时候才会将返回结果缓存。
测试方法:
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:application.xml");
AccountService accountService = applicationContext.getBean(AccountService.class);
Account account = accountService.getById(2);
accountService.getById(2);
System.out.println("更新缓存-------------");
account.setUsername("xxxx");
accountService.updateCache(account);
account = accountService.getById(2);
System.out.println(account);
System.out.println("2-------------");
}
输出:
get 2 from DB
更新缓存-------------
do put account
Account{id=2, username='xxxx'}
2-------------
输出分析:
- 首次getById的时候,取数据库
- 并再次取,不会从数据库取
- 输出更新缓存-------------
- 改变account内容,不改变Id
- 更新缓存
- 再次getById直接从缓存中取
- 输出更新后的account内容
清理
例如需要强制刷删除某个缓存,或者清空缓存。
@CacheEvict(value = "accountCache",key = "#id")
public void removeOneCache(int id){
}
@CacheEvict(value = "accountCache",key = "#account.getId()")
public void removeOneCache(Account account){
}
@CacheEvict(value = "accountCache",allEntries = true)
public void clearAll(){
}
只需要使用@CacheEvict
注解,就能将缓存中能与注解参数key作为健值的项清除?。使用allEntries
表示删除所有数据项。
总结:在
@Cacheable
中,key参数作用为设置缓存的健;在@CacheEvict
,@CachePut
注解中,参数key作为匹配缓存中数据的健的条件。
多参数缓存
使用注解中的key参数,将多个参数拼接结果作为key。 例如:
@Cacheable(value = "accountCache",key = "#name.concat(#id)")
public Account getMultiKeyAccount(int id, String name){
System.out.println("get from DB");
return new Account(id,name);
}
不管是一个参数缓存还是在多参数缓存中,缓存中的key总是只能有一个,并且唯一,所有在多参数缓存中,只需要设定好多个参数组合成缓存系统中key的方式,就没有什么问题。
spring cache 原理
spring cache使用基于动态代理的AOP实现缓存机制。当客户端通过spring 获取某个对象foo,实际上获取的不是该对象,而是一个代理对象proxy_foo。当客户端调用foo的get方法时,实际上是在代理类中执行方法,并且能实现执行方法前后做一些操作。
在函数运行前,获取参数(作为key),运行后,获取执行结果(作为value);然后将它们存储在缓存中;下次访问时,先依据参数从缓存中查找,命中?直接返回,就不会执行函数了。
spring cache 中的坑
使用的限制
spring cache是基于动态生成的 proxy 代理机制来对方法的调用进行切面,这里关键点是对象的引用问题,如果对象的方法是内部调用(即 this 引用)而不是外部引用,则会导致 proxy 失效,那么我们的切面就失效,也就是说上面定义的各种注释包括 @Cacheable
、@CachePut
和 @CacheEvict
都会失效。 例如:
@Cacheable(value = "accountCache")
public Account getById(int id){
return getFromDB(id);
}
public Account getById2(int id){
return this.getById(id);
}
如果直接使用getById2
,那么是不能缓存返回值的。只有在getById2
上也添加@Cacheable(value = "accountCache")
注解,才能将返回的值添加到缓存。
总结:方法有内部调用带有Cache相关注解的方法失效 可以考虑基于AspectJ实现AOP解决该问题。因为动态代理机制是运行时代理,运行时客户端实际上调用的是代理类的方法,当出现内部调用时,直接调用原函数,而不是代理后的函数;而AspectJ代理是编译期的代理,客户端调用的是已经编译好的方法,cache也会在方法中实现,即使使用内部引用,也会对方法的整体调用,调用修改后的方法。
@CacheEvict 的可靠性问题
我们看到,@CacheEvict
注释有一个属性 beforeInvocation
,缺省为 false,即缺省情况下,都是在实际的方法执行完成后,才对缓存进行清空操作。期间如果执行方法出现异常,则会导致缓存清空不被执行。
例如,修改AccountService中clearAll方法:
@CacheEvict(value = "accountCache",allEntries = true)
public void clearAll(){
throw new RuntimeException();
}
测试方法
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:application.xml");
AccountService accountService = applicationContext.getBean(AccountService.class);
accountService.getById(1);
accountService.getById(1);
System.out.println("异常清理-------------");
try {
accountService.clearAll();
}
catch (Exception e){
//e.printStackTrace();
}
System.out.println("检查--------------------");
accountService.getById(1);
}
输出一行
get 1 from DB
只输出一行,说明最后一次也命中cache,异常导致清空缓存失败。