一、缓存
1、本地缓存
1.1 使用 hashmap 本地缓存
1 2 3 4 5 6 7 8 9 10 11 12
| private Map<String,Object> cache=new HashMap<>();
public Map<String, List<Catalog2Vo>> getCategoryMap() { Map<String, List<Catalog2Vo>> catalogMap = (Map<String, List<Catalog2Vo>>) cache.get("catalogMap"); if (catalogMap == null) { catalogMap = getCategoriesDb(); cache.put("catalogMap",catalogMap); } return catalogMap; }
|
1.2 整合 redis 进行测试
导入依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
|
配置 redis 主机地址
1 2 3 4
| spring: redis: host: 192.168.56.10 port: 6379
|
使用 springboot 自动配置的 RedisTemplate 优化菜单获取业务
1 2 3 4 5 6 7 8 9 10
| ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); String catalogJson = ops.get("catalogJson"); if (catalogJson == null) { Map<String, List<Catalog2Vo>> categoriesDb = getCategoriesDb(); String toJSONString = JSON.toJSONString(categoriesDb); ops.set("catalogJson",toJSONString); return categoriesDb; } Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {}); return listMap;
|
内存泄漏及解决办法
当进行压力测试时后期后出现堆外内存溢出 OutOfDirectMemoryError
产生原因:
1)、springboot2.0 以后默认使用 lettuce 操作 redis 的客户端,它使用通信
2)、lettuce 的 bug 导致 netty 堆外内存溢出
解决方案:由于是 lettuce 的 bug 造成,不能直接使用-Dio.netty.maxDirectMemory 去调大虚拟机堆外内存
1)、升级 lettuce 客户端。 2)、切换使用 jedis
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <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>
|
1.3 高并发下缓存失效问题
缓存击穿
只查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,将失去缓存的意义
风险:
利用不存在的数据进行攻击,数据库瞬间压力增大,最终导致崩溃
解决:
null 结果缓存,并加入短暂过期时间
缓存雪崩
缓存雪崩是指我们设置缓存时 key 采用了相同的过期时间,导致缓存在某一时刻失效,请求全部转发到 DB,DB 瞬间压力过大雪崩。
解决:
将原有的失效时间基础上增加一个随机值,比如 1-5 分钟的随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存击穿
- 对于一些设置过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。
- 如果这个 key 在大量请求同时进行前正好失效,那么所有对这个 key 的数据查询都落到 db,我们称为缓存击穿
解决:
加锁。大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去 db。
1.4 加锁解决缓存击穿问题
将查询 db 的方法加锁,这样在同一时间只有一个方法能查询数据库,就能解决缓存击穿的问题了
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
| public Map<String, List<Catalog2Vo>> getCategoryMap() { ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); String catalogJson = ops.get("catalogJson"); if (StringUtils.isEmpty(catalogJson)) { System.out.println("缓存不命中,准备查询数据库。。。"); Map<String, List<Catalog2Vo>> categoriesDb = getCategoriesDb(); String toJSONString = JSON.toJSONString(categoriesDb); ops.set("catalogJson",toJSONString); return categoriesDb; } System.out.println("缓存命中。。。。"); Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {}); return listMap; }
private synchronized Map<String, List<Catalog2Vo>> getCategoriesDb() { String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson"); if (StringUtils.isEmpty(catalogJson)) { System.out.println("查询了数据库"); 。。。。。 return listMap; }else { Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {}); return listMap; } }
|
1.5 锁时序问题
在上述方法中,我们将业务逻辑中的确认缓存没有
和查数据库
放到了锁里,但是最终控制台却打印了两次查询了数据库。这是因为在将结果放入缓存的这段时间里,有其他线程确认缓存没有,又再次查询了数据库,因此我们要将结果放入缓存
也进行加锁
优化代码逻辑后
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public Map<String, List<Catalog2Vo>> getCategoryMap() { ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); String catalogJson = ops.get("catalogJson"); if (StringUtils.isEmpty(catalogJson)) { System.out.println("缓存不命中,准备查询数据库。。。"); synchronized (this) { String synCatalogJson = stringRedisTemplate.opsForValue().get("catalogJson"); if (StringUtils.isEmpty(synCatalogJson)) { Map<String, List<Catalog2Vo>> categoriesDb= getCategoriesDb(); String toJSONString = JSON.toJSONString(categoriesDb); ops.set("catalogJson", toJSONString); return categoriesDb; }else { Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(synCatalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {}); return listMap; } } } System.out.println("缓存命中。。。。"); Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {}); return listMap; }
|
优化后多线程访问时仅查询一次数据库
2、分布式缓存
2.1 本地缓存面临问题
当有多个服务存在时,每个服务的缓存仅能够为本服务使用,这样每个服务都要查询一次数据库,并且当数据更新时只会更新单个服务的缓存数据,就会造成数据不一致的问题
所有的服务都到同一个 redis 进行获取数据,就可以避免这个问题
2.2 分布式锁
当分布式项目在高并发下也需要加锁,但本地锁只能锁住当前服务,这个时候就需要分布式锁
2.3 分布式锁的演进
基本原理
我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。“占坑”可以去 redis,可以去数据库,可以去任何大家都能访问的地方。等待可以自旋的方式。
阶段一
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
| public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() { Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111"); if (lock) { Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap(); stringRedisTemplate.delete("lock"); return categoriesDb; }else { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return getCatalogJsonDbWithRedisLock(); } }
public Map<String, List<Catalog2Vo>> getCategoryMap() { ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); String catalogJson = ops.get("catalogJson"); if (StringUtils.isEmpty(catalogJson)) { System.out.println("缓存不命中,准备查询数据库。。。"); Map<String, List<Catalog2Vo>> categoriesDb= getCategoriesDb(); String toJSONString = JSON.toJSONString(categoriesDb); ops.set("catalogJson", toJSONString); return categoriesDb; } System.out.println("缓存命中。。。。"); Map<String, List<Catalog2Vo>> listMap = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catalog2Vo>>>() {}); return listMap; }
|
问题:
1、setnx 占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁
解决:设置锁的自动过期,即使没有删除,会自动删除
阶段二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() { Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111"); if (lock) { stringRedisTemplate.expire("lock", 30, TimeUnit.SECONDS); Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap(); stringRedisTemplate.delete("lock"); return categoriesDb; }else { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return getCatalogJsonDbWithRedisLock(); } }
|
问题:
1、setnx 设置好,正要去设置过期时间,宕机。又死锁了。
解决:
设置过期时间和占位必须是原子的。redis 支持使用 setnx ex 命令
阶段三
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() { Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "1111",5, TimeUnit.SECONDS); if (lock) { Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap(); try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } stringRedisTemplate.delete("lock"); return categoriesDb; }else { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return getCatalogJsonDbWithRedisLock(); } }
|
问题:
1、删除锁直接删除???
如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。
解决:
占锁的时候,值指定为 uuid,每个人匹配是自己的锁才删除。
阶段四
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
| public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() { String uuid = UUID.randomUUID().toString(); ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS); if (lock) { Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap(); String lockValue = ops.get("lock"); if (lockValue.equals(uuid)) { try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } stringRedisTemplate.delete("lock"); } return categoriesDb; }else { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } return getCatalogJsonDbWithRedisLock(); } }
|
问题:
1、如果正好判断是当前值,正要删除锁的时候,锁已经过期,别人已经设置到了新的值。那么我们删除的是别人的锁
解决:
删除锁必须保证原子性。使用 redis+Lua 脚本完成
阶段五-最终形态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisLock() { String uuid = UUID.randomUUID().toString(); ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); Boolean lock = ops.setIfAbsent("lock", uuid,5, TimeUnit.SECONDS); if (lock) { Map<String, List<Catalog2Vo>> categoriesDb = getCategoryMap(); String lockValue = ops.get("lock"); 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(); } return getCatalogJsonDbWithRedisLock(); } }
|
保证加锁【占位+过期时间】和删除锁【判断+删除】的原子性。更难的事情,锁的自动续期
2.4 Redisson
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),从而让使用者能够将精力更集中地放在处理业务逻辑上。
本文我们仅关注分布式锁的实现,更多请参考官方文档
2.4.1 环境搭建
导入依赖
1 2 3 4 5
| <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.13.4</version> </dependency>
|
开启配置
1 2 3 4 5 6 7 8 9 10
| @Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient(){ Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.56.102:6379"); RedissonClient redisson = Redisson.create(config); return redisson; } }
|
2.4.2 可重入锁(Reentrant Lock)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisson() { Map<String, List<Catalog2Vo>> categoryMap=null; RLock lock = redissonClient.getLock("CatalogJson-Lock"); lock.lock(); try { Thread.sleep(30000); categoryMap = getCategoryMap(); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); return categoryMap; } }
|
如果负责储存这个分布式锁的 Redisson 节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,所以就设置了过期时间,但是如果业务执行时间过长,业务还未执行完锁就已经过期,那么就会出现解锁时解了其他线程的锁的情况。
所以 Redisson 内部提供了一个监控锁的看门狗,它的作用是在 Redisson 实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是 30 秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
在本次测试中CatalogJson-Lock
的初始过期时间 TTL 为 30s,但是每到 20s 就会自动续借成 30s
另外 Redisson 还通过加锁的方法提供了leaseTime
的参数来指定加锁的时间。超过这个时间后锁便自动解开了。不会自动续期!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
lock.lock(10, TimeUnit.SECONDS);
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); if (res) { try { ... } finally { lock.unlock(); } } 如果传递了锁的超时时间,就执行脚本,进行占锁; 如果没传递锁时间,使用看门狗的时间,占锁。如果返回占锁成功future,调用future.onComplete(); 没异常的话调用scheduleExpirationRenewal(threadId); 重新设置过期时间,定时任务; 看门狗的原理是定时任务:重新给锁设置过期时间,新的过期时间就是看门狗的默认时间; 锁时间/3是定时任务周期
|
edisson 同时还为分布式锁提供了异步执行的相关方法:
1 2 3 4
| RLock lock = redisson.getLock("anyLock"); lock.lockAsync(); lock.lockAsync(10, TimeUnit.SECONDS); Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
|
RLock 对象完全符合 Java 的 Lock 规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出 IllegalMonitorStateException 错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量 Semaphore 对象.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithRedisson() { Map<String, List<Catalog2Vo>> categoryMap=null; RLock lock = redissonClient.getLock("CatalogJson-Lock"); lock.lock(); try { Thread.sleep(30000); categoryMap = getCategoryMap(); } catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); return categoryMap; } }
|
最佳实战:自己指定锁时间,时间长点即可
2.4.3 读写锁(ReadWriteLock)
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
| @GetMapping("/read") @ResponseBody public String read() { RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock"); RLock rLock = lock.readLock(); String s = ""; try { rLock.lock(); System.out.println("读锁加锁"+Thread.currentThread().getId()); Thread.sleep(5000); s= redisTemplate.opsForValue().get("lock-value"); }finally { rLock.unlock(); return "读取完成:"+s; } }
@GetMapping("/write") @ResponseBody public String write() { RReadWriteLock lock = redissonClient.getReadWriteLock("ReadWrite-Lock"); RLock wLock = lock.writeLock(); String s = UUID.randomUUID().toString(); try { wLock.lock(); System.out.println("写锁加锁"+Thread.currentThread().getId()); Thread.sleep(10000); redisTemplate.opsForValue().set("lock-value",s); } catch (InterruptedException e) { e.printStackTrace(); }finally { wLock.unlock(); return "写入完成:"+s; } }
|
写锁会阻塞读锁,但是读锁不会阻塞读锁,但读锁会阻塞写锁
总之含有写的过程都会被阻塞,只有读读不会被阻塞
上锁时在 redis 的状态
2.4.4 信号量(Semaphore)
信号量为存储在 redis 中的一个数字,当这个数字大于 0 时,即可以调用acquire()
方法增加数量,也可以调用release()
方法减少数量,但是当调用release()
之后小于 0 的话方法就会阻塞,直到数字大于 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @GetMapping("/park") @ResponseBody public String park() { RSemaphore park = redissonClient.getSemaphore("park"); try { park.acquire(2); } catch (InterruptedException e) { e.printStackTrace(); } return "停进2"; }
@GetMapping("/go") @ResponseBody public String go() { RSemaphore park = redissonClient.getSemaphore("park"); park.release(2); return "开走2"; }
|
2.4.5 闭锁(CountDownLatch)
可以理解为门栓,使用若干个门栓将当前方法阻塞,只有当全部门栓都被放开时,当前方法才能继续执行。
以下代码只有offLatch()
被调用 5 次后 setLatch()
才能继续执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @GetMapping("/setLatch") @ResponseBody public String setLatch() { RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch"); try { latch.trySetCount(5); latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } return "门栓被放开"; }
@GetMapping("/offLatch") @ResponseBody public String offLatch() { RCountDownLatch latch = redissonClient.getCountDownLatch("CountDownLatch"); latch.countDown(); return "门栓被放开1"; }
|
闭锁在 redis 的存储状态
3、缓存数据的一致性
3.1 双写模式
当数据更新时,更新数据库时同时更新缓存
存在问题
由于卡顿等原因,导致写缓存 2 在最前,写缓存 1 在后面就出现了不一致
这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能得到最新的正确数据
3.2 失效模式
数据库更新时将缓存删除
存在问题
当两个请求同时修改数据库,一个请求已经更新成功并删除缓存时又有读数据的请求进来,这时候发现缓存中无数据就去数据库中查询并放入缓存,在放入缓存前第二个更新数据库的请求成功,这时候留在缓存中的数据依然是第一次数据更新的数据
解决方法
1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
2、读写数据的时候(并且写的不频繁),加上分布式的读写锁。
3.3 解决方案
无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?
- 如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可
- 如果是菜单,商品介绍等基础数据,也可以去使用 canal 订阅 binlog 的方式。
- 缓存数据+过期时间也足够解决大部分业务对于缓存的要求。
- 通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心
脏数据,允许临时脏数据可忽略);
总结:
- 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保
证每天拿到当前最新数据即可。 - 我们不应该过度设计,增加系统的复杂性
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
4、 SpringCache
这部分可以参考我之前的学习笔记:https://oy6090.top/posts/3830795892/
4.1 导入依赖
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
|
4.2 自定义配置
指定缓存类型并在主配置类上加上注解@EnableCaching
1 2 3 4 5 6 7 8 9 10 11 12
| spring: cache: type: redis redis: time-to-live: 3600000 use-key-prefix: true cache-null-values: true
|
默认使用 jdk 进行序列化,自定义序列化方式需要编写配置类
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
| @EnableConfigurationProperties(CacheProperties.class) @Configuration @EnableCaching public class MyCacheConfig {
@Bean public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); config = config.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.3 自定义序列化原理
缓存使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
@Cacheable(value = {"category"},key = "#root.methodName",sync = true) public Map<String, List<Catalog2Vo>> getCatalogJsonDbWithSpringCache() { return getCategoriesDb(); }
@Override @CacheEvict(value = {"category"},allEntries = true) public void updateCascade(CategoryEntity category) { this.updateById(category); if (!StringUtils.isEmpty(category.getName())) { categoryBrandRelationService.updateCategory(category); } }
|
第一个方法缓存结果后
第二个方法调用清除缓存后
4.4 Spring-Cache 的不足之处
- 读模式
缓存穿透:查询一个 null 数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁 ? 默认是无加锁的;使用 sync = true 来解决击穿问题
缓存雪崩:大量的 key 同时过期。解决:加随机时间。加上过期时间
- 写模式:(缓存与数据库一致)
总结:
常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用 Spring-Cache):
写模式(只要缓存的数据有过期时间就足够了)
特殊数据:特殊设计