基础17-缓存

JSR107

JSR是Java-Specification-Requests的缩写,意思是Java规范提案。

JSR107中Java Caching定义了5个核心接口,分别是:CachingProvider,CacheManager、Cache、Entry、Expiry;

接口名称

接口作用

CachingProvider

定义了创建、配置、获取、管理、控制多个CacheManager,一个应用可以在运行期访问多个CachingProvider;

CacheManager

定义了创建、配置、获取、管理、控制多个唯一命名的Cache,这些Cache存在于CacheManager的上下文中,一个CacheManager仅仅被一个CachingProvider所拥有;

Cache

一个类似于Map的数据结构并临时存储以Key作为索引的值,一个Cache仅仅被一个CacheManager所拥有;

Entry

一个存储在Cache中的key-value对

Expiry

每一个存储在Cache中的条目有一个定义的有效期,一旦超过这个时间,条目为过期状态,一旦过期,条目将不可访问、更新、删除;有效期可以通过ExpiryPolicy设置;

JSR107其实是一套接口,是一种规范,需要市场上其他的缓存框架去遵守,去实现该接口,但实际情况是,JSR107规范使用的不多;

<dependency>

            <groupId>javax.cache</groupId>

            <artifactId>cache-api</artifactId>

</dependency>

Spring缓存抽象

Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存框架,并支持使用JSR107注解来简化开发;

①Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;

②Cache接口下Spring提供了各种XXCache的实现,比如常见的RedisCache、EhCacheCache等;

③每次调用需要缓存功能的方法时,Spring会检查指定参数的指定目标方法是否已经被调用过,如果被调用过则直接从缓存中获取方法调用后的结果,如果没有则调用方法并缓存结果后再返回给用户,下次调用就直接从缓存中获取;

④使用Spring缓存对象时需要注意2点:确定方法需要被缓存以及对应的缓存策略、从缓存中读取之前缓存的数据;

这里有几个常见的概念与注解:

Cache

缓存接口,定义缓存操作,其常见实现有:RedisCache、EhCacheCache、ConcurrentMapCache等

CacheManager

缓存管理器,负责管理各种缓存Cache组件

@Cacheable

主要针对方法配置,能够根据方法的请求参数对其结果进行缓存

@CacheEvict

清空缓存

@CachePut

保证方法被调用,又希望结果被缓存

@EnableCaching

开启基于注解的缓存

keyGenerator

缓存数据时key的生成策略

serialize

缓存数据时value序列化策略

Spring基础缓存注解

验证项目,还是使用之前已经写好的druid + jpa项目,且druid可以从侧面验证缓存效果,下面是基础验证逻辑:

第一步:加入chache的<dependency/>依赖;

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

第二步:@EnableCaching开启基于注解的缓存;注意这里要在@SpringBootApplication上添加:

@MapperScan(value = "com.springboot.web.mapper")

@SpringBootApplication

@EnableCaching

public class SpringBootMainApplication {

    public static void main(String[] args) {

        SpringApplication.run(SpringBootMainApplication.class, args);

    }

}

第三步:标注缓存注解;

注解名称

作用

参数

@Cacheable

将方法的运行结果进行缓存,以后遇到相同的数据,则直接从缓存中获取,不用调用方法;

cacheNames/value:指定缓存组件的名字;

key:缓存数据使用的key,可以用它来指定;默认是使用方法参数的值;可以使用SpEL表达式来表达;

keyGenerator:key的生成器,通过该属性来指定key的生成器的组件id;该属性与key只能二选一

cacheManager:指定缓存管理器;或者通过cacheResolver来指定缓存缓存解析器;该属性与cacheResolver也是二选一

condition:指定符合条件的情况下才进行缓存;也可以使用SpEL来表达;

unless:否定缓存,当unless指定的条件为true,则方法返回值不会被缓存;可以获取到结果进行判断;

sync:是否使用异步模式;

@CachePut

既调用方法,同时又更新缓存数据;

主要用于修改了数据库的某个数据,同时更新缓存,这样即调用了方法,也同时更新了缓存数据;

跟@Cacheable一致;

注意事项:该cache的key要与其他cache中的key保持一致,否则其他cache不会更新缓存;

@CacheEvict

缓存清除;

与@CachePut类似,@CachePut针对的是update业务场景,而@CacheEvict针对的是delete业务场景;

allEntries:是否清除全部缓存,默认是false,即不清除全部缓存;

beforeInvocation:是否先执行清理缓存,后执行方法;默认是false即先执行方法,后清理缓存,这样当方法抛出异常后,缓存就不会被清理;

@Caching

@Cacheable + @CachePut + @CacheEvict组合

就是把3个注解拼接到一起,规则跟具体某个注解保持一致;

@CacheConfig

该注解用于注解类,其他注解用于注解方法;

该注解可以把其他注解的公共属性抽取出来,配置在@CacheConfig中;

 

注意事项:

①@CachePut注解的方法必须被执行,而@Cacheable注解的方法则不一定被执行;

②@CacheEvict/@CachePut/@Cacheable注意缓存时的key要保持一致,否则就会当成2个独立的缓存进行存储;

注解常用的SpEL基础元素

名字

位置

描述

示例

methodName

root object

当前被调用的方法名字

#root.methodName

method

root object

当前被调用的方法

#root.method.name

target

root object

当前被调用的目标对象

#root.target

targetClass

root object

当前被调用的目标对象类

#root.targetClass

args

root object

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

#root.args[0]

caches

root object

当前方法调用使用的缓存列表,即配置的cacheNames/value属性的数量

#root.caches[0].name

argumentname

evaluation context

方法参数的名字,可以直接用#参数名、#p0、#a0的形式来表示,0代表参数的索引;

#id/#a0/#p0

result

evaluation context

方法执行后的返回值,指的是满足unless、condition、beforeInvocation=false后的返回值;

#result

注解基础代码

    @GetMapping("/userinfo/{id}")

//    @Cacheable(cacheNames = {"userinfo"},key = "#root.args[0]")

    @Cacheable(cacheNames = {"userinfo"},keyGenerator = "testKeyGenerator")

    public Userinfo getUserInfoById(

            @PathVariable

                    Integer id){

        System.out.println("-------------------->");

        Optional<Userinfo> userinfo = userInfoRepository.findById(id);

        return userinfo.get();

}

--------------------------------------------------------------------------------------------

@Configuration

public class MyKeyGenerator {

 

    @Bean("testKeyGenerator")

    public KeyGenerator keyGenerator(){

        return new KeyGenerator() {

            @Override

            public Object generate(Object o, Method method, Object... objects) {

                return method.getName() + "[" + Arrays.asList(objects).toString() + "]";

            }

        };

    }

}

效果:

注解基础源码

打开CacheAutoConfiguration,先看一下类声明:

@Configuration(proxyBeanMethods = false)

@ConditionalOnClass(CacheManager.class)

@ConditionalOnBean(CacheAspectSupport.class)

@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")

@EnableConfigurationProperties(CacheProperties.class)

@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,

HibernateJpaAutoConfiguration.class, RedisAutoConfiguration.class })

@Import({ CacheConfigurationImportSelector.class,

CacheManagerEntityManagerFactoryDependsOnPostProcessor.class })

public class CacheAutoConfiguration {

.............

}

属性以spring.cache开头;

继续跟踪CacheConfigurationImportSelector源码:

static class CacheConfigurationImportSelector implements ImportSelector {

@Override

public String[] selectImports(AnnotationMetadata importingClassMetadata) {

CacheType[] types = CacheType.values();

String[] imports = new String[types.length];

for (int i = 0; i < types.length; i++) {

imports[i] = CacheConfigurations.getConfigurationClass(types[i]);

}

return imports;

}

}

通过debug方式,可以看到该方法返回如下数组:

这里找一个最简单的cache,即SimpleCacheConfiguration,以这个为例来大致浏览下:

@Configuration(proxyBeanMethods = false)

@ConditionalOnMissingBean(CacheManager.class)

@Conditional(CacheCondition.class)

class SimpleCacheConfiguration {

@Bean

ConcurrentMapCacheManager cacheManager(CacheProperties cacheProperties,

CacheManagerCustomizers cacheManagerCustomizers) {

ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();

List<String> cacheNames = cacheProperties.getCacheNames();

if (!cacheNames.isEmpty()) {

cacheManager.setCacheNames(cacheNames);

}

return cacheManagerCustomizers.customize(cacheManager);

}

}

这里关键的是ConcurrentMapCacheManager,继续观察下这个类的类声明:

public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderAware {

}

接口CacheManager只有2个方法:getCache、getCacheNames,那我们重点关注下getCache方法:

@Nullable

    public Cache getCache(String name) {

        Cache cache = (Cache)this.cacheMap.get(name);

        if (cache == null && this.dynamic) {

            synchronized(this.cacheMap) {

                cache = (Cache)this.cacheMap.get(name);

                if (cache == null) {

                    cache = this.createConcurrentMapCache(name);

                    this.cacheMap.put(name, cache);

                }

            }

        }

        return cache;

    }

这里看一下变量cacheMap是ConcurrentMap<String, Cache>类型,getCache方法还是用的synchronized来进行同步;

Web应用上的缓存,肯定涉及到多线程的问题,到时候看看其他的CacheConfiguration是怎么处理多线程问题的;

如何启用Redis来作为缓存

第一步:添加Redis对应的<dependency/>;

<dependency>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-starter-data-redis</artifactId>

</dependency>

第二步:根据个人需要,来使用RedisTemplate或者StringRedisTemplate;

@Autowired

RedisTemplate redisTemplate; //k-v都是object

@Autowired

StringRedisTemplate stringRedisTemplate; // k-v都是字符串

第三步:根据数据结构,去获取数据,例如:

stringRedisTemplate.opsFor数据结构().该数据结构对应的命令;

例如:

stringRedisTemplate.opsForValue().append(“msg”,”hello word”);

String msg = stringRedisTemplate.opsForValue().get(“msg”);

RedisAutoConfiguration源码分析

先查看一下该类的源码:

@Configuration(proxyBeanMethods = false)

@ConditionalOnClass(RedisOperations.class)

@EnableConfigurationProperties(RedisProperties.class)

@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })

public class RedisAutoConfiguration {

@Bean

@ConditionalOnMissingBean(name = "redisTemplate")

public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)

throws UnknownHostException {

RedisTemplate<Object, Object> template = new RedisTemplate<>();

template.setConnectionFactory(redisConnectionFactory);

return template;

}

@Bean

@ConditionalOnMissingBean

public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)

throws UnknownHostException {

StringRedisTemplate template = new StringRedisTemplate();

template.setConnectionFactory(redisConnectionFactory);

return template;

}

}

这里有2个@Bean注解,说明要往IOC容器中注册2个Bean组件,这也从侧面证明了第2步中对应的RedisTemplate/StringRedisTemplate;

Redis属性配置都是以spring.redis来开头的;

这里重点关注下RedisTemplate类的几个变量:

    private boolean enableTransactionSupport = false;

    private boolean exposeConnection = false;

    private boolean initialized = false;

    private boolean enableDefaultSerializer = true;

    @Nullable

    private RedisSerializer<?> defaultSerializer;

    @Nullable

    private ClassLoader classLoader;

    @Nullable

    private RedisSerializer keySerializer = null;

    @Nullable

    private RedisSerializer valueSerializer = null;

    @Nullable

    private RedisSerializer hashKeySerializer = null;

    @Nullable

    private RedisSerializer hashValueSerializer = null;

    private RedisSerializer<String> stringSerializer = RedisSerializer.string();

    @Nullable

    private ScriptExecutor<K> scriptExecutor;

    private final ValueOperations<K, V> valueOps = new DefaultValueOperations(this);

    private final ListOperations<K, V> listOps = new DefaultListOperations(this);

    private final SetOperations<K, V> setOps = new DefaultSetOperations(this);

    private final StreamOperations<K, ?, ?> streamOps = new DefaultStreamOperations(this, new ObjectHashMapper());

    private final ZSetOperations<K, V> zSetOps = new DefaultZSetOperations(this);

    private final GeoOperations<K, V> geoOps = new DefaultGeoOperations(this);

    private final HyperLogLogOperations<K, V> hllOps = new DefaultHyperLogLogOperations(this);

    private final ClusterOperations<K, V> clusterOps = new DefaultClusterOperations(this);

这里有几个RedisSerializer类型的变量,而RedisSerializer是一个接口,下面是Spring Boot提供的接口实现类:

为什么会有RedisSerializer类型的key/value呢,这是因为默认情况下,keySerializer/valueSerializer/hashKeySerializer/hashValueSerializer都取自defaultSerializer,而defaultSerializer又是JdkSerializationRedisSerializer类型,即一般情况下,放在Redis的value会采用Serializer方式来存储到Redis中;

可以看看deserializeMixedResults方法源代码,当value取特定值的时候,会要求value必须实现Serializer接口;

如果我想把Object按照json方式存储到Redis,那么有2种方式:

①自己通过第三方jar包,把object转化成json串;

②自己编写一个@Configuration类,里面生成一个RedisTemplate/StringRedisTemplate的Bean,通过setValueSerializer方法,把Spring Boot提供的Jackson2JsonRedisSerializer传入即可;

(valueSerializer是通过@Nullable来注解的,因此无法通过属性配置文件来配置)

RedisCacheManager

每一种cache都对应一个CacheManager,这里是RedisCacheManager,通过自定义CacheManager,然后注解中通过cacheManager属性来配置自定义CacheManager即可;

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值