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

本文通过一个简单的例子进行展开,通过对比我们原来的自定义缓存和 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<T> {

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 的配置文件来支持基于注释的缓存

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

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">

<context:component-scan base-package="com.rollenholt.spring.cache"/>

<context:annotation-config/>

<cache:annotation-driven/>

<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">

<property name="caches">

<set>

<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">

<property name="name" value="default"/>

</bean>

<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">

<property name="name" value="accountCache"/>

</bean>

</set>

</property>

</bean>

</beans>

注意这个 spring 配置文件有一个关键的支持缓存的配置项:

<cache:annotation-driven />

这个配置项缺省使用了一个名字叫 cacheManager 的缓存管理器,这个缓存管理器有一个 spring 的缺省实现,即 org.springframework.cache.support.SimpleCacheManager,这个缓存管理器实现了我们刚刚自定义的缓存管理器的逻辑,它需要配置一个属性 caches,即此缓存管理器管理的缓存集合,除了缺省的名字叫 default 的缓存,我们还自定义了一个名字叫 accountCache 的缓存,使用了缺省的内存存储方案 ConcurrentMapCacheFactoryBean,它是基于 java.util.concurrent.ConcurrentHashMap 的一个内存缓存实现方案。

然后我们编写测试程序:

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 AccountService2Test {

private AccountService2 accountService2;

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

@Before

public void setUp() throws Exception {

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext2.xml");

accountService2 = context.getBean("accountService2", AccountService2.class);

}

@Test

public void testInject(){

assertNotNull(accountService2);

}

@Test

public void testGetAccountByName() throws Exception {

logger.info("first query...");

accountService2.getAccountByName("accountName");

logger.info("second query...");

accountService2.getAccountByName("accountName");

}

}

上面的测试代码主要进行了两次查询,第一次应该会查询数据库,第二次应该返回缓存,不再查数据库,我们执行一下,看看结果

01:10:32.435 [main] INFO  c.r.s.c.example2.AccountService2Test - first query...

01:10:32.456 [main] INFO  c.r.s.cache.example2.AccountService2 - real querying account... accountName

01:10:32.457 [main] INFO  c.r.s.cache.example2.AccountService2 - real querying db... accountName

01:10:32.458 [main] INFO  c.r.s.c.example2.AccountService2Test - second query...

可以看出我们设置的基于注释的缓存起作用了,而在 AccountService.java 的代码中,我们没有看到任何的缓存逻辑代码,只有一行注释:@Cacheable(value=”accountCache”),就实现了基本的缓存方案,是不是很强大?

强大的Spring缓存技术(上)的更多相关文章

  1. 强大的Spring缓存技术(下)

    基本原理 一句话介绍就是Spring AOP的动态代理技术. 如果读者对Spring AOP不熟悉的话,可以去看看官方文档 扩展性 直到现在,我们已经学会了如何使用开箱即用的 spring cache ...

  2. 强大的Spring缓存技术(中)

    好,到目前为止,我们的 spring cache 缓存程序已经运行成功了,但是还不完美,因为还缺少一个重要的缓存管理逻辑:清空缓存. 当账号数据发生变更,那么必须要清空某个缓存,另外还需要定期的清空所 ...

  3. 利用Spring.Net技术打造可切换的分布式缓存读写类

    利用Spring.Net技术打造可切换的Memcached分布式缓存读写类 Memcached是一个高性能的分布式内存对象缓存系统,因为工作在内存,读写速率比数据库高的不是一般的多,和Radis一样具 ...

  4. Spring Cache缓存技术的介绍

    缓存用于提升系统的性能,特别适用于一些对资源需求比较高的操作.本文介绍如何基于spring boot cache技术,使用caffeine作为具体的缓存实现,对操作的结果进行缓存. demo场景 本d ...

  5. 【Java EE 学习 78 上】【数据采集系统第十天】【Service使用Spring缓存模块】

    一.需求分析 调查问卷中或许每一个单击动作都会引发大量的数据库访问,特别是在参与调查的过程中,只是单击“上一页”或者“下一页”的按钮就会引发大量的查询,必须对这种问题进行优化才行.使用缓存策略进行查询 ...

  6. 分布式缓存技术redis学习系列(一)——redis简介以及linux上的安装

    redis简介 redis是NoSQL(No Only SQL,非关系型数据库)的一种,NoSQL是以Key-Value的形式存储数据.当前主流的分布式缓存技术有redis,memcached,ssd ...

  7. 分布式缓存技术redis学习(一)——redis简介以及linux上的安装

    redis简介 redis是NoSQL(No Only SQL,非关系型数据库)的一种,NoSQL是以Key-Value的形式存储数据.当前主流的分布式缓存技术有redis,memcached,ssd ...

  8. 分布式缓存技术redis系列(一)——redis简介以及linux上的安装

    redis简介 redis是NoSQL(No Only SQL,非关系型数据库)的一种,NoSQL是以Key-Value的形式存储数据.当前主流的分布式缓存技术有redis,memcached,ssd ...

  9. 《现代体系结构上的UNIX系统:内核程序员的对称多处理和缓存技术(修订版)》——2.4 双路组相联高速缓存...

    本节书摘来自异步社区<现代体系结构上的UNIX系统:内核程序员的对称多处理和缓存技术(修订版)>一书中的第2章,第2.4节,作者:[美]Curt Schimmel著,更多章节内容可以访问云 ...

随机推荐

  1. Python面试题

    1.Python装饰器 详情 2.设置多个Python项目使用不同版本的Python和第三方库 使用PyEnv 详情 3.PEP8 详情 4.参数传递 按引用传递 5.列表解析,字典解析 详情 6.列 ...

  2. 在linux下写一只优雅的爬虫---优雅的获取沈航所有学生的个人信息

    一:ubuntu下安装python集成环境pycharm以及免费激活 安装 首先去下载最新的pycharm 2016.2.3,进行安装.可以直接在官网下载.选择自己所对应的版本 PyCharm 的激活 ...

  3. char wchar 互转 多字符 宽字符 的N种方式

    1:  用 CString  如果没有mfc 可以用 ATL 中的 CString  #include <atlstr.h>     CStringA v1 = "111&quo ...

  4. NIO 连接

    http://www.iteye.com/magazines/132-Java-NIO

  5. 编译FFmpeg成一个SO库<转>

    转帖地址:http://www.ihubin.com/blog/android-ffmpeg-demo-3/ ============================================= ...

  6. ASP.NET Misconfiguration: Missing Error Handling

    Abstract: An ASP .NET application must enable custom error pages in order to prevent attackers from ...

  7. (int)、int.Parse()、int.TryParse()和Convert.ToInt32()的区别

    C#中(int).int.Parse().int.TryParse()和Convert.ToInt32()的区别   原文链接:http://www.cnblogs.com/leolis/p/3968 ...

  8. Rethrowing exceptions and preserving the full call stack trace

    refer:http://weblogs.asp.net/fmarguerie/archive/2008/01/02/rethrowing-exceptions-and-preserving-the- ...

  9. swt controls里的控件list

    swt controls里的控件list,怎么显示滚动条,并且滚动条自动移动到最下边时,显示最新内容 package com.jokul; import org.eclipse.swt.widgets ...

  10. wcf,jquery,post,跨域

    参照了网上的很多资料,vs2012 项目是wcf服务. .demo地址http://files.cnblogs.com/files/dswyzx/WcfServiceDemoa.rar