18.缓存
1.哪些数据适合放入缓存
即时性,数据一致性要求不高的
访问量大且更新频率不高的数据(读多,写少)
距离:物流信息,并不是每走一米就要更新一次
2.本地缓存的问题
Map<> catche=new Map<>();
对于分布式系统,第一次数据进入第一个微服务,缓存进去
第二次想拿数据的时候负载均衡到了第二个微服务,这样就拿不到缓存了
而且如果修改了数据,会产生数据一致性的问题,本地缓存在分布式系统下是不可靠的
3.项目整合redis测试
product项目引入依赖
<!--redis依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
yml文件
使用配置好的StringRedisTemplate-》可以在RedisAutoConfiguration里查看配置形象
基本就用前五个就够了 Zset是带排序的集合
测试
4.将redis加入业务逻辑
@Override public Map<String, List<Catalog2Vo>> getCatelogJson() { String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson"); //如果缓存中没有 if(StringUtils.isEmpty(catalogJson)){ //查询数据库 Map<String, List<Catalog2Vo>> db = this.getCatelogJsonFromDB(); //查到的数据放入缓存 //缓存中存的所有对象都应该是json字符串,因为他是跨语言跨平台的兼容的--序列化过程 String s = JSON.toJSONString(db); stringRedisTemplate.opsForValue().set("catalogJson",s); return db;//直接把数据库查到的东西返回 } //反序列化得到map TypeReference<Map<String, List<Catalog2Vo>>> typeReference = new TypeReference<Map<String, List<Catalog2Vo>>>() { }; Map<String, List<Catalog2Vo>> stringListMap = JSON.parseObject(catalogJson, typeReference); return stringListMap; }
压力测试吞吐量达到了1200+
压力测试内存泄漏问题--新版本springboot好像解决了,我并没有遇到这个问题
堆外内存溢出异常:OutOfDirectMemoryError
原因:springboot2.0以后默认使用lettuce作为操作redis的客户端,他使用netty进行网络通信,这其实是lettuce操作netty产生的bug
netty如果没有指定堆外内存,会使用-Xmx100m指定的内存,可以通过-Dio.netty.maxDirectMemory调大堆外内存,但是只是延后而已
真正的解决方案:
1.升级lettuce客户端
2.切换使用jedis--这是一个老版客户端
在springboot的依赖下排除io.lettuce.lettuce-core包,增加redis.clients.jedis依赖
缓存击穿,穿透,雪崩
缓存穿透:
指的是查询一个不存在的数据,由于缓存不命中,将去查询数据库,但是数据库也没有此纪录,我们没有将这次查询的null写入缓存,这将导致这个不存在的数据每次请求都要去储存层去查询,失去了缓存的意义
风险:利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃
解决:null结果缓存,并且加入短暂的过期时间
缓存雪崩:
指的是在我们设置缓存时,key采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩
解决:原有的失效时间基础上增加一个随机的值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效事件
缓存击穿:
对于一些设置了过期时间的key,如果这些key可能会在某一时间点被超高并发地访问,是一种非常热点的数据,如果这个key在大量请求同时进来前正好失效,那么对所有这个key的数据查询都落在了db
解决:加锁,大量并发只让一个去查数据库,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去db
雪崩和穿透都很好解决,最主要是击穿怎么加锁的问题
方法1:synchronized(this){。。。}/public synchronized。。。
在springBoot中所有的组件都是单例的
本地锁
//从数据库查询的方法 public Map<String, List<Catalog2Vo>> getCatelogJsonFromDB() { synchronized(this){ String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson"); //得到锁以后,应该再去缓存中确定一次,如果没有才继续查询 if(!StringUtils.isEmpty(catalogJson)){ Map<String, List<Catalog2Vo>> parseObject = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() { }); return parseObject; } List<CategoryEntity> entityList = baseMapper.selectList(null); // 查询所有一级分类 List<CategoryEntity> level1 = getByParentCid(entityList, 0L); Map<String, List<Catalog2Vo>> parent_cid = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> { // 拿到每一个一级分类 然后查询他们的二级分类 List<CategoryEntity> entities = getByParentCid(entityList, v.getCatId()); List<Catalog2Vo> catelog2Vos = null; if (entities != null) { catelog2Vos = entities.stream().map(l2 -> { Catalog2Vo catelog2Vo = new Catalog2Vo(v.getCatId().toString(), l2.getName(), l2.getCatId().toString(), null); // 找当前二级分类的三级分类 List<CategoryEntity> level3 = getByParentCid(entityList, l2.getCatId()); // 三级分类有数据的情况下 if (level3 != null) { List<Catalog3Vo> catalog3Vos = level3.stream().map(l3 -> new Catalog3Vo(l3.getCatId().toString(), l3.getName(), l2.getCatId().toString())).collect(Collectors.toList()); catelog2Vo.setCatalog3List(catalog3Vos); } return catelog2Vo; }).collect(Collectors.toList()); } return catelog2Vos; })); return parent_cid; } }
如果作用于分布式的话,其问题在于本地锁只能锁住当前进程,如果我们在每台机器都加锁的话相当于我们放了多于机器数量的线程进来,所以我们需要分布式锁
为什么会多于--》我们以上的代码造成的非原子性,放入缓存的操作也需要放在synchronized里,这样就不会多于而是等于了
//从数据库查询的方法 public Map<String, List<Catalog2Vo>> getCatelogJsonFromDB() { synchronized(this){ String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson"); //得到锁以后,应该再去缓存中确定一次,如果没有才继续查询 if(!StringUtils.isEmpty(catalogJson)){ Map<String, List<Catalog2Vo>> parseObject = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() { }); return parseObject; } List<CategoryEntity> entityList = baseMapper.selectList(null); // 查询所有一级分类 List<CategoryEntity> level1 = getByParentCid(entityList, 0L); Map<String, List<Catalog2Vo>> parent_cid = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> { // 拿到每一个一级分类 然后查询他们的二级分类 List<CategoryEntity> entities = getByParentCid(entityList, v.getCatId()); List<Catalog2Vo> catelog2Vos = null; if (entities != null) { catelog2Vos = entities.stream().map(l2 -> { Catalog2Vo catelog2Vo = new Catalog2Vo(v.getCatId().toString(), l2.getName(), l2.getCatId().toString(), null); // 找当前二级分类的三级分类 List<CategoryEntity> level3 = getByParentCid(entityList, l2.getCatId()); // 三级分类有数据的情况下 if (level3 != null) { List<Catalog3Vo> catalog3Vos = level3.stream().map(l3 -> new Catalog3Vo(l3.getCatId().toString(), l3.getName(), l2.getCatId().toString())).collect(Collectors.toList()); catelog2Vo.setCatalog3List(catalog3Vos); } return catelog2Vo; }).collect(Collectors.toList()); } return catelog2Vos; })); //查到的数据放入缓存 //缓存中存的所有对象都应该是json字符串,因为他是跨语言跨平台的兼容的--序列化过程 String s = JSON.toJSONString(parent_cid); stringRedisTemplate.opsForValue().set("catalogJson",s,1, TimeUnit.DAYS); return parent_cid; } }
方法2:分布式锁--》性能稍微差一点,但是想要锁住所有必须要分布式锁
分布式锁
Redis分布式锁
redis分布式锁的原理:set lock 1 nx命令(不存在我才往里面放),同一时刻只能设置成功一个
问题一:解锁失败会导致死锁--》设置一个过期时间
问题二:过期时间之前如果继续闪断怎么办,依然会导致死锁-》使用原子操作
问题三:如果业务超时了,我们的锁早就过期了,我们的线程就会去删除其他线程的锁,所以更多的线程能拿到这一把锁--》占锁的时候指定key为lock,value为uuid,如果是自己的锁才删除
问题四:依旧会删掉别人的锁,因为如果在判断了value为uuid后业务超时,在delete之前别的线程抢到了锁,后面delete依然会删别人的锁--》获取值对比和删除锁也必须是一个原子操作--Lua脚本解锁
//redis锁 public Map<String, List<Catalog2Vo>> getCatelogJsonFromDBWithRedis() { String uuid = UUID.randomUUID().toString(); ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); Boolean lock = ops.setIfAbsent("lock", uuid,500, TimeUnit.SECONDS); if (lock) { Map<String, List<Catalog2Vo>> categoriesDb = getDataFromDB(); String lockValue = ops.get("lock"); // 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"; stringRedisTemplate.execute( new DefaultRedisScript<Long>(script, Long.class), // 脚本和返回类型 Arrays.asList("lock"), // 参数 lockValue); // 参数值,锁的值 return categoriesDb; }else { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 睡眠0.1s后,重新调用 //自旋 return getCatelogJsonFromDBWithRedis(); } }
private Map<String, List<Catalog2Vo>> getDataFromDB() { String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson"); //得到锁以后,应该再去缓存中确定一次,如果没有才继续查询 if (!StringUtils.isEmpty(catalogJson)) { Map<String, List<Catalog2Vo>> parseObject = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() { }); return parseObject; } List<CategoryEntity> entityList = baseMapper.selectList(null); // 查询所有一级分类 List<CategoryEntity> level1 = getByParentCid(entityList, 0L); Map<String, List<Catalog2Vo>> parent_cid = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> { // 拿到每一个一级分类 然后查询他们的二级分类 List<CategoryEntity> entities = getByParentCid(entityList, v.getCatId()); List<Catalog2Vo> catelog2Vos = null; if (entities != null) { catelog2Vos = entities.stream().map(l2 -> { Catalog2Vo catelog2Vo = new Catalog2Vo(v.getCatId().toString(), l2.getName(), l2.getCatId().toString(), null); // 找当前二级分类的三级分类 List<CategoryEntity> level3 = getByParentCid(entityList, l2.getCatId()); // 三级分类有数据的情况下 if (level3 != null) { List<Catalog3Vo> catalog3Vos = level3.stream().map(l3 -> new Catalog3Vo(l3.getCatId().toString(), l3.getName(), l2.getCatId().toString())).collect(Collectors.toList()); catelog2Vo.setCatalog3List(catalog3Vos); } return catelog2Vo; }).collect(Collectors.toList()); } return catelog2Vos; })); //查到的数据放入缓存 //缓存中存的所有对象都应该是json字符串,因为他是跨语言跨平台的兼容的--序列化过程 String s = JSON.toJSONString(parent_cid); stringRedisTemplate.opsForValue().set("catalogJson", s, 1, TimeUnit.DAYS); return parent_cid; }
Redission分布式锁
product项目 导入redisson依赖,这个是原生的依赖,后续可以使用spring-boot-starter-redisson
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.4</version> </dependency>
// redission通过redissonClient对象使用 // 如果是多个redis集群,可以配置 @Bean(destroyMethod = "shutdown") public RedissonClient redisson() { Config config = new Config(); // 创建单节点模式的配置 config.useSingleServer().setAddress("redis://192.168.116.128:6379"); return Redisson.create(config); }
可重入锁
方法A调用方法B,在A拿到锁的同时,B也会拿到锁,所有的锁都应该是可重入的
测试加锁
@ResponseBody @GetMapping("/hello") public String hello(){ //获取一把锁,只要锁的名字一样就是同一把锁 RLock myLock = redissonClient.getLock("myLock"); myLock.lock();//加锁,阻塞式的等待 try{ System.out.println("业务代码"+Thread.currentThread().getId()); Thread.sleep(3000); }catch (Exception e){ }finally { System.out.println("释放锁"+Thread.currentThread().getId()); myLock.unlock();//解锁 } return "hello"; }
开启两个网站同时访问hello测试效果
假设解锁代码没有运行,redisson会不会出现死锁?不会,redisson有一个看门狗,管理锁的自动续期
如果业务超长,看门狗会自动续期30s,加锁的业务只要运行完成,就不会给当前的锁续期,即使不手动解锁,锁默认在30s后自动删除
ps:我们可以给lock方法设置一个自动过时时间lock(10,TimeUnit.Seconds),如果使用了这个方法,在锁超时的时候不会自动续期,所有这里设定的时间一定要远大于业务时间
如果传递了锁的超时时间,就执行脚本,进行占锁;
如果没传递锁时间,使用看门狗的时间,占锁。如果返回占锁成功future,调用future.onComplete();
没异常的话调用scheduleExpirationRenewal(threadId);
重新设置过期时间,定时任务;
看门狗的原理是定时任务:重新给锁设置过期时间,新的过期时间就是看门狗的默认时间;
锁时间/3是定时任务周期;
读写锁--保证一定能读到最新的数据
读写锁成对存在,写锁控制了读锁。
不停的刷新读数据,测试一下读写锁
@GetMapping("/write") @ResponseBody public String writeValue() { RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock"); String s=""; RLock rLock = lock.writeLock(); //改数据加写锁 rLock.lock(); try { System.out.println("正在写数据不允许读"); s = UUID.randomUUID().toString(); Thread.sleep(10000); redisTemplate.opsForValue().set("writeValue",s); } catch (Exception e) { e.printStackTrace(); }finally { rLock.unlock(); } return s; } @GetMapping("/read") @ResponseBody public String readValue() { RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock"); String s=""; RLock rLock = lock.readLock(); //读数据加读锁 rLock.lock(); try { System.out.println("拿到锁开始读取数据"); s=redisTemplate.opsForValue().get("writeValue"); } catch (Exception e) { e.printStackTrace(); }finally { rLock.unlock(); } return s; }
写+读:等待写锁释放 互斥
写+写:等待锁的释放 互斥
读+写:等待读锁释放 互斥
读+读:相当于无锁状态
信号量-秒杀场景可以使用
//模拟停车位,停车一辆,停车位减少1 @GetMapping("/park") @ResponseBody public String park() throws InterruptedException { RSemaphore park = redissonClient.getSemaphore("park"); //park.acquire();获取一个值,即停车位减一,阻塞 boolean b = park.tryAcquire();//非阻塞 if(b){ System.out.println("正在执行业务"); }else { System.out.println("已无车位"); } return "ok"+b; } @GetMapping("/go") @ResponseBody public String go() throws InterruptedException { RSemaphore park = redissonClient.getSemaphore("park"); park.release();//信号值+1,即车位加一 return "ok"; }
闭锁
//模拟放假关门 @GetMapping("/closedoor") @ResponseBody public String CloseDoor() throws InterruptedException { RCountDownLatch door = redissonClient.getCountDownLatch("door"); boolean b = door.trySetCount(3);//如果没有这个值的话会设置为3,有的话按默认值来 door.await();//等待闭锁全部完成 return "门已关上"+b; } @GetMapping("go/{id}") @ResponseBody public String PeopleGo(@PathVariable("id") String id){ RCountDownLatch door = redissonClient.getCountDownLatch("door"); door.countDown();//计数完之后资源会直接被删除 return id+"-已经走了"; }
分布式锁一致性解决
//redis锁 缓存数据一致性问题: // 1)双写模式 在改数据库的时候也要删缓存改缓存--麻烦 // 漏洞:由于卡顿,第二个修改请求从中插入,导致第二次缓存写在前面,第一次缓存写在后面==》脏数据问题 // 2)失效模式 数据库改完之后直接把数据库删掉,等待下次主动查询进行更新 // 漏洞:三个请求,第一个请求写删数据,第二个写的过程比较长,正在写的时候,第三个进来,发现没有缓存,就去数据库里读到了第一个请求的数据,如果三号更新缓存的速度慢于二号,就会导致缓存数据不一致 // 解决:如果加锁只允许一个写的话,数据经常修改的话,我们还要不要加入缓存? // 1.看业务是否允许暂时的数据不一致问题 2.加读写锁,在写写的时候按顺序排队,读读无所谓 // 3.缓存数据+过期时间,保证数据最终一致性 4.可以使用canal订阅binlog的方式 public Map<String, List<Catalog2Vo>> getCatalogJsonFromDBWithRedission() { RLock lock = redissonClient.getLock("catalogJson-lock");//锁的粒度:防止锁重名 lock.lock(); Map<String, List<Catalog2Vo>> dataFromDB; try{ dataFromDB=getDataFromDB(); }finally { lock.unlock(); } return dataFromDB; }
总结:
- 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。
- 我们不应该过度设计,增加系统的复杂性
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
SpringCahe--从这里开始修改业务代码
简介
Cache接口的实现包括RedisCache、EhCacheCache、ConcurrentMapCache等
每次调用需要缓存功能的方法时,spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
使用Spring缓存抽象时我们需要关注以下两点:
1、确定方法需要缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据
product项目整合springCache
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
编写配置,只要前面两个就行了
spring:
redis:
# 指定redis中的过期时间为1h
time-to-live: 3600000
#key-prefix: CACHE_ #前缀添加,如果不指定就会使用缓存的名字作为前缀
#use-key-prefix: true
#防止缓存穿透默认是true
#cache-null-values: true
几个注解的解释:
@Cacheable 触发将数据保存到缓存的操作
@CacheEvict 触发将数据从缓存删除的操作
@CachePut 不影响方法执行更新缓存
@Caching 组合以上多个操作
@CacheConfig 在类级别共享缓存的相同配置
@EnableCaching 开启缓存功能,放在主类上
测试缓存
//每一个需要缓存的数据我们都来指定要放到哪个名字的缓存(缓存分区--按照业务类型分,可以是数组) @Cacheable(value = "category",key = "#root.methodName") //现在只需要加上缓存注解就行了,如果缓存中有连方法都不会被调用,指定key属性可以指定缓存的key值,支持spel表达式 @Override public List<CategoryEntity> getLevel1Categorys() { List<CategoryEntity> categoryEntities = this.list(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0)); return categoryEntities; }
连续刷新页面只会打印一个方法:
查看redis储存的缓存
发现数据是序列化之后的东西,如果需要变成我们熟悉的json,那么就需要自己配置
//默认使用jdk进行序列化(可读性差),默认ttl为-1永不过期,自定义序列化方式需要编写配置类 @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; } }
使用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 = "'getCatelogJson'") }) //方式二 CacheEvict(value="category",allEntries=true)删除分区的所有数据 @Transactional @Override public void updateCascade(CategoryEntity category) { this.updateById(category); categoryBrandRelationService.updateCategory(category.getCatId(), category.getName()); } //每一个需要缓存的数据我们都来指定要放到哪个名字的缓存(缓存分区--按照业务类型分,可以是数组) @Cacheable(value = "category",key = "#root.methodName") //现在只需要加上缓存注解就行了,如果缓存中有连方法都不会被调用,指定key属性可以指定缓存的key值,支持spel表达式 @Override public List<CategoryEntity> getLevel1Categorys() { List<CategoryEntity> categoryEntities = this.list(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0)); return categoryEntities; } @Cacheable(value="category",key = "#root.methodName") @Override public Map<String, List<Catalog2Vo>> getCatelogJson() { List<CategoryEntity> entityList = baseMapper.selectList(null); // 查询所有一级分类 List<CategoryEntity> level1 = getByParentCid(entityList, 0L); Map<String, List<Catalog2Vo>> parent_cid = level1.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> { // 拿到每一个一级分类 然后查询他们的二级分类 List<CategoryEntity> entities = getByParentCid(entityList, v.getCatId()); List<Catalog2Vo> catelog2Vos = null; if (entities != null) { catelog2Vos = entities.stream().map(l2 -> { Catalog2Vo catelog2Vo = new Catalog2Vo(v.getCatId().toString(), l2.getName(), l2.getCatId().toString(), null); // 找当前二级分类的三级分类 List<CategoryEntity> level3 = getByParentCid(entityList, l2.getCatId()); // 三级分类有数据的情况下 if (level3 != null) { List<Catalog3Vo> catalog3Vos = level3.stream().map(l3 -> new Catalog3Vo(l3.getCatId().toString(), l3.getName(), l2.getCatId().toString())).collect(Collectors.toList()); catelog2Vo.setCatalog3List(catalog3Vos); } return catelog2Vo; }).collect(Collectors.toList()); } return catelog2Vos; })); return parent_cid; }
/ 调用该方法时会将结果缓存,缓存名为category,key为方法名 // sync表示该方法的缓存被读取时会加锁 // value等同于cacheNames // key如果是字符串"''" @Cacheable(value = {"category"},key = "#root.methodName",sync = true) public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithSpringCache() { return getCategoriesDb(); }
SpringCache原理与不足
1)、读模式
- 缓存穿透:查询一个null数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true
- 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;
- 使用sync = true来解决击穿问题
- 缓存雪崩:大量的key同时过期。解决:加随机时间。
2)、写模式:(缓存与数据库一致)
- 读写加锁。
- 引入Canal,感知到MySQL的更新去更新Redis
- 读多写多,直接去数据库查询就行
3)、总结:
- 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):
- 写模式(只要缓存的数据有过期时间就足够了)
特殊数据:特殊设计