Spring Cache

缓存理解

让数据更接近使用者

基本机制:先从缓存中读取数据,如果没有再从慢速设备上读取实际数据(数据也会存入缓存)
缓存什么:

  • 经常读取且不经常修改的数据
  • 昂贵(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> T get(Object key, Class<T> 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<String> 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风格的

 

<cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true"/>

注解风格的

 

@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风格的<cache:annotation-driven key-generator=""/>或注解风格的CachingConfigurer中指定keyGenerator。

条件缓存

 

// @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<User>,现在你执行了一个 update(user)的方法,你一定不希望清除整个缓存而想替换掉update的元素

这个在现有的抽象上没有很好的方案,可以考虑通过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进行缓存抽象。



作者:jiangmo
链接:https://www.jianshu.com/p/33c019de9115
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值