Introduce
Spring 3.1 引入了激动人心的基于凝视(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(比如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中加入少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。
springCache 是spring整合的
缓存官方文档
image.pngCache Abstraction 缓存抽象,如其名字,它本质上不是一个具体的缓存实现方案(比如EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中加入少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。
不用SpringCache的缓存
//缓存改写2:使用redis作为本地缓存
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
String catalogJson = ops.get("catalogJson");
if (StringUtils.isEmpty(catalogJson)) {
Map<String, List<Catalog2Vo>> categoriesDb = getCategoriesDb();
String toJSONString = JSON.toJSONString(categoriesDb);
ops.set("catalogJson",toJSONString);
return categoriesDb;
}
Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {});
return listMap;
使用SpringCache
pom
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--jedis客户端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--缓存-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
redis.yml
spring:
redis:
host: 192.168.16.129
port: 6379
cache:
type: redis
redis:
time-to-live: 3600000
#是否缓存空值,防止缓存穿透
cache-null-values: true
bootstrap-dev.properties
spring.profiles.active=dev
spring.application.name=gulimail-product-config
spring.cloud.nacos.config.server-addr=xxxxxx:8848
spring.cloud.nacos.config.file-extension=yaml
spring.cloud.nacos.config.group=gulimail_product_group
spring.cloud.nacos.config.namespace=72616c9f-6977-460d-9a1b-b43859b57beb
spring.cloud.nacos.config.ext-config[0].data-id=gulimail-product-redis-dev.yaml
spring.cloud.nacos.config.ext-config[0].group=gulimail_product_group
spring.cloud.nacos.config.ext-config[0].refresh=true
配置类
package com.pl.gulimail.product.config;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableConfigurationProperties(CacheProperties.class)//不加这个注解,配置文件不生效
@Configuration
@EnableCaching
public class MyCacheConfig {
//要想配置文件生效 @EnableConfigurationProperties(CacheProperties.class)
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//序列化key value
config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
//过期时间
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
//缓存件有没有前缀
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
//是否缓存空数据
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
//是否用缓存前缀
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
开启缓存
@EnableCaching
package com.pl.gulimail.product;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@EnableCaching
@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.pl.gulimail.product.sql.dao")
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients(basePackages = {"com.pl.gulimail.product.sql.feign"})
public class GulimalProductApplication {
public static void main(String[] args) {
SpringApplication.run(GulimalProductApplication.class, args);
}
}
测试使用缓存
-
@Cacheable
: Triggers cache population. 处罚将数据保存到缓存的操作。 -
@CacheEvict
: Triggers cache eviction. 触发将数据从缓存中删除的操作。 -
@CachePut
: Updates the cache without interfering with the method execution. 不影响方法执行更行缓存。 -
@Caching
: Regroups multiple cache operations to be applied on a method. 组合史上多个操作。 -
@CacheConfig
: Shares some common cache-related settings at class-level.在类级别共享缓存配置。
code
读模式
//value 缓存分区 按照业务类型分区,
//@Cacheable(value = {"category","sss"},key = "#root.methodName",sync = true) 这条数据往category,sss两个区中存放数据
@Cacheable(value = {"category"})
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithSpringCache() {
return getCategoriesDb();
}
SimpleKey是默认给的名字,数据是jdk序列化的样子
image.png
指定key并引入配置类,value数据为json格式后
//value 缓存分区 按照业务类型分区,
//@Cacheable(value = {"category","sss"},key = "#root.methodName",sync = true) 这条数据往category,sss两个区中存放数据
@Cacheable(value = {"category"},key = "#root.methodName",sync = true)
public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithSpringCache() {
return getCategoriesDb();
}
可以发现key为方法名,TTL失效时间也按照配置文件中指定的一样生效了。该方法只有在第一次访问或者缓存失效后会被调用查询数据库,其余时间直接从缓存中读取数据。
image.pngimage.png
notice:
n1使用@Cacheable的特点:
1.如果方法中有姐不在调用了。
2.缓存的数据,默认的key是默认生成的,缓存名字:: SimpleKey(自动生成的key值)
3.缓存的value,默认使用jdk序列化机制,将序列化后的数据缓存到redis。
4.默认ttl过期时间 -1。永不过期。
自定义:
1.指定生成缓存中的key(注解上指定key)
2.指定缓存存活时间
3.将数据保存为json格式
key:
image.png支持SqEL动态获取key,
如果想用方法名作为key
#root.methodName
如果想获取方法的参数
{@code #root.args[1]}, {@code #p1} or {@code #a1}
配置文件中指定前缀
image.png测试缓存空值和指定前缀
image.png不使用缓存前缀
image.png没有前缀,只显示指定的key
image.png写模式
指定删除指定的一个key
/**
* 级联更新所有数据
* CacheEvict: 失效模式
* @param category
*/
@Override
@Transactional
@CacheEvict(value = {"category"},key = "'getCatalogJsonDbWithSpringCache'") //指定删除哪个分区,哪个key
public void updateCascade(CategoryEntity category) {
this.updateById(category);
}
key是遵循SqEL表达式的,字符串需要加单引号。
执行更行操作
image.png image.png指定删除多个key
/**
* 级联更新所有数据
* CacheEvict: 失效模式
* @param category
*/
@Override
@Transactional
@Caching(evict = {
@CacheEvict(value = {"category"},key = "'getCategoryLeve1'"),
@CacheEvict(value = {"category"},key = "'getCatalogJsonDbWithSpringCache'")
})
public void updateCascade(CategoryEntity category) {
this.updateById(category);
}
指定删除区中的的所有key
/**
* 级联更新所有数据
* CacheEvict: 失效模式
* @param category
*/
@Override
@Transactional
@CacheEvict(value = {"category"},key = "'getCatalogJsonDbWithSpringCache'",allEntries = true)
public void updateCascade(CategoryEntity category) {
this.updateById(category);
}
双写模式
这次更新完事后,将数据放到缓存中一份,方法中必须有返回值
/**
* 级联更新所有数据
* CacheEvict: 失效模式
* @param category
*/
@Override
@Transactional
@CacheEvict(value = {"category"},key = "'getCatalogJsonDbWithSpringCache'",allEntries = true)
@cachePut
public CategoryEntity updateCascade(CategoryEntity category) {
return this.updateById(category);
}
Spring-Cache的不足之处
1.读模式(@Cacheable)
缓存穿透:查询一个null数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;
使用sync = true来解决击穿问题
缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间,即使通过配置文件指定了相同的随机时间,但是每次出发写缓存的时间都不一致,所以就算TTL时间相同,但是彼此失效的时间不同。
可以看出Spring-Cache对读操作的所有问题,都是有对应的解决办法的。
notice:
只有@Cacheable有锁,其他注解没有加锁的属性。
<wiz_code_mirror><pre class=" CodeMirror-line " role="presentation">@Cacheable(value = {"category"},key = "#root.methodName",sync = true)</pre></wiz_code_mirror>
2.写模式:(缓存与数据库一致)
因为@CacheEvict,@CachePut都没有锁属性,所以都不可以加锁,这些注解对于缓存雪崩的场景是没法应对的。
解决办法:
a、读写加锁。
b、引入Canal,感知到MySQL的更新去更新Redis
c 、读多写多,直接去数据库查询就行
3.总结:
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):
写模式(只要缓存的数据有过期时间就足够了)
特殊数据:特殊设计
网友评论