首先,为什么时候缓存,就不用说了;
一、使用SpringCache+Redis实现缓存
SpringCache使用Cache和CacheManager接口来统一不同的缓存技术,而Redis只是其中的一种实现,SpringCache的官方手册:
1、pom.xml:
<!--使用SpringCache实现缓存--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!--SpringCache缓存实现我们选择Redis,而redis客户端我们使用Jedis--> <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> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
2、配置文件中,我们只需要配置Redis的连接信息和缓存使用Redis即可:
#我们使用redis作为springCache的缓存实现 spring: redis: host: 192.168.174.141 port: 6379 timeout: 5000 cache: type: redis
3、在启动类上开启缓存:
@EnableCaching
4、使 @用Cacheable 注解,表示对某个方法的返回结果进行缓存
@Override @Cacheable(cacheNames = {"category"}, key = "#root.method.name") public List<CategoryEntity> getLevel1Categorys() { System.out.println("进入getLevel1Categorys方法"); return this.list(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0)); }
5、测试,看看缓存是否生效,我们,多访问几次首页后:
显然,缓存已经生效;但是缓存的TTL=-1,即永不过期,所以我们可以在配置文件中增加一个缓存过期时间;
spring: cache: type: redis redis: time-to-live: 60000 #单位ms
再次测试:
二、将缓存在Redis数据保存为json格式
Redis实现SpringCache的原理:
CacheAutoConfiguration —> RedisCacheConfiguration ——> 自动配置了 RedisCacheManager ——>初始化所有的缓存,每个缓存决定使用什么配置 ——>如果 RedisCacheConfiguration 有就用已有的,没有就使用默认配置
——>所以,如果想修改缓存的配置,只需要给容器中放一个 RedisCacheConfiguration 即可
——>就会应用到当前RedisCacheManager管理的所有缓存分区中
1、新增我们自己的缓存配置 MyCacheConfig.java
@Configuration @EnableCaching public class MyCacheConfig { @Autowired StringRedisTemplate redis; @Bean RedisCacheConfiguration redisCacheConfiguration(){ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return config; } }
2、再次进行测试:
显然,我们的Json序列化已经完成了,但是出现了另一个问题,就是TTL时间又变为-1(即永不过期了);
该怎么办呢?
3、我们需要将原有的缓存配置,拷贝一份出来,到新的缓存配置中:
//让CacheProperties的配置生效 @EnableConfigurationProperties(CacheProperties.class) @Configuration @EnableCaching public class MyCacheConfig { /** * 将原配置文件中的所有配置,都继承过来 * @return */ @Bean RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){ RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .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; } }
4、再次测试:
5、再看看其他几个配置项:
spring: cache: type: redis redis: time-to-live: 60000 #单位ms key-prefix: CACHE_ #添加统一缓存前缀 use-key-prefix: true #是否使用上面添加的缓存前缀 cache-null-values: true #是否缓存null空值,防止缓存穿透
三、为不同的cacheName设置不同的过期时间
如果,我们想为不同的场景下,不同的cacheName配置不同的缓存参数,如过期时间怎么办?
1、我们在配置文件中,自定义配置:application.yml:
cache: specs: category1: timeToLiveInSeconds: 60 category2: timeToLiveInSeconds: 200
2、使用配置对象收集:
CacheSpecs.java:
@Data public class CacheSpecs { private Integer timeToLiveInSeconds; }
CacheSpecConfig.java:
@Configuration @ConfigurationProperties(prefix = "cache") @Data public class CacheSpecConfig { private Map<String, CacheSpecs> specs=new HashMap<>(); }
3、改造我们刚刚的Cache配置类,MyCacheConfig.java:
//让CacheProperties的配置生效 @EnableConfigurationProperties(CacheProperties.class) @Configuration @EnableCaching public class MyCacheConfig { @Autowired CacheSpecConfig cacheSpecConfig; /** * 如果有,就将原配置文件中的所有配置,都继承过来 * @return */ @Bean public CacheManager cacheManager(RedisConnectionFactory cf, CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .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 RedisCacheManager.builder(cf).cacheDefaults(config).initialCacheNames(cacheSpecConfig.getSpecs().keySet()) .withInitialCacheConfigurations(getRedisCacheConfigurationMap(config)).build(); } private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap(RedisCacheConfiguration defaultConfig) { Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(); Map<String, CacheSpecs> specs = cacheSpecConfig.getSpecs(); for (Map.Entry<String, CacheSpecs> item : specs.entrySet()) { redisCacheConfigurationMap.put(item.getKey(),defaultConfig.entryTtl(Duration.ofSeconds(item.getValue().getTimeToLiveInSeconds()))); } return redisCacheConfigurationMap; } }
4、然后我们需要缓存的地方如何写那?
5、启动测试:
显然,我们为各自定义的过期时间已经生效;
且JSON序列化器也生效;
四、使用@CacheEvict、@CachePut注解,删除或更新缓存
1、比如,我们分类的更新操作:
@Transactional @Override @CacheEvict(cacheNames = "catelog1", key = "'getLevel1Categorys'") public void updateCascade(CategoryEntity category) { this.updateById(category); //更新其他受影响数据 categoryBrandRelationService.updateCategoryName(category.getCatId(), category.getName()); }
代表,此方法调用成功后,则删除 cacheName为catelog1的缓存;
2、但是实际情况是,我们想一次删除多个缓存呢?应该使用@Caching包装多个操作:
@Transactional @Override @Caching(evict = { @CacheEvict(cacheNames = "category1", allEntries = true), @CacheEvict(cacheNames = "category2", allEntries = true) }) public void updateCascade(CategoryEntity category) { this.updateById(category); //更新其他受影响数据 categoryBrandRelationService.updateCategoryName(category.getCatId(), category.getName()); }
3、测试可行!
4、使用 @CachePut 可用来更新缓存,即当前操作有返回值,清除缓存的同时,加入新的缓存
@CachePut(cacheNames = "category1", key = "'getLevel1Categorys'")
五、高并发场景下的缓存问题:
1、缓存穿透
指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无记录,然后我们也没有将这次查询得到的null写入缓存,这将导致这个不存在的数据每次请求都要到数据库中去查询,缓存失去了作用;
风险:
利用一定不存在的数据进行攻击,数据库瞬间压力增大,最终导致崩溃;
解决:
null结果也放入缓存,并加入短暂过期时间,我们使用SpringCache默认null值就是写入缓存的,
cache-null-values: true如果部分情况下,我们不想将null 写入缓存,可以使用
unless = "#result == null"
2、缓存雪崩
指我们有大量的缓存key在同一时间,同时失效,请求一下子全部到了数据库,DB瞬间压力过重而雪崩
解决:
在缓存时间增加随机数,降低时间重复率,但是我觉得意义不大,反倒容易弄巧成拙;
3、缓存击穿
指对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是非常“热点”的数据;如果这个key在大量请求同时进来的时候正好失效,那么所有的对这个key的数据查询都会落到数据库,我们称为缓存击穿;比如:后半夜所有人睡觉时候有些key失效了,但是到了早上8点钟,突然很多人一下子进来,这个时候就相当于缓存击穿了;
解决:
加锁,大量并发去查同一个值,指让一个人查,其他人等待,在SpringCache中默认缓存方法是没加synchronized锁的,但是如果想加,我们可以增加同步
sync = true这样,同一时间进来的查询就会被加锁;
但是,只是加的本地锁,其实是已经够用了,如果我们想实现集群之间的锁,可以使用分布式锁,可以使用Redisson、Curator进行实现,分布式锁环节有讲过;
4、缓存数据一致性问题(缓存和数据库不一致)
指,在我们更新数据的时候,肯定要更新缓存,不然就不一致了;但是如何更新呢?两种方案:
双写模式:更新数据库后更新缓存
失效模式:更新数据库后,清空缓存
但是,无论“双写模式”还是“失效模式”,严格上,都还是有可能出现缓存不一致问题,即多个实例同时更新会出事,或者出现了更新数据库时成功,更新或清除缓存时不成功的情况,然后缓存里面存的还是旧数据(错数据);这种情况该怎么办?
1、无论是用户维度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间都会自动失效一次;(所以,一定要为缓存设置过期时间)
2、缓存数据+过期时间也足以解决大部分业务对缓存的要求;
3、通过加锁保证并发读写,写写的时候按顺序排好队,读读无所谓,所以适合选择使用读写锁。
4、如果很重要的缓存,可以使用Canal,Canal可以将自己伪装成DB数据库的从节点,订阅主节点的binlog日志方式,在数据库发生改变的时候,主动去更新缓存,与主程序分离;