缓存是实际工作中非常常用的一种提高性能的方法, 我们会在许多场景下来使用缓存。

本文通过一个简单的例子进行展开,通过对比我们原来的自定义缓存和 spring 的基于注释的 cache 配置方法,展现了 spring cache 的强大之处,然后介绍了其基本的原理,扩展点和使用场景的限制。通过阅读本文,你应该可以短时间内掌握 spring 带来的强大缓存技术,在很少的配置下即可给既有代码提供缓存能力。

概述
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 和自定义缓存管理者,具有相当的灵活性和扩展性
本文将针对上述特点对 Spring cache 进行详细的介绍,主要通过一个简单的例子和原理介绍展开,然后我们将一起看一个比较实际的缓存例子,最后会介绍 spring cache 的使用限制和注意事项。好吧,让我们开始吧

我们以前如何自己实现缓存的呢
这里先展示一个完全自定义的缓存实现,即不用任何第三方的组件来实现某种对象的内存缓存。

场景如下:

对一个账号查询方法做缓存,以账号名称为 key,账号对象为 value,当以相同的账号名称查询账号的时候,直接从缓存中返回结果,否则更新缓存。账号查询服务还支持 reload 缓存(即清空缓存)

首先定义一个实体类:账号类,具备基本的 id 和 name 属性,且具备 getter 和 setter 方法

public class Account {

private int id;
private String name;

public Account(String name) {
    this.name = name;
}
public int getId() {
    return id;
}
public void setId(int id) {
    this.id = id;
}
public String getName() {
    return name;
}
public void setName(String name) {
    this.name = name;
}


然后定义一个缓存管理器,这个管理器负责实现缓存逻辑,支持对象的增加、修改和删除,支持值对象的泛型。如下:

import com.google.common.collect.Maps;
import java.util.Map;
/** 
 * @author wenchao.ren 
 * 2015/1/5. 
 */ 
 public class CacheContext {
private Map<String, T> cache = Maps.newConcurrentMap();

public T get(String key){
    return  cache.get(key);
}

public void addOrUpdateCache(String key,T value) {
    cache.put(key, value);
}

// 根据 key 来删除缓存中的一条记录
public void evictCache(String key) {
    if(cache.containsKey(key)) {
        cache.remove(key);
    }
}

// 清空缓存中的所有记录
public void evictCache() {
    cache.clear();
}


好,现在我们有了实体类和一个缓存管理器,还需要一个提供账号查询的服务类,此服务类使用缓存管理器来支持账号查询缓存,如下:

import com.google.common.base.Optional; 
 import org.slf4j.Logger; 
 import org.slf4j.LoggerFactory; 
 import org.springframework.stereotype.Service;import javax.annotation.Resource;
/** 
 * @author wenchao.ren 
 * 2015/1/5. 
 */ 
 @Service 
 public class AccountService1 {
private final Logger logger = LoggerFactory.getLogger(AccountService1.class);

@Resource
private CacheContext<Account> accountCacheContext;

public Account getAccountByName(String accountName) {
    Account result = accountCacheContext.get(accountName);
    if (result != null) {
        logger.info("get from cache... {}", accountName);
        return result;
    }

    Optional<Account> accountOptional = getFromDB(accountName);
    if (!accountOptional.isPresent()) {
        throw new IllegalStateException(String.format("can not find account by account name : [%s]", accountName));
    }

    Account account = accountOptional.get();
    accountCacheContext.addOrUpdateCache(accountName, account);
    return account;
}

public void reload() {
    accountCacheContext.evictCache();
}

private Optional<Account> getFromDB(String accountName) {
    logger.info("real querying db... {}", accountName);
    //Todo query data from database
    return Optional.fromNullable(new Account(accountName));
}


现在我们开始写一个测试类,用于测试刚才的缓存是否有效

import org.junit.Before; 
 import org.junit.Test; 
 import org.slf4j.Logger; 
 import org.slf4j.LoggerFactory; 
 import org.springframework.context.support.ClassPathXmlApplicationContext;import static org.junit.Assert.*;
public class AccountService1Test {
private AccountService1 accountService1;

private final Logger logger = LoggerFactory.getLogger(AccountService1Test.class);

@Before
public void setUp() throws Exception {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext1.xml");
    accountService1 = context.getBean("accountService1", AccountService1.class);
}

@Test
public void testInject(){
    assertNotNull(accountService1);
}

@Test
public void testGetAccountByName() throws Exception {
    accountService1.getAccountByName("accountName");
    accountService1.getAccountByName("accountName");

    accountService1.reload();
    logger.info("after reload ....");

    accountService1.getAccountByName("accountName");
    accountService1.getAccountByName("accountName");
}


按照分析,执行结果应该是:首先从数据库查询,然后直接返回缓存中的结果,重置缓存后,应该先从数据库查询,然后返回缓存中的结果. 查看程序运行的日志如下:

00:53:17.166 [main] INFO c.r.s.cache.example1.AccountService - real querying db… accountName 
 00:53:17.168 [main] INFO c.r.s.cache.example1.AccountService - get from cache… accountName 
 00:53:17.168 [main] INFO c.r.s.c.example1.AccountServiceTest - after reload …. 
 00:53:17.168 [main] INFO c.r.s.cache.example1.AccountService - real querying db… accountName 
 00:53:17.169 [main] INFO c.r.s.cache.example1.AccountService - get from cache… accountName


可以看出我们的缓存起效了,但是这种自定义的缓存方案有如下劣势:

缓存代码和业务代码耦合度太高,如上面的例子,AccountService 中的 getAccountByName()方法中有了太多缓存的逻辑,不便于维护和变更
不灵活,这种缓存方案不支持按照某种条件的缓存,比如只有某种类型的账号才需要缓存,这种需求会导致代码的变更
缓存的存储这块写的比较死,不能灵活的切换为使用第三方的缓存模块
如果你的代码中有上述代码的影子,那么你可以考虑按照下面的介绍来优化一下你的代码结构了,也可以说是简化,你会发现,你的代码会变得优雅的多!

Spring cache是如何做的呢
我们对AccountService1 进行修改,创建AccountService2:

import com.google.common.base.Optional; 
 import com.rollenholt.spring.cache.example1.Account; 
 import org.slf4j.Logger; 
 import org.slf4j.LoggerFactory; 
 import org.springframework.cache.annotation.Cacheable; 
 import org.springframework.stereotype.Service;/** 
 * @author wenchao.ren 
 * 2015/1/5. 
 */ 
 @Service 
 public class AccountService2 {
private final Logger logger = LoggerFactory.getLogger(AccountService2.class);

// 使用了一个缓存名叫 accountCache
@Cacheable(value="accountCache")
public Account getAccountByName(String accountName) {

    // 方法内部实现不考虑缓存逻辑,直接实现业务
    logger.info("real querying account... {}", accountName);
    Optional<Account> accountOptional = getFromDB(accountName);
    if (!accountOptional.isPresent()) {
        throw new IllegalStateException(String.format("can not find account by account name : [%s]", accountName));
    }

    return accountOptional.get();
}

private Optional<Account> getFromDB(String accountName) {
    logger.info("real querying db... {}", accountName);
    //Todo query data from database
    return Optional.fromNullable(new Account(accountName));
}


我们注意到在上面的代码中有一行:

@Cacheable(value="accountCache")

这个注释的意思是,当调用这个方法的时候,会从一个名叫 accountCache 的缓存中查询,如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的 key 就是参数 accountName,value 就是 Account 对象。“accountCache”缓存是在 spring*.xml 中定义的名称。我们还需要一个 spring 的配置文件来支持基于注释的缓存