当前位置:   article > 正文

SpringCache_spring-cache

spring-cache

简介

给每一个需要缓存的业务加上缓存的使用逻辑。
缓存的使用,就得考虑缓存的两种用法模式

  • 读模式:先从缓存中读取,缓存没有,在从数据库中读取,并把数据放到缓存中,然后返回数据
  • 写模式:如何保证缓存与数据库中的数据是一致的?可以使用双写模式或者失效模式
    • 双写模式:写完数据库,缓存跟着改一下
    • 失效模式: 写完数据库,对应的缓存删了,等待下次主动查询,更新缓存即可
    • 脏数据的问题,可以通过加读写锁解决

如果每一个需要使用缓存的业务代码,都是这种编码模式,每次都这样写,要麻烦了,希望有一个简单的方式整合使用缓存。

基于此,spring专门抽取一个叫SpringCache的框架,是对缓存的一个抽象层。

Spring3.1开始定义了org.springframework.cache.Cacheorg.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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

配置:

spring:
  cache:
    type: redis
    redis:
      ##存活时间,毫秒为单位
      time-to-live: 100000
      ##key的前缀,建议不配置这个,让它默认使用分区名做前缀
      key-prefix: CACHE_
      ##是否使用前缀
      use-key-prefix: true
      ##是否缓存控制,防止缓存穿透
      cache-null-values: true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

使用:

  • @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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

@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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

还是上面的接口,再次测试:发现缓存里的数据变成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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

@CacheEvict的使用

数据库数据修改,删除缓存

//失效模式的使用
@CacheEvict(value = "category",key = "'getLevel1Categorys'")
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
    this.updateById(category);
    categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

如果修改了数据库,要删除的缓存不知一处呢?

    //同时进行多种缓存操作
    @Caching(evict ={
            @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
            @CacheEvict(value = "category",key = "'getCatalogJson'")
    } )
  • 1
  • 2
  • 3
  • 4
  • 5

或者

    //allEntries = true指定删除category分区内的所有数据
    @CacheEvict(value = "category",allEntries = true)
  • 1
  • 2

自定义key生成策略

https://blog.csdn.net/liuyueyi25/article/details/118422143
  • 1

如果希望使用自定义的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) + ")";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

然后在使用的地方,利用注解中的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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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):写模式(只要缓存的数据有过期时间就足够了)
 *      特殊数据:特殊设计
 * 
 * 
 * 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

@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);
}
入参不能太多,太多是不适合直接上缓存的,可以把接口进一步封装,针对具体的业务进一步包的高级一点,减少入参
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

当然还可以使用单引号自定义字符串作为缓存的key值

@Cacheable(key = "'countUsers'")
public int countUsers() {
    return userMapper.countUsers();
}
  • 1
  • 2
  • 3
  • 4
本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/785258
推荐阅读
相关标签
  

闽ICP备14008679号