缓存理解
让数据更接近使用者
基本机制:先从缓存中读取数据,如果没有再从慢速设备上读取实际数据(数据也会存入缓存)
缓存什么:
- 经常读取且不经常修改的数据
- 昂贵(CPU/IO)的且对于相同的请求有相同的计算结果的数据
如CPU--L1/L2--内存--磁盘
如Maven找依赖:本机仓库--本地服务器仓库--远程仓库服务
基本概念
缓存命中率
- 命中率 = 从缓存中读取次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])
- Miss率 = 没有从缓存中读取的次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])
TTL(Time To Live )
存活期,即从缓存中创建时间点开始直到它到期的一个时间段(不管在这个时间段内有没有访问都将过期
TTI(Time To Idle)
空闲期,即一个数据多久没被访问将从缓存中移除的时间。
Eviction policy
移除策略,即如果缓存满了,从缓存中移除数据的策略
常见:
- FIFO(First In First Out):先进先出算法,即先放入缓存的先被移除;
- LRU(Least Recently Used):最久未使用算法,使用时间距离现在最久的那个被移除;
- LFU(Least Frequently Used):最近最少使用算法,一定时间段内使用次数(频率)最少的那个被移除;
- 带过期时间:
- volatile-lru:从已设置过期时间的数据集中淘汰最近最少使用的数据
- volatile-ttl:从已设置过期时间的数据集中淘汰将要过期的数据
- volatile-random:从已设置过期时间的数据集中淘汰随机的数据
- allkeys-lru
- allkeys-random
何时移除:
- 访问Key A时,移除举例最远的过期Key
- 定时任务
- CPU空闲时
Spring Cache
自Spring 3.1起,提供了类似于@Transactional注解事务的注解Cache支持,且提供了Cache抽象,在此之前一般通过AOP实现。
使用Spring Cache的好处:
- 提供基本的Cache抽象,方便切换各种底层Cache;
- 通过注解Cache可以实现类似于事务一样,缓存逻辑透明的应用到我们的业务代码上,且只需要更少的代码就可以完成;
- 提供事务回滚时也自动回滚缓存;
- 支持比较复杂的缓存逻辑;
对于Spring Cache抽象,主要从以下几个方面学习:
- Cache API及默认提供的实现
- Cache注解
- 实现复杂的Cache逻辑
Cache API及默认提供的实现
package org.springframework.cache;
public interface Cache {
String getName(); //缓存的名字
Object getNativeCache(); //得到底层使用的缓存,如Ehcache
ValueWrapper get(Object key); //根据key得到一个ValueWrapper,然后调用其get方法获取值
T get(Object key, Class type);//根据key,和value的类型直接获取value
void put(Object key, Object value);//往缓存放数据
void evict(Object key);//从缓存中移除key对应的缓存
void clear(); //清空缓存
interface ValueWrapper { //缓存值的Wrapper
Object get(); //得到真实的value
}
}
默认提供了如下实现:
- ConcurrentMapCache:使用java.util.concurrent.ConcurrentHashMap实现的Cache;
- GuavaCache:对Guava com.google.common.cache.Cache进行的Wrapper,需要Google Guava 12.0或更高版本,@since spring 4;
- EhCacheCache:使用Ehcache实现
- JCacheCache:对javax.cache.Cache进行的wrapper,@since spring 3.2;spring4将此类更新到JCache 0.11版本;
另外,因为我们在应用中并不是使用一个Cache,而是多个,因此Spring还提供了CacheManager抽象,用于缓存的管理:
public interface CacheManager {
Cache getCache(String name); //根据Cache名字获取Cache
Collection getCacheNames(); //得到所有Cache的名字
}
默认提供的实现:
- ConcurrentMapCacheManager/ConcurrentMapCacheFactoryBean
- GuavaCacheManager;
- EhCacheCacheManager/EhCacheManagerFactoryBean;
- JCacheCacheManager/JCacheManagerFactoryBean;
另外还提供了CompositeCacheManager用于组合CacheManager,即可以从多个CacheManager中轮询得到相应的Cache。
除了GuavaCacheManager之外,其他Cache都支持Spring事务的,即如果事务回滚了,Cache的数据也会移除掉。
Spring不进行Cache的缓存策略的维护,这些都是由底层Cache自己实现,Spring只是提供了一个Wrapper,提供一套对外一致的API。
除了这些默认的Cache之外,我们可以写自己的Cache实现;而且即使不用之后的Spring Cache注解,我们也尽量使用Spring Cache API进行Cache的操作,如果要替换底层Cache也是非常方便的。
Cache注解
启用Cache注解
XML风格的
注解风格的
@Configuration
@ComponentScan(basePackages = "com.sishuok.spring.service")
// 使用@EnableCaching启用Cache注解支持
@EnableCaching(proxyTargetClass = true)
// 实现CachingConfigurer,然后注入需要的cacheManager和keyGenerator;
//从spring4开始默认的keyGenerator是SimpleKeyGenerator
public class AppConfig implements CachingConfigurer {
@Bean
@Override
public CacheManager cacheManager() {
try {
net.sf.ehcache.CacheManager ehcacheCacheManager
= new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());
EhCacheCacheManager cacheCacheManager = new EhCacheCacheManager(ehcacheCacheManager);
return cacheCacheManager;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Bean
@Override
public KeyGenerator keyGenerator() {
return new SimpleKeyGenerator();
}
}
@CachePut
public @interface CachePut {
String[] value(); //缓存的名字,可以把数据写到多个缓存
String key() default ""; //缓存key,如果不指定将使用默认的KeyGenerator生成,后边介绍
String condition() default ""; //满足缓存条件的数据才会放入缓存,condition在调用方法之前和之后都会判断
String unless() default ""; //用于否决缓存更新的,不像condition,该表达只在方法执行之后判断,此时可以拿到返回值result进行判断了
}
应用到写数据的方法上,如新增/修改方法,调用方法时会自动把相应的数据放入缓存
@CachePut(value = "user", key = "#user.id")
public User save(User user) {
users.add(user);
return user;
}
@CacheEvict
// 注解
public @interface CacheEvict {
String[] value(); //请参考@CachePut
String key() default ""; //请参考@CachePut
String condition() default ""; //请参考@CachePut
boolean allEntries() default false; //是否移除所有数据
boolean beforeInvocation() default false;//是调用方法之前移除/还是调用之后移除
}
// 使用
@CacheEvict(value = "user", key = "#user.id") //移除指定key的数据
public User delete(User user) {
users.remove(user);
return user;
}
@CacheEvict(value = "user", allEntries = true) //移除所有数据
public void deleteAll() {
users.clear();
}
@Cacheable
应用到读取数据的方法上,即可缓存的方法,如查找方法:先从缓存中读取,如果没有再调用方法获取数据,然后把数据添加到缓存中
// ======注解
public @interface Cacheable {
String[] value(); //请参考@CachePut
String key() default ""; //请参考@CachePut
String condition() default "";//请参考@CachePut
String unless() default ""; //请参考@CachePut
}
// =====使用
@Cacheable(value = "user", key = "#id")
public User findById(final Long id) {
System.out.println("cache miss, invoke find by id, id:" + id);
for (User user : users) {
if (user.getId().equals(id)) {
return user;
}
}
return null;
}
运行流程
1、首先执行@CacheEvict(如果beforeInvocation=true且condition 通过),如果allEntries=true,则清空所有
2、接着收集@Cacheable(如果condition 通过,且key对应的数据不在缓存),放入cachePutRequests(也就是说如果cachePutRequests为空,则数据在缓存中)
3、如果cachePutRequests为空且没有@CachePut操作,那么将查找@Cacheable的缓存,否则result=缓存数据(也就是说只要当没有cache put请求时才会查找缓存)
4、如果没有找到缓存,那么调用实际的API,把结果放入result
5、如果有@CachePut操作(如果condition 通过),那么放入cachePutRequests
6、执行cachePutRequests,将数据写入缓存(unless为空或者unless解析结果为false);
7、执行@CacheEvict(如果beforeInvocation=false 且 condition 通过),如果allEntries=true,则清空所有
提供的SpEL上下文数据
Spring Cache提供了一些供我们使用的SpEL上下文数据,下表直接摘自Spring官方文档:
Key生成器
如果在Cache注解上没有指定key的话@CachePut(value = "user"),会使用KeyGenerator进行生成一个key
public interface KeyGenerator {
Object generate(Object target, Method method, Object... params);
}
@Override
public Object generate(Object target, Method method, Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1 && params[0] != null) {
return params[0];
}
return new SimpleKey(params);
我们也可以自定义自己的key生成器,然后通过xml风格的
条件缓存
// @Cacheable将在执行方法之前( #result还拿不到返回值)判断condition
// 如果返回true,则查缓存;
@Cacheable(value = "user", key = "#id", condition = "#id lt 10")
public User conditionFindById(final Long id)
// 如下@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition
// 如果返回true,则放入缓存;
@CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'")
public User conditionSave(final User user)
//根据运行流程,如下@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless
//如果返回false,则放入缓存;(即跟condition相反)
@CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'")
public User conditionSave2(final User user)
// @CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了)
// 且判断condition,如果返回true,则移除缓存
@CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'")
public User conditionDelete(final User user)
@Caching
有时候我们可能组合多个Cache注解使用;比如用户新增成功后,我们要添加id-->user;username--->user;email--->user的缓存;此时就需要@Caching组合多个注解标签了。
如用户新增成功后,添加id-->user;username--->user;email--->user到缓存;
@Caching(
put = {
@CachePut(value = "user", key = "#user.id"),
@CachePut(value = "user", key = "#user.username"),
@CachePut(value = "user", key = "#user.email")
}
)
public User save(User user)
// 注解
public @interface Caching {
Cacheable[] cacheable() default {}; //声明多个@Cacheable
CachePut[] put() default {}; //声明多个@CachePut
CacheEvict[] evict() default {}; //声明多个@CacheEvict
}
自定义缓存注解
比如之前的那个@Caching组合,会让方法上的注解显得整个代码比较乱,此时可以使用自定义注解把这些注解组合到一个注解中,如:
@Caching(
put = {
@CachePut(value = "user", key = "#user.id"),
@CachePut(value = "user", key = "#user.username"),
@CachePut(value = "user", key = "#user.email")
}
)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface UserSaveCache {
}
这样我们在方法上使用如下代码即可,整个代码显得比较干净。
@UserSaveCache
public User save(User user)
实例
@Caching(
put = {
@CachePut(value = "user", key = "#user.id"),
@CachePut(value = "user", key = "#user.username"),
@CachePut(value = "user", key = "#user.email")
}
)
public User save(User user)
@Caching(
put = {
@CachePut(value = "user", key = "#user.id"),
@CachePut(value = "user", key = "#user.username"),
@CachePut(value = "user", key = "#user.email")
}
)
public User update(User user)
@Caching(
evict = {
@CacheEvict(value = "user", key = "#user.id"),
@CacheEvict(value = "user", key = "#user.username"),
@CacheEvict(value = "user", key = "#user.email")
}
)
public User delete(User user)
@CacheEvict(value = "user", allEntries = true)
public void deleteAll()
@Caching(
cacheable = {
@Cacheable(value = "user", key = "#id")
}
)
public User findById(final Long id)
@Caching(
cacheable = {
@Cacheable(value = "user", key = "#username")
}
)
public User findByUsername(final String username)
@Caching(
cacheable = {
@Cacheable(value = "user", key = "#email")
}
)
public User findByEmail(final String email)
实现复杂的Cache逻辑
往缓存放数据/移除数据是有条件的,而且条件可能很复杂,考虑使用SpEL表达式
@CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canCache() and #root.caches[0].get(#user.id).get().username ne #user.username", beforeInvocation = true)
public void conditionUpdate(User user)
或更复杂的直接调用目标对象的方法进行操作
这个比较厉害,可以直接调用目标对象的方法
@Caching(
evict = {
@CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canEvict(#root.caches[0], #user.id, #user.username)", beforeInvocation = true)
}
)
public void conditionUpdate(User user){...}
public boolean canEvict(Cache userCache, Long id, String username) {
User cacheUser = userCache.get(id, User.class);
if (cacheUser == null) {
return false;
}
return !cacheUser.getUsername().equals(username);
}
如上方式唯一不太好的就是缓存条件判断方法也需要暴露出去;而且缓存代码和业务代码混合在一起,不优雅;因此把canEvict方法移到一个Helper静态类中就可以解决这个问题了:
@CacheEvict(value = "user", key = "#user.id", condition = "T(com.sishuok.spring.service.UserCacheHelper).canEvict(#root.caches[0], #user.id, #user.username)", beforeInvocation = true)
public void conditionUpdate(User user)
对于:id--->user;username---->user;email--->user;更好的方式可能是:id--->user;username--->id;email--->id;保证user只存一份;如:
@CachePut(value="cacheName", key="#user.username", cacheValue="#user.username")
public void save(User user)
@Cacheable(value="cacheName", key="#user.username", cacheValue="#caches[0].get(#caches[0].get(#username).get())")
public User findByUsername(String username)
spring cache的缺陷
例如有一个缓存存放 list
这个在现有的抽象上没有很好的方案,可以考虑通过condition在之前的Helper方法中解决;当然不是很优雅。
也就是说Spring Cache注解还不是很完美,我认为可以这样设计:
@Cacheable(cacheName = "缓存名称",key="缓存key/SpEL", value="缓存值/SpEL/不填默认返回值", beforeCondition="方法执行之前的条件/SpEL", afterCondition="方法执行后的条件/SpEL", afterCache="缓存之后执行的逻辑/SpEL")
value也是一个SpEL,这样可以定制要缓存的数据;afterCache定制自己的缓存成功后的其他逻辑。
当然Spring Cache注解对于大多数场景够用了,如果场景复杂还是考虑使用AOP吧;如果自己实现请考虑使用Spring Cache API进行缓存抽象。