谷粒商城-08-p139-p172

139、商城业务-nginx-搭建域名访问一(反向代理)

image-20220428180249012

image-20220428180307819

image-20220428180325044

image-20220428180356332

image-20220428180409514

image-20220428180425895

1、安装nginx

  • 随便启动一个 nginx 实例,只是为了复制出配置

docker run -p 80:80 --name nginx -d nginx:1.10

  • 将容器内的配置文件拷贝到当前目录:docker container cp nginx:/etc/nginx .

  • 别忘了后面的点

  • 修改文件名称:mv nginx conf 把这个 conf 移动到/mydata/nginx 下

  • 终止原容器:docker stop nginx

  • 执行命令删除原容器:docker rm $ContainerId

  • 创建新的 nginx;执行以下命令

# 执行前确保上面的步骤执行了  创建了相关的配置文件 否则nginx无法启动
docker run -p 80:80 --name nginx -v /mydata/nginx/html:/usr/share/nginx/html -v /mydata/nginx/logs:/var/log/nginx -v /mydata/nginx/conf:/etc/nginx -d nginx:1.10 

  • 给 nginx 的 html 下面放的所有资源可以直接访问;

2、搭建域名访问

要求是 : 访问gulimall.com 能访问到我们自己本机idea中启动的product服务

思路:我们通过配置本地的host域名映射我们虚拟机的中的nginx,然后nginx在根据转发规把请求转发回我们自己的idea中的本地服务。

前置说明:因为我们的nginx的安装在云服务器上的,本地Host文件解析域名后访问云服务器, 结果请求被拦截虽然可以用谷歌浏览器安装ModHeader插件解决,但是我们在把请求映射到nginx后,在让nginx转发回我们本地的服务是无法完成的,因为云服务器无法ping通我们本地的ip 原因 本地pc属于移动宽带的内网环境,说白了就是cmd下ipconfig里的ip是你电脑宽带运营商的一个大网络(外网)下的一个内网ip,服务器无法主动与内网ip建立连接,连路由都做不到。所以不可能ping通的。可以考虑使用VPN在您的电脑和服务器之间建立VPN连接,这样服务器和您的电脑就能像在同一个内网一样互相访问了

所以接下来是的基于虚拟机安装nginx的操作笔记

2.1、配置本地host

利用host工具,用管理员权限打开,老师资料中的SwitchHost添加以下配偶

119.3.105.108 gulimall.com
119.3.105.108 auth.gulimall.com
119.3.105.108 search.gulimall.com
119.3.105.108 item.gulimall.com

image-20220428223929754

2.2 配置nginx

# 查看我们nginx的总配置文件
[root@ecs-284198 conf]# cat /mydata/nginx/conf/nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;  
}
[root@ecs-284198 conf]#
# 当前总文件的最后一行 include /etc/nginx/conf.d/*.conf;  他会加载这个路径下的所有以 .conf结尾的文件作为配置文件,并且在包含到我们的当前的总配置文件中,这样就不会导致一个配置文件很大,所以接下来我们到 conf.d 的目录下新建一个gulimall.conf文件

# cd 到 /mydata/nginx/conf/conf.d/会有一个default.conf,然后我们复制一个default.conf为gulimall.conf
[root@ecs-284198 conf.d]# cd  /mydata/nginx/conf/conf.d/
[root@ecs-284198 conf.d]# ll
total 8
-rw-r--r-- 1 root root 1097 Jan 31  2017 default.conf
-rw-r--r-- 1 root root 1065 Apr 28 21:56 gulimall.conf
[root@ecs-284198 conf.d]#

# 修改后的gulimall.conf 如下
server {
    listen       80;
    server_name  gulimall.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location / {
        proxy_pass  http://192.168.100.106:10000;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

# 其中 proxy_pass  http://192.168.100.106:10000; 中的IP使我们本地的ip,访问会我们自己本地idea启动的product服务
# 但是这样会有一个问题  如果我们的product服务有多个实列,就不好办了,其实这个proxy_pass  http://192.168.100.106:10000应该配置成我们的网卡 gateway, 让gateway负载均衡到我们的服务中去,下一节我们会解决这个问题


安装ModHeader插件(自行了解)

image-20220428202200872

3、测试

http://gulimall.com

image-20220428225355022

140、商城业务-nginx-搭建域名访问二(负载就均衡到网关)

本节课的目的就是为把请求转给网关,让网关转给我们的服务

1、修改总nginx的配置文件

# 在http 模块中配置 upstream 
[root@ecs-284198 conf]# cat nginx.conf
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;


 +  upstream gulimall{
 +  server 192.168.100.106:88;
 + }
    include /etc/nginx/conf.d/*.conf;
}

# 配置文件中upstream 叫上有服务器 然后取了一个名字叫gulimall,然后里面的里面配置的是我们网关的地址,就是以后所有请求我们上游服务器gulimall 的地址都会转发到网关去

2、修改gulimall.conf

# 在子配置文件gulimall.conf中,的location 块添加 proxy_set_header Host $host; 表示nginx在转发的时候把host也带上,如果不加,nginx会丢失请求头,host,cookies等信息  把原来请求我们product的服务改成proxy_pass  http://gulimall;其中 http://gulimall中的gulimall 就是我们在总配置文件中取的上游服务器的名字,

# 整配置文件的意思,就是监听到gulimall域名发送过来的80请求,都会转到上游服务器为gulimall中去,并且带上host信息,而上游服务器又是转发到网关的。
[root@ecs-284198 conf]# cat conf.d/gulimall.conf
server {
    listen       80;
    server_name  gulimall.com;

    #charset koi8-r;
    #access_log  /var/log/nginx/log/host.access.log  main;

    location / {
       + proxy_set_header Host $host;
       + proxy_pass  http://gulimall;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}
    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

3、修改gateway服务的yaml配置

spring:
  cloud:
    gateway:
      routes:
        #        - id: test_route
        #          uri: https://www.baidu.com
        #          predicates:
        #            - Query=url,baidu
        #
        #        - id: qq_route
        #          uri: https://www.qq.com
        #          predicates:
        #            - Query=url,qq

        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**,/hello
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}

        - id: third_party_route
          uri: lb://gulimall-third-party
          predicates:
            - Path=/api/thirdparty/**
          filters:
            - RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}

        - id: member_route
          uri: lb://gulimall-member
          predicates:
            - Path=/api/member/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}

        - id: ware_route
          uri: lb://gulimall-ware
          predicates:
            - Path=/api/ware/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}


        - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
              
           # 注意要放最后   
        - id: gulimall_host_route
          uri: lb://gulimall-product
          predicates:
            - Host=gulimall.com




## 默认前端项目我们都加,/api 为前缀
#        - id: admin_route 随意起的一个id,区别其他路由就可以
#          uri: lb://renren-fast lb:表示loadBanlence负载均衡 后面跟的是服务名
#          predicates:
#            - Path=/api/**  断言以api开头的路径都转到renren-fast的服务去
#          filters:
#            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
#            路径重写 会将/api/(?<segment>.*) 重写成/renren-fast/$\{segment}
#             其实只是将/api替换成/renren-fast 后面的片段(?<segment>.*)不变
## http://localhost:88/api/captcha.jpg   http://localhost:8080/renren-fast/captcha.jpg
## http://localhost:88/api/product/category/list/tree http://localhost:10000/product/category/list/tree

4、测试

141、性能压测-压力测试-基本介绍

压力测试考察当前软硬件环境下系统所能承受的最大负荷并帮助找出系统瓶颈所在。压测都 是为了系统在线上的处理能力和稳定性维持在一个标准范围内,做到心中有数。 使用压力测试,我们有希望找到很多种用其他测试方法更难发现的错误。

有两种错误类型是: 内存泄漏,并发与同步

有效的压力测试系统将应用以下这些关键条件:重复并发量级随机变化

1 、性能指标

  • 响应时间(Response Time: RT)

响应时间指用户从客户端发起一个请求开始,到客户端接收到从服务器端返回的响

应结束,整个过程所耗费的时间。

  • HPS(Hits Per Second) :每秒点击次数,单位是次/秒。

  • TPS(Transaction per Second):系统每秒处理交易数,单位是笔/秒。

  • QPS(Query per Second):系统每秒处理查询次数,单位是次/秒。

对于互联网业务中,如果某些业务有且仅有一个请求连接,那么 TPS=QPS=HPS,一 般情况下用 TPS 来衡量整个业务流程,用 QPS 来衡量接口查询次数,用 HPS 来表 示对服务器单击请求。

  • 无论 TPS、QPS、HPS,此指标是衡量系统处理能力非常重要的指标,越大越好,根据经 验,一般情况下:

金融行业:1000TPS~50000TPS,不包括互联网化的活动

保险行业:100TPS~100000TPS,不包括互联网化的活动

制造行业:10TPS~5000TPS

互联网电子商务:10000TPS~1000000TPS

互联网中型网站:1000TPS~50000TPS

互联网小型网站:500TPS~10000TPS

  • 最大响应时间(Max Response Time) 指用户发出请求或者指令到系统做出反应(响应) 的最大时间。

  • 最少响应时间(Mininum ResponseTime) 指用户发出请求或者指令到系统做出反应(响应)的最少时间。

  • 90%响应时间(90% Response Time) 是指所有用户的响应时间进行排序,第 90%的响应时间。

  • 从外部看,性能测试主要关注如下三个指标

吞吐量:每秒钟系统能够处理的请求数、任务数。

响应时间:服务处理一个请求或一个任务的耗时。

错误率:一批请求中结果出错的请求所占比例。

142、性能压测-压力测试-Apache JMeter安装使用

1 JMeter 安装

Jmeter 课件中已经有

https://jmeter.apache.org/download_jmeter.cgi

下载对应的压缩包,解压运行 jmeter.bat 即可

JVM参数、工具、调优笔记:https://blog.csdn.net/hancoder/article/details/108312012

2 JMeter 压测示例

1、设置语言

image-20220429113848699

2 、添加线程组(创建用户组)

表示要创建多少个用户并发

image-20220429114111506

image-20220429114434047

3、创建好用户组之后 那么我们要测试什么? 比如我们要测试 http请求

image-20220429114508428

4、添加 查看结果树 和 汇总报告

image-20220429114540241

5、比如测试百度的接口

image-20220429114615206

image-20220429114624505

测试完后查看结果报告。

6、测试自己的接口

(方便查看和效率我们暂时测试2000个)

image-20220429115529220

# 线程组参数详解: 
1、线程数:虚拟用户数。一个虚拟用户占用一个进程或线程。设置多少虚拟用户数在这里 也就是设置多少个线程数。 
2、Ramp-Up Period(in seconds)准备时长:设置的虚拟用户数需要多长时间全部启动。如果 线程数为 10,准备时长为 2,那么需要 2 秒钟启动 10 个线程,也就是每秒钟启动 5 个 线程。
3、循环次数:每个线程发送请求的次数。如果线程数为 10,循环次数为 100,那么每个线 程发送 100 次请求。总请求数为 10*100=1000 。如果勾选了“永远”,那么所有线程会 一直发送请求,一到选择停止运行脚本。
4、Delay Thread creation until needed:直到需要时延迟线程的创建。 
5、调度器:设置线程组启动的开始时间和结束时间(配置调度器时,需要勾选循环次数为 永远) 
6、持续时间(秒):测试持续时间,会覆盖结束时间 
7、启动延迟(秒):测试延迟启动时间,会覆盖启动时间 
8、启动时间:测试启动时间,启动延迟会覆盖它。当启动时间已过,手动只需测试时当前 时间也会覆盖它。 
9、结束时间:测试结束时间,持续时间会覆盖它。

image-20220429115539469

image-20220429115554673

image-20220429115610697

image-20220429115622172

7、优化一 加大运行内存

image-20220429115942427

在测试 (结果有提升)

image-20220429115959911


# 结果分析
1、有错误率同开发确认,确定是否允许错误的发生或者错误率允许在多大的范围内; 
2、 Throughput 吞吐量每秒请求的数大于并发数,则可以慢慢的往上面增加;若在压测的机 器性能很好的情况下,出现吞吐量小于并发数,说明并发数不能再增加了,可以慢慢的 往下减,找到最佳的并发数; 
3、压测结束,登陆相应的 web 服务器查看 CPU 等性能指标,进行数据的分析; 
4、最大的 tps,不断的增加并发数,加到 tps 达到一定值开始出现下降,那么那个值就是 最大的 tps。 
5、最大的并发数:最大的并发数和最大的 tps 是不同的概率,一般不断增加并发数,达到 一个值后,服务器出现请求超时,则可认为该值为最大的并发数。 
6、压测过程出现性能瓶颈,若压力机任务管理器查看到的 cpu、网络和 cpu 都正常,未达到 90%以上,则可以说明服务器有问题,压力机没有问题。
7、影响性能考虑点包括: 数据库、应用程序、中间件(tomact、Nginx)、网络和操作系统等方面 
8、首先考虑自己的应用属于 CPU 密集型还是 IO 密集型

dockers 各个容器的内存情况

docker stats #命令可以监听dockers容器中的每一个容器的内存情况

image-20220429133847286

143、性能压测-压力测试-JMeter在win下地址占用bug解决

windows 本身提供的端口访问机制的问题。

Windows 提供给 TCP/IP 链接的端口为 1024-5000,并且要四分钟来循环回收他们。就导致

我们在短时间内跑大量的请求时将端口占满了。

Jmeter Address Already in use错误解决
报错原因:

1、windows系统为了保护本机,限制了其他机器到本机的连接数.
2、TCP/IP 可释放已关闭连接并重用其资源前,必须经过的时间。关闭和释放之间的此时间间隔通称 TIME_WAIT 状态或两倍最大段生命周期(2MSL)状态。此时间期间,重新打开到客户机和服务器的连接的成本少于建立新连接。减少此条目的值允许 TCP/IP 更快地释放已关闭的连接,为新连接提供更多资源。如果运行的应用程序需要快速释放和创建新连接,而且由于 TIME_WAIT 中存在很多连接,导致低吞吐量,则调整此参数。

修改操作系统注册表
1、打开注册表:运行-regedit
2、直接输入找到HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\TCPIP\Parameters
3、右击Parameters新建 DWORD32值,name:TcpTimedWaitDelay,value:30(十进制) ——> 设置为30秒回收(默认240)
4、新建 DWORD值,name:MaxUserPort,value:65534(十进制) ——> 设置最大连接数65534
注意:修改时先选择十进制,再填写数字。
5、重启系统

144、性能压测-性能监控-对内存与垃圾回收

1 jvm 内存模型

image-20220429121823861

程序计数器 Program Counter Register:

  • 记录的是正在执行的虚拟机字节码指令的地址,

  • 此内存区域是唯一一个在JAVA虚拟机规范中没有规定任何OutOfMemoryError的区域

虚拟机:VM Stack

  • 描述的是 JAVA 方法执行的内存模型,每个方法在执行的时候都会创建一个栈帧, 用于存储局部变量表,操作数栈,动态链接,方法接口等信息

  • 局部变量表存储了编译期可知的各种基本数据类型、对象引用

  • 线程请求的栈深度不够会报 StackOverflowError 异常

  • 栈动态扩展的容量不够会报 OutOfMemoryError 异常

  • 虚拟机栈是线程隔离的,即每个线程都有自己独立的虚拟机栈

本地方法:Native Stack

  • 本地方法栈类似于虚拟机栈,只不过本地方法栈使用的是本地方法

堆:Heap

  • 几乎所有的对象实例都在堆上分配内存

    image-20220429123536030

2 、堆

所有的对象实例以及数组都要在堆上分配。堆是垃圾收集器管理的主要区域,也被称为“GC 堆”;也是我们优化最多考虑的地方。 堆可以细分为:

新生代

  • Eden 空间

  • From Survivor 空间

  • To Survivor 空间

老年代

  • 永久代/元空间

  • Java8 以前永久代,受 jvm 管理,java8 以后元空间,直接使用物理内存。因此,

默认情况下,元空间的大小仅受本地内存限制。

垃圾回收

image-20220429124507485

从 Java8 开始,HotSpot 已经完全将永久代(Permanent Generation)移除,取而代之的是一

个新的区域—元空间(MetaSpace)

image-20220429124519266

image-20220429124525859

145、性能压测-性能监控-jvisualvm使用

1、jconsole jvisualvm

Jdk 的两个小工具 jconsole、jvisualvm(升级版的 jconsole);通过命令行启动,可监控本地和

远程应用。远程应用需要配置

1.1 jconsole 连接

image-20220429130417851

image-20220429130428334

2 jvisualvm 能干什么

2.1 启动 jvisualvm

jvisualvm的启动也是直接在 cmd 输入 jvisualvm 即可,如果是jdk 的高版本话jdk 的默认是不带的,所以需要自己安装

image-20220429132232648

监控内存泄露,跟踪垃圾回收,执行时内存、cpu 分析,线程分析…

image-20220429125311052

运行:正在运行的

休眠:sleep

等待:wait

驻留:线程池里面的空闲线程

监视:阻塞的线程,正在等待锁

3 、安装插件方便查看 gc

Cmd 启动 jvisualvm

工具->插件

image-20220429125402555

image-20220429125412306

image-20220429132655384

  • 如果 503 错误解决:

  • 打开网址 https://visualvm.github.io/pluginscenters.html

  • cmd 查看自己的 jdk 版本,找到对应的

image-20220429125444245

复制下面查询出来的链接。并重新设置上即可

image-20220429125455562

image-20220429132902642

146、性能压测-优化-中间件对性能的影响

1、中间件指标

image-20220429135245430

  • 当前正在运行的线程数不能超过设定的最大值。一般情况下系统性能较好的情况下,线 程数最小值设置 50 和最大值设置 200 比较合适。

  • 当前运行的 JDBC 连接数不能超过设定的最大值。一般情况下系统性能较好的情况下, JDBC 最小值设置 50 和最大值设置 200 比较合适。

  • GC频率不能频繁,特别是 FULL GC 更不能频繁,一般情况下系统性能较好的情况下,JVM 最小堆大小和最大堆大小分别设置 1024M 比较合适。

2 、数据库指标

image-20220429135337034

  • SQL 耗时越小越好,一般情况下微秒级别。

  • 命中率越高越好,一般情况下不能低于 95%。

  • 锁等待次数越低越好,等待时间越短越好。

// 可以查看各个容器的使用资源(内存,CPU 。。 )情况
[root@ecs-284198 ~]# docker stats

使用JMeter 分别单压测下面表单中的压测内容中的项目,并纪录数据,我们实验的步骤是按照我们的下面的表格中的 “压测内容” 从上往下的,并且每一步的都包括上一步的链路的,第一步测试nginx就是一个nginx,第二步测Gateway,链路是从gateway->nginx,第三步链路是gateway->nginx->简单服务(返回字符串) 以此内推,统计如下:

在使用JMeter 压力测试我们自己写的服务的时候,可以打开jvisualvm 查看内存占用情况

压测内容压测线程数吞吐量/s90%响应时间99%响应时间
Nginx(浪费CPU)502120101204
Gateway(浪费CPU)509200921
简单服务(返回字符串)509850848
首页一级菜单渲染50350260491
首页菜单渲染(开缓存)50465119306
首页菜单渲染(开缓存、优化数据库、关日志)50465127304
三级分类数据获取504(优化加索引)1327513756
三级分类(优化业务)501540925891
首页全量数据获取502.724001426556
首页全量数据获取(动静分类)504.91491316421
Nginx+GateWay50
Gateway+简单服务5030002867
全链路(Nginx+GateWay+简单服务)5065084537

product微服务的 -Xmx1024m -Xms1024m -Xmn512m

中间件越多,性能损失越大,大多都损失在网络交互了;

  • 业务:

  • Db(MySQL 优化)

  • 模板的渲染速度(缓存)

  • 静态资源

3 JVM 分析 & 调优

jvm 调优,调的是稳定,并不能带给你性能的大幅提升。服务稳定的重要性就不用多说了,保证服务的稳定,gc 永远会是 Java 程序员需要考虑的不稳定因素之一。复杂和高并发下的服务,必须保证每次 gc 不会出现性能下降,各种性能指标不会出现波动,gc 回收规律而且 干净,找到合适的 jvm 设置。Full gc 最会影响性能,根据代码问题,避免 full gc 频率。可以适当调大年轻代容量,让大对象可以在年轻代触发 yong gc,调整大对象在年轻代的回收频次,尽可能保证大对象在年轻代回收,减小老年代缩短回收时间;

1 、几个常用工具

jstack查看 jvm 线程运行状态,是否有死锁现象等等信息
jinfo可以输出并修改运行时的 java 进程的 opts。
jps与 unix 上的 ps 类似,用来显示本地的 java 进程,可以查看本地运行着几个 java 程序,并显示他们的进程号。
jstat一个极强的监视 VM 内存工具。可以用来监视 VM 内存内的各种堆和非堆的大小 及其内存使用量。
jmap打印出某个 java 进程(使用 pid)内存内的所有’对象’的情况(如:产生那些对象, 及其数量)

2 、命令示例

jstat 工具特别强大,有众多的可选项,详细查看堆内各个部分的使用量,以及加载类的数量。使用时,需加上查看进程的进程 id,和所选参数。

jstat -class pid显示加载 class 的数量,及所占空间等信息
jstat -compiler pid显示 VM 实时编译的数量等信息。
jstat -gc pid可以显示 gc 的信息,查看 gc 的次数,及时间
jstat -gccapacity pid堆内存统计,三代(young,old,perm)内存使用和占用大小
jstat -gcnew pid新生代垃圾回收统计
jstat -gcnewcapacity pid新生代内存统计
jstat -gcold pid老年代垃圾回收统计
除了以上一个参数外,还可以同时加上 两个数字,如:jstat -printcompilation 3024 250 6 是 每 250 毫秒打印一次,一共打印 6 次,还可以加上-h3 每三行显示一下标题。

jstat -gcutil pid 1000 100 : 1000ms 统计一次 gc 情况统计 100 次;

在使用这些工具前,先用 JPS 命令获取当前的每个 JVM 进程号,然后选择要查看的 JVM。

jinfo 是 JDK 自带的命令,可以用来查看正在运行的 java 应用程序的扩展参数,包括 Java System 属性和 JVM 命令行参数;也可以动态的修改正在运行的 JVM 一些参数。当系统崩溃时,jinfo 可以从 core 文件里面知道崩溃的 Java 应用程序的配置信息

jinfo pid输出当前 jvm 进程的全部参数和系统属性
jinfo -flag name pid可以查看指定的 jvm 参数的值;打印结果:-无此参数,+有
jinfo -flag [+|-]name pid开启或者关闭对应名称的参数(无需重启虚拟机)
jinfo -flag name=value pid修改指定参数的值
jinfo -flags pid输出全部的参数
jinfo -sysprops pid输出当前 jvm 进行的全部的系统属性
jmap 可以生成 heap dump 文件,也可以查看堆内对象分析内存信息等,如果不使用这个命 令,还可以使用-XX:+HeapDumpOnOutOfMemoryError 参数来让虚拟机出现 OOM 的时候自动 生成 dump 文件。

# jmap -dump:live,format=b,file=dump.hprof pid 
dump 堆到文件,format 指定输出格式,live 指明是活着的对象,file 指定文件名。eclipse 可 以打开这个文件

# jmap -heap pid 
打印 heap 的概要信息,GC 使用的算法,heap 的配置和使用情况,可以用此来判断内存目 前的使用情况以及垃圾回收情况

# jmap -finalizerinfo pid 
打印等待回收的对象信息

# jmap -histo:live pid 
打印堆的对象统计,包括对象数、内存大小等。jmap -histo:live 这个 命令执行,JVM 会先触发 gc,然后再统计信息

# jmap -clstats pid 
打印 Java 类加载器的智能统计信息,对于每个类加载器而言,对于每个类加载器而言,它 的名称,活跃度,地址,父类加载器,它所加载的类的数量和大小都会被打印。此外,包含 的字符串数量和大小也会被打印

-F 强制模式。如果指定的 pid 没有响应,请使用 jmap -dump 或 jmap -histo 选项。此 模式下,不支持 live 子选项。 jmap -F -histo pid

jstack 是 jdk 自带的线程堆栈分析工具,使用该命令可以查看或导出 Java 应用程序中线程堆 栈信息。

# jstack pid
输出当前 jvm 进程的全部参数和系统属性

3 、调优项

官方文档:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html#BGBCIEFC

147、性能压测-优化-简单优化吞吐量测试

product微服务的 -Xmx1024m -Xms1024m -Xmn512m

中间件越多,性能损失越大,大多都损失在网络交互了;

  • 业务:

  • Db(MySQL 优化)

  • 模板的渲染速度(缓存)

  • 静态资源

148、性能压测-优化-nginx动静分离

image-20220429142812580

Nginx动静分离
由于动态资源和静态资源目前都处于服务端,所以为了减轻服务器压力,我们将js、css、img等静态资源放置在Nginx端,以减轻服务器压力

1、动静态文件上传到 mydata/nginx/html/static/index/css,这种格式

image-20220429142958923

image-20220429143015030

image-20220429143031363

image-20220429143047296

image-20220429143102403

image-20220429143115216

2、修改index.html的静态资源路径

修改index.html的静态资源路径,加上static前缀src=“/static/index/img/img_09.png”

3、修改/mydata/nginx/conf/conf.d/gulimall.conf

如果遇到有/static为前缀的请求,转发至html文件夹

    location /static {
        root   /usr/share/nginx/html;
    }


    location / {
        proxy_pass http://gulimall;
	proxy_set_header Host $host;
    }

4、堆内存设置

image-20220429143349239

149、性能压测-优化-模拟线上应用内存奔溃宕机情况

略 。。。

150、性能压测-优化-优化三级分类获取数据

优化前

对二级菜单的每次遍历都需要查询数据库,浪费大量资源

优化后

仅查询一次数据库,剩下的数据通过遍历得到并封装

//优化业务逻辑,仅查询一次数据库
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;

总结:总体的部分包含:中间件(nginx, tomcat , gateway,rabbitMq等耗时操作)解决方案是提高每个中间件的性能

第二:是我们自己的应用耗时,①、业务逻辑耗时,优化代码逻辑,适当的开启多线程等,②db耗时,可以创建索引,优化索引,开启新建缓存等

151、缓存-缓存使用-本地缓存与分布式缓存

以上的所有的操作通过优化sql 优化代码, 扩大内存等等方式解决性能问题,接下来的缓存也是一种方式

1 、缓存使用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落盘工作。

哪些数据适合放入缓存?

  • 即时性、数据一致性要求不高的

  • 访问量大且更新频率不高的数据(读多,写少)

举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。

image-20220429193203557

伪代码:

data = cache.load(id);
//从缓存加载数据 
If(data == null){ 
      data = db.load(id);
    //从数据库加载数据 
    cache.put(id,data);
    //保存到 cache 中 
}
return data

**注意:**在开发中,凡是放入缓存中的数据我们都应该指定过期时间,使其可以在系统即使没有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致问题。

152、缓存-缓存使用-整合redis测试

1、引入 redis-starter

<dependency> 
  <groupId>org.springframework.boot</groupId> 
  <artifactId>spring-boot-starter-data-redis</artifactId> 
</dependency>

2、配置 redis

spring:
   redis:
      host: 119.3.105.108
      port: 6379

3、使用 RedisTemplate 操作 redis

package com.atguigu.gulimall.product;


import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.io.ByteArrayInputStream;
import java.io.PrintStream;
import java.util.UUID;

@SpringBootTest
class GulimallProductApplicationTests {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Test 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);
    }



}

153、缓存-缓存使用-改造三级分类业务

改造CategoryServiceImpl中的getCatalogJson 方法

	 @Autowired
    private StringRedisTemplate redisTemplate;


    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
    //给缓存中放JSON字符串,拿出来的JSON字符串,还用逆转为能用的额对象类型:【序列化与反序列化】

        //1、加入缓存逻辑,缓存中存在的数据是JSON字符串(JSON夸语言,跨平台兼容)
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)){
            //2、缓存中没有  查询数据库
            Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
            //3、查到数据在放入缓存,将对象转为JSON放入缓存中
            String s = JSON.toJSONString(catalogJsonFromDb);
            redisTemplate.opsForValue().set("catalogJSON",s);
            return catalogJsonFromDb;
        }

        //转为我们指定的对象
        Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });

        return result;
    }



    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
        System.out.println("查询了数据库.....");
        List<CategoryEntity> selectList = baseMapper.selectList(null);
        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);
        //2、封装数据
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1、每一个的一级分类,查到这个一级分类的二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
            //2、封装上面面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //1、找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
                    if (level3Catelog != null) {
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            //2、封装成指定格式
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        return parent_cid;

    }

154、缓存-缓存使用-压力测试出的内存泄露及解决

根据上面我们通过redis缓存优化后 在压测得过程中,会产生堆外内存溢出:OutOfDirectMemoryError

# TODO 产生堆外内存溢出:OutOfDirectMemoryError
springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信。
lettuce的bug导致netty堆外内存溢出 -Xmx300m;netty如果没有指定堆外内存,默认使用-Xmx300m
可以通过-Dio.netty.maxDirectMemory进行设置
解决方案:不能使用-Dio.netty.maxDirectMemory只去调大堆外内存。
1)、升级lettuce客户端。   
2)、切换使用jedis
//  redisTemplate:
//  lettuce、jedis操作redis的底层客户端。Spring再次封装redisTemplate;

我们的解决方案是 更换客户端

 <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>

155、缓存-缓存使用-缓存击穿-穿透-雪崩

image-20220429224956158

缓存失效问题

先来解决大并发读情况下的缓存失效问题;

image-20220429225043502

1 、缓存穿透

  • 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

  • 在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。

  • 解决:

缓存空结果、并且设置短的过期时间。

2 、缓存雪崩

image-20220429225101350

  • 缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。

  • 解决:

原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

3 、缓存击穿

image-20220429225131458

  • 对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。

  • 这个时候,需要考虑一个问题:如果这个 key 在大量请求同时进来前正好失效,那么所 有对这个 key 的数据查询都落到 db,我们称为缓存击穿。

  • 解决:

加锁

156、缓存-缓存使用-加锁解决缓存击穿问题

1、使用本地锁解决缓存击穿的问题,如果是一个单体应用,我们可以用本地锁:synchronized,JUC(Lock),并且锁也是单列的,比如如果用synchronized (this){ } 来加锁,这个this必须是单列的否则无法锁住,在分布式情况下,想要锁住所有,必须使用分布式锁

代码案列:

//从数据库查询并封装分类数据
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithLocalLock() {
//        //1、如果缓存中有就用缓存的
//        Map<String, List<Catelog2Vo>> catalogJson = (Map<String, List<Catelog2Vo>>) cache.get("catalogJson");
//        if(cache.get("catalogJson") == null) {
//            //调用业务  xxxxx
//            //返回数据又放入缓存
//            cache.put("catalogJson",parent_cid);
//        }
//        return catalogJson;
        //只要是同一把锁,就能锁住需要这个锁的所有线程
        //1、synchronized (this):SpringBoot所有的组件在容器中都是单例的。
        //TODO 本地锁:synchronized,JUC(Lock),在分布式情况下,想要锁住所有,必须使用分布式锁

        synchronized (this) {
            //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
            return getDataFromDb();
        }


    }



   private Map<String, List<Catelog2Vo>> getDataFromDb() {
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (!StringUtils.isEmpty(catalogJSON)) {
            //缓存不为null直接返回
            Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return result;
        }
        System.out.println("查询了数据库.....");

        List<CategoryEntity> selectList = baseMapper.selectList(null);


        List<CategoryEntity> level1Categorys = getParent_cid(selectList, 0L);

        //2、封装数据
        Map<String, List<Catelog2Vo>> parent_cid = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            //1、每一个的一级分类,查到这个一级分类的二级分类
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
            //2、封装上面面的结果
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
                catelog2Vos = categoryEntities.stream().map(l2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                    //1、找当前二级分类的三级分类封装成vo
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, l2.getCatId());
                    if (level3Catelog != null) {
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                            //2、封装成指定格式
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }


            return catelog2Vos;
        }));


        //3、查到的数据再放入缓存,将对象转为json放在缓存中
        String s = JSON.toJSONString(parent_cid);
        redisTemplate.opsForValue().set("catalogJSON", s, 1, TimeUnit.DAYS);
        return parent_cid;
    }

注意点: 我们在获取锁之后立马再去查询一次缓存,此时没有在去查询数据库,查询数据之后在把数据放入缓存,然后才能释放锁。

image-20220429232734492

157、缓存-缓存使用-本地锁在分布式下的问题

1、测试多个服务的情况下用本地锁的问题

cope上多个服务

image-20220429232500602

端口也可以这样配置

image-20220430143518818

image-20220429232629726

运行三个实列 然后测试,要用nginx转发网关,然后网关来调用这个三个实列。

显然这个三个实列都会查一次。

image-20220429232722632

158、缓存-分布式锁-分布式锁原理与使用

1、基本原理

image-20220429233020969

2、分布式锁演进-阶段一

image-20220429235645899

   public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        //1、占分布式锁。去redis占坑
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
        if (lock) {
            //加锁成功...执行业务
            Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
            redisTemplate.delete("lock");//删除锁

            return dataFromDb;
        } else {
            //加锁失败...重试。synchronized()//休眠106ms重试
            return getCatalogJsonFromDbwithRedisLock();//自旋的方式、,

        }

    }

3、分布式锁演进-阶段二

image-20220429235747514

代码实现

image-20220429235824910

4、分布式锁演进-阶段三

image-20220429235902165

代码实现

image-20220429235919130

5、分布式锁演进-阶段四

image-20220429235947874

5、分布式锁演进-阶段五-最终形态

image-20220430000015589

5.1 最终代码实现-手写redis
 //从数据库查询并封装分类数据
    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("获取分布式锁成功...");
            //加锁成功... 执行业务
            //2、设置过期时间,必须和加锁是同步的,原子的
            //redisTemplate.expire("lock",30,TimeUnit.SECONDS);
            Map<String, List<Catelog2Vo>> dataFromDb;
            try {
                dataFromDb = getDataFromDb();
            } finally {
                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);
            }

            //获取值对比+对比成功删除=原子操作  lua脚本解锁
//            String lockValue = redisTemplate.opsForValue().get("lock");
//            if(uuid.equals(lockValue)){
//                //删除我自己的锁
//                redisTemplate.delete("lock");//删除锁
//            }
            return dataFromDb;
        } else {
            //加锁失败...重试。synchronized ()
            //休眠100ms重试
            System.out.println("获取分布式锁失败...等待重试");
            try {
                Thread.sleep(200);
            } catch (Exception e) {

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


    }
  1. 2 最终代码实现-手写redisson
      public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
            //1、锁的名字。 锁的粒度,越细越快。
            //锁的粒度:具体缓存的是某个数据,11-号商品;  product-11-lock product-12-lock   product-lock
            RLock lock = redisson.getLock("CatalogJson-lock");
            lock.lock();
    
    
            Map<String, List<Catelog2Vo>> dataFromDb;
            try {
                dataFromDb = getDataFromDb();
            } finally {
                lock.unlock();
            }
    
            return dataFromDb;
    
    
        }
    

159、缓存-分布式锁-Redisson-简介&整合

1、Redisson简介

https://github.com/redisson/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、整合

2.1、引入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>
这个用作连续,后面可以使用redisson-spring-boot-starter

2.2 、配置文件

2、开启配置https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95

package com.atguigu.gulimall.product.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/30
 * @描述:
 */
@Configuration
public class MyRedissonConfig {

    /**
     * 所有对Redisson的使用都是通过RedissonClient对象
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson(@Value("${spring.redis.host}") String url) throws IOException {
        //1、创建配置
        //Redis url should start with redis:// or rediss://
        Config config = new Config();
        config.useSingleServer().setAddress("redis://"+url+":6379");
        //2、根据Config创建出RedissonClient示例  创建单例模式的配置 需要集群修改这里
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }

}

spring:
  redis:
    host: 119.3.105.108
    port: 6379

2.3、使用

package com.atguigu.gulimall.product.web;

import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryService;
import com.atguigu.gulimall.product.vo.Catelog2Vo;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;
import java.util.Map;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/28
 * @描述:
 */
@Controller
public class IndexController {
    @Autowired
    RedissonClient redisson;

    @ResponseBody
    @GetMapping("/hello")
    public String hello(){
        //1、获取一把锁,只要锁的名字一样,就是同一把锁
        RLock lock = redisson.getLock("my-lock");

        //2、加锁
        lock.lock(); //阻塞式等待。默认加的锁都是30s时间。
        //1)、锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心业务时间长,锁自动过期被删掉
        //2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。

//        lock.lock(10,TimeUnit.SECONDS); //10秒自动解锁,自动解锁时间一定要大于业务的执行时间。
        //问题:lock.lock(10,TimeUnit.SECONDS); 在锁时间到了以后,不会自动续期。
        //1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
        //2、如果我们未指定锁的超时时间,就使用30 * 1000【LockWatchdogTimeout看门狗的默认时间】;
        //    只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10s都会自动再次续期,续成30s
        //    internalLockLeaseTime【看门狗时间】 / 3,10s

        //最佳实战
        //1)、lock.lock(30,TimeUnit.SECONDS);省掉了整个续期操作。手动解锁
        try{
            System.out.println("加锁成功,执行业务..."+Thread.currentThread().getId());
            Thread.sleep(30000);
        }catch (Exception e){

        }finally {
            //3、解锁  将设解锁代码没有运行,redisson会不会出现死锁
            System.out.println("释放锁..."+Thread.currentThread().getId());
            lock.unlock();
        }

        return "hello";
    }


}

160、缓存-分布式锁-Redisson-lock测试

image-20220430143810161

启动两个服务取同时获取锁即可

161、缓存-分布式锁-Redisson–lock看门狗原理-Redisson如何解决死锁

可重入锁(Reentrant Lock)

分布式锁:github.com/redisson/redisson/wiki/8.-分布式锁和同步器

A调用B。AB都需要同一锁,此时可重入锁就可以重入,A就可以调用B。不可重入锁时,A调用B将死锁

// 参数为锁名字
RLock lock = redissonClient.getLock("CatalogJson-Lock");//该锁实现了JUC.locks.lock接口
lock.lock();//阻塞等待
// 解锁放到finally // 如果这里宕机:有看门狗,不用担心
lock.unlock();

基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

锁的续期:大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟(每到20s就会自动续借成30s,是1/3的关系),也可以通过修改Config.lockWatchdogTimeout来另行指定。

// 加锁以后10秒钟自动解锁,看门狗不续命
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
   try {
     ...
   } finally {
       lock.unlock();
   }
}
如果传递了锁的超时时间,就执行脚本,进行占锁;
如果没传递锁时间,使用看门狗的时间,占锁。如果返回占锁成功future,调用future.onComplete();
没异常的话调用scheduleExpirationRenewal(threadId);
重新设置过期时间,定时任务;
看门狗的原理是定时任务:重新给锁设置过期时间,新的过期时间就是看门狗的默认时间;
锁时间/3是定时任务周期;


Redisson同时还为分布式锁提供了异步执行的相关方法:

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 对象.

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;
    }
}

最佳实战:自己指定锁时间,时间长点即可

162、缓存-分布式锁-Redisson-读写锁测试

基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。

分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

上锁时在redis的状态

HashWrite-Lock
key:mode  value:read
key:sasdsdffsdfsdf... value:1

读写锁案列

 @Autowired
    StringRedisTemplate redisTemplate;

    //保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁、独享锁)。读锁是一个共享锁
    //写锁没释放读就必须等待
    // 读 + 读: 相当于无锁,并发读,只会在redis中记录好,所有当前的读锁。他们都会同时加锁成功
    // 写 + 读: 等待写锁释放
    // 写 + 写: 阻塞方式
    // 读 + 写: 有读锁。写也需要等待。
    // 只要有写的存在,都必须等待
    @GetMapping("/write")
    @ResponseBody
    public String writeValue(){
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
        String s = "";
        RLock rLock = lock.writeLock();
        try {
            //1、改数据加写锁,读数据加读锁
            rLock.lock();
            System.out.println("写锁加锁成功..."+Thread.currentThread().getId());
            s = UUID.randomUUID().toString();
            Thread.sleep(30000);
            redisTemplate.opsForValue().set("writeValue",s);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            System.out.println("写锁释放"+Thread.currentThread().getId());
        }

        return  s;
    }

    @GetMapping("/read")
    @ResponseBody
    public String readValue(){
        RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
//        ReentrantReadWriteLock writeLock = new ReentrantReadWriteLock();
        String s = "";
        //加读锁
        RLock rLock = lock.readLock();
        rLock.lock();
        try {
            System.out.println("读锁加锁成功"+Thread.currentThread().getId());
            s = redisTemplate.opsForValue().get("writeValue");
            Thread.sleep(30000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            System.out.println("读锁释放"+Thread.currentThread().getId());
        }

        return  s;
    }    
    总结 :  注意这里的读和些都是统一把锁 rw-lock

163、缓存-分布式锁-Redisson-读写锁补充

同上

164、缓存-分布式锁-Redisson-闭锁测试

闭锁(CountDownLatch)

基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。

以下代码只有offLatch()被调用5次后 setLatch()才能继续执行

RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();

// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();

案列:

  /**
     * 放假,锁门
     * 1班没人了,2
     * 5个班全部走完,我们可以锁大门
     */
    @GetMapping("/lockDoor")
    @ResponseBody
    public String lockDoor() throws InterruptedException {
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.trySetCount(5);
        door.await(); //等待闭锁都完成

        return "放假了...";
    }

    @GetMapping("/gogogo/{id}")
    @ResponseBody
    public String gogogo(@PathVariable("id") Long id){
        RCountDownLatch door = redisson.getCountDownLatch("door");
        door.countDown();//计数减一;
//        CountDownLatch
        return id+"班的人都走了...";
    }

测试 先调用 @GetMapping("/lockDoor")方法:  在调用5@GetMapping("/gogogo/{id}")方法

165、缓存-分布式锁-Redisson-信号量测试

信号量(Semaphore)

可以用于限流场景,一个请求过来先获取一个信号,比如设置了1000个则1000个请求完必须先等其他的释放之后才能操作。

信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire()方法增加数量,也可以调用release()方法减少数量,但是当调用release()之后小于0的话方法就会阻塞,直到数字大于0

基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();

信号量 案列 步骤:

image-20210528083606120

    /**
    * 当前方法就是来停车的
     * 车库停车,
     * 3车位
     * 信号量也可以用作分布式限流;
     */
    @GetMapping("/park")
    @ResponseBody
    public String park() throws InterruptedException {

        RSemaphore park = redisson.getSemaphore("park");
//        park.acquire();//获取一个信号,获取一个值,占一个车位
          //park.acquire(); 强行去占一个锁,如果没有锁会阻塞一直等待下去
        //park.tryAcquire(); 该方法是尝试获取锁,有就获取返回true没有就是false然后照样执行后面的代码
        boolean b = park.tryAcquire();
        if(b){
            //执行业务
        }else {
            return "error";
        }

        return "ok=>"+b;
    }

    // 当前方法就是开走一辆车,清空一个车位
    @GetMapping("/go")
    @ResponseBody
    public String go() throws InterruptedException {
        RSemaphore park = redisson.getSemaphore("park");
        park.release();//释放一个车位

        //
//        Semaphore semaphore = new Semaphore(5);
//        semaphore.release();
//
//        semaphore.acquire();

        return "ok";
    }

166、缓存-分布式锁-缓存一致性解决

image-20220430151944513

image-20220430151959300

缓存数据一致性-解决方案

• 无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?

• 1、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可

• 2、如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式。

• 3、缓存数据+过期时间也足够解决大部分业务对于缓存的要求。

• 4、通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略);

总结:

• 我们能放入缓存的数据本就不应该是实时性、一致性要求超高的。所以缓存数据的时候加上过期时间,保 证每天拿到当前最新数据即可。

• 我们不应该过度设计,增加系统的复杂性

• 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。

image-20220430152050127

**缓存不一致详解 :**http://mp.weixin.qq.com/s?__biz=MzIxNzQwNjM3NA==&mid=2247506036&idx=2&sn=2387def94f560ad2dd770c9b085362db&chksm=97f8d6bda08f5fab534f07ff7b0a0b2f5c5e6f64f2126afc2ff7a605ca8168ebf516e4533308&mpshare=1&scene=24&srcid=0413pamxvHUu7nlsnkDzUGoF&sharer_sharetime=1649807913441&sharer_shareid=f08549c1a63f98e0a2efda7fd1f42a93#rd

167、缓存-SpringCache-简介

SpringCache

随便找篇cache文章阅读:https://blog.csdn.net/er_ving/article/details/105421572

每次都那样写缓存太麻烦了,spring从3.1开始定义了Cache、CacheManager接口来统一不同的缓存技术。并支持使用JCache(JSR-107)注解简化我们的开发

Cache接口的实现包括RedisCache、EhCacheCache、ConcurrentMapCache等

每次调用需要缓存功能的方法时,spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。

使用Spring缓存抽象时我们需要关注以下两点:

1、确定方法需要缓存以及他们的缓存策略
2、从缓存中读取之前缓存存储的数据

1 、简介

  • Spring 从 3.1 开始定义了 org.springframework.cache.Cache和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术; 并支持使用 JCache(JSR-107)注解简化我们开发;

  • Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合; Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache ,

ConcurrentMapCache 等;

  • 每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已 经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓 存结果后返回给用户。下次调用直接从缓存中获取。

  • 使用 Spring 缓存抽象时我们需要关注以下两点;

  • 1、确定方法需要被缓存以及他们的缓存策略

  • 2、从缓存中读取之前缓存存储的数据

2 、基础概念

image-20220430162420055

3、注解

Cache缓存接口,定义缓存操作,实现有:RedisCache, EhCache,ConcurrentMapCache等
CacheManager缓存管理器,管理各种缓存(Cache)组件
@Cacheable主要针对方法配置,能够根据方法请求参数对其结果进行缓存
@CacheEvict清空缓存
@CachePut保证方法被调用,又希望结果被缓存
@EnableCaching开启基于注解的缓存
keyGenerator缓存数据时key生生策略
Serialize缓存数据时value的序列化策略

@Cacheable/@CachePut/@CacheEvict 主要的参数

value缓存的名称,在 spring 配置文件中定义,必须指定至少一个例如∶@Cacheable(value=“mycache”)或者@Cacheable(value={“cache1”“cache2”}
key缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合例如∶@Cacheable(value=“testcache”,key=“#userName”)
condition缓存的条件,可以为空,使用 SpEL编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存,在调用方法之前之后都能判断例如@Cacheable(value=“testcache”,condition=“#userNam e.length()>2”)
allEntries(@CacheEvict )是否清空所有缓存内容,缺省为 false,如果指定为true,则方法调用后将立即清空所有缓存例如∶@CachEvict(value=“testcache”,allEntries=true)
beforelnvocation (@cacheEvict)是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存例如∶@CachEvict(value=“testcache”, beforelnvocation=true)
unless (@CachePut)(@Cacheable)用于否决缓存的,不像condition,该表达式只在方法执行之后判断,此时可以拿到返回值result进行判断。条件为true不会缓存,fasle才缓存例如∶ @Cacheable(value=“testcache"unless=”#result =nul")

4、表达式语法

Cache SpEL available metadata

名字位置描述示列
methodNameroot object当前被调用的方法名#root.methodName
methodroot object当前被调用的方法#root.methodName
targetroot object当前被调用的目标对象#root.target
targetClassroot object当前被调用的目标对象类#root.targetClass
argsroot object当前被调用的方法的参数列表#root.args[0]
cachesroot object当前方法调用使用的缓存列表(如@Cacheable(value={cache1",#root.caches【0】.name “cache2”})),则有两个cache#root.caches[0].name
argument nameevaluation context方法参数的名字.可以直接 掺参数名,也可以使用 #p0或#a0的形式,0代表参数的索引#iban、#a0、#p0
resultevaluation context方法执行后的返回值(仅当方法执行之后的判断有效,如’unless’,'cache put的表达式’cache evict’的表达式beforelnvocation=false)#result

168、缓存-SpringCache-整合&体验Cacheable

整合SpringCache简化缓存开发
 *      1)、引入依赖
 *          spring-boot-starter-cache、spring-boot-starter-data-redis
 *      2)、写配置
 *          (1)、自动配置了哪些
 *              CacheAuroConfiguration会导入 RedisCacheConfiguration;
 *              自动配好了缓存管理器RedisCacheManager
 *          (2)、配置使用redis作为缓存
 *              spring.cache.type=redis
 *      3)、测试使用缓存
 *          @Cacheable: Triggers cache population.:触发将数据保存到缓存的操作
 *          @CacheEvict: Triggers cache eviction.:触发将数据从缓存删除的操作
 *          @CachePut: Updates the cache without interfering with the method execution.:不影响方法执行更新缓存
 *          @Caching: Regroups multiple cache operations to be applied on a method.:组合以上多个操作
 *          @CacheConfig: Shares some common cache-related settings at class-level.:在类级别共享缓存的相同配置
 *          1)、开启缓存功能 @EnableCaching
 *          2)、只需要使用注解就能完成缓存操作
 *
 *      4)、原理:
 *          CacheAutoConfiguration ->  RedisCacheConfiguration ->
 *          自动配置了RedisCacheManager->初始化所有的缓存->每个缓存决定使用什么配置
 *          ->如果redisCacheConfiguration有就用已有的,没有就用默认配置
 *          ->想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可
 *          ->就会应用到当前RedisCacheManager管理的所有缓存分区中

1、引入依赖

<dependency> 
	<groupId>org.springframework.boot</groupId> 
	<artifactId>spring-boot-starter-data-redis</artifactId> 
</dependency>
<dependency> 
	<groupId>org.springframework.boot</groupId> 
	<artifactId>spring-boot-starter-cache</artifactId> 
</dependency>

2、编写配置文件

在product的服务下新建一个application.properties文件,我们的redis的配置之前已经在yml文件中已经配置过了

# 配置缓存的 类型为 redis
spring.cache.type=redis
#缓存的时间 单位是以毫秒为单位
spring.cache.redis.time-to-live=3600000

# spring.cache.cache-names=cache1,cache2 .... 配置缓存的名字,如果这里配置了那么所有的缓存的名字
# 都要在这里事先配置,会给你禁用掉动态生产缓存名字

# 缓存的前缀,这里指定了就回你用我们指定的前缀,如果没有指定默认就使用缓存的名字(分区)作为前缀,建议使用默认的分区
# spring.cache.redis.key-prefix=CACHE_
# 是否开启缓存的前缀
spring.cache.redis.use-key-prefix=false
# 是否缓存null值 防止缓存穿透
spring.cache.redis.cache-null-values=true

3、主启动类开启

@EnableCaching
@SpringBootApplication
public class ValidApplication {
	public static void main(String[] args) {
		SpringApplication.run(ValidApplication.class, args);
	}

}

4、测试代码

修改CategoryServiceImpl中的getLevel1Categorys方法


    /**
     * 1、每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】
     * 2、 @Cacheable({"category"}) 这个注解只是指定了缓存的名字(分区),并没有指定缓存的key那么就会默认的
     *      代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。
     *      如果缓存中没有,会调用方法,最后将方法的结果放入缓存
     * 3、默认行为
     *      1)、如果缓存中有,方法不用调用。
     *      2)、key默认自动生成;缓存的名字::SimpleKey [](自主生成的key值)
     *      3)、缓存的value的值。默认使用jdk序列化机制,将序列化后的数据存到redis
     *      4)、默认ttl时间 -1;
     *
     *    自定义:
     *      1)、指定生成的缓存使用的key:  key属性指定,接受一个SpEL
     *             SpEL的详细https://docs.spring.io/spring/docs/5.1.12.RELEASE/spring-framework-reference/integration.html#cache-spel-context
     *      2)、指定缓存的数据的存活时间: 配置文件中修改ttl
     *      3)、将数据保存为json格式:
     *              自定义RedisCacheConfiguration即可
     * 4、Spring-Cache的不足;
     *      1)、读模式:
     *          缓存穿透:查询一个null数据。解决:缓存空数据;ache-null-values=true
     *          缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁;?默认是无加锁的;sync = true(加锁,解决击穿)
     *          缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间。:spring.cache.redis.time-to-live=3600000
     *      2)、写模式:(缓存与数据库一致)
     *          1)、读写加锁。
     *          2)、引入Canal,感知到MySQL的更新去更新数据库
     *          3)、读多写多,直接去数据库查询就行
     *    总结:
     *      常规数据(读多写少,即时性,一致性要求不高的数据);完全可以使用Spring-Cache;写模式(只要缓存的数据有过期时间就足够了)
     *      特殊数据:特殊设计
     *
     *   原理:
     *      CacheManager(RedisCacheManager)->Cache(RedisCache)->Cache负责缓存的读写
     *
     *  1、value = {"category"} : 相当于是一个分区
     *  2、key = "#root.method.name" 在category分区下的key为方法的名字如果指定常量为key key = "‘k1’"
     *  3、默认是无加锁的;sync = true(加锁,解决击穿  加的是本地的锁)
     * @return
     */
    @Cacheable(value = {"category"},key = "#root.method.name",sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        System.out.println("getLevel1Categorys.....");
        long l = System.currentTimeMillis();
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        return categoryEntities;

    }

5、测试

169、缓存-SpringCache-@Cacheable细节设置

见上方测试代码中的多行注解中

170、缓存-SpringCache-自定义缓存配置

package com.atguigu.gulimall.product.config;

import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @创建人: 放生
 * @创建时间: 2022/4/30
 * @描述: 
 */
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {

//    @Autowired
//    CacheProperties cacheProperties;

    /**
     * 配置文件中的东西没有用上;
     *
     * 1、原来和配置文件绑定的配置类是这样子的
     *      @ConfigurationProperties(prefix = "spring.cache")
     *      public class CacheProperties
     *
     * 2、要让他生效
     *      @EnableConfigurationProperties(CacheProperties.class)
     *
     * @return
     */
    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//        config = config.entryTtl();
        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;
    }
}

171、缓存-SpringCache-@CacheEvict

    /**
     * 级联更新所有关联的数据
     * @CacheEvict:失效模式
     * 1、同时进行多种缓存操作  @Caching
     * 2、指定删除某个分区下的所有数据 @CacheEvict(value = "category",allEntries = true)
     * 3、存储同一类型的数据,都可以指定成同一个分区。分区名默认就是缓存的前缀
     * 失效模式下既可以使用@Caching 也可以使用@CacheEvict,@Caching是一个可以组合多个注解的注解,如本案列中
     * 组合多个 @CacheEvict,当方法执行是会删除category分区下key为getLevel1Categorys和getCatalogJson的缓存
     * 而@CacheEvict是指定分区为category下所有的key都删除(allEntries = true)
     * 
     * 4、如果使用@CachePut 就是双写模式,会把方法返回值重写放回指定的key中去 返回值不能为void
     * @param category
     */

//    @Caching(evict = {
//            @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
//            @CacheEvict(value = "category",key = "'getCatalogJson'")
//    })
    //category:key
    @CacheEvict(value = "category",allEntries = true) //失效模式
//    @CachePut //双写模式,返回值不能为void
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());

    }

172、缓存-SpringCache-原理与不足

# Spring-Cache的不足;
     *      1)、读模式:
     *          缓存穿透:查询一个null数据。解决:缓存空数据;ache-null-values=true
     *          缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁;?默认是无加锁的;sync = true(加锁,解决击穿)
     *          缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间。:spring.cache.redis.time-to-live=3600000
     *      2)、写模式:(缓存与数据库一致)
     *          1)、读写加锁。
     *          2)、引入Canal,感知到MySQL的更新去更新数据库
     *          3)、读多写多,直接去数据库查询就行

ew 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;
}

}






# 171、缓存-SpringCache-@CacheEvict



```java
    /**
     * 级联更新所有关联的数据
     * @CacheEvict:失效模式
     * 1、同时进行多种缓存操作  @Caching
     * 2、指定删除某个分区下的所有数据 @CacheEvict(value = "category",allEntries = true)
     * 3、存储同一类型的数据,都可以指定成同一个分区。分区名默认就是缓存的前缀
     * 失效模式下既可以使用@Caching 也可以使用@CacheEvict,@Caching是一个可以组合多个注解的注解,如本案列中
     * 组合多个 @CacheEvict,当方法执行是会删除category分区下key为getLevel1Categorys和getCatalogJson的缓存
     * 而@CacheEvict是指定分区为category下所有的key都删除(allEntries = true)
     * 
     * 4、如果使用@CachePut 就是双写模式,会把方法返回值重写放回指定的key中去 返回值不能为void
     * @param category
     */

//    @Caching(evict = {
//            @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
//            @CacheEvict(value = "category",key = "'getCatalogJson'")
//    })
    //category:key
    @CacheEvict(value = "category",allEntries = true) //失效模式
//    @CachePut //双写模式,返回值不能为void
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());

    }

172、缓存-SpringCache-原理与不足

# Spring-Cache的不足;
     *      1)、读模式:
     *          缓存穿透:查询一个null数据。解决:缓存空数据;ache-null-values=true
     *          缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁;?默认是无加锁的;sync = true(加锁,解决击穿)
     *          缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间。:spring.cache.redis.time-to-live=3600000
     *      2)、写模式:(缓存与数据库一致)
     *          1)、读写加锁。
     *          2)、引入Canal,感知到MySQL的更新去更新数据库
     *          3)、读多写多,直接去数据库查询就行
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值