Spring Cache抽象详解

本文详细介绍了缓存的工作机制及其在提升系统性能方面的作用,并深入探讨了Spring Cache注解支持下的缓存管理和操作。文章包括缓存设计的重要指标、缓存移除策略以及如何在Java中使用Spring Cache。

对缓存的理解--以空间换时间

缓存--(牺牲了空间或说提供更多的空间)让数据更接近于使用者

工作机制是--先从缓存中读取数据,如果没有再从慢速设备上读取实际数据(数据也会存入缓存)

缓存什么--那些经常读取且不经常修改的数据/那些昂贵(CPU/IO)的且对于相同的请求有相同的计算结果的数据

举例1--CPU--L1/L2--内存--磁盘

CPU需要数据时先从L1/L2中读取,如果没有到内存中找,如果还没有会到磁盘上找。

举例2--Maven

还有如用过Maven的朋友都应该知道,我们找依赖的时候,先从本机仓库找,再从本地服务器仓库找,最后到远程仓库服务器找。

举例3--京东仓储

还有如京东的物流为什么那么快?他们在各个地都有分仓库,如果该仓库有货物那么送货的速度是非常快的。

几个重要的指标

缓存命中率--表明缓存是否运行良好的

即【从缓存中读取数据的次数】与【总读取次数】的比率,命中率越高越好:

命中率 = 从缓存中读取次数 / (总读取次数[从缓存中读取次数 + 从慢速设备上读取的次数])

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)

空闲期,即一个数据多久没被访问将从缓存中移除的时间。

★实际设计缓存时以上重要指标都应该考虑进去,当然根据实际需求可能有的指标并不会采用进设计去

★MySQL数据库中的索引也是以空间换取时间,缓存也是以空间换取时间。所以有时为了提高效率(减少响应时间),就要适当的牺牲空间

Java中缓存的应用

在Java中,我们一般对调用方法进行缓存控制,比如我调用"findUserById(Long id)",那么我应该在调用这个方法之前先从缓存中查找有没有,如果没有再掉该方法如从数据库加载用户,然后添加到缓存中,下次调用时将会从缓存中获取到数据。

基于注解的对Cache的支持

Spring 3.1起,提供了基于注解的对Cache的支持。使用Spring Cache的好处:

基于注解,代码清爽简洁;

基于注解也可以实现复杂的逻辑;

可以对缓存进行回滚;

Spring Cache不是具体的缓存技术,而是基于具体的缓存产品(如Guava、EhCache、Redis等)的共性进行了一层封装,但是可以通过简单的配置切换底层使用的缓存。

★Spring Cache并非缓存产品,而是基于各种缓存产品(如Guava、EhCache、Redis等)的共性进行了一层封装,结合SpringBoot的开箱即用的特性用起来会非常方便,因为Spring Cache通过注解隔离了具体的缓存产品,让用户更加专注于应用层面。具体的底层缓存技术究竟采用了Guava、EhCache还是Redis,只需要简单的配置就可以实现方便的切换。

Spring Cache总

Cache API及默认提供的实现

Cache注解

实现复杂的Cache逻辑

Cache API及默认提供的实现--Cache接口和CacheManager接口

Cache接口

源码

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版本。

 

CacheManager接口

源码

package org.springframework.cache;  

import java.util.Collection;  

public interface CacheManager {  

    Cache getCache(String name); //根据Cache名字获取Cache   

    Collection<String> getCacheNames(); //得到所有Cache的名字  

}  

分析--在应用中并不是使用一个Cache,而是多个,因此Spring还提供了CacheManager抽象,用于缓存的管理。接口方法主要包括根据缓存名获取缓存和获取所有缓存的名字集合

提供的默认实现

ConcurrentMapCacheManager/ConcurrentMapCacheFactoryBean:管理ConcurrentMapCache;

GuavaCacheManager;

EhCacheCacheManager/EhCacheManagerFactoryBean;

JCacheCacheManager/JCacheManagerFactoryBean;

 

CompositeCacheManager

CompositeCacheManager用于组合CacheManager,即可以从多个CacheManager中轮询得到相应的Cache。

 

调用cacheManager.getCache(cacheName) 时,会先从第一个cacheManager中查找有没有cacheName的cache,如果没有接着查找第二个,如果最后找不到,因为fallbackToNoOpCache=true,那么将返回一个NOP的Cache否则返回null。

NoOpCacheNoOpCacheManager的一个内部类

 

Spring不进行Cache的缓存策略的维护,这些都是由底层Cache自己实现,Spring只是提供了一个Wrapper,提供一套对外一致的API。

★使用实例

代码方式--直接使用Spring提供的API

@Test  

public void test() throws IOException {  

    //创建底层Cache  

    net.sf.ehcache.CacheManager ehcacheManager  

            = new net.sf.ehcache.CacheManager(new ClassPathResource("ehcache.xml").getInputStream());  

  

    //创建Spring的CacheManager  

    EhCacheCacheManager cacheCacheManager = new EhCacheCacheManager();  

    //设置底层的CacheManager  

    cacheCacheManager.setCacheManager(ehcacheManager);  

  

    Long id = 1L;  

    User user = new User(id, "zhang", "zhang@gmail.com");  

  

    //根据缓存名字获取Cache  

    Cache cache = cacheCacheManager.getCache("user");  

    //往缓存写数据  

    cache.put(id, user);  

    //从缓存读数据  

    Assert.assertNotNull(cache.get(id, User.class));  

}  

代码方式说明

EhCacheCacheManager用来根据缓存名称(cacheName)创建缓存Cache,然后通过缓存Cache来进行缓存的操作。

XML配置--Spring容器获取对象

 

spring提供EhCacheManagerFactoryBean来简化ehcache cacheManager的创建,这样注入configLocation,会自动根据路径从classpath下找,比编码方式简单多了,然后就可以从spring容器获取cacheManager进行操作了。此处的transactionAware表示是否事务环绕的,如果true,则如果事务回滚,缓存也回滚,默认false。

注解方式代码

@Bean  

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);  

    }  

}  

注解方式说明--主要就是注册下CacheManager

★三种使用方式比较:代码方式、XML配置方式、注解方式

Spring是一种产品,提供这么多种方式是为了满足不同的编程习惯的程序员的编程习惯。

代码方式最为灵活,也需要对源码有一定的了解,做深入的、个性化需求的开发最好还是采用对源码封装的方式,也就是直接操作代码的方式。

XML配置和注解方式较为简洁。但是做深入的、个性化的开发,灵活性有些受制约。

★先生成CacheManager,生成后CacheManager根据CacheName创建缓存Cache,然后各种操作都是通过Cache进行的

可以写自己的Cache实现;即使不用Spring Cache注解,也尽量使用Spring Cache API进行Cache的操作,因为替换底层Cache是非常方便的

Cache注解

启用Cache注解

XML风格

 

还可以指定一个 key-generator,即默认的key生成策略。

注解风格代码

@Configuration

@ComponentScan(basePackages = "com.sishuok.spring.service")

@EnableCaching(proxyTargetClass = true)

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();

    }

}

注解风格说明

使用@EnableCaching启用Cache注解支持。注入需要的cacheManager。也可以对keyGenerator进行配置,不配置则采用默认的。从spring4开始默认的keyGenerator是SimpleKeyGenerator。

@CachePut

@CachePut注解说明

public @interface CachePut {  

    String[] value();              //缓存的名字,可以把数据写到多个缓存  

    String key() default "";       //缓存key,如果不指定将使用默认的KeyGenerator生成,后边介绍  

    String condition() default ""; //满足缓存条件的数据才会放入缓存,condition在调用方法之前和之后都会判断  

    String unless() default "";    //用于否决缓存更新的,不像condition,该表达只在方法执行之后判断,此时可以拿到返回值result进行判断了  

}  

@CachePut注解使用

应用到写数据的方法上,如新增/修改方法,调用方法时会自动把相应的数据放入缓存。比如下面调用该方法时,会把user.id作为key,返回值作为value放入缓存

 

@CacheEvict

@CacheEvict注解说明

public @interface CacheEvict {  

    String[] value();                        //请参考@CachePut  

    String key() default "";                 //请参考@CachePut  

    String condition() default "";           //请参考@CachePut  

    boolean allEntries() default false;      //是否移除所有数据  

    boolean beforeInvocation() default false;//是调用方法之前移除/还是调用之后移除  

}

@CacheEvict注解使用

应用到移除数据的方法上,如删除方法,调用方法时会从缓存中移除相应的数据

 

@Cacheable

@Cacheable注解说明

public @interface Cacheable {  

    String[] value();             //请参考@CachePut  

    String key() default "";      //请参考@CachePut  

    String condition() default "";//请参考@CachePut  

    String unless() default "";   //请参考@CachePut    

}

@Cacheable注解使用

应用到读取数据的方法上,即可缓存的方法,如查找方法:先从缓存中读取,如果没有再调用方法获取数据,然后把数据添加到缓存中

 

@Cacheable、@CachePut、@CacheEvict注解的拦截解析逻辑

实际上就是AOP,拦截处理,在 CacheAspectSupport 的 execute 方法中进行的处理。

 

处理逻辑如下(没细看,不知是否如此,而且不同版本的Spring可能有所不同,真想明白这个逻辑,请先自己仔细想一遍,手动实现一下,再比对Spring源码分析自己的处理逻辑跟Spring中的是否一致,谁的处理更好,为什么?自己先想想怎么处理这个逻辑才合理!!!

 

如果有@CachePut操作,即使有@Cacheable也不会从缓存中读取;问题很明显,如果要混合多个注解使用,不能组合使用@CachePut和@Cacheable;官方说应该避免这样使用(解释是如果带条件的注解相互排除的场景);不过个人感觉还是不要考虑这个好,让用户来决定如何使用,否则一会介绍的场景不能满足。

注解实现复杂的缓存逻辑,需要通过Spring Expression Language (SpEL)

Spring Cache提供了一些供使用的SpEL上下文数据,下表直接摘自Spring官方文档

名字

位置

描述

示例

methodName

root对象

当前被调用的方法名

#root.methodName

method

root对象

当前被调用的方法

#root.method.name

target

root对象

当前被调用的目标对象

#root.target

targetClass

root对象

当前被调用的目标对象类

#root.targetClass

args

root对象

当前被调用的方法的参数列表

#root.args[0]

caches

root对象

当前方法调用使用的缓存列表(如@Cacheable(value={"cache1", "cache2"})),则有两个cache

#root.caches[0].name

argument name

执行上下文

当前被调用的方法的参数,如findById(Long id),我们可以通过#id拿到参数

#user.id

result

执行上下文

方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,'cache evict'的beforeInvocation=false)

#result

通过这些数据可以实现比较复杂的缓存逻辑。

Key生成器

如果在Cache注解上没有指定key的话@CachePut(value = "user"),会使用KeyGenerator进行生成一个key。默认提供了SimpleKeyGenerator生成器。

KeyGenerator接口

public interface KeyGenerator {

 

Object generate(Object target, Method method, Object... params);

 

}

SimpleKeyGenerator生成key的源码--如果只有一个参数,就使用参数作为key,否则使用SimpleKey作为key

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);

}

可以自定义自己的key生成器,然后通过xml风格的<cache:annotation-driven key-generator=""/>或注解方式指定keyGenerator

条件缓存

根据运行流程,如下@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存

 

根据运行流程,如下@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存

 

根据运行流程,如下@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反)

 

根据运行流程,如下@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存

 

@Caching--组合多个Cache注解使用

@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) {  }

 

这样可以在根据id、根据username、根据email查找user的方法上使用@Cacheable,就可以使用缓存了。

@Caching的定义

@Target({ElementType.METHOD, ElementType.TYPE})

@Retention(RetentionPolicy.RUNTIME)

@Inherited

@Documented

public @interface Caching {

Cacheable[] cacheable() default {};

CachePut[] put() default {};

CacheEvict[] evict() default {};

}

★自定义缓存注解--美团应用中就有这个的实际应用,定义了针对美团项目的注解

上面的@Caching组合,会让方法上的注解显得整个代码比较乱,此时可以使用自定义注解把这些注解组合到一个注解中。

这样在方法上使用如下代码即可,整个代码显得比较干净

@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")  

        }  

)  

@Target({ElementType.METHOD, ElementType.TYPE})  

@Retention(RetentionPolicy.RUNTIME)  

@Inherited  

public @interface UserSaveCache {  }  

自定义缓存注解示例1--新增和修改

定义

@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 saveUser(User user){} 

修改时使用

@UserSaveCache

public User updateUser(User user){} 

自定义缓存注解示例2--删除

定义

@Caching(  

        evict = {  

                @CacheEvict(value = "user", key = "#user.id"),  

                @CacheEvict(value = "user", key = "#user.username"),  

                @CacheEvict(value = "user", key = "#user.email")  

        }  

)  

@Target({ElementType.METHOD, ElementType.TYPE})  

@Retention(RetentionPolicy.RUNTIME)  

@Inherited  

public @interface UserDelCache {  }

删除时使用

@UserDelCache 

public User deleteUser(User user){}

@Caching查询示例

示例1

@Caching(  

        cacheable = {  

                @Cacheable(value = "user", key = "#id")  

        }  

)  

public User findById(final Long id) 

示例2

@Caching(  

        cacheable = {  

                @Cacheable(value = "user", key = "#username")  

        }  

)  

public User findByUsername(final String username) 

示例3

@Caching(  

        cacheable = {  

                @Cacheable(value = "user", key = "#email")  

        }  

)  

public User findByEmail(final String email)

转载于:https://my.oschina.net/u/3866531/blog/1842301

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值