title: 学习笔记:cache 和spring cache 技术(1) author: Eric liu tags: [] categories:

  • hexo

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

本文通过一个简单的例子进行展开,通过对比我们原来的自定义缓存和 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;  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;  @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;  @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”),就实现了基本的缓存方案,是不是很强大?

学习笔记:cache 和spring cache 技术(1)相关推荐

  1. 深入浅出DPDK学习笔记(3)——— Cache和内存

    深入浅出DPDK学习笔记(3)--- Cache和内存 系统架构的演进 Cache系统简介 Cache的种类 TLB Cache Cache地址映射和变换 全关联型Cache 直接关联型Cache 组 ...

  2. 【Spring学习笔记 九】Spring声明式事务管理实现机制

    什么是事务?事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用,关乎数据准确性的地方我们一定要用到事务,防止业务逻辑出错. 什么是事务管理,事务管理对于企业应用而言至 ...

  3. 学习笔记:SpringCloud 微服务技术栈_实用篇①_基础知识

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 前言 学习视频链接 SpringCloud + RabbitMQ + Docker + Redis + 搜 ...

  4. VBNET学习笔记---MS VBnet数据库访问技术,概念,介绍,发展历程.

    VBNET学习笔记---MS VBnet数据库访问技术,概念,介绍,发展历程. 2013-02-20 1.数据库访问技术 a.JET与DAO JET(Joint Engine Technology)数 ...

  5. [原创]java WEB学习笔记107:Spring学习---AOP切面的优先级,重用切点表达式

    本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱 ...

  6. 学习笔记:SpringCloud 微服务技术栈_实用篇②_黑马旅游案例

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 前言 学习视频链接 SpringCloud + RabbitMQ + Docker + Redis + 搜 ...

  7. 学习笔记:SpringCloud 微服务技术栈_高级篇⑤_可靠消息服务

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 前言 学习视频链接 SpringCloud + RabbitMQ + Docker + Redis + 搜 ...

  8. Spring Boot基础学习笔记20:Spring Security入门

    文章目录 零.学习目标 一.Spring Security (一)Spring Security概述 (二)Spring Boot整合Spring Security实现的安全管理功能 二.基础环境搭建 ...

  9. Spring Boot基础学习笔记18:Spring Boot整合Redis缓存实现

    文章目录 零.学习目标 一.Spring Boot支持的缓存组件 二.基于注解的Redis缓存实现 (一)安装与启动Redis (二)创建Spring Boot项目 - RedisCacheDemo0 ...

最新文章

  1. eslint不报错 vue_【简易教程】基于Vue-cli使用eslint指南
  2. 20个Pandas数据实战案例,干货多多
  3. GIT Windows服务端搭建笔记
  4. 采用开源软件搭建WebGIS系统(6)数据格式
  5. Java SE中的Bootstrap CDI 2.0
  6. 1.Java 面试题整理(基础篇一)
  7. 【声学基础】概述——传播
  8. 第一册:lesson forty three。
  9. kali安装百度网盘客户端
  10. 使用vue-video-player播放视频 一个页面多个视频,并且只能唯一播放
  11. javascript之函数的定义传参
  12. Java实现qq邮件发送-支持群发
  13. #LeetCode15. 三数之和 @FDDLC
  14. 前端页面闪烁提示用户
  15. Lesson 52 A pretty carpet 漂亮的地毯
  16. Web渗透攻击之vega
  17. HTML 列表标签<ol><ul><dl>详解
  18. 李开复给大学生的第3封信:成功、自信、快乐
  19. 本地项目代码如何提交同步到gitee仓库
  20. 2019.08_【FSGAN】_Subject Agnostic Face Swapping and Reenactment 论文翻译

热门文章

  1. blender 简单 uv 贴图
  2. 8.Deep Interest Evolution Network for Click-Through Rate Prediction论文详解
  3. Linux系统时间同步:ntpdate--在内网中同步时间
  4. ubuntu安装KVM
  5. java数据结构堆_Java 数据结构-堆实现
  6. 地图不显示_地图不显示脚步,枪声没有方向标记,职业比赛有何不同?
  7. mysql如何防止插入重复数据_防止MySQL重复插入数据的三种方法
  8. 散列表(Hash Table)
  9. maven多项目打包报错---子模块相互依赖打包时所遇到的问题:依赖的程序包找不到 package xxx does not exist
  10. Interface Builder 和UIController的联系