性能优化

缓存与分布式锁
缓存:要学会合理使用缓存,这样能极大提升系统性能
在这里插入图片描述
在这里插入图片描述
伪代码:
在这里插入图片描述

最简单的缓存模型:Map
这种在类中的缓存成为:本地缓存
本地缓存:缓存组件和程序是在同一线程中,在同一个JVM 中的。
在这里插入图片描述
在这里插入图片描述
本地缓存在单个项目运行是没问题的,但在分布式环境中使用有很多问题。
1.一次请求负载均衡到一个product 中,该product 中没有数据,先去数据库查询一次,将数据放进缓存中并返回。但第二次请求负载均衡到另一个product 服务中,此时的product 也没有数据,那么又要在进行一次数据库查询。
2.最致命的问题:数据一致性,当一次请求负载均衡到一个product 中,修改了其中的数据,那么缓存中的数据也进行了修改。但下次请求负载均衡到另一个product 中,那么此时请求的数据就和刚才的product 中的缓存数据不一致。

解决:分布式系统下,不应该用本地缓存。
在这里插入图片描述
这个缓存中间件就用:Redis

引入Redis 依赖:
在这里插入图片描述
配置redis:
在这里插入图片描述

   @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        //1.加入缓存逻辑.
        //规定:缓存中存的所有数据都是JSON 格式的,因为它是跨平台兼容的
        //当从缓存中拿出来的数据,要逆转成我们需要的对象【序列化与反序列化】
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)){
            //2.能进入此判断,说明缓存中没有数据,所以要查询数据库
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
            String s = JSON.toJSONString(catalogJsonFromDb);

            //3. 将查到的数据放入缓存
            redisTemplate.opsForValue().set("catalogJSON",s);

            //将数据库查出的数据直接返回
            return catalogJsonFromDb;
        }

        //将数据专为指定的对象
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});

        return  result;
    }

压测:Redis 的报错:堆外内存溢出(和JVM 本身无关)
在这里插入图片描述

导致的原因:
1.springboot2.0 以后默认使用lettuce 作为操作redis 的客户端,它使用netty 进行网络通信。
2.lettuce 的bug 导致netty 堆外内存溢出。当我们使用参数: -Xmx300m,如果netty 没有设置指定对外内存,那么它默认使用:-Xmx300m (这个300m 是直接物理内存,即内存条的内存)。
netty 内存设置: -Dio.netty.maxDirectMemory 进行设置
解决方案: 不能只使用 -Dio.netty.maxDirectMemory 只去调大堆外内存,这样是会延缓异常的出现,并不会阻止出现异常。

  1. 升级lettuce 客户端
  2. 切换使用jedis
    lettuce ,jedis都是操作jedis 的底层客户端。Spring 对它们进行了再封装:StringRedisTemplate

目前先使用第二种方法:切换使用jedis。之后会进行更换
在这里插入图片描述

缓存击穿,穿透,雪崩
缓存失效:缓存没有命中,没有查到数据,缓存没有使用到
缓存穿透:查询一个不存在的数据。
在这里插入图片描述

将空结果也放入缓存,避免缓存穿透问题。

缓存雪崩:大面积数据同时失效。
在这里插入图片描述

在这里插入图片描述

代码操作:
1.空结果缓存:解决缓存穿透
2.设置过期时间(加随机值):解决缓存雪崩
3.加锁:解决缓存击穿

比较难的是“加锁”步骤,要找到会被击穿的那部分数据库查询
在这里插入图片描述
但是这种加锁的方式只在单体服务器中适用,在分布式场景中,这样的方式不适用。
每台服务器上都只有一个锁(当前对象),锁的数量和服务器的数量一样,那么此时也有相同的线程在查询数据库。而我们加锁要实现的效果是:只留下一个锁,让一个线程进行数据库查询。这里就要用到分布式锁,但它也有缺点,就是性能会下降。
synchronized 和JUC包下的(lock) 都是本地锁,只能锁住当前服务的资源。
在这里插入图片描述
在高并发情况下使用本地锁有一个问题:
在这里插入图片描述
1 号线程确定缓存中没有数据去查数据库,查完数据库以后,它就已经释放了锁。而此时1 号线程要与redis 进行交互,因为底层涉及到网络通信等步骤,所以会浪费时间,而在这段时间中2 号线程已经得到了锁,在缓存中也没有得到相应的数据,所以2 号线程又会去查询一遍数据库,等到2 号释放完锁,3 号线程得到锁时,1 号线程才可能把查到的数据放入缓存中。3 号线程才能从缓存中得到数据。所以这样造成的现象就是:查询了两遍数据库。

解决:要把查询结果放入缓存的步骤也加到锁里面。
在这里插入图片描述

本地锁在分布式情况的问题:每个服务器上的服务都查询了一次数据库。

分布式锁原理和使用
在这里插入图片描述

redis 中有一个命令 : set key value NX
这个NX 就代表:如果没有这个key,就set 进这个value,如果有这个key就不set。
让每个服务都对redis 都发送这个命令,就能实现分布式锁的功能。


在这里插入图片描述

问题:如果在加完锁以后,代码出现异常,没有执行删除锁操作,这样会造成死锁的现象。可能会想到用try/catch 解决,即出现异常时也直接放行然后删除锁。但如果在加完锁以后服务器断点了,还是会出现死锁现象。
解决:设置锁自动过期,即使没有删除锁,会自动删除锁。

在这里插入图片描述
这段代码同样也有一个问题,就是在加完锁时,电脑断电,没有经过设置过期时间的代码,也会造成死锁。
解决:将加锁和设置过期时间设置成原子操作。

在这里插入图片描述

在这里插入图片描述

将加锁和删除锁原子化的操作也有问题:假设设置锁过期的时间为10 s,而我的业务代码要执行30s ,所以在第10 s时,属于当前业务的锁已经过期了,第二个线程已经进来也加了锁,锁过期的时间也是10s,如此下去,第一次业务在执行删除锁的时候,很有可能会把其他业务的锁也删除,这样下去加的锁就没有意义了。
所以要保证在删除锁的时候不是删除的别人的锁。
解决:给这个锁的值设置一个UUID。

在这里插入图片描述

这里又会引发出一个问题:就是在进行redis 获取锁进行验证时,也要耗费时间,假如耗费0.3s,而我上面的业务代码执行了9.5s,那么此时到达redis 的时候,锁确实还是我们的,并且也获取到这个值进行了返回,返回也用了0.3s。那么这次交互的时间+业务时间花费了10.1s。也就是在代码获取redis 锁信息的时候,锁已经自动删了。而锁自动删了,其他的线程就会来占坑,继续加锁+设置锁过期时间。而在这是正好有一个线程加的锁的位置正好就是当前业务所返回锁信息的位置,虽然代码返回的是当前业务的锁,但是在redis 中已经被标记为别人的锁,这也会是一种隐患。
解决:获取值对比+对比成功删除锁 = 原子操作。
想要实现以上操作,就要用到redis 官方的Lua 脚本
在这里插入图片描述

所以总结来就是一句话:加锁时和设置锁过期操作保持原子性。 删锁时和获取锁数据保持原子性。

    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        //1.加入缓存逻辑.
        //规定:缓存中存的所有数据都是JSON 格式的,因为它是跨平台兼容的
        //当从缓存中拿出来的数据,要逆转成我们需要的对象【序列化与反序列化】
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)){
            System.out.println("缓存不命中。。。将要查询数据库");
            //2.能进入此判断,说明缓存中没有数据,所以要查询数据库
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();


            //将数据库查出的数据直接返回
            return catalogJsonFromDb;
        }

        System.out.println("缓存命中。。。直接返回");

        //将数据专为指定的对象
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String, List<Catelog2Vo>>>(){});

        return  result;
    }




    /**
     * 使用Redis 分佈式锁
     * @return
     */
    //从数据库查询并封装分类数据
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {

        //1. 占分布式锁,去redis 占坑
        String uuid = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);

        if (lock){
            System.out.println("获取分布式锁成功...");
            //加锁成功
            Map<String, List<Catelog2Vo>> dataFromDb = null;
            try{

               dataFromDb = getDataFromDb();
            }finally {
                //Lua 脚本
                String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
                //删除锁操作
                Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
            }

            return dataFromDb;
        }else{
            System.out.println("获取分布式锁失败...等待重试");
            //加锁失败
            try{
                //休眠200ms 重试加锁
                Thread.sleep(200);
            }catch (Exception e){

            }
            return getCatalogJsonFromDbWithRedisLock(); //自旋的方式
        }
    }

JUC 包下的Lock 和synchronized 都是本地锁,在分布式下都用不上。分布式锁有很多,例如读写锁…那如何来实现Redis 的各种分布式锁呢?这就要用到Redlock 锁框架。在Java 中的Redlock 框架名称叫:Redisson
在这里插入图片描述
这里面可以提空java 的集合类型,我们平常在类中自己new 的集合都只能用于当前类下,而该框架提供的这些集合类型,都是放在Redis 中的,是能被公共访问的。
要使用它肯定就是先导入依赖:
在这里插入图片描述

  1. 配置redisson

在这里插入图片描述

在这里插入图片描述
可重入锁:比如当A 方法要调用B 方法,A,B都有锁。那么A 和B应该持有同一把锁,B 直接把A 的锁拿来用,这样A 才能调用B。如果A 持有1 号锁,B 也要持有1 号锁,B不能直接通用A 的锁,那么A 永远都调用不了B,因为A 只有等B 执行完以后才释放锁,这样就会造成死锁,也叫不可重入锁。

在这里插入图片描述
问题: 如果在加锁过程中,机器停电,那锁会不会不会被释放,然后造成redisson的死锁?
亦或者锁自动过期而引发的一系列问题?
答:不会,redisson 的lock 方法会有锁的自动续期,如果业务超长,超过了锁的删除时间,运行期间redisson 会自动给锁续上30 s(如果没设置删除锁时间,那么默认都是30 s)。所以不用担心业务时间过长,锁自动过期删除的问题。但是这个效果仅限于lock(),如果给lock 设定了过期时间,那么rdisson 就不会为锁自动续期了。而redssion 也会检测到此现象然后产生报错,但是后面的线程依然能得到锁。所以要设置锁过期时间的话一定要 > 业务运行时间。
加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁(服务器断电导致没执行到删除锁的代码),锁默认会在30 s以后自动删除。这样解决了死锁问题。

关于lock方法的源码解读:
1.如果我们传递了锁的超时时间,就发送给redis 执行脚本,进行占锁,默认超时时间就是我们设置的时间
2.如果我们未指定锁的超时时间,就是用30 s(watchdog 变量)的时间。只要占锁成功,就会启动一个定时任务(重新给锁设置过期时间,新的过期时间就是watchdog 变量 的时间)。这个续期操作是按照watchdog 变量 的时间 / 3 来进行计算的,即10s 就会进行一次续期操作,将锁持续时间续到30 s。
但是最佳实战代码还是要用lock(时间参数),将时间设置为30s,当一段代码执行超过30 s也是没有意思的,数据库连接什么的都应该是连接不上了。这样做还能节省续期操作。然后手动进行删除锁操作。
在这里插入图片描述

redisson 的阻塞等待源代码实现:
就是通过一个while(true) 不断循环去获取锁。
在这里插入图片描述

读写锁(ReadWriteLock)
业务查询时就用读锁,业务修改数据时就有写锁.
实现效果:如果别人正在修改数据,我们还想读取到最新数据,那么我们必须等待写锁释放了,我们读取数据才能进行;如果大家都是并发读,那么互不影响。并发写,就得一一排队。所以读写锁总是成对出现的。写锁控制了读锁,写锁存在,读锁就得等待。写锁也得等待自身挨个释放以后才能执行后面的写锁。如果写锁不存在,单独使用读锁,这和没加锁是一样的。
写锁是排他锁(写锁相互排斥),读锁是共享锁
读 + 读:相当于无锁,并发读,只会在redis 记录好所有的读锁,他们都会同时加锁成功
写 + 读: 写锁先执行的话,读锁等待写锁释放
写 + 写: 阻塞方式
读 + 写: 读锁先执行的话,写锁也需要等待
只要有写锁的存在,都必须等待。

读锁和写锁在redis 中存在的方式就是:读锁是value 就是read,写锁时value 就是write.
在这里插入图片描述

信号量:
这里拿停车位来进行一个说明:
在这里插入图片描述
给redis 中存一个键值对: key: park,value: 3
每执行一次acquire(),value 就会减少1。当value 减少为0 时,acquire() 就无法再进行请求这个信号量,它就要开始回旋,等待release() 释放信号量。所以release() 就是使信号量 + 1。当信号量到3 时,release() 就要开始回旋,等待acquire() 来请求这个信号量。

这个操作可以用于分布式的限流操作:比如当前服务顶多能承受每秒1 万的并发请求,如果所有请求都并发执行就可能会把服务压爆。那么就可以让所有服务一上来就执行acquire() 来获取一个信号量,这个信号量总量就是1 万,能获取到信号量就说明有空余的线程给这个请求进行处理,如果获取不了信号量就只能等别人调用了release() 释放了信号量才来处理这个请求。
而且还可以用tryAcquire() 来进行尝试获取信号量,这个方法不会造成阻塞,即如果获取不到信号量,就溜了。在限流操作中,可以使用该方法来尝试获取信号量,如果获取不到信号量,就返回给页面:“当前流量过大,请稍后操作”
信号量有tryAcquire()操作,读写锁也是有的(rwlock.readLock().tryLock(),rwlock.writeLock().tryLock()),原理一样。但没有tryRelease().

闭锁:
举例:学校放学后的锁门。
1 个班没人,无法锁大门。要5 个班全部没人了,才能锁上大门。
在这里插入图片描述
在JUC 包下也有闭锁,也是CountDownLatch() 和这些方法… 使用起来都是一样的。但还是那句话,JUC 是本地锁,这个是分布式锁。
JUC 包下也有信号量和读写锁,和分布式锁的使用方法也是一样的。

问题:缓存中的数据如何和数据库的数据保持一致(缓存数据一致性)?
解决:1.双写模式 2.失效模式
双写模式:数据库改完,也把缓存一起也改了。
在这里插入图片描述

失效模式:在更新数据库的数据时把缓存中的数据给删了,等待下次查询时发现缓存中没有响应数据,再去数据库中查,然后再写入到缓存数据中。
在这里插入图片描述
但是两种模式都在大并发情况下都会有一些漏洞:
双写模式:有一个人进行数据库修改数据时,此时它的服务器出现卡顿,而此时又有另一个人在另一个服务器也在修改数据库的同一块数据,因为它服务器没有卡顿,所以这另一个人更新的数据就先写入到缓存中,而第一个人的缓存则后出现在缓存中。而正确的出现顺序是第二个人的数据在缓存中应该覆盖第一个人的数据。这就是缓存的脏读。
解决:1. 给双写模式加上一个锁,即只有等一个人操作完修改数据库和修改缓存数据时,才能让另一个人继续这样操作。保证双写操作的原子性。
2. 看该项目的数据延迟时间,因为数据是先更新数据库再更新缓存的,所以这其中一定会有延迟,这得看该项目的容忍延迟的时间有多大,如果说数据库中的数据和缓存中的数据的同步率在10 s以内,那么就必须要用锁;如果该项目锁容忍延迟的时间在一天左右,即一天之后才能将最新的数据显示在页面上,那么这种情况在代码层面就可以不用管,直接将Redis 删除缓存数据的时间设置为一天就好了。

失效模式:现在有三个请求,第一个请求,修改1 号数据,然后删除缓存中原本的1号数据,该请求执行完了。现在来了第二个请求,它将1 号数据改成2 号数据,因为它的服务器负载比较重,花的时间比较长,

此时第三个请求也进来了,它要读取1 号数据,在缓存中没有读到,就去数据库中读,但此时第二个请求还没将1 号数据修改成2 号数据(即数据库还没提交最新的修改),然后第三个请求就读到数据库中被第一个请求修改的1 号数据,再进行更新缓存。如果此时第三个请求的更新缓存操作比第二个请求的删除缓存操作更迟执行,那么第三个请求的更新缓存操作就没人能阻止了。(第三个请求读到了第一个请求的1 号数据,把它写入到缓存中。而实际应该是要把第二个请求所修改的1 号数据写入到缓存中。)
解决:这个问题是写+读的并发问题,所有的乱序问题都能通过加锁来解决,但是加锁之后系统会比较笨重,所以就要考虑一个问题:如果这个数据经常要修改的话,还要经常经过锁的阻塞,那么这个系统就会很慢,还不如不加锁,直接查询数据库。
所以经常要修改的数据,还要求实时性要求高的,那么就直接读数据库。

在这里插入图片描述

Canal: 由alibaba 开发,是数据库的从服务器,当数据库数据一有变化,它就同步过来。
在这里插入图片描述
目前我们系统还在架构,所以就使用:失效模式,然后设置缓存的所有数据的过期时间,数据过期下一次查询触发主动更新。 如果读写数据的时候,加上分布式的读写锁(经常写,经常读肯定会有性能影响。)

缓存的使用逻辑:
缓存的两种用法模式:
1.读模式:先读取缓存中数据,缓存中没有就读取数据库中的数据,把从数据库中读到的数据放入缓存中,方便下一次读取。
2.写模式:如何保证缓存中的数据和数据库中的数据是一样的呢? 可以使用双写模式 或者失效模式。双写模式:比如修改了1 号数据,如果这个数据也要缓存中需要有,那么在给缓存发送一份新的数据,把之前缓存中的1 号数据给覆盖掉。失效模式:从数据库改完一个数据以后,可以把缓存了这个数据的直接清掉还有包含整个数据的所有数据都可以清掉,这样可以保证,下一次请求中缓存没有,可以去数据库中返回一份新的数据,并写到缓存中。

很多服务都需要用到缓存,那是不是就要想上面一样一个个去编写这样一套代码呢?
当然不是,Spring 针对于这种,抽取了一个SpringCache

在这里插入图片描述
Cache(缓存):操作缓存中增删改查数据。
CacheManager(缓存管理器): 管理缓存
在这里插入图片描述
整合SpringCache,简化缓存开发
1.引入依赖
因为是由Spring 来做的缓存整合,所以要引入Spring Cache 的依赖:
在这里插入图片描述

因为使用redis 作为缓存的环境,所以要引入redis 的依赖
在这里插入图片描述

2.写配置
1.Spring Cache 帮我们配置了:
CacgeAutoConfiguration 会导入 RedisCacheConfiguration;
2. 配置使用redis 作为缓存 在这里插入图片描述

3.测试使用缓存
在这里插入图片描述
1)开启缓存功能
在这里插入图片描述

2)只需要使用注解就能完成缓存操作。
在这里插入图片描述

1、默认行为
1) 如果数据在结果有,方法不用调用
2) key 默认自动生成:缓存的名字:: SimpleKey[] (自主生成的key 值)。category::SimpleKey[] 在这里插入图片描述
3) 缓存的value 的值,默认使用jdk 序列化机制。将序列化后的数据存到redis
在这里插入图片描述
4) 默认ttl(缓存过期时间):-1(永不过期)
在这里插入图片描述

我们需要对其配置进行自定义:
1)、指定生成的缓存使用的key:key 属性指定,接收一个SpEL
在这里插入图片描述

2)、指定缓存的数据的存活时间
在这里插入图片描述

3)、将数据保存为json 格式(因为如果是另外一个程序:PHP,来引用这些缓存的数据,会出现数据不兼容的问题。JSON 是兼容的。)
但是要把缓存数据类型转换成JSON 就要使用自定义的配置类。
原理:
在这里插入图片描述

当编写了自定义Redis 配置文件的配置类时,原来在application.properties 文件中的配置已经没用了。所以在配置类中就要配置Redis 所需要的所有自定义配置。

在这里插入图片描述

在这里插入图片描述
@Cacheable:将注释方法的返回值放入缓存,下次执行该方法时,查看缓存中是否有该返回值,有则不再执行该方法。这是读模式使用的缓存。
给查询的方法都带上这个@Cacheable
在这里插入图片描述

当对数据进行修改,就可以用双写模式和失效模式。支持双写模式的注解:@CachePut。它可以更新缓存,将方法返回的数据再返回到缓存中。 支持失效模式的注解:@CacheEvict (修改数据的方法都带上这个注释)
因为要删除的缓存有两个,所以要用Caching 来进行
在这里插入图片描述
也可以使用CacheEvict 的另一个属性:allEntries = true,即删除category 区中的所有缓存。
在这里插入图片描述

Spring-Cache 的不足:
1)读模式:
缓存穿透:查询一个null 数据。解决:缓存空数据
在这里插入图片描述
缓存击穿:大量并发进来同时查询一个正好过期的数据。解决方案:加锁? 默认是无锁的。
@Cacheable(sync = true),加锁解决。这个锁是一个本地锁,无需加分布式锁。

缓存雪崩:大量的key 同时过期。 解决:加随机时间,加上过期时间(其实不用加随机时间都行,因为每个节点的过期时间都是在存储于缓存中时记录的,而每个节点又是不同事时间进行存储的,所以一般不用加随机时间)。
在这里插入图片描述

  1. 写模式(缓存与数据库一致)
    1、读写加锁
    2、引入Canal,感知到Mysql 的更新去更新数据库
    3、读多写多,直接去数据库查询就行

一个项目只要保证缓存与数据库数据最终一致就行了,而不需要实时一致,所以Spring-Cache 对写模式的操作很少。
总结:
常规数据:(读多写少,即时性,一致性要求不要的数据);完全可以使用Spring-Cache;写模式(只要缓存的数据有过期时间就足够了)
特殊数据,后期特殊处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值