赞
踩
首先需要安装Redis,如何安装可以看我的这篇文章
接下来说明如何使用,以及一些Redis的相关知识。
哪些数据适合放入缓存?
本地缓存:和微服务同一个进程。缺点:分布式时本地缓存不能共享
分布式缓存:缓存中间件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置redis主机地址
spring:
redis:
host: xxx(你的ip)
port: 6379
public class RedisTests{
@Autowired
StringRedisTemplate stringRedisTemplate;
public void testStringRedisTemplate(){
ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
// 保存
ops.set("hello", "world_" + UUID.randomUUID().toString());
// 查询
String hello = ops.get("hello");
System.out.println(hello);
}
}
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService{ @Autowired CategoryBrandRelationService categoryBrandRelationService; @Autowired private StringRedisTemplate redisTemplate; @Override private Map<String, List<Catalog2Vo>> getCatalogJson(){ // 给缓存中放json字符串,拿出的json字符串,用逆转为能用的对象类型(序列化与反序列化)。 // 1. 加入缓存逻辑,缓存中存的数据是json字符串。 json的优点是:跨语言,跨平台兼容 String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if(StringUtils.isEmpty(catalogJSON )){ // 2. 缓存中没有,查询数据库 Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb(); // 3. 查到的数据再放入缓存,将对象转为json放入缓存中 // 使用alibaba的fastjson包,可以将任意对象转换为json字符串 String s = JSON.toJSONString(catalogJsonFromDb); redisTemplate.opsForValue().set("catalogJSON", s); return catalogJsonFromDb; } // 转为指定的对象 Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){}); return result; } // 从数据库查询并封装分类数据 private Map<String, List<Catalog2Vo>> getCatalogJsonFromDb(){ List<CategoryEntity> categoryEntities = this.list(); //查出所有一级分类 List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L); Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k->k.getCatId().toString(), v -> { //遍历查找出二级分类 List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId()); List<Catalog2Vo> catalog2Vos=null; if (level2Categories!=null){ //封装二级分类到vo并且查出其中的三级分类 catalog2Vos = level2Categories.stream().map(cat -> { //遍历查出三级分类并封装 List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId()); List<Catalog2Vo.Catalog3Vo> catalog3Vos = null; if (level3Catagories != null) { catalog3Vos = level3Catagories.stream() .map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName())) .collect(Collectors.toList()); } Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos); return catalog2Vo; }).collect(Collectors.toList()); } return catalog2Vos; })); return listMap; } }
缓存穿透:查询一个一定不存在的数据,由于缓存是不命中的,将去查询数据库,但是数据库也无此记录,没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
风险:利用不存在的数据进行攻击,数据库瞬间压力增大,最终导致崩溃。
解决:缓存空对象、布隆过滤器、mvc拦截器
缓存雪崩是指在我们设置缓存时key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决方案:
规避雪崩:缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
如果缓存数据库是分布式部署,将热点数据均匀分布在不同缓存数据库中。
设置热点数据永远不过期。
出现雪崩:降级 熔断
事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
事后:利用 redis 持久化机制保存的数据尽快恢复缓存
缓存雪崩和缓存击穿不同的是:
缓存击穿 指 并发查同一条数据。缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
设置热点数据永远不过期。
加互斥锁:业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db去数据库加载,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
缓存击穿:加锁
不好的方法是synchronized(this),肯定不能这么写 ,不具体写了。
锁时序问题:之前的逻辑是查缓存没有,然后取竞争锁查数据库,这样就造成多次查数据库。
解决方法:竞争到锁后,再次确认缓存中没有,再去查数据库。
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService{ @Autowired CategoryBrandRelationService categoryBrandRelationService; @Autowired private StringRedisTemplate redisTemplate; @Override private Map<String, List<Catalog2Vo>> getCatalogJson(){ // 给缓存中放json字符串,拿出的json字符串,用逆转为能用的对象类型(序列化与反序列化)。 /* 1. 空结果缓存:解决缓存穿透 2. 设置过期时间(加随机值):解决缓存雪崩 3. 加锁:解决缓存击穿 */ // 1. 加入缓存逻辑,缓存中存的数据是json字符串。 json的优点是:跨语言,跨平台兼容 String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if(StringUtils.isEmpty(catalogJSON )){ // 2. 缓存中没有,查询数据库 Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb(); return catalogJsonFromDb; } // 转为指定的对象 Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){}); return result; } // 从数据库查询并封装分类数据 private Map<String, List<Catalog2Vo>> getCatalogJsonFromDb(){ // 在非分布式的场景下,只要是同一把锁,就能锁住需要这个锁的所有线程 // synchronized(this): SpringBoot所有的组件在容器中都是单例的。 synchronized(this){ // 双重检测,得到锁后,应该再去缓存中确认一次,如果没有才需要继续查询 String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if(!StringUtils.isEmpty(catalogJSON)){ // 如果不为null,直接返回 Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){}); return result; } List<CategoryEntity> categoryEntities = this.list(); //查出所有一级分类 List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L); Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k->k.getCatId().toString(), v -> { //遍历查找出二级分类 List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId()); List<Catalog2Vo> catalog2Vos=null; if (level2Categories!=null){ //封装二级分类到vo并且查出其中的三级分类 catalog2Vos = level2Categories.stream().map(cat -> { //遍历查出三级分类并封装 List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId()); List<Catalog2Vo.Catalog3Vo> catalog3Vos = null; if (level3Catagories != null) { catalog3Vos = level3Catagories.stream() .map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName())) .collect(Collectors.toList()); } Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos); return catalog2Vo; }).collect(Collectors.toList()); } return catalog2Vos; })); // 3. 查到的数据再放入缓存,将对象转为json放入缓存中 // 使用alibaba的fastjson包,可以将任意对象转换为json字符串 String s = JSON.toJSONString(listMap); redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS); return listMap; } } }
分布式锁
分布式项目时,但本地锁只能锁住当前服务,需要分布式锁。
redis分布式锁的原理:setnx,同一时刻只能设置成功一个。
前提:锁的key是一定的,value可以变。
没获取到锁阻塞或者sleep一会,设置好了锁,玩意服务出现宕机,没有执行删除锁逻辑,这就造成了死锁
解决:设置过期时间业务还没执行完锁就过期了,别人拿到锁,自己执行完去删了别人的锁。
解决:锁续期(redisson有看门狗)。删锁的时候明确是自己的锁。如uuid判断uuid对了,但是将要删除的时候锁过期了,别人设置了新值,那删除了别人的锁。
解决:删除锁必须保证原子性(保证判断和删锁是原子的)。使用redis+Lua脚本完成,脚本是原子的。
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService{ @Autowired CategoryBrandRelationService categoryBrandRelationService; @Autowired private StringRedisTemplate redisTemplate; @Override private Map<String, List<Catalog2Vo>> getCatalogJson(){ // 给缓存中放json字符串,拿出的json字符串,用逆转为能用的对象类型(序列化与反序列化)。 /* 1. 空结果缓存:解决缓存穿透 2. 设置过期时间(加随机值):解决缓存雪崩 3. 加锁:解决缓存击穿 */ // 1. 加入缓存逻辑,缓存中存的数据是json字符串。 json的优点是:跨语言,跨平台兼容 String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if(StringUtils.isEmpty(catalogJSON )){ // 2. 缓存中没有,查询数据库 Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock(); return catalogJsonFromDb; } // 转为指定的对象 Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){}); return result; } // 从数据库查询并封装分类数据 private Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock(){ // 1) 占分布式锁 String uuid = UUID.randomUUID().toString(); Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS); if(lock){ // 加锁成功 // 2) 设置过期时间,必须和加锁是同步的,原子性的 // redisTemplate.expire("lock", 30, TimeUnit.SECONDS); Map<String, List<Catalog2Vo>> dataFromDb; try{ dataFromDb = getDataFromDb(); }finally{ // get与delete的原子操作 String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" + " return redis.call(\"del\",KEYS[1])\n" + "else\n" + " return 0\n" + "end"; Long lockValue = stringRedisTemplate.execute( new DefaultRedisScript<Long>(script, Long.class), // 脚本和返回类型 Arrays.asList("lock"), // 参数 uuid); // 参数值,锁的值 } // 获取值对比 + 对比成功删除 = 原子操作,因此采用lua脚本 /* String lockValue = redisTemplate.opsForValue().get("lock"); if(uuid.equals(lockValue)){ // 删除我自己的锁 redisTemplate.delete("lock"); // 删除锁 } */ return dataFromDb; }else{ // 加锁失败 // 休眠 100ms try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return getCatalogJsonFromDbWithRedisLock(); // 自旋的方式 } return getDataFromDb(); } private Map<String, List<Catalog2Vo>> getDataFromDb(){ // 双重检测,得到锁后,应该再去缓存中确认一次,如果没有才需要继续查询 String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if(!StringUtils.isEmpty(catalogJSON)){ // 如果不为null,直接返回 Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){}); return result; } List<CategoryEntity> categoryEntities = this.list(); //查出所有一级分类 List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L); Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k->k.getCatId().toString(), v -> { //遍历查找出二级分类 List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId()); List<Catalog2Vo> catalog2Vos=null; if (level2Categories!=null){ //封装二级分类到vo并且查出其中的三级分类 catalog2Vos = level2Categories.stream().map(cat -> { //遍历查出三级分类并封装 List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId()); List<Catalog2Vo.Catalog3Vo> catalog3Vos = null; if (level3Catagories != null) { catalog3Vos = level3Catagories.stream() .map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName())) .collect(Collectors.toList()); } Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos); return catalog2Vo; }).collect(Collectors.toList()); } return catalog2Vos; })); // 3. 查到的数据再放入缓存,将对象转为json放入缓存中 // 使用alibaba的fastjson包,可以将任意对象转换为json字符串 String s = JSON.toJSONString(listMap); redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS); return listMap; } }
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.6</version>
</dependency>
application.yml
spring:
application:
name: springboot-redisson
redis:
redisson:
file: classpath:redisson.yml
redisson.yml
# 单节点配置 singleServerConfig: # 连接空闲超时,单位:毫秒 idleConnectionTimeout: 10000 # 连接超时,单位:毫秒 connectTimeout: 10000 # 命令等待超时,单位:毫秒 timeout: 3000 # 命令失败重试次数,如果尝试达到 retryAttempts(命令失败重试次数) 仍然不能将命令发送至某个指定的节点时,将抛出错误。 # 如果尝试在此限制之内发送成功,则开始启用 timeout(命令等待超时) 计时。 retryAttempts: 3 # 命令重试发送时间间隔,单位:毫秒 retryInterval: 1500 # # 重新连接时间间隔,单位:毫秒 # reconnectionTimeout: 3000 # # 执行失败最大次数 # failedAttempts: 3 # 密码 password: 1234 # 单个连接最大订阅数量 subscriptionsPerConnection: 5 # 客户端名称 clientName: null # # 节点地址 address: "redis://127.0.0.1:6379" # 发布和订阅连接的最小空闲连接数 subscriptionConnectionMinimumIdleSize: 1 # 发布和订阅连接池大小 subscriptionConnectionPoolSize: 50 # 最小空闲连接数 connectionMinimumIdleSize: 500 # 连接池大小 connectionPoolSize: 1000 # 数据库编号 database: 0 # DNS监测时间间隔,单位:毫秒 dnsMonitoringInterval: 5000 # 线程池数量,默认值: 当前处理核数量 * 2 threads: 16 # Netty线程池数量,默认值: 当前处理核数量 * 2 nettyThreads: 32 # 编码,不使用默认编码,因为set进去之后是乱码 #codec: !<org.redisson.codec.MarshallingCodec> {} # 传输模式 transportMode : "NIO"
import org.junit.jupiter.api.Test; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class SpringbootRedissonApplicationTests { @Autowired private RedissonClient redissonClient; @Test void contextLoads() { redissonClient.getBucket("hello").set("bug"); String test = (String) redissonClient.getBucket("hello").get(); System.out.println(test); } }
具体使用方式可以查看官网,有详细的说明
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService { @Autowired CategoryBrandRelationService categoryBrandRelationService; @Autowired private StringRedisTemplate redisTemplate; @Autowired RedissonClient redisson; @Override private Map<String, List<Catalog2Vo>> getCatalogJson() { // 给缓存中放json字符串,拿出的json字符串,用逆转为能用的对象类型(序列化与反序列化)。 /* 1. 空结果缓存:解决缓存穿透 2. 设置过期时间(加随机值):解决缓存雪崩 3. 加锁:解决缓存击穿 */ // 1. 加入缓存逻辑,缓存中存的数据是json字符串。 json的优点是:跨语言,跨平台兼容 String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if (StringUtils.isEmpty(catalogJSON)) { // 2. 缓存中没有,查询数据库 Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedissonLock(); return catalogJsonFromDb; } // 转为指定的对象 Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() { }); return result; } /* * 缓存里面的数据如何和数据库保持一致 * 缓存数据的一致性 * 1)双写模式:写完数据库后,马上写到缓存里。 * 缺点:脏数据问题,这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据 * 2)失效模式:写完数据库后,马上删掉缓存的数据。 * 缺点:也会产生脏数据问题。 * 解决办法:1. 对于实时性要求高的数据,直接读数据库。 * 2. 使用读写锁(业务不关心脏数据,允许临时脏数据可忽略) * 3. 缓存数据一致性-解决-Canal */ private Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedissonLock() { // 1. 锁的名字。(锁的粒度,越细越快) // 锁的粒度:具体缓存的是某个数据,例11号商品:product-11-lock redisson.getLock("catalogJson-lock"); lock.lock(); Map<String, List<Catalog2Vo>> dataFromDb; try { dataFromDb = getDataFromDb(); } finally { lock.unlock(); } return dataFromDb; } private Map<String, List<Catalog2Vo>> getDataFromDb() { // 双重检测,得到锁后,应该再去缓存中确认一次,如果没有才需要继续查询 String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if (!StringUtils.isEmpty(catalogJSON)) { // 如果不为null,直接返回 Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>() { }); return result; } List<CategoryEntity> categoryEntities = this.list(); //查出所有一级分类 List<CategoryEntity> level1Categories = getCategoryByParentCid(categoryEntities, 0L); Map<String, List<Catalog2Vo>> listMap = level1Categories.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> { //遍历查找出二级分类 List<CategoryEntity> level2Categories = getCategoryByParentCid(categoryEntities, v.getCatId()); List<Catalog2Vo> catalog2Vos = null; if (level2Categories != null) { //封装二级分类到vo并且查出其中的三级分类 catalog2Vos = level2Categories.stream().map(cat -> { //遍历查出三级分类并封装 List<CategoryEntity> level3Catagories = getCategoryByParentCid(categoryEntities, cat.getCatId()); List<Catalog2Vo.Catalog3Vo> catalog3Vos = null; if (level3Catagories != null) { catalog3Vos = level3Catagories.stream() .map(level3 -> new Catalog2Vo.Catalog3Vo(level3.getParentCid().toString(), level3.getCatId().toString(), level3.getName())) .collect(Collectors.toList()); } Catalog2Vo catalog2Vo = new Catalog2Vo(v.getCatId().toString(), cat.getCatId().toString(), cat.getName(), catalog3Vos); return catalog2Vo; }).collect(Collectors.toList()); } return catalog2Vos; })); // 3. 查到的数据再放入缓存,将对象转为json放入缓存中 // 使用alibaba的fastjson包,可以将任意对象转换为json字符串 String s = JSON.toJSONString(listMap); redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS); return listMap; } }
首先理一下:
每次都那样写缓存太麻烦了,spring从3.1开始定义了org.springframework.cache.Cache org.springframework.cache.CacheManager接口来统一不同的缓存技术。并支持使用JCache(JSR-107)注解简化我们的开发。
Cache接口的实现包括RedisCache、EhCacheCache、ConcurrentMapCache等。
每次调用需要缓存功能的方法时,spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点:
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
spring:
cache:
#指定缓存类型为redis
type: redis
redis:
# 指定redis中的过期时间为1h
time-to-live: 3600000
在主类上加上注解
@EnableCaching
在方法上注解:代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。反之则调用方法,最后将方法的结果放入缓存。
每一个需要缓存的数据都来指定要放到哪个名字的缓存。(缓存的分区 [按照业务类型分] )
默认行为:
@Cacheable({"category"})
@Override
public List<CategoryEntity> getLevel1Categorys(){
List<CategoryEntity> categoryEntities = baseMapper.selectList();
return categoryEntities;
}
自定义:
@Cacheable({"category"}, key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys(){
List<CategoryEntity> categoryEntities = baseMapper.selectList();
return categoryEntities;
}
@Configuration public class MyCacheConfig { @Bean public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { CacheProperties.Redis redisProperties = cacheProperties.getRedis(); org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration .defaultCacheConfig(); //指定缓存序列化方式为json config = config.serializeValuesWith( RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); //设置配置文件中的各项配置,如过期时间 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; } }
第一个方法存放缓存,第二个方法清空缓存
// 调用该方法时会将结果缓存,缓存名为category,key为方法名 // sync表示该方法的缓存被读取时会加锁 // value等同于cacheNames // key如果是字符串"''" @Cacheable(value = {"category"},key = "#root.methodName",sync = true) public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithSpringCache() { return getCategoriesDb(); } //调用该方法会删除缓存category下的所有cache,如果要删除某个具体,用key="''" @Override @CacheEvict(value = {"category"},allEntries = true) public void updateCascade(CategoryEntity category) { this.updateById(category); if (!StringUtils.isEmpty(category.getName())) { categoryBrandRelationService.updateCategory(category); } }
如果要清空多个缓存,用@Caching(evict={@CacheEvict(value=“”)})
1)读模式
缓存穿透:查询一个null数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;
使用sync = true来解决击穿问题
缓存雪崩:大量的key同时过期。解决:加随机时间。
2) 写模式:(缓存与数据库一致)
读写加锁。
引入Canal,感知到MySQL的更新去更新Redis
读多写多,直接去数据库查询就行
3)总结:
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):
写模式(只要缓存的数据有过期时间就足够了)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。