Spring-Cache的基本使用和缺点

Spring官方支持了一些配置,帮助我们操作缓存。(官方学习文档)可以帮我们简化很多操作,只需要使用几个注解,就可以帮助我们对更改的操作写入到缓存中。(本文推荐使用失效模式

SpringCache的原理:

SpringCache里面有一个CacheManager缓存管理器,可以帮我们造出来很多缓存的组件,这些组件来进行缓存的读写。看源码:

这里创建的就是RedisCache。再来看源码中对缓存的增删改查:

@Override
	protected Object lookup(Object key) {

		byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key));

		if (value == null) {
			return null;
		}

		return deserializeCacheValue(value);
	}

@Override
	@SuppressWarnings("unchecked")
	public synchronized <T> T get(Object key, Callable<T> valueLoader) {

		ValueWrapper result = get(key);

		if (result != null) {
			return (T) result.get();
		}

		T value = valueFromLoader(key, valueLoader);
		put(key, value);
		return value;
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.cache.Cache#put(java.lang.Object, java.lang.Object)
	 */
	@Override
	public void put(Object key, @Nullable Object value) {

		Object cacheValue = preProcessCacheValue(value);

		if (!isAllowNullValues() && cacheValue == null) {

			throw new IllegalArgumentException(String.format(
					"Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.",
					name));
		}

		cacheWriter.put(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue), cacheConfig.getTtl());
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.cache.Cache#putIfAbsent(java.lang.Object, java.lang.Object)
	 */
	@Override
	public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {

		Object cacheValue = preProcessCacheValue(value);

		if (!isAllowNullValues() && cacheValue == null) {
			return get(key);
		}

		byte[] result = cacheWriter.putIfAbsent(name, createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
				cacheConfig.getTtl());

		if (result == null) {
			return null;
		}

		return new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result)));
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.cache.Cache#evict(java.lang.Object)
	 */
	@Override
	public void evict(Object key) {
		cacheWriter.remove(name, createAndConvertCacheKey(key));
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.cache.Cache#clear()
	 */
	@Override
	public void clear() {

		byte[] pattern = conversionService.convert(createCacheKey("*"), byte[].class);
		cacheWriter.clean(name, pattern);
	}

,先介绍一下具体使用方案:

首先引入依赖:

1.引入SpringCache依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
  2.引入redis开发场景
          <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
          <exclusions>

然后要写一下配置,springcache使用的序列化是jdk自带的序列化,会把文件转为二进制。为了更好的可读性和扩展,我们把它转为json。


@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {

    /**
     * 1.原来的配置类形式
     * @ConfigurationProperties(prefix = "spring.cache")
     * public class CacheProperties {
     * 因为这个并没有放到容器中,所以要让他生效 @EnableConfigurationProperties(CacheProperties.class)
     * 因为这个和配置文件已经绑定生效了
     * @return
     */
    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties CacheProperties) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//        config=  config.entryTtl();
        config = config.serializeKeysWith(RedisSerializationContext
                .SerializationPair
                .fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext
                .SerializationPair
                .fromSerializer(new GenericFastJsonRedisSerializer()));

        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;


    }
}

第一个注解代表绑定好配置文件和配置之间的联系,我们看一下源码:

这里中做了绑定,但是没有把其注入到spring容器中,所以我们需要指定其关系,以便从配置文件中读取到值。 所以我们还需要创建一个appliation.properties配置文件

#指定缓存类型
spring.cache.type=redis
#指定存活时间(ms)
spring.cache.redis.time-to-live=86400000
#指定前缀
spring.cache.redis.use-key-prefix=true
#是否缓存空值,可以防止缓存穿透
spring.cache.redis.cache-null-values=true

第三个注解代表开启缓存功能,这样子我们就可以使用一些注解来实现缓存操作了。

 @Cacheable:把数据保存到缓存中
 @CacheEvict: 将数据从缓存删除
 @CachePut: 不影响方法执行更新缓存
 @Caching:组合多个缓存操作
 @CacheConfig:  一个类上共享缓存相同配置

 实操开始:

    @Cacheable(value = {"category"}, key = "'getLevel1Categorys'")
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        System.out.println("getLevel1Categorys");
        List<CategoryEntity> entities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return entities;
    }

当我们使用了这个注解后,一个请求过来,会去缓存中查找,如果没有则查数据库然后放到缓存中。 value指定的就是之前说的分区名字,而key就是这个缓存的key叫什么,这里使用的是SpEl表达式,所以是字符串的话要加单引号也可可以#root.MethedName直接获取方法名字。

这是Redis储存的效果 (另一个是另一个接口的暂时不管),我们清空控制台然后刷新页面再次请求就可以发现这次控制台没有打印任何东西,而是直接走的缓存。

那么如果我们要对一个数据进行修改呢怎么做?

这里使用的是失效模式,我们只需要加上这个注解,value依然是指定的分区名字,key是需要删除的缓存的key,这样当做出删除操作后,直接会对所指定的缓存数据进行删除,然后当新的请求来后就在缓存中找如果没有则进入数据库中查找并放入缓存中,如果我们要删除多个接口怎么办呢?

//删除多个缓存方式1    @Caching(evict = {
//            @CacheEvict(value = {"category"}, key = "'getLevel1Categorys'"),
//            @CacheEvict(value = {"category"}, key = "'getCatalogJson'")
//    })
    @CacheEvict(value = {"category"},allEntries = true)
    public void updateCascade(CategoryEntity category) {
        System.out.println("删除");
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }

两种方式,一是每一个都去指定然后删除,而是用分区代表所需要删除的分区是什么,allEntries = true表示直接清空掉整个分区,这样就达到了效果。

SpringCache的不足:

1.读模式:

        1.缓存穿透:查询一个null数据。解决方法:准许缓存一个空数据,在配置文件中指定即可。

        2.缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方法:加锁。

        3.缓存雪崩:大量的key同时过期,解决方法:加随机时间(容易弄巧成拙),加上过期时间

2.写模式:

         1.读写加锁,适用于读多写少的系统。

         2.引入canal,感知到mysql的更新了数据库。

        3..读多写多,建议直接查数据库吧。

现在从源码角度分析,缓存击穿怎么解决:

 第一个get加了本地锁Syc,虽然不不是分布式锁但是一定程度上可以起到作用,稍后我们来debug打断点看其是否起到作用。

第一次请求发现用的 是lookup方法而不是get先不管我们继续往下走,

前面都是一些判断,因为缓存为空,所以走到这里会执行业务逻辑方法,去数据库中查询数据。

然后回到这里,将我们查到的数据进行包装, 我们继续放行:

来到了RedisCache的put方法,然后整个业务流程结束。 

我们发现默认是无加锁的,但那个get的方法意义呢?

这有一个属性,sync默认false,当我们改为true后看注释的解释:

 将其改为true后,则会加入本地锁。我们再debug看一下:

这次直接进入到了加看本地锁的方法中, 看第一行代码,先调用第一个get方法,而这个方法就是之前的lookup方法,如果缓存中有直接返回,如果没有:valueFromLoader(key, valueLoader);从加载器中读值,放入缓存中。这里面调用了一个call方法,call方法调用的是

这个我们之前见过,就是执行本地业务代码的,然后直接返回,然后调用put方法进行保存。

我们发现只有查询是加锁,其他的都是公用的没有加锁。读模式做了处理,写模式并没有管。

总结:

        常规(读多写少,及时性,一致性不高的数据完全可以用SpringCache),

       如果是要求高的特殊数据需要特殊处理。

Redisson锁和Spring Cache是两个不同的概念,但它们可以在同一个应用中一起使用来实现分布式缓存和分布式锁的功能。 Redisson是一个基于Redis的Java驻留内存数据网格(In-Memory Data Grid)和分布式应用程序框架。它提供了一套丰富的分布式对象和服务,包括分布式锁、分布式集合、分布式对象、分布式消息队列等。通过使用Redisson,我们可以方便地在分布式环境中实现数据的共享和同步。 Spring CacheSpring框架提供的一个缓存抽象层,它可以与不同的缓存技术集成,包括Redis。Spring Cache通过使用缓存注解,如@Cacheable、@CachePut和@CacheEvict,来简化应用程序中的缓存操作。通过使用Spring Cache,我们可以将方法的结果缓存到Redis中,以提高应用程序的性能和响应速度。 当Redisson锁和Spring Cache一起使用时,我们可以在需要对某个方法进行同步控制的同时,将方法的计算结果缓存到Redis中。这样可以避免多个线程同时执行相同的方法,提高系统的并发性能。例如,我们可以通过在方法上添加@Cacheable注解,并使用Redisson提供的分布式锁来确保只有一个线程可以执行该方法,并将结果缓存到Redis中。 需要注意的是,Redisson锁和Spring Cache是两个独立的功能模块,它们可以单独使用,也可以结合使用。在使用时,需要根据具体的业务需求和技术架构来选择适合的方案。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值