【项目学习】谷粒商城学习记录4 - 高级篇(性能压测 & 缓存)
【项目学习】谷粒商城学习记录4 - 高级篇(性能压测 & 缓存)
一、性能压测
1、Jmeter
(1) Jmeter安装
- jmeter官网download页
- 选择支持java 8+的.zip版本下载,解压后打开bin/jemter.bat, 并修改语言
- 选择支持java 8+的.zip版本下载,解压后打开bin/jemter.bat, 并修改语言
2、Nginx动静分离
- 为什么要动静分离?
- 未分离的项目静态资源放在后端,无论是动态请求还是静态请求都会来到后台,这极大的损耗了后台Tomcat性能(大部分性能都用来处理静态请求)
动静分离后,后台只会处理动态请求,而静态资源直接由nginx返回。 - nginx.conf 配置文件,Windows和Linux有点区别
注意:匹配静态资源时,是找/static/,然后将请求在
D:/tools/Nginx/nginx-1.22.0/html
目录下面找,如:请求http://gulimall.com/static/index/img/img_09.png
经过nginx转发就变成在路径D:/tools/Nginx/nginx-1.22.0/html/static/index/img/img_09.png
。worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; client_max_body_size 1024m; sendfile on; keepalive_timeout 65; upstream gulimall { server 本地ip:88; } server { listen 80; #监听此端口 server_name gulimall.com; #监听此域名 location /static/ { root D:/tools/Nginx/nginx-1.22.0/html; } location / { proxy_set_header Host $host; proxy_pass http://gulimall; } } }
- 未分离的项目静态资源放在后端,无论是动态请求还是静态请求都会来到后台,这极大的损耗了后台Tomcat性能(大部分性能都用来处理静态请求)
二、缓存
1、背景
(1) 为什么需要缓存
- 频繁的请求数据造成了效率的下降,尤其是许多不经常改变的数据。引入缓存,能让数据库更关注于数据持久化,同时减少请求次数。
- 哪些数据适合用缓存?
- 对即时性和数据一致性要求不高
- 访问量大且更新频率低的数据(读多,写少)
- 缓存维护和请求流程:
注意: 在开发中, 凡是放入缓存中的数据我们都应该指定过期时间, 使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。 避免业务崩溃导致的数据永久不一致问题
(2) 本地缓存与分布式缓存
-
本地缓存可以用map实现
-
本地缓存存在的问题:
- 集群下本地缓存不共享, 存在于jvm中
- 数据一致性不能维护 不同机器的缓存在分布式情况下不能维护一致性
-
解决方法:分布式缓存(Redis作为缓存中间件)
- redis将缓存从服务集群中抽离,保证了数据一致性
- redis通过建立集群+分片操作,提高缓存容量
2、springboot整合redis
(1) 添加依赖并配置
- 在product模块的pom.xml文件中引入:
<!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
- 添加配置信息:
spring: redis: host: 101.201.39.44 port: 20002
- 测试
@Autowired StringRedisTemplate stringRedisTemplate; @Test public void teststringRedisTemplate() { //hello world ValueOperations<String, String> ops = stringRedisTemplate.opsForValue(); //保存 ops.set("hello", "world_" + UUID.randomUUID().toString()); //查询 String hello = ops.get("hello"); System.out.println("之前保存的数据是" + hello); }
- 测试结果:
3、缓存使用-改造三级分类业务
(1) 重写方法getCatalogJson
-
为CategoryServiceIpml类添加自动注入的redisTemplate
@Autowired private StringRedisTemplate redisTemplate;
-
将之前实现的
getCatalogJson
重命名为getCatalogJsonFromDb
,并删除@Override -
重写新的
getCatalogJson
@Override public Map<String, List<Catalog2Vo>> getCatalogJson() { //给缓存中放json字符串,拿出的json字符串,还能逆转为可用的对象类型: 【序列化与反序列化】 //1、加入缓存逻辑 //JSON是跨语言,跨平台兼容的 String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if(StringUtils.isEmpty(catalogJSON)) { //2、缓存中没有,就查询数据库 Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb(); //3、查到的数据再放入缓存,将对象转为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; }
-
测试结果
(2) 压测产生堆外内存溢出问题
- springboot2.0以后默认使用lettuce作为操作redis的客户端,它使用netty进行网络通信
- lettuce的bug导致netty堆外内存溢出 -Xmx300m: netty如果没有指定 堆外内存,默认会使用这个 -Xmx300m。可以通过
-Dio.neety.maxDirectMemory
进行设置 - 解决方案:
- 1)、升级lettuce客户端
- 2)、切换使用jedis
- 暂时解决方案:
<!-- redis --> <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> <!-- jedis客户端 --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
(3) RedisTemplate底层原理
Lettuce和Jedis是redis的客户端,RedisTempalte是对Lettuce和Jedis的再一层封装
-
- RedisAutoConfiguration自动配置类,会导入Lettuce和Jedis的配置类
- RedisAutoConfiguration自动配置类,会导入Lettuce和Jedis的配置类
-
- JedisConfiguration.java 类会给容器放一个@Bean: jedisConnectionFactory
4、高并发下一些缓存失效的问题
(1) 缓存穿透
- 概念: 当高并发查询一个数据库和缓存都不存在的数据,每次查询都会去查数据库,导致数据库崩溃失效。如果我们第一次查的时候将查到的null加入缓存并设置过期时间,这时高并发请求就会在缓存中查到结果,不会查数据库了。
- *风险: 利用不存在数据进行攻击,数据库瞬时压力增大,最终导致数据库崩溃。
- 解决方法:
- 数据库查到的null值放入缓存,并加入短暂过期时间
- 布隆过滤器(请求先查布隆过滤器,再查缓存,数据库)
(2) 缓存雪崩
- 概念: 我们设置的key值采用了相同的过期时间,当缓存某一刻同时失效后,此时出现高并发访问时,会将请求全部转发到数据库,导致数据库瞬时压力过重。
- 解决方法:
-
- 将原有的失效时间基础上增加一个随机值,这样相同过期时间的重复率就大大降低了,很难引起集体失效的事件
-
- 降级和熔断
-
- 采用哨兵或集群模式,从而构建高可用的Redis服务
-
- 如果已发生缓存雪崩: 熔断,降级
(3) 缓存击穿
- 概念: 一条高热度的key过期了,面对高并发访问,会将所有请求转发到数据库
- 解决方法:
- 加分布式锁,第一个请求获得锁,去查数据库,其他人等待。
- 热点数据不设置过期时间
5、本地锁与分布式锁
(1) 本地锁 synchronized
- 给整个缓存方法枷锁,防止缓存击穿。
- 问题: 本地锁时序问题,意思就是当第一个线程释放锁后,还没来得及将数据放入缓存,第二个线程就进去锁了,(即没锁住)
- 解决:把存入缓存的操作放在锁中
- 完整代码:
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDb() { //只要是同一把锁,就能锁住需要这个锁的所有线程 //synchronized(this); springboot 所有组件在容器内都是单例的 // TODO 本地锁: synchronized, JUC(Lock) //this代表当前实例对象 synchronized (this) { //得到锁后,我们应该再去缓存中确定一次, 如果没有才需要继续查询 String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"); if(!StringUtils.isEmpty(catalogJSON)) { Map<String, List<Catalog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2Vo>>>(){}); return result; } System.out.println("查询了数据库"); //获得所有数据 List<CategoryEntity> selectList = baseMapper.selectList(null); //1、 获得所有一级分类数据 List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L); //2、封装数据 Map<String, List<Catalog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> { //查到当前1级分类的2级分类 List<CategoryEntity> category2level = getParent_cid(selectList, level1.getCatId()); List<Catalog2Vo> catalog2Vos = null; if (category2level != null) { catalog2Vos = category2level.stream().map(level12 -> { //查询当前二级分类的三级分类 List<CategoryEntity> category3level = getParent_cid(selectList, level12.getCatId()); List<Catalog2Vo.Catalog3Vo> catalog3Vos = null; if (category3level != null) { catalog3Vos = category3level.stream().map(level13 -> { return new Catalog2Vo.Catalog3Vo(level12.getCatId().toString(), level13.getCatId().toString(), level13.getName()); }).collect(Collectors.toList()); } return new Catalog2Vo(level1.getCatId().toString(), catalog3Vos, level12.getCatId().toString(), level12.getName()); }).collect(Collectors.toList()); } return catalog2Vos; })); String s = JSON.toJSONString(collect); redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS); return collect; } }
(2) 分布式锁
- 原理: 通过使用
set key value NX
命令,表示当没有key时才会set。通过这个命令来设置lock,设置成功相当于拿到了锁
- 存在问题:
只要是流程图上,任意两个操作之间,都要考虑这里突然宕机对方法的影响
- 问题1:设置锁后,业务执行时宕机,导致没有删除锁,造成了死锁问题
- 解决: 通过
set key value EX 100 NX
中的EX设置上自动失效时间,即使宕机,锁也会自动失效。 即保证原子加锁
- 解决: 通过
- 问题2: 如果事务执行耗时,导致还没结束,锁自动失效了。那再次删除锁会影响其他线程
- 解决:通过value设置唯一标识uuid,删锁前比较解决。当然如果对比存在时间差,导致返回true后,锁突然失效,这时删除依然会删除别的线程设置的锁。
- 进一步解决:通过lua脚本解锁
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
- 问题1:设置锁后,业务执行时宕机,导致没有删除锁,造成了死锁问题
- 完整分布式锁代码:
//使用分布式锁 public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() { //1、分布式锁。去redis占坑,同时设置过期时间 //每个线程设置随机的uuid, 也可以成为token String uuid = UUID.randomUUID().toString(); //只有键key不存在的时候才会设置key的值,保证分布式情况下下一个锁能进线程 //原子操作设置锁,同时还指定了过期时间 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS); if(lock) { //加锁成功....执行业务 System.out.println("获取分布式锁成功...."); Map<String, List<Catalog2Vo>> dataFromDb = null; //无论能否成功执行事务都要进行删除锁操作 try { dataFromDb = getDataFromDB(); } finally { //2、查询uuid是否是自己,是自己的lock就删除 //查询+删除,必须是原子操作,lua脚本解锁 String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1]\n" + "then\n" + " return redis.call('del', KEYS[1])\n" + "else\n" + " return 0\n" + "end"; //删除锁, 把key和value传给lua脚本 Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(luaScript, Long.class), Arrays.asList("lock"), uuid); } return dataFromDb; } else { //加锁失败,重试 try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } return getCatalogJsonFromDbWithRedisLock(); //自旋 } }
6、Redisson
(1) 概念
- 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) springboot整合Redisson
- 引入依赖
<!-- Redisson --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.12.0</version> </dependency>
- 创建配置类config/MyRedissonConfig,使用单节点模式
@Configuration public class MyRedissonConfig { /** * 所有Redisson的使用都是通过RedissonClient对象 * @return * @throws IOException */ @Bean(destroyMethod = "shutdown") public RedissonClient reisson() throws IOException { //1、创建配置 Config config = new Config(); config.useSingleServer().setAddress("redis://101.201.39.44:20002"); //2、根据Config创建出RedissonClient示例 RedissonClient redissonClient = Redisson.create(config); return redissonClient; } }
- 测试:
@Autowired RedissonClient redissonClient; @Test public void name() { System.out.println(redissonClient); }
(3) Redisson 可重入锁
- 概念:当a业务包含b业务时,并且a业务与b业务都需要抢占统一资源,当a业务执行到b业务时,b业务发现该资源已上锁,如果是可重入锁b业务就可拿到锁,执行业务;反之如果此时b业务拿不到资源,就是不可重入锁,这样程序就会死锁.
如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,所以就设置了过期时间,但是如果业务执行时间过长,业务还未执行完锁就已经过期,那么就会出现解锁时解了其他线程的锁的情况。
-
在web/IndexController添加测试代码
@ResponseBody @GetMapping("/hello") public String hello() { //1、获取一把锁,只要锁名字一样,就是同一把锁,"my-lock"是锁名字,也是Redis的哈希模型的对外key RLock lock = redissonClient.getLock("my-lock"); //加锁 lock.lock(); //阻塞式等待,默认加的锁等待时间为30s,每到20s(经过三分之一看门狗时间后)就会自动续借30s //1. 锁的自动续期,如果在业务执行期间业务没有执行完成,redisson会为该锁自动续期 //2. 加锁的业务只要运行完成,就不会自动续期,即使不手动解锁,锁会在默认的30s后自动删除 try { System.out.println("加锁成功!执行业务...." + Thread.currentThread().getId()); Thread.sleep(30000); } catch (Exception e) { } finally { //解锁,假设代码没有运行,redisson不会出现死锁 System.out.println("锁释放..." + Thread.currentThread().getId()); lock.unlock(); } return "hello"; }
-
测试结果:
-
存在问题: 看门狗机制有死锁风险,默认30s, 每过三分之一时间会自动续期,即20s时看门狗会自动续期。这个方法并不是最佳实践
-
最佳实践:
- 指定锁的过期时间,看门狗不会自动续期。相当于我们手动设置时间,这样节省了续期时间,如果业务超时那也说明业务执行有问题。
lock.lock(30, TimeUnit.SECONDS);
- 指定锁的过期时间,看门狗不会自动续期。相当于我们手动设置时间,这样节省了续期时间,如果业务超时那也说明业务执行有问题。
(4) Redisson 公平锁
- 概念: 相当于大家都在阻塞,但是锁一旦释放,是最先的线程获得锁
(5) Redisson 读写锁
- 概念:
- 基于Redis的Ression分布式可容入锁RReadWriteLock, java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都集成了RLock接口。
- 分布式可重入读写锁,允许同时有多个读锁和一个写锁处于加锁状态。Redisson的可重入读写锁,读读不互斥,读写互斥,写写互斥。
- 读写锁同时使用:
- 保证一定能督导最新数据,修改期间,写锁是一个排他锁(互斥锁),读锁是一个共享锁。
- 写锁没释放读就必须等待
- 读 + 读:相当于无锁,并发读,只会在redis中记录好,所有当前的读锁,它们都会同时加锁成功
- 写 + 读: 等待写锁释放
- 写 + 写:阻塞方式
- 读 + 写:有读锁,写也需要等待
- 使用方法:
- 加锁:
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock"); //最常见的使用方式 //读锁 rwlock.readLock().lock(); //写锁 rwlock.writeLock().lock();
- 也可以通过参数来指定加锁时间,关闭看门狗的自动续期,超过这个时间后锁会自动解开
//10s后锁会自动解锁 //无需调用unlock()方法手动解锁 rwlock.readLock().lock(10, TimeUnit.SECONDS); //或 rwlock.writeLock(10, TimeUnit.SECONDS); //尝试加锁,最多等待100s, 上锁以后10s自动解锁 boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS); //或 boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS); //解锁 rwlock.readLock().unlock(); rwlock.writeLock().unlock();
- 加锁:
- 测试代码:
@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 = (String) 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; } }
(5) Redisson 信号量
- 概念: 信号量是存储在redis中的一个数字,当这个数字大于0时,即调用acquire()方法减少数量,也可以调用release()方法增加数量,但当调用acquire()之后小于0的话方法就会阻塞,直到数字大于0。有点像操作系统时学的PV操作。
- 测试代码:
@GetMapping("/park") @ResponseBody public String park() { RSemaphore park = redissonClient.getSemaphore("park"); try { park.acquire(1); } catch (InterruptedException e) { e.printStackTrace(); } return "停车,占一个车位1"; } @GetMapping("/go") @ResponseBody public String go() { RSemaphore park = redissonClient.getSemaphore("park"); park.release(1); return "开走,放出一个车位1"; }
(6) Redisson实现缓存一致性
- 方法:
- 双写模式:写数据库后同步更新缓存
- 失效模式:写数据库后,删除在缓存中的记录
- 用Redisson重写三级分类缓存方法:
//使用 Redisson 实现分布式锁 public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedissonLock() { // 1、锁的名字,决定了锁的粒度,越细越快 //所得粒度:具体缓存的是某个数据, 如:11-号商品:product-11-lock RLock lock = redisson.getLock("CatalogJson-lock"); lock.lock(); Map<String, List<Catalog2Vo>> dataFromDb = null; //无论能否成功执行事务都要进行删除锁操作 try { dataFromDb = getDataFromDB(); } finally { lock.unlock(); } return dataFromDb; }
7、SpringCache
(1). 简介
- 官方文档
- SpringCache简单来说就是spring做的对缓存的功能简化,定义了对缓存的接口功能,然后对接各个不同的缓存技术
- Spring 从 3.1 开始定义了 org.springframework.cache.Cache和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术;并支持使用 JCache(JSR-107) 注解简化我们开发;
- Cache 接口为缓存的组件规范定义, 包含缓存的各种操作集合;Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache ,ConcurrentMapCache 等;
- 使用 Spring 缓存抽象时我们需要关注以下两点;
1、 确定方法需要被缓存以及他们的缓存策略
2、 从缓存中读取之前缓存存储的数据 - 主要注解:
- SpEL表达式:
(2). 整合SpringCache
- 流程:
1. 引入依赖
- product模块添加
<!-- SpringCache --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
2. 写配置,添加注解
- product模块添加application.properties添加相关配置
spring.cache.type=redis
- procduct模块启动类添加开启缓存功能注解
@EnableCaching
3. 功能测试
- 为 getLevel1Categorys()方法添加缓存注解
@Cacheable
- 测试结果:
(3). 主动修改参数
- SpringCache存在很多默认行为,比如默认key, ttl, 还有序列化方式,但是我们想要自主设置key,ttl还有使用JSON进行序列化。这些都是可以实现的
默认行为
自定义缓存配置 (自己设置)
- 指定生成的缓存使用的key, key属性指定,接收一个SpEL (这个SpEL其实就是指定key值得方式,比如
#root.method.name
就是用当前方法作为key,这样的好处就是能实现以方法名作为key,或者以第一个方法传入参数来作为key等) - 指定缓存数据存活的时间,可以在配置文件中修改
- 将数据保存为json格式(这个就没那么容易实现)
代码修改
- 修改后的配置
spring.cache.type=redis spring.cache.redis.time-to-live=3600000
- 修改后的代码
//每一个需要缓存的数据我们都来指定要放到哪个名字的缓存中【缓存的分区(按照业务类型分)】 @Cacheable(value="category", key = "#root.method.name") //代表当前方法的结果需要缓存,如果缓存中有,方法不调用。 @Override public List<CategoryEntity> getLevel1Categorys() { return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0)); }
测试结果
(4). 自定义缓存配置
1.原理
2.创建自定义配置类
@EnableConfigurationProperties({CacheProperties.class})
@EnableCaching
@Configuration
public class MyCacheConfig {
/**
* 需要将配置文件中的配置设置上
* 1、使配置类生效
* 1)开启配置类与属性绑定功能EnableConfigurationProperties
*
* @ConfigurationProperties(prefix = "spring.cache") public class CacheProperties
* 2)注入就可以使用了
* @Autowired CacheProperties cacheProperties;
* 3)直接在方法参数上加入属性参数redisCacheConfiguration(CacheProperties redisProperties)
* 自动从IOC容器中找
* <p>
* 2、给config设置上
*/
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
//获得默认配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//设置key的序列化方式为String
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
//设置value的序列化方式为JSON
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;
}
}
3.修改配置文件
spring.cache.type=redis
spring.cache.redis.time-to-live=3600000
spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
#是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true
4.测试结果
- 测试空值是否缓存
(5). 注解@CacheEvict
- 注解
@CacheEvict
主要是能实现缓存删除,和@Cacheable一样也可以指定分区和key值
1.@CacheEvict简单使用
- 为updateCascade()方法添加上 @CacheEvict注解,相当于实现失效模式
- 测试
- 启动所有服务和前端,删除redis的缓存,然后访问gulimall.com添加缓存,然后前端修改分类图标信息,进行修改,看能否实现删除缓存
- 测试结果:
2.@CacheEvict进一步使用,删除多个缓存
-
首先恢复之前学习缓存使用的getCatalogJson()方法,使用SpringCache进行缓存
@Cacheable(value = "category", key="#root.methodName") @Override public Map<String, List<Catalog2Vo>> getCatalogJson() { System.out.println("查询了数据库"); //获得所有数据 List<CategoryEntity> selectList = baseMapper.selectList(null); //1、 获得所有一级分类数据 List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L); //2、封装数据 Map<String, List<Catalog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), level1 -> { //查到当前1级分类的2级分类 List<CategoryEntity> category2level = getParent_cid(selectList, level1.getCatId()); List<Catalog2Vo> catalog2Vos = null; if (category2level != null) { catalog2Vos = category2level.stream().map(level12 -> { //查询当前二级分类的三级分类 List<CategoryEntity> category3level = getParent_cid(selectList, level12.getCatId()); List<Catalog2Vo.Catalog3Vo> catalog3Vos = null; if (category3level != null) { catalog3Vos = category3level.stream().map(level13 -> { return new Catalog2Vo.Catalog3Vo(level12.getCatId().toString(), level13.getCatId().toString(), level13.getName()); }).collect(Collectors.toList()); } return new Catalog2Vo(level1.getCatId().toString(), catalog3Vos, level12.getCatId().toString(), level12.getName()); }).collect(Collectors.toList()); } return catalog2Vos; })); return collect; }
-
删除多个缓存有两种写法:
- 第一种:使用注解
@Caching
指定多个缓存操作//删除多个缓存 方法一 @Caching(evict = { @CacheEvict(value = "category", key = "'getLevel1Categorys'"), @CacheEvict(value = "category", key = "'getCatalogJson'") }) @Transactional @Override public void updateCascade(CategoryEntity category) { this.updateById(category); categoryBrandRelationService.updateCategory(category.getCatId(), category.getName()); }
- 第二种:删除一个分区下的所有缓存
@CacheEvict(value = {"category"}, allEntries = true) @Transactional @Override public void updateCascade(CategoryEntity category) { this.updateById(category); categoryBrandRelationService.updateCategory(category.getCatId(), category.getName()); }
- 第一种:使用注解
-
测试结果(方法一)
-
测试结果(方法二)
(6). 其他注解介绍
@Cacheable:标注方法上:当前方法的结果存入缓存,如果缓存中有,方法不调用
@CacheEvict:触发将数据从缓存删除的操作【删除缓存】【可实现失效模式】
@CachePut:不影响方法执行更新缓存【更新缓存】【可实现双写模式】
@Caching:组合以上多个操作【实现双写+失效模式】
@CacheConfig:在类级别共享缓存的相同配置
(7). SpringCache 存在的问题:
1. 读模式:
- 缓存穿透:解决方法:缓存空数据,通过注解ache-null-values=true实现
- 缓存击穿:解决方法:加锁,通过注解@Cacheable(value="xxx", key="xxx",sync=true)实现
- 缓存雪崩:解决方法:加上过期时间,通过配置spring.cache.redis.time-to-live=360000实现
2. 写模式:(缓存与数据库一致性)(没有解决)
- 读写加锁
- canal, 感知mysql去更新缓存
- 读多写多,之间操作数据库
总结:
- 常规数据(读多写少,即时性,一致性要求不高的数据)﹔完全可以使用Spring-Cache,写模式(只要缓存的数据有过期时间就可以)
特殊数据:特殊设计
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通