Spring:Spring Cache

一 缓存概述

1 缓存的概念

缓存作为系统架构中提升性能的一种重要支撑技术,在企业级应用中的地位越来越突显。缓存技术日新月异,可选的缓存套件也琳琅满目,让人迎接不暇。
可以将缓存定义为一种存储机制,它将数据保存子某个地方,并以一种更快的方式提供服务。较为常见的一种情况是在应用中使用缓存机制,以避免方法的多次执行,从而客服性能缺陷,也可减少应用服务器或者数据库的压力。缓存的策略有很多种,在应用系统中可根据实际情况选择,通常会吧一些静态数据或者变化频率不高的数据放到缓存中,如:配置参数、字典表等。而有些场景可能要寻求替代方案,比如提升全文检索的速度,在复杂的场景下建议使用搜索引擎,如solr或elasticSearch。
通常在web应用开发中,不同层级对应的缓存要求和缓存策略都有所不同。

下面介绍下缓存的两个比较重要的概念:

缓存命中率

即从缓存中读取数据的次数与总读取次数的比例。一般来说,命中率越高越好。

[1]命中率 = 从缓存中读取的次数/总读取次数(从缓存中读取的次数 + 从数据源读取数据的次数)

[2]Miss率 = 没有从缓存中读取的次数/(从缓存中读取的次数 + 从数据源读取数据的次数)

这是一个非常重要的监控指标,如果要做缓存,就一定要监控这个指标,来看缓存是否工作良好。如果命中率低下,缓存将毫无意义。

过期策略

即如果缓存满了,从缓存中移除数据的策略。常见的由LFU、LRU、FIFO

① FIFO(First In First Out):先进先出策略,当缓存达到阈值时先移除最先缓存的数据。

② LRU(Least Recently Used):最久未使用策略,当缓存达到阈值时先移除使用时间间距最长的数据。

③ LFU(Least Frequently Used):最近最少使用策略,当缓存达到阈值时先移除一定时间段内使用次数最少的数据。

④ TTL(Time To Live):存活期,设置缓存数据在缓存中存放的时间,当时间到达时自动移除。

⑤ TTI(Time To Idle):空闲期,当一个数据的访问间隔达到制定的时间标准时自动移除。

从Spring 3.1开始,提供了类似于@Transactional事物注解的缓存注解,且提供了Cache层的抽象。此外,JSR-107也从Spring 4.0开始得到全面支持。Spring提供了一种可以自方法级别进行缓存的缓存抽象。通过使用AOP对方法进行织入,如果已经为特定方法入参执行过该方法,那么不必执行实际方法就可以返回被缓存的结果。为了启动AOP缓存功能,需要使用缓存注解对类中的相关方法进行标记,以便Spring为其生成具备缓存功能的代理类。需要注意的是,Spring Cache仅提供了一种抽象而未提供具体实现。在此之前,我们一般会自己使用AOP来做一定程度的封装实现,使用Spring Cache带来的好处如下:

① 支持开箱即用(Out-Of-The-Box),并提供基本的Cache抽象,方便切换各种底层Cache。

② 类似于Spring提供的数据库事物管理,通过Cache注解即可实现缓存逻辑透明化,让开发者关注业务逻辑。

③ 当事物回滚时,缓存也会自动回滚。

④ 支持比较复杂的缓存逻辑。

⑤ 提供缓存变成的一致性抽象,方便代码维护。

JSR-107是Java缓存规范JCache API对Java对象缓存进行了标准化,方便高效开发,摆脱了实现缓存有效期、互斥、假脱机和缓存一致性等负担。该规范提供了API、RI和TCK。有兴趣的可以访问:https://jcp.org/en/jsr/detail?id=107 自行了解。

需要注意的是,Spring Cache并不针对多进程的应用环境进行专门的处理,也就是说,当应用程序处于分布式或者集群环境下时,需要针对具体的缓存进行响应的配置。另外,在Spring Cache抽象的操作中没有锁的概念,当多线程编发操作同一个缓存项时,将可能得到过期的书。有些缓存实现提供了锁的功能,如果需要考虑上面的场景,则可以详细了解具体缓存的一些相关特性,如EhCache提供了针对缓存元素key的读、写锁。

2 使用Spring Cache

    @Cacheable(cacheNames = "test")
    public Object test(String id){
        return getObject(id);
    }

    private Object getObject(String id) {
        System.out.println("调用数据查询方法,id为:"+id);
        return new Object();
    }

在service的实现方法上添加 @Cacheable 注解在方法内部不考虑缓存逻辑,直接实现业务。

当调用这个方法的时候,会先从users缓存中查询匹配的缓存对象,如果存在则直接返回;如果不存在,则执行方法体内的逻辑,并将返回值放入名为 test 的缓存段中。对应缓存的key为id的值,value就是方法返回的Object对象,缓存名称需要在applicationContext.xml中定义。

现在还需要一个Spring配置文件来支持基于注解的缓存:

<?xml version="1.0" encoding="UTF-8" ?>
	<cache:annotation-driven/>
	<bean id="xxxServiceBean" class="xxx.xxxService"/>
	<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
		<property name = caches>
			<set>
				<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
				<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="test"/>
			</set>
		</property>
	</bean>

Spring通过<cache:annotation-driven/>即可启用基于注解的缓存驱动,这个配置项么人使用了一个定义为cacheManager的缓存管理器。SimpleCacehManager是这个缓存管理器的默认实现,它通过对其属性caches的配置来实现刚刚自定义的缓存管理器逻辑。从上面的代码中可以看到,除了默认的default缓存外,还自定义了一个名为test的缓存,使用了默认的内存存储方案ConcurrentMapCacheFactoryBean,它是一个基于java.util.concurrent.ConcurrentHashMap的内存缓存实现方案。

从例子中可以看出,使用Spring Cache非常简单,只需要两步:

① 缓存定义:确定需要缓存的方法和缓存策略。

② 缓存配置:在xml配置缓存。

二 掌握Spring Cache抽象

1 缓存注解

Spring Cache提供了5种可以在方法级别或者类级别上使用的缓存注解。这些注解定义了哪些方法的返回值会被缓存或者从缓存中移除。注意,只有使用public定义的方法才可以被缓存,而private方法、protected方法或者使用default修饰符的方法都不能被缓存。当在一个类上使用注解时,该类中每个公共发发的返回值都将被缓存到指定的缓存项中或者从中移除。

① @Cacheable

@Cacheable是最主要的注解,它指定了呗注解方法的返回值是可被缓存的。其工作原理是Spring首先在缓存中查找数据,如果没有则执行方法并缓存结果,然后返回数据。缓存名是必须提供的,可以使用引号、Value或者cacheNames属性来定义名称。

@Cacheable("test")
//Spring 3.x 提供value属性
@Cacheable(value="test")
//Spring 4.x 新增了value的别名cacheNames  可提供多个缓存名称,名称间使用逗号分隔
@Cacheable(cacheNames={"test","test2"})

Spring Cache不仅可以使用参数作为缓存key,也可以使用自定义键,以便获取存储在某一缓存段中的数据。

键生成器

缓存的本质就是键/值对集合。在默认情况下,缓存抽闲使用方法签名及参数值作为一个键值,并将该键与方法调用的结果组成键/值对。如果在Cache注解上没有指定key,则Spring会使用KeyGenerator来生成一个key。

public interface KeyGenerator{
	Object generate(Object targer,Method method,Object... params);
}

Spring提供了默认的SimpleKeyGenerator生成器。Spring4.0废弃了3.x的DefaultKeyFenerator而用SimpleKeyGenerator取代,原因是DefaultKeyGenerator在有多个入参时只是简单的把所有入参放在一起使用hashCode()方法生成key值,这样很容易曹成key冲突。SimpleKeyGenerator使用一个复合键SimpleKey来解决这个问题。

public static Object generateKey(Object... params){
	if(params.length == 0){
		return SimpleKey.EMPTY;
	}
	if(params.length == 1){
		Object param = params[0];
		if(param != null && !param.getClass().isArray()){
			return param;
		}
	}
	return new SimpleKey(params);
}

通过看spring源码学习效率更高,从上面的代码中可以发现其生成规则如下:

[1]如果方法没有入参,则使用SimpleKey.EMPTY作为key

[2]如果只有一个参数,则使用该参数为key

[3]如果有多个入参,则返回包含所有入参的一个SimpleKey。

此外,还可以在声明中指定键值。@Cacheable 注解提供了实现该功能的key属性,通过该属性,可以使用SpEL指定自定义键,

@Cacheable(cacheNames="user",key="#user.code")
public User getUser(User user,boolean checkLogout){}

如上所示,参数中有一个boolean值用于区分用户是否已经注销,而这个boolean值并不想做为key 的一部分,那么可以根据key属性执行使用userCode作为缓存键。

当然,如果key生成策略设计一些比较复杂的算法,而这个key生成策略又是通用的,则可以通过实现org.springframework.cache.interceptor.KeyGenerator接口来定义新的key生成器。

加入自定义了一个MyKeyGenerator类并且实现了KeyGenerator接口以实现自定义的key生成器,那么可以这么使用:

@Cacheable(cacheNames="user",keyGenerator="myKeyGenerator")
public User getUser(User user,boolean checkLogout){}

带条件缓存

使用@Cacheable注解的condition属性可按条件进行缓存,condition属性使用了SpEL表达式动态评估方法入参是否满足缓存条件。对于使用了@Cacheable注解声明了getUser()方法,我们启动了一个缓存判断条件,只对年龄小于35岁的用户启用缓存

@Cacheable(cacheNames="user",unless="user.age >= 35")
public User getUser(User user,boolean checkLogout){}

#user应用方法的同名入参变量,通过.age访问user入参对象的age属性值。在调用方法前,将对注解中声明的条件进行评估,满足条件才缓存。与condition属性相反,可使用unless属性排除某些不希望缓存的对象

② @CachePut

@CachePut注解与@Cacheable注解效果几乎一样,它首先执行方法,然后将返回值放入缓存。当希望使用方法返回值来更新缓存时,便可以选择这种方法:

@CachePut(value = "users")
public User getUser(int id){
	return users.get(id);
}

在上述代码中,首先执行getUser()方法,然后将结果值放入users缓存中。与@Cacheable注解一样,@CachePut注解也提供了key、condition和unless属性

需要注意的是,在同一个方法内不能同时使用@CachePut和@Cacheable注解,因为他们拥有不同的特性。当@Cacheable注解跳过方法直接获取缓存时,@CachePut注解会强制执行方法以更新缓存,这会导致意想不到的情况,如当注解都带入了条件属性,就会使得他们相互排斥。还需要注意的是@CachePut注解的condition属性设置的缓存条件也不应该依赖于方法返回的结果如:condition="#result",因为缓存条件时在方法执行前预先验证的。

③ @CacheEvict

@CacheEvict注解时@Cachable注解的反向操作,它负责从给定的缓存中移除一个值。大多数缓存框架都提供了缓存数据的有效期,使用该注解可以显示的从缓存中删除失效的缓存数据。该注解通常用于更新或删除用户的操作。下面的方法定义从数据库中删除一个用户,而@CacheEvict注解也完成了相同的工作,从users缓存中删除了呗缓存的用户。

@CacheEvict(value = "users")
public User removeUser(int id){
	return users.removeUser(id);
}

与@Cacheable注解一样,@CacheEvict注解也提供了key和condition属性,通过这些属性可以使用SpEL表达式指定自定义的键和条件。

此外,@CacheEvict注解还具有两个与@Cacheable注解不同的属性:allEntries属性定义了是否移除缓存的所有条目,其默认行为是不溢出;beforeInvocation属性定义了在调用方法之前还是在调用方法之后完成移除操作。与@Cacheable注解不同的是,在默认情况下,@CacheEvict注解在方法调用之后运行。

allEntries属性

allEntries是布尔类型的,用来表示是否需要清除缓存中的所有元素。默认为false。当指定allEntries为true时,Spring Cache将忽略指定的key,清除缓存中的所有内容。

@CacheEvict(value = "users",allEntries=true)
public User removeUser(int id){
	return users.removeUser(id);
}

beforeInvocation属性

需要知道,清除操作默认是在对应方法执行成功后触发的,即方法如果因为抛出异常而未能成功返回时则不会触发清除操作。使用beforeInvocation属性可以改变触发清除操作的时间。当指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

@CacheEvict(value = "users",beforeInvocation=true)
public User removeUser(int id){
	return users.removeUser(id);
}

需要注意的是,在相同的方法上使用@Cacheable和@CacheEvict注解并使用它们指向相同的缓存没有任何意义,因为这相当于数据被缓存之后又被立即清除了,所以需要避免在同一方法上同时使用这两个注解。

④ @Caching

@Caching是一个组注解,可以为一个方法定义提供基于@Cacheable、@CacheEvict或者@CachePut注解的数组。为了方便说吗@Caching注解的使用方法,定义User、Member和Visitor三个实体类,他们彼此有一个简单的层次结构,User是一个抽象类,Member和Visitor类是User的子类。

@Service(value="cacheGroupUserService")
public class UserService{
	private Map<Integer,User> map = new HashMap<Integer,User>();
	static{//定义数据源
		map.put(1,new Member("1","user1"));
		map.put(2,new Visitor("2","user2"));
	}
	
	//缓存组的使用
	@Caching(cacheable = {
		@Cacheable(value = "members" , condition = "#obj instanceof T(com.Member)"),
		@Cacheable(value = "visitors" , condition = "#obj instanceof T(com.Visitors)")
	})
	public User getUser(User obj){
		return map.get(Integer.valueOf(obj.getUserId)));
	}
}

⑤ @CacheConfig

在Spring4.0之前并没有类级别的全局缓存注解。前面我们所了解的注解都是基于方法的,如果在同一个类中需要缓存的方法注解属性都相似,则需要一个个的重复增加。Spring 4.0增加了@CacheConfig类级别的注解来解决这个问题。

@CacheConfig(cacheNames="users",keyGenerator="MyKeyGenerator")
public class UserService{
	@Cacheable
	public User findUser(int userId){}
}

在@CacheConfig注解中定义了类级别的缓存users和自定义key生成器,那么在findUser方法中不在需要重复指定,而是默认使用类级别的定义。

2 缓存管理器

CacheManager是SPI(Service Provider Interface,服务提供程序接口),提供了访问缓存名称和缓存对象的方法,同时也提供了管理缓存。操作缓存和移除缓存的方法。

① SimpleCacheManager

通过使用SimpleCacheManager可以配置缓存列表,并利用这些缓存进行相关的操作。因为SimpleCacheManager是缓存管理器的简化版本,所以在下面都将使用该实现类。下面代码是针对该缓存管理器的一个配置demo。对应缓存的定义使用ConcurrentMapCacheFactoryBean类来对ConcurrentMapCache进行实例化,该实例使用JDK的ConcurrentMap实现

<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
	<property name="caches">
		<set>
			<bean id="users" class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"/>
		</set>
	</property>
</bean>

②NoOpCacheManager

NoOpCacheManager主要用于测试目的,但实际上它并不缓存任何数据。下面的代码给出了该缓存管理器的配置定义,

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

③ ConcurrentMapCacheManager

ConcurrentMapCacheManager使用了JDK的ConcurrentMap。它提供了与SimpleCacheManager类似的功能,但并不需要像它一样定义缓存。

<bean id="cacheManager" class="org.springframework.cache.concurrent.ConcurrentMapCacheManager"/>

④ CompositeCacheManager

CompositeCacheManager能够定义多个缓存管理器。当在应用程序上下文中声明<cache:annotation-driven>标记时,它只提供一个缓存管理器,这往往不能满足使用需求。而CompositeCacheManager定义将多个缓存管理器定义组合在一起,从而扩展了该功能。此外,CompositeCacheManager还提供了一种机制,通过使用fallbackToNoOpCache属性货到NoOpCacheManager。下面示例中定义是一个CompositeCacheManager,将一个简单的缓存管理器与HazelCast缓存管理器捆绑在一起。简单的缓存管理器定义了members缓存,而HazelCast缓存管理器则为visitors定义了缓存管理器。下面的实例展示了可以在不同的缓存中存储不同类型的对象,而不同的缓存则由不同的缓存管理器进行管理。

<bean class="org.springframework.cache.support.CompositeCacheManager">
	<property name="cacheManagers">
		<list>
			<bean class="org.springframework.cache.support.SimpleCacheManager">
				<property name="caches">
					<set>
						<bean id="members" 
                class="org.springframework.cache.concurrentConcurrentMapCacheFactoryBean"/>
					</set>
				</property>
			</bean>
			<bean class="com.hazelcast.spring.cache.HazelcastCacheManager">
				<constructor-arg ref="hazelcast"/>
			</bean>
		</list>
	</property>
</bean>

3 使用SpEL表达式

在Spring Cache注解属性中(如key、condition和unless),Spring的缓存抽象使用了SpEL表达式,从而提供了属性值的动态生成及足够的灵活性。下面使用表达式来自定义键的生成:

@Cacheable(cacheNames="users",key="#user.userCode")
public User getUser(int userId){
	return userDAO.getUser(userId);
}

还可以添加条件,只对年龄小于35的用户进行缓存

@Cacheable(cacheNames="users",condition="#user.age <35")
public User getUser(int userId){
	return userDAO.getUser(userId);
}

SpEL表达式可基于上下文并通过使用缓存抽象,提供与root对象想关联的缓存特定的内置参数

4 基于XML的Cache声明

如果不想使用注解或者由于其他原因而无法获得项目的源码,也可以用XML的方式配置Spring Cache。其配置方式和transaction管理器的advice类似

<!-- 定义需要使用缓存的类 -->
<bean id="userService" class="com.smart.service.UserService"/>
<!-- 定义缓存 -->
<cache:advice id="cacheAdvice" cache-manager="cacheManager">
	<cache:caching cache="users">
		<cache:cacheable method="findUser" key="#userId"/>
		<cache:cache-evict method="loadUsers" all-entries="true"/>
	</cache:caching>
</cache:advice>
<aop:config>
	<aop:advisor advice-ref="cacheAdvice" pointcut="execution(*com.service.UserService.*(..))"/>
</aop:config>

如上配置文件为UserService开启了缓存。使用cache:advice定义包装了两个需要使用缓存的方法,其中findUser()定义了Cacheable,而loadUsers()定义了CacheEvict,并且定义了公共的缓存users。

aop:config定义了cacheAdvice的切入点。XML声明式配置支持所有注解的方法,因此二者之间可以很容易的替换,当然也可以在项目中同时使用这两种方式。

5 以变成方式初始化缓存

在实际项目中,有时可能需要在使用之前就完成缓存的初始化。实现该方法很简单,首先访问缓存管理器,然后将数据手工加载到不同的缓存中。

@Service(value="cacheGroupUserService")
public class UserService{
	private Map<Integer,User> map = new HashMap<Integer,User>();
	static{//定义数据源
		map.put(1,new Member("1","user1"));
		map.put(2,new Visitor("2","user2"));
	}
	
	private CacheManager cacheManager;
	
	@Autowired
	public void setCacheManager(CacheManager cacheManager){
		this.cacheManager = cacheManager;
	}
	
	@PostConstruct
	public void setup(){
		Cache usersCache = cacheManager.getCache("users");
		Set<Integer> set = map.keySet();
		for(Integer key:set){
			usersCache.put(key,map.get(key));
		}
	}
	
	@Cacheable(value="users")
	public User getUser(int userId){
		return map.get(userId);
	}
}

上面展示了Spring Bean的@PostConstrut注解方法中初始化缓存users。此外该Bean包含了getUser()方法,并且已经被注解用于缓存操作;接下来创建UserService类作为Spring服务Bean,并且自动注入CacheManager,然后在@PostConstruct中通过键值吧所有数据防止到缓存中,该键值将用于检索。

下面通过基于Java类的配置方式准备好Spring容器配置信息。

@Configuration
@ComponentScan(basePackages={"com.cache"})
@EnableCaching
public class ApplicationConfig{
	@Bean
	public CacheManager cacheManager(){
		SimpleCacheManager cacheManager = new SimpleCacheManager();
		cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("users")));
		return cacheManager;
	}
}

用UserMain测试以上缓存实现效果

public class UserMain{
	public static void main(String[] args){
		ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
		UserService userService = (UserService)context.getBean("initUserService");
		User user1 = userService.getUser(1);
		System.out.println(user1);
		User user2 = userService.getUser(2);
		System.out.println(user2);
	}
}

如上所示,首先从上下文中查找initUserService Bean,然后连续调用getUser()方法。

6 自定义缓存注解

之前介绍的@Caching组合也许会让方法上的注解显得比较杂乱,Spring提供了自定义注解,可把这些注解组合到一个注解类中,从而解决这个问题。

@Caching(
	put={
		@CachePut(cacheNames="user",key="#user.id"),
		@CachePut(cacheNames="user",key="#user.name"),
		@CachePut(cacheNames="user",key="#user.email")
	}
)
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface UserSaveCache{}

//这样在需要的方法上面直接使用即可
@UserSaveCache
public User save(User user){}

猜你喜欢

转载自blog.csdn.net/yongqi_wang/article/details/86741869