通常在Web应用开发中,不同层级对应的缓存要求和缓存策略不同,如下图是系统不同层级对应的缓存技术选型:
即从缓存中读取数据的次数与总读取次数的比率。
即如果缓存满了,从缓存中移除数据的策略。常见的有LFU、LRU、FIFO。
FIFO:先进先出策略,即先放入缓存的数据先被移除。
LRU:最久未使用策略,即使用时间距离现在最久的那个数据被移除
LFU:最近最少使用策略,即一定时间段内使用次数最少的那个数据被移除
TTL:存活期,即从缓存中创建时间点开始直至到期的一个时间段。
TTI:空闲期,即一个数据多久没被访问就从缓存中移除的时间。
Java对象的缓存和序列化是息息相关的,一般情况下,需要被缓存的实体类需要实现Serializable,只有实现了Serializable接口的类,JVM才可以对其对象进行序列化。对于Redis、EhCache等缓存套件来说,被缓存的对象应该是可以序列化的,否则在网络传输、硬盘存储时都会抛出序列化异常。
使用Spring Cache注解的实例:
@Service(value = "userServiceBean")
public class UserService {
@Cacheable(cacheNames = "users")// 使用了一个缓存名叫 users
public User getUserById(String userId) {
//方法内部实现不考虑缓存逻辑,直接实现业务
System.out.println("执行用户业务查询方法查找." + userId);
return getFromDB(userId);
}
private User getFromDB(String userId) {
System.out.println("从数据库中查询..." + userId);
return new User(userId);
}
}
getUserById()方法标注了@Cacheable(cacheName="users"),当调用这个方法时,会先从users缓存中查询匹配的缓存对象,如果存在则直接返回;如果不存在,则执行方法体内的逻辑,并返回值放入缓存中。对应缓存的key为userId的值,value就是userId所对应的User对象,缓存名称需要在applicationContext.xml中定义。
还需要一个Spring配置文件来支持基于注解的缓存,如下代码:
Spring通过注意:只有使用public定义的方法才可以被缓存,而private方法、protected方法或者使用default修饰符的方法都不能被缓存。当一个类上使用注解时,该类中每个公共方法的返回值都将被缓存到指定的缓存项中或者从中移除。
(1)@Cacheable
它指定了被注解方法的返回值是可被缓存的。缓存名师必须提供的,可以使用引号、Value或者cacheNames属性来定义名称。
比如:@Cacheable("users")、@Cacheable(value="users")、@Cacheable(cacheNames="users"),还可以以列表的形式提供多个缓存,在该列表中使用逗号分隔缓存名称,并用花括号括起来。
1>键生成器
如果在Cache注解上没有指定key,则Spring会使用KeyGenerator来生成一个key。Spring默认提供了SimpleKeyGenerator生成器。按如下规则生成key:
A:如果方法没有参数,则使用SimpleKey.EMPTY作为key。
B:如果只有一个参数,则使用该参数作为key。
C:如果有多个入参,则返回包含所有参数的一个SimpleKey。
也可以在声明中指定键值。比如:@Cacheable(cacheName="user",key="#user.userCode")。通过key属性可以使用SpEL指定自定义键。
如果key生成策略比较复杂,可以通过实现org.springframework.cache.interceptor.KeyGenerator接口来定义个性化的key生成器。比如自定义了MyKeyGenerator类并实现了KeyGenerator接口以实现自定义的key生成器,可如下使用:@Cacheable(cacheNames="users",keyGenerator="myKeyGenerator")
2>带条件的缓存
使用@Cacheable注解的condition属性可按条件进行缓存,condition属性使用了SpEL表达式动态评估入参是否满足缓存条件。如下只对年龄小于35岁的user进行缓存:
@Cacheable(value = "users", condition = "#user.age < 35")
public User getUser(User user) {
return users.get(Integer.valueOf(user.getUserId()));
}
与cindition属性相反,可使用unless属性排除某些不希望缓存的对象,如下:
@Cacheable(value = "users", unless = "#user.age >= 35")
public User getUser(User user) {
return users.get(Integer.valueOf(user.getUserId()));
}
(2)@CachePut
@CachePut与@Cacheable效果几乎一样,也提供了key、condition和unless属性。当希望使用方法返回值来更新缓存时,可选择这个注解。
注意:在同一个方法内不能同时使用@CachePut和@Cacheable注解,因为它们拥有不同的特性。当@Cacheable注解跳过方法直接获取缓存时,@CachePut注解会强制执行方法以更新缓存,这会导致意想不到的情况发生,如当注解都带入了条件属性,就会使得他们彼此排斥。
(3)@CacheEvict
@CacheEvict注解是@Cacheable注解的反向操作,它负责从给定的缓存中移除一个值。@CacheEvict注解也提供了key和condition属性。@CacheEvict注解还具有两个与@Cacheable注解不同的属性:allEntries属性定义了是否移除缓存的所有条目,其默认行为是不移除;beforeInvacation属性定义了在调用方法之前还是在调用方法之后完成移除操作,默认为false,是在方法执行成功后触发,即方法如果因为抛出异常而未能成功返回时则不会触发清除操作。@CacheEvict默认情况下在方法调用之后运行。
(4)@Caching
@Caching是一个组注解,可以为一个方法定义提供基于@Cacheable、@CacheEvict或者@CachePut注解的数组。比如定义User、Member和Visitor3个实体类,User是抽象类,而Member和Visitor类扩展了该类。如下使用@Caching注解,同时声明了两个@Cacheable注解,并使其指向两个不同的缓存项:members和visitors。然后根据两个@Cacheable注解定义中的条件对方法的参数进行检查,并将对象存储在members或visitors缓存中。
@Caching(cacheable = {
@Cacheable(value = "members", condition = "#obj instanceof T(com.smart.cache.cachegroup.Member)"),
@Cacheable(value = "visitors", condition = "#obj instanceof T(com.smart.cache.cachegroup.Visitor)")
})
public User getUser(User obj) {
return ppl.get(Integer.valueOf(obj.getUserId()));
}
(5)@CacheConfig
@CacheConfig是类级别的全局缓存注解。
CacheManager提供了访问缓存名称和缓存对象的方法,同时也提供了管理缓存、操作缓存和移除缓存的方法。
(1)SimpleCacheManager
通过使用SimpleCacheManager可以配置缓存列表,并利用这些缓存进行相关的操作。对应缓存的定义,使用ConcurrentMapCacheFactoryBean类来对ConcurrentMapCache进行实例化,该实例使用了JDK的ConcurrentMap实现。如下所示:
(2)NoOpCacheManager
主要用于测试,但实际上并不缓存任何数据。
(3)ConcurrentMapCacheManager
ConcurrentMapCacheManager使用了JDK的ConcurrentMap,它提供了与SimpleCacheManager类似的功能,但并不需要像前面那样定义缓存。
(4)CompositeCacheManager
CompositeCacheManager定义将多个缓存管理器定义组合在一起。下面的代码将SimpleCacheManager和HazelCast缓存管理器组合在一起。