从头认识Spring Cache

概述:

Spring 3.1 引入了激动人心的基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。

Spring Cache特点:

Spring 的缓存技术还具备相当的灵活性,不仅能够使用 SpEL(Spring Expression Language)来定义缓存的 key 和各种 condition,还提供开箱即用的缓存临时存储方案,也支持和主流的缓存方式:例如 EHCache 等集成。

特点总结如下:

  • 通过少量的配置 annotation 注释即可使得既有代码支持缓存
  • 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存
  • 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
  • 支持 AspectJ,并通过其实现任何方法的缓存支持
  • 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性
本文将通过实际的例子来解析Spring Cache和自定义缓存以及第三方缓存组件的区别,最后将会详细的介绍Spring Cache相关注解。
1.在不使用第三方缓存组件的情况下,自定义缓存实现
场景:
通过图书ID查询图书信息的方法做缓存,以图书ID为 key,图书名称为 value,当以相同的图书ID查询图书信息的时候,直接从缓存中返回结果,则不经过数据库查询,反之则查询数据库更新缓存。当然还支持 reload 缓存

具体的实体类代码如下:

package com.my.data.cache.dao;

import java.io.Serializable;

/**
 * 图书领域对象
 * @author wbw
 *
 */
public class Book implements Serializable {

	/**
	 * 序列化版本号
	 */
	private static final long serialVersionUID = -2710076757833997658L;
    /**
     * 图书ID
     */
	private String bookId;
	/**
	 * 图书名称
	 */
	private String bookName;
	/**
	 * @return the 图书ID
	 */
	public String getBookId() {
		return bookId;
	}
	/**
	 * @param 图书ID the bookId to set
	 */
	public void setBookId(String bookId) {
		this.bookId = bookId;
	}
	/**
	 * @return the 图书名称
	 */
	public String getBookName() {
		return bookName;
	}
	/**
	 * @param 图书名称 the bookName to set
	 */
	public void setBookName(String bookName) {
		this.bookName = bookName;
	}
	
	
}
然后定义缓存管理器,主要用于处理新增缓存对象、删除缓存对象、更新缓存对象、查询缓存对象、清空缓存等操作

具体代码如下:

package com.my.cacheManage;

import java.util.concurrent.ConcurrentHashMap;

/**
 * 自定义缓存控制器 、回调接口、监听器
 * 缓存代码和业务逻辑耦合度高
 * 不灵活
 * 缓存存储写的很死,不能灵活的与第三方缓存插件相结合
 * 
 * @author wangbowen
 *
 * @param <T>
 */
public class CacheManagerHandler<T> {
    //ConcurrentHashMap jdk1.5 线程安全 分段锁
	private  ConcurrentHashMap<String,T> cache = new ConcurrentHashMap<String,T>();
	
	/**
	 * 根据key获取缓存对象
	 * @param key 缓存对象名
	 * @return  缓存对象
	 */
	public T getValue(Object key){
		return cache.get(key);
	}
	/**
	 * 新增或更新 
	 * @param key
	 * @param value
	 */
	public void  put(String key,T value){
		cache.put(key, value);
	}
	
   /**
    * 新增缓存对象
    * @param key 缓存对象名称
    * @param value 缓存对象
    * @param time 缓存时间(单位:毫秒) -1表示时间无限制
    * @param callBack
    */
	public void  put(String key,T value,long time,CacheCallBack callBack){
		cache.put(key, value);
		if(time!=-1){
			//启动监听
			new CacheListener(key,time,callBack);
		}
	}
	/**
	 * 根据key删除缓存中的一条记录
	 * @param key
	 */
	public void evictCache(String key){
		if(cache.containsKey(key)){
			cache.remove(key);
		}
	}
	/**
	 * 获取缓存大小
	 * @return
	 */
	public int getCacheSize(){
		return  cache.size();
	}
	/**
	 * 清空缓存
	 */
	public void evictCache(){
		cache.clear();
	}
}
定义图书服务接口

package com.my.data.cache.service;

import com.my.data.cache.domain.Book;
/**
 * 图书服务接口
 * @author wbw
 *
 */
public interface BookService {
    /**
     * 根据图形ID查询图书
     * @param bookId 图书ID
     * @return 图书信息
     */
	public Book findBookById(String bookId);

	
}
图书服务接口实现类
package com.my.service.impl;

import com.my.cacheManage.CacheManagerHandler;
import com.my.domain.Account;
import com.my.service.MyAccountService;
/**
 * 实现类
 * @author wangbowen
 *
 */
public class BookServiceImpl implements BookService {
    /**
     * 缓存控制器
     */
	private CacheManagerHandler<Book> myCacheManager;
	
	/**
	 * 初始化
	 */
	public BookServiceImpl(){
		myCacheManager = new CacheManagerHandler<Book>();
	}	
	 
	@Override
	public Book getBookByID(String id) {
		Account result = null;
		if(id!=null){
			//先查询缓存中是否有,直接返回
			 result = myCacheManager.getValue(id);
			if(result!=null){
			System.out.println("从缓存查询到:"+id);	
			return result;
			}else{
			 result = getFormDB(id);
			if(result!=null){//将数据查询出来的结果更新到缓存集合中
				myCacheManager.put(id, result);
				return result;
			}else{
				System.out.println("数据库为查询到"+id+"账户信息");
			}
			}
		}
		return null;
	}
    /**
     * 从数据库中查询
     * @param name
     * @return
     */
	private Book getFormDB(String id) {
		System.out.println("从数据库中查询:"+id);
		return new Book(id);
	}


}
运行执行:

package com.my.cache.test;

import com.my.service.MyAccountService;
import com.my.service.impl.MyAccountServiceImpl;

/**
 * 测试
 * @author wbw
 *
 */
public class MyCacheTest {
public static void main(String[] args) {
     BookService s = new BookServiceImpl(); 
    s.getBookByid("1");// 第一次查询,应该是数据库查询
    s.getBookByid("1");// 第二次查询,应该直接从缓存返回
   

}
}
控制台输出信息:

从数据库中查询:1
从缓存查询到:1
虽然自定义缓存能实现缓存的基本功能,但是这种自定义缓存存在很大的缺点:

1.缓存代码和实际业务耦合度高,不便于后期修改。

2.不灵活,需要按照某种缓存规则进行缓存,不能根据不同的条件进行缓存

3.兼容性太差,不能与第三方缓存组件兼容。

Spring Cache基于注解的实现方式:

领域对象:


package com.my.data.cache.domain;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="book")
public class Book implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = -6283522837937163003L;
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", nullable = true)
	private Integer id;
    private String isbn;
    private String title;

    public Book(String isbn, String title) {
        this.isbn = isbn;
        this.title = title;
    }
    public Book() {
	}
	public Book(int id, String isbn, String title) {
		super();
		this.id = id;
		this.isbn = isbn;
		this.title = title;
	}


	public int getId() {
		return id;
	}




	public void setId(int id) {
		this.id = id;
	}




	public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @Override
    public String toString() {
        return "Book{" + "isbn='" + isbn + '\'' + ", title='" + title + '\'' + '}';
    }
}
图书服务接口

package com.my.data.cache.service;

import java.util.List;

import com.my.data.cache.domain.Book;

public interface BookService {

	public Book findById(Integer bid);

	public List<Book> findBookAll();

	public void insertBook(Book book);

	public Book findByTitle(String title);
	
	public int countBook();
	
	public void modifyBook(Book book);
	
	 public Book findByIsbn(String isbn);
	
	
}
图书服务接口,这里 ORM框架使用的是Spring Data 通过基于注解的查询方式能更简便的与数据交互

package com.my.data.cache.service.impl;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.my.data.cache.annotation.LogAnnotation;
import com.my.data.cache.domain.Book;
import com.my.data.cache.exception.MyException;
import com.my.data.cache.repository.BookRepository;
import com.my.data.cache.service.BookService;

@Service
@Transactional
public class BookServiceImpl  implements BookService{
	private static final Logger log = LoggerFactory.getLogger(BookServiceImpl.class);
	@Autowired
	private BookRepository bookRepository;
	
	
	//将缓存保存进andCache,并使用参数中的bid加上一个字符串(这里使用方法名称)作为缓存的key
	@Cacheable(value="andCache",key="#bid+'findById'")
	@LogAnnotation(value="通过Id查询Book")
	public Book findById(Integer bid) {
		this.simulateSlowService();
		return bookRepository.findById(bid);
	}
	
	@Override
	public List<Book> findBookAll() {
		return bookRepository.findBookAll();
	}
    
	//将缓存保存进andCache,并当参数title的长度小于32时才保存进缓存,默认使用参数值及类型作为缓存的key 
	@Cacheable(value="andCache",condition="#title.length >5") 
	public Book findByTitle(String title){
		return null;
		
	}
	/**
	 * 新增
	 * @param book
	 * @return
	 */
	public void insertBook(Book book){
		bookRepository.save(book);
	}
	
	@Override
	public int countBook() {
		return bookRepository.countBook();
	}  
	//清除掉指定key中的缓存
	@CacheEvict(value="andCache",key="#book.id + 'findById'")
	public void modifyBook(Book book) {
		log.info("清除指定缓存"+book.getId()+"findById");
		bookRepository.save(book);
	}
	
	//清除掉全部缓存  
	@CacheEvict(value="andCache",allEntries=true,beforeInvocation=true)  
	public void ReservedBook() {  
		log.info("清除全部的缓存");  
	}

	// Don't do this at home
    private void simulateSlowService() {
        try {
            long time = 5000L;
            Thread.sleep(time);
        } catch (InterruptedException e) {
            throw new MyException("程序出错", e);
        }
    }

	@Override
	public Book findByIsbn(String isbn) {
		return bookRepository.findByIsbn(isbn);
	}
}
BookRepository接口

package com.my.data.cache.repository;

import java.util.List;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import com.my.data.cache.dao.CommonRepository;
import com.my.data.cache.domain.Book;
/**
 * 接口
 * @author wbw
 *
 */
public interface BookRepository extends CrudRepository<Book, Integer> {

  @Query("select b from Book b  where 1=1")
  public   List<Book> findBookAll();
  /**
   * 根据isbn查询
   * @param name
   * @return
   */
  @Query("select  b from Book b where b.id =?1")
  public  Book findById(Integer bid);
   /**
    * 统计size  
    * @return
    */
  @Query("select count(*) from Book where 1=1 ")
  public int countBook();
  /**
   * 根据命名规范查询 findBy+属性
   * @param isbn
   * @return
   */
  public Book findByIsbn(String isbn);

}

Controller 代码:

package com.my.data.cache.controller;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.my.data.cache.domain.Book;
import com.my.data.cache.service.BookService;

@RestController 
@RequestMapping("/book")  
public class BookController {
	private static final Logger log = LoggerFactory.getLogger(BookController.class);
    
	@Autowired
    private BookService bookService;
	
    
    @RequestMapping("/{id}")
	public @ResponseBody Book index(@PathVariable("id") Integer id){
		Book b = bookService.findById(id);
	    log.info(b.getIsbn()+"------>"+b.getTitle());
  		return b;
	}
    
    @RequestMapping(value = "/list", method = RequestMethod.GET)
    public  @ResponseBody List<Book> list(){
    	List<Book> b = bookService.findBookAll();
		return  b;
    	
    }
    @RequestMapping(value = "/add")
    public String  insertBook(){
    	Book b = new Book();
    	b.setId(4);
    	b.setIsbn("1111");
    	b.setTitle("相信自己");
    	bookService.insertBook(b);
    	return "success";
    	
    }    
    /**
     * 更新
     * @return
     */
    @RequestMapping(value = "/update")
    public String update(){
    	Book b = new Book();
    	b.setId(1);
    	b.setIsbn("1");
    	b.setTitle("爱的力量");
    	bookService.modifyBook(b);
    	return "success";
    }
}

测试-------这里我们采用Spring Boot 启动服务的方式,

package com.my.data.cache;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

import com.my.data.cache.domain.Book;
import com.my.data.cache.service.BookService;


/**
 * 
 * 启动器
 *
 */
@SpringBootApplication
@EnableCaching//扫描cahce注解
public class Application1  implements CommandLineRunner{
	
	@Autowired
    private BookService bookService;
	@Override
	public void run(String... args) throws Exception {
		Book b1 = bookService.findByIsbn("1");
		Book b2 = bookService.findByIsbn("2");
		Book b3 = bookService.findById(3);
	    System.out.println(b1);
        System.out.println(b2);
        System.out.println(b3);
		
	}
	public static void main(String[] args) {
		 SpringApplication.run(Application1.class,args);
	}

}
第一次访问indexI()方法,可以从下面的控制台信息看出:发出了sql语句从数据库查询数据,然后将查询的数据缓存,下次有相同条件访问相同的请求则直接从缓存中取数据

Hibernate: select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.title as title3_0_ from book book0_ where book0_.isbn=?
Hibernate: select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.title as title3_0_ from book book0_ where book0_.isbn=?
Hibernate: select book0_.id as id1_0_, book0_.isbn as isbn2_0_, book0_.title as title3_0_ from book book0_ where book0_.id=?
Book{isbn='1', title='爱的力量'}
2016-03-10 11:22:40.107  INFO 8132 --- [  restartedMain] com.my.data.cache.Application1           : Started Application1 in 42.661 seconds (JVM running for 46.34)
第二次访问indexI()方法,则直接从缓存中获取数据,不在查询数据库

Book{isbn='1', title='爱的力量'}
2016-03-10 11:27:43.936  INFO 6436 --- [  restartedMain] com.my.data.cache.Application1           : Started Application1 in 19.363 seconds (JVM running for 20.063)
从上面Spring Cahce的示例代码可以看出,Spring Cache 通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果,并没有太多的缓存业务逻辑代码。
Spring Cache 部分注解介绍:

  • @Cacheable 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存
  • @CachePut 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用
  • @CacheEvict 主要针对方法配置,能够根据一定的条件对缓存进行清空,清除全部的缓存@CacheEvict(value="缓存名字",allEntries=true,beforeInvocation=true)
Spring Cache实现原理:
通过 Spring AOP动态代理技术
Spring Cache的扩展性:
在现实的业务中总是很复杂,当你的用户量上去或者性能跟不上,总需要进行扩展,这个时候你或许对其提供的内存缓存不满意了,因为其不支持高可用性,也不具备持久化数据能力,这个时候,你就需要自定义你的缓存方案了。
此部分引用别人的代码示例:

首先,我们需要提供一个 CacheManager 接口的实现,这个接口告诉 spring 有哪些 cache 实例,spring 会根据 cache 的名字查找 cache 的实例。另外还需要自己实现 Cache 接口,Cache 接口负责实际的缓存逻辑,例如增加键值对、存储、查询和清空等。

利用 Cache 接口,我们可以对接任何第三方的缓存系统,例如 EHCacheOSCache,甚至一些内存数据库例如 memcache 或者redis 等。下面我举一个简单的例子说明如何做。

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 类的实例。

下面是MyCache的定义:

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 配置文件告诉它

<cache:annotation-driven /> 

 <bean id="cacheManager" class="com.rollenholt.spring.cache.MyCacheManager">
     <property name="caches"> 
       <set> 
         <bean 
           class="com.rollenholt.spring.cache.MyCache"
           p:name="accountCache" /> 
       </set> 
     </property> 
   </bean> 
测试:
Account account = accountService.getAccountByName("someone"); 
logger.info("passwd={}", account.getPassword()); 
account = accountService.getAccountByName("someone"); 
logger.info("passwd={}", account.getPassword()); 

Spring Cache的注意和限制

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

上面介绍过 spring cache 的原理,即它是基于动态生成的 proxy 代理机制来对方法的调用进行切面,这里关键点是对象的引用问题.

如果对象的方法是内部调用(即 this 引用)而不是外部引用,则会导致 proxy 失效,那么我们的切面就失效,也就是说上面定义的各种注释包括 @Cacheable、@CachePut 和 @CacheEvict 都会失效,我们来演示一下。


public Account getAccountByName2(String accountName) { 
   return this.getAccountByName(accountName); 
 } 

 @Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache 
 public Account getAccountByName(String accountName) { 
   // 方法内部实现不考虑缓存逻辑,直接实现业务
   return getFromDB(accountName); 
 }

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

要避免这个问题,就是要避免对缓存方法的内部调用,或者避免使用基于 proxy 的 AOP 模式,可以使用基于 aspectJ 的 AOP 模式来解决这个问题。

@CacheEvict 的可靠性问题

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

// 清空 accountCache 缓存
 @CacheEvict(value="accountCache",allEntries=true)
 public void reload() { 
   throw new RuntimeException(); 
 }
 
测试:
 accountService.getAccountByName("someone"); 
   accountService.getAccountByName("someone"); 
   try { 
     accountService.reload(); 
   } catch (Exception e) { 
    //...
   } 
   accountService.getAccountByName("someone"); 

注意上面的代码,我们在 reload 的时候抛出了运行期异常,这会导致清空缓存失败。上面的测试代码先查询了两次,然后 reload,然后再查询一次,结果应该是只有第一次查询走了数据库,其他两次查询都从缓存,第三次也走缓存因为 reload 失败了。

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












你可能感兴趣的:(从头认识Spring Cache)