目录
简介
给每一个需要缓存的业务加上缓存的使用逻辑。
缓存的使用,就得考虑缓存的两种用法模式
- 读模式:先从缓存中读取,缓存没有,在从数据库中读取,并把数据放到缓存中,然后返回数据
- 写模式:如何保证缓存与数据库中的数据是一致的?可以使用双写模式或者失效模式
- 双写模式:写完数据库,缓存跟着改一下
- 失效模式: 写完数据库,对应的缓存删了,等待下次主动查询,更新缓存即可
- 脏数据的问题,可以通过加读写锁解决
如果每一个需要使用缓存的业务代码,都是这种编码模式,每次都这样写,要麻烦了,希望有一个简单的方式整合使用缓存。
基于此,spring
专门抽取一个叫SpringCache
的框架,是对缓存的一个抽象层。
Spring3.1
开始定义了org.springframework.cache.Cache
和org.springframework.cache.CacheManager
接口来统一不同的缓存技术并使用JCache(JSR-107)
注解简化开发;
整合SpringCache简化缓存开发
依赖:
<!--SpringCache-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--引入redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置:
spring:
cache:
type: redis
redis:
##存活时间,毫秒为单位
time-to-live: 100000
##key的前缀,建议不配置这个,让它默认使用分区名做前缀
key-prefix: CACHE_
##是否使用前缀
use-key-prefix: true
##是否缓存控制,防止缓存穿透
cache-null-values: true
使用:
- @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.:在类级别共享缓存的相同配置
启动类:
启动类开启缓存功能:@EnableCaching
@Cacheable使用
有些情形下注解式缓存是不起作用的:同一个bean
内部方法调用,子类调用父类中有缓存注解的方法等。后者不起作用是因为缓存切面必须走代理才有效,这时可以手动使用CacheManager来获得缓存效果。
//首页接口
@GetMapping(value = {"/","index.html"})
private String indexPage(Model model) {
//1、查出所有的一级分类
List<CategoryEntity> categoryEntities = categoryService.getLevel1Categorys();
//给model中放数据,就会放到页面的请求域中
model.addAttribute("categories",categoryEntities);
//视图解析器会自动进行拼串
//classpath:/templates/+返回值+.html
return "index";
}
/**
* 获取以及分类(首页用)
* @return
*/
//value表示分区,key表示缓存的键,可以使用spel表达式
//key="'xx'" 常量
@Cacheable(value = "category",key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys() {
long start = System.currentTimeMillis();
List<CategoryEntity> entities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
long end = System.currentTimeMillis();
System.out.println("消耗时间:" + (end - start));
return entities;
}
@Cacheable(value = “category”,key = “#root.method.name”)
代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。
如果缓存中没有,会调用方法,最后将方法的结果放入缓存
这一个注解就相当于读模式
每一个需要缓存的数据我们都来指定要放到哪个名字的缓存[缓存的分区(按照业务类型分)]
测试:访问当前接口后。redis里自动创建了缓存,以后再访问改接口,就直接走缓存了。仅仅一个注解就搞定了读模式的缓存使用,简直不要太爽
发现:
默认时间为-1
,永久;
缓存的value
值,默认使用jdk
序列化机制,将序列化后的数据存到redis
El表达式使用的例子:
原理
SpringCache
原理:
- CacheAutoConfiguration->RedisCacheConfiguration->
- 自动配置了RedisCacheManager->初始化所有缓存->每个缓存决定使用什么配置
- ->如果redisCacheConfiguration有就用已经有的,没有就用默认的
- ->所以如果想自定义缓存配置,只需要给容器中放一个RedisCacheConfiguration,即可
- ->就会应用到当前RedisCacheManager管理的所有缓存分区中
自定义缓存配置
设置key,value
的序列化:
@Configuration
@EnableCaching
public class MyCacheConfig {
@Bean
RedisCacheConfiguration redisCacheConfiguration(){
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
//设置key,value的序列化机制
redisCacheConfiguration = redisCacheConfiguration.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));
return redisCacheConfiguration;
}
}
还是上面的接口,再次测试:发现缓存里的数据变成json
格式了
发现配置文件里的配置没生效。
修改:
@EnableConfigurationProperties({CacheProperties.class})
@Configuration
@EnableCaching
public class MyCacheConfig {
/**
* 1。原来和配置文件绑定的配置类是这样子的
* @ConfigurationProperties(prefix = "spring.cache")
* 2.要让它生效,当前配置类@EnableConfigurationProperties({CacheProperties.class}),
* 当前类开启属性配置的绑定功能
* 3.从容器获取CacheProperties配置,就能获取到配置文件中的所有配置了
* 4.在把配置设置上即可
*
* @param cacheProperties:会自动从ioc容器中获取
* @return
*/
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
//设置key,value的序列化机制
redisCacheConfiguration = redisCacheConfiguration.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
//设置配置文件中的配置
if (redisProperties.getTimeToLive() != null) {
redisCacheConfiguration = redisCacheConfiguration.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
redisCacheConfiguration = redisCacheConfiguration.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
redisCacheConfiguration = redisCacheConfiguration.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
redisCacheConfiguration = redisCacheConfiguration.disableKeyPrefix();
}
return redisCacheConfiguration;
}
}
@CacheEvict的使用
数据库数据修改,删除缓存
//失效模式的使用
@CacheEvict(value = "category",key = "'getLevel1Categorys'")
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}
如果修改了数据库,要删除的缓存不知一处呢?
//同时进行多种缓存操作
@Caching(evict ={
@CacheEvict(value = "category",key = "'getLevel1Categorys'"),
@CacheEvict(value = "category",key = "'getCatalogJson'")
} )
或者
//allEntries = true指定删除category分区内的所有数据
@CacheEvict(value = "category",allEntries = true)
自定义key生成策略
https://blog.csdn.net/liuyueyi25/article/details/118422143
如果希望使用自定义的key生成策略,只需继承KeyGenerator,并声明为一个bean
@Component("selfKeyGenerate")
public static class SelfKeyGenerate implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getSimpleName() + "#" + method.getName() + "(" + JSON.toJSONString(params) + ")";
}
}
然后在使用的地方,利用注解中的keyGenerator来指定key生成策略
/**
* 对应的redisKey 为: get vv::ExtendDemo#selfKey([id])
*
* @param id
* @return
*/
@Cacheable(value = "vv", keyGenerator = "selfKeyGenerate")
public String selfKey(int id) {
return "selfKey:" + id + " --> " + UUID.randomUUID().toString();
}
SpringCache的不足
* 4、Spring-Cache的不足之处:
* 1)、读模式
* 缓存穿透:查询一个null数据。
解决方案:缓存空数据,spring-cache可以解决cache-null-values: true
* 缓存击穿:大量并发进来同时查询一个正好过期的数据。
解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题,这个加的是本地锁,
高并发下,可能同一个微服务的每一个集群节点,都会去查一次数据库,有缓存之后,才都从缓存里获取数据
不过无所谓了,对数据库几乎没啥影响@Cacheable(value = "category",key = "#root.method.name",sync =true)
如果要用分布式锁Spring-Cache,做不到,得自己写
* 缓存雪崩:大量的key同时过期。
解决:加随机时间。spring-cache可以解决加上过期时间time-to-live: 100000
* 2)、写模式:(缓存与数据库一致)
* 1)、读写加锁。
* 2)、引入Canal,感知到MySQL的更新去更新Redis
* 3)、读多写多,直接去数据库查询就行
*
* 总结:
* 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)
* 特殊数据:特殊设计
*
*
*
@Cacheable使用两个或多个参数作为缓存的key
@Cacheable使用两个或多个参数作为缓存的key
常见的如分页查询:使用单引号指定分割符,最终会拼接为一个字符串
接口要包的高级一点,不然不好上缓存;通用接口入参很多,不适合用缓存
@Cacheable(key = "#page+'-'+#pageSize")
public List<User> findAllUsers(int page,int pageSize) {
int pageStart = (page-1)*pageSize;
return userMapper.findAllUsers(pageStart,pageSize);
}
入参不能太多,太多是不适合直接上缓存的,可以把接口进一步封装,针对具体的业务进一步包的高级一点,减少入参
当然还可以使用单引号自定义字符串作为缓存的key值
@Cacheable(key = "'countUsers'")
public int countUsers() {
return userMapper.countUsers();
}