[{"content":" debian的安装 详细文档 : https://wiki.debian.org/zh_CN/FrontPage 详细的wiki https://www.debian.org/doc/manuals/debian-handbook/index.zh-cn.html 手册 https://www.debian.org/doc/manuals/debian-reference/index.zh-cn.html 参考手册 https://www.debian.org/releases/stable/arm64/index.zh-cn.html 安装手册 https://wiki.debian.org/zh_CN/NetworkConfiguration 网络配置 一键装机脚本见我的GitHub: https://github.com/loganoxo/Config/tree/master/linux/install 一、debian虚拟机的安装 1、用虚拟机安装debian12 vmware 桥接模式的虚拟机不能用quanx的代理连接国外网站; nat模式是虚拟机自己组成子网,通过nat转发流量到宿主机,所以可以通过quanx连接国外网站; 但是nat模式下,宿主机默认不能直接通过虚拟机的内部 IP（例如 172.x.x.x）进行远程连接（如 SSH），因为 NAT 模式下虚拟机的网络是一个私有子网，与宿主机的网络隔离,除非用端口转发(windows上的nat) mac中,vmware_fusion提供了自定义网络,选这个,其实是用的桥接网络,加上nat转发流量连接外网; 宿主机既可以ssh连接虚拟机,虚拟机也可以使用quanx访问国外网站 选择第二个 Graphical install, 安装界面可以用鼠标点击; 第一个 Install 的安装界面只能用键盘不能用鼠标 美国: United States 步骤1:语言选择美国英文:安装过程中显示的语言,所选语言也将是系统安装后的默认语言;安装后可用 sudo dpkg-reconfigure locales 重新配置 步骤2:选择区域, 中国; 会影响时区; 先选other,再asia,再选china 步骤3:因为没有找到符合你选择的语言和国家组合(我选的是美国英文+中国时区)的预设区域设置，系统会提示你重新选语言,依然选美国英文 步骤4:Configure the keyboard,键盘布局,选择American English;安装后可用 sudo dpkg-reconfigure keyboard-configuration 重新配置 步骤5:hostname, 设置为 prod 或 dev 或 test ; 这样的格式 步骤6:domain name; 设置为 vm.local ; 这样的格式 步骤7:输入两次root用户的密码; 步骤8:full name; 全名,真实姓名(英文的姓+名), 用于显示的; 就设置为 helq就可以了 ; 步骤9:username; 用于登录的; 设为helq 步骤10:输入两次helq用户的密码; 步骤11:Partition disks; 选择 Guided - use entire disk ; 不分区,使用整个磁盘(虚拟机创建的虚拟磁盘); 步骤12:会提示:Partition disks; Note that all data on the disk you select will be erased, but not before you have confirmed that you really want to make the changes. 就是个磁盘擦除警告,直接选择那个磁盘,回车下一步 步骤13:Selected for partitioning:The disk can be partitioned using one of several different schemes. If you are unsure, choose the first one. 选择分区方案; 直接选择第一个: All files in one partition 步骤14:This is an overview of your currently configured partitions and mount points. Select a partition to modify its settings (file system, mount point, etc.), a free space to create partitions, or a device to initialize its partition table. 即当前配置的分区和挂载点的概述, 选择Finish partitioning and write changes to disk,继续回车 步骤15:If you continue, the changes listed below will be written to the disks. Otherwise, you will be able to make further changes manually; 一个确认提示, 选择yes, 回车继续;如果选择默认的no, 会回到上一步 步骤16:Configure the package manager;Scanning your installation media finds the label; 是否扫描额外的安装介质; 选择no ,回车继续 步骤17:A network mirror can be used to supplement the software that is included on the installation media. This may also make newer versions of software available; 网络镜像的配置; debian12不用镜像,安装包里自带了,直接选no回车继续; debian11安装时,这里也选no;但是11.7.0那个包安装时,即便有梯子下载速度也很慢,所以debian这里需要断网; 但是11.11.0的包我安装的时候联网就通过了; 步骤18:Configuring popularity-contest; 提示是否开启匿名信息统计,选择no回车 步骤19:选择默认的软件包:SSH server、standard system utilities; 回车继续 步骤20: 安装结束; 回车 continue 会自动重启; 关机后需要把 虚拟机的设置里面的 CD/DVD 驱动器 取消连接 二、debian的环境搭建 1、软件包的源配置 /etc/apt/sources.list debian12那个系统DVD安装好后,发现 /etc/apt/sources.list 中的内容是 deb cdrom:[Debian GNU/Linux 12.8.0 _Bookworm_ - Official arm64 DVD Binary-1 with firmware 20241109-11:05]/ bookworm contrib main non-free-firmware,因为这个系统最初通过离线 DVD 安装的,安装的DVD中包含所有的了 a、备份原来的 1 2 su cp /etc/apt/sources.list /etc/apt/sources.list.bak Copied! b、解释 deb: 二进制软件包的来源，这些包是已经编译好的，可以直接安装到你的系统中,它是普通用户用来安装应用程序和库的来源 deb-src: 源码软件包的来源。源码包包含了软件的源代码，你可以从中编译和安装软件,如果需要修改或自行编译软件，可以用到这个源 bookworm/bullseye: debian12/debian11 ;是发行版的代号 main：完全开源的软件，符合 Debian 自由软件指南; contrib：自由软件，但需要依赖非自由的软件（如驱动或库） non-free：非自由软件（可能有版权或许可限制，用户可以使用但不能修改） non-free-firmware：从 non-free 中分离出来的非自由固件，专用于硬件驱动和设备运行支持 c、使用官方的源(需要梯子) 1 su #进入root用户 Copied! I、debian12 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 cat \u0026gt; /etc/apt/sources.list \u0026lt;\u0026lt; EOF # 提供主要的软件包库,是系统大部分软件的来源,包括基础的操作系统组件和应用程序包,用于升级系统和安装软件 deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware deb-src http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware # 提供安全性修复的更新,包含及时修复的安全漏洞补丁,用于修复系统或软件中的已知漏洞 deb http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware deb-src http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware # 快速修复一些关键问题,目的是解决一些无法等到下一个点版本(例如从 12.1 到 12.2)发布才能修复的问题 deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware deb-src http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware # 反向移植,指从 Debian 的较高版本中(例如 bookworm)选取特定的软件包,并在当前稳定版本(例如 bullseye)上进行重新编译和打包;用户可以在稳定版系统中使用更高版本的应用程序或工具,而无需升级整个系统到测试版或不稳定版 # deb http://deb.debian.org/debian bookworm-backports main contrib non-free non-free-firmware # deb-src http://deb.debian.org/debian bookworm-backports main contrib non-free non-free-firmware EOF Copied! debian11 只需把 bookworm 改为 bullseye d、使用国内镜像源 1 2 3 4 5 6 7 8 9 10 11 12 13 cat \u0026gt; /etc/apt/sources.list \u0026lt;\u0026lt; EOF # 默认注释了源码镜像以提高 apt update 速度，如有需要可自行取消注释 deb http://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free # deb-src http://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free deb http://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free # deb-src http://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free deb http://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free # deb-src http://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free # deb http://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main contrib non-free # # deb-src http://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main contrib non-free deb http://security.debian.org/debian-security bullseye-security main contrib non-free # deb-src http://security.debian.org/debian-security bullseye-security main contrib non-free EOF Copied! 上面是debian11的配置; debian12的去找镜像网站有相关配置,这里不写了\ne、更新 1 2 3 apt update \u0026amp;\u0026amp; apt-get update apt list --upgradable #查看可以更新的软件包 apt upgrade Copied! f、apt相关命令 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 sudo apt update # 更新包索引 sudo apt upgrade # 升级所有已安装的软件包到最新版本 apt list python3 # 显示是否已安装 sudo apt install \u0026lt;package\u0026gt; # 安装指定的软件包 sudo apt remove \u0026lt;package\u0026gt; # 删除指定的软件包（保留配置文件） sudo apt purge \u0026lt;package\u0026gt; # 完全删除指定的软件包（包括配置文件） apt search \u0026lt;keyword\u0026gt; # 搜索与关键字相关的软件包 apt show \u0026lt;package\u0026gt; # 查看指定软件包的详细信息 apt list --installed # 列出所有已安装的软件包 apt list # 列出所有可用的软件包 dpkg -l | grep \u0026lt;package\u0026gt; # 检查指定软件包是否已安装 sudo apt autoremove # 清理无用的依赖（卸载后遗留的包） sudo apt autoclean # 清理下载的已过期或无用的软件包缓存 sudo apt clean # 清理所有下载的包缓存（慎用） sudo apt download \u0026lt;package\u0026gt; # 下载但不安装软件包 apt-cache depends \u0026lt;package\u0026gt; # 检查指定软件包的依赖关系 apt-cache rdepends \u0026lt;package\u0026gt; # 检查哪些软件包依赖指定的软件包 sudo apt --fix-broken install # 手动修复被破坏的软件包 apt list \u0026lt;package\u0026gt; # 列出与指定包相关的信息（包括状态、版本等） sudo apt full-upgrade # 执行全面升级（可能会移除不需要的包） sudo apt install -f # 修复依赖关系并安装未完成的软件包 sudo apt hold \u0026lt;package\u0026gt; # 将指定软件包标记为保持当前版本（不升级） sudo apt unhold \u0026lt;package\u0026gt; # 取消对软件包的保持标记（允许升级） apt policy \u0026lt;package\u0026gt; # 查看指定软件包的版本和安装来源 sudo apt-add-repository \u0026lt;repository\u0026gt; # 添加 PPA 或自定义的源 sudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y # 一次性更新包索引并升级所有软件包（跳过确认） sudo apt dist-upgrade # 执行发行版升级（处理复杂依赖变化） sudo apt-mark hold \u0026lt;package\u0026gt; # 阻止软件包升级（标记为 hold 状态） sudo apt-mark unhold \u0026lt;package\u0026gt; # 取消 hold 标记，允许升级 sudo apt list --upgradable # 列出可以升级的软件包 apt-cache search \u0026lt;package\u0026gt; # 使用 apt-cache 搜索软件包 sudo apt-get check # 检查系统是否有破损的依赖关系 sudo apt-get changelog \u0026lt;package\u0026gt; # 查看指定软件包的变更日志 sudo dpkg-reconfigure \u0026lt;package\u0026gt; # 重新配置已安装的软件包 sudo dpkg --configure -a # 修复未正确配置的软件包 Copied! 2、网络配置 静态ip + 自定义dns 自定义dns时, 不要去修改,/etc/resolv.conf,因为可能重启后失效; 去在 /etc/network/interfaces 配置 ip addr 查看当前的ip vmware中配置了端口转发 0、resolvconf 默认情况下,resolvconf并没有安装, 所以在 /etc/network/interfaces 中配置的 dns-nameservers 并不会自动同步到 /etc/resolv.conf, 并且每次系统重启或者使用命令 systemctl restart networking.service 后, resolv.conf 都会被 dhclient 覆盖; dhclient 是常用的 DHCP 客户端，它会根据 DHCP 服务器提供的 DNS 信息自动更新 /etc/resolv.conf, 查看 /etc/dhcp/dhclient.conf 中有request 里面有 domain-name, domain-name-servers, 和 domain-search;是 DHCP 客户端从 DHCP 服务器请求的选项,如果 DHCP 服务器提供了这些选项，dhclient 默认会更新 /etc/resolv.conf 在不安装额外程序的情况下, 也可以使用命令 sudo chattr +i /etc/resolv.conf ,给这个文件设置只读权限, 这样就不会被覆盖了; sudo chattr -i /etc/resolv.conf 恢复权限 resolvconf 依赖于 /etc/network/interfaces、DHCP 等服务来收集 DNS 配置信息，并通过这些信息更新 /etc/resolv.conf。resolvconf会将来自多个来源（例如 DHCP 客户端、静态配置、VPN 配置等）的 DNS 信息汇总并合并，最终由 resolvconf 更新 /etc/resolv.conf; 安装 resolvconf 后, /etc/resolv.conf 被替换为符号链接,指向 /run/resolvconf/resolv.conf; 原来的resolv.conf中的内容被删除,备份在/etc/resolvconf/resolv.conf.d/original中 安装了 resolvconf 后，会在/etc/dhcp/dhclient-enter-hooks.d/ 目录下创建 resolvconf 脚本, dhclient 会通过这个脚本将获取到的 DNS 信息交给 resolvconf 管理; 这些动态信息(包括在/etc/network/interfaces中配置的)通常被写入 /run/resolvconf/interface/ 目录中 resolvconf 使用多个配置文件片段来管理 DNS 信息,配置文件路径包括： /etc/resolvconf/resolv.conf.d/head：内容会添加在动态生成的 /etc/resolv.conf 文件的最开头,通常是注释行（例如，文件生成说明）或管理员希望固定放在前面的配置 /etc/resolvconf/resolv.conf.d/base：基本 DNS 信息,配置一些静态的dns,始终会添加到resolv.conf; resolvconf收集到的动态信息会跟base文件中的配置进行merge;如:base中配置了 search a.b.c,收集到的动态信息是 search x.y.z,最终结果就是:search a.b.c x.y.z /etc/resolvconf/resolv.conf.d/tail：尾部内容;可以用来加入一些额外的选项，比如 options inet6;如果不需要任何追加内容，可以将该文件留空。 可以直接在 /etc/network/interfaces 中配置 dns-nameservers 和 dns-search, 在resolvconf的帮助下会同步到 resolv.conf 文档目录: /usr/share/doc/resolvconf/README.gz gunzip README.gz resolvconf 和 systemd-resolved 两个软件冲突,安装一个就会卸载另一个; systemd-resolved的功能比resolvconf更多,如mdns和dns缓存等,但我不会用,也没必要用 a、原始配置的解释 1、运行 cat /etc/resolv.conf ; 显示:\n1 2 3 domain localdomain search localdomain nameserver 172.16.106.2 Copied! 一、找到这个 nameserver, 写在后面 /etc/network/interfaces 中的 gateway; nameserver就是使用的dns服务器, 在vmware虚拟机中,这个默认被配置为nat的网关ip,用来转发流量和dns解析; 但是也不是必须与 网关ip相同, 可以配置成多个不同的dns, 写多行 nameserver, 配置的dns中就算没有网关ip, nat的外网访问依然还是保持不变, 所以网关是网关,dns是dns,两个不同的功能和概念; 虚拟机linux中的网关ip虽然默认没有文件配置(除非配置静态ip),但可以执行命令: ip route | grep default 可以查看到 在 VMware 创建的 NAT 模式的虚拟机中，虚拟机的网关 IP 并不是直接存储在虚拟机的配置文件(不在linux虚拟机的文件系统中)中，而是通过 VMware 的网络配置文件定义，并由虚拟机通过 DHCP 或静态配置使用,可能会存放在宿主机 /Library/Preferences/VMware Fusion/networking (VNET_8_HOSTONLY_SUBNET)中,这是标识了nat能用的子网段,以 .0 结尾,但是网关ip用的是以 .2 结尾的; 在 /Library/Preferences/VMware Fusion/vmnet8/nat.conf 中有下面的这个配置,也标识了网关ip;注意这是纯nat模式,和自定义模式还不相同; 1 2 3 # NAT gateway address ip = 172.16.12.2 netmask = 255.255.255.0 Copied! mac上vmware fusion自定义模式实际上是桥接, 在 /Library/Preferences/VMware Fusion/networking 中的 VNET_2_HOSTONLY_SUBNET 中有显示;在 /Library/Preferences/VMware Fusion/vmnet2/nat.conf 中也有如下表示(vmnet2 也在软件中显示的我的自定网络的名字): 1 2 3 # NAT gateway address ip = 172.16.106.2 netmask = 255.255.255.0 Copied! nat 模式的网关 ip xxx.xxx.xxx.2 通常以 .2 结尾, 因为约定俗成; .0 是一个网段的网络标识地址，表示整个子网常表示子网本身，不能分配给设备或服务使用; .1 通常用于路由器,在家庭路由器中，192.168.1.1 是常见的默认网关,虽然 VMware 可以选择使用 .1，但它通常会避开这个地址，以避免与一些软件或网络设备的默认配置产生冲突 二、这个文件就是被修改,重启后也会恢复原样; 若要进行配置,需要修改后执行 chattr +i /etc/resolv.conf 变为只读的;恢复: chattr -i /etc/resolv.conf 三、domain 和 search 最好保持不变, 就用 localdomain, 更安全, 因为外网中没有localdomain的顶级域名 localdomain 就是个默认的域名占位符,假如我 ping aaa, 他会尝试去dns中查找 aaa 和 aaa.localdomain; 可以删除localdomain,替换为自己要用的 search 就是一个搜索域, 用于补全主机名,假如配置成 com, 我执行 ping baidu 就能直接ping成功; 但是不能和本地hosts文件配合使用,如在hosts中配置: 1.1.1.1 a.com ; 则我执行 ping a, 是不会成功的, 因为会直接去dns服务器中查找(nat模式虚拟中就是那个网关ip),网络上并没有 a.com 这个地址,所以不会成功,这种只能 ping a.com; domain 和 search 功能相同,原理也相同; 但是1、search若配置后, 会覆盖domain 的配置; 2、search 支持多个 以空格分开, domain 仅支持一个 2、运行 cat /etc/network/interfaces ; 显示:\n1 2 3 4 5 6 7 8 9 10 11 12 # This file describes the network interfaces available on your system # and how to activate them. For more information, see interfaces(5). source /etc/network/interfaces.d/* # The loopback network interface auto lo iface lo inet loopback # The primary network interface allow-hotplug ens160 iface ens160 inet dhcp Copied! 详情见下面修改静态ip的示例 b、静态ip+dns配置 ens160 :网络接口名称,必须使用原本默认的, 但是有些老linux使用的是 eth0,所以需要修改前预先查看当前系统使用的 网络接口名称; 在后面修改的配置文件示例作出相应的修改才行; 原来用的哪个,修改后也应该用哪个;可以用 ip link show 或者 ifconfig, 或者直接看 /etc/network/interfaces文件 如果要配置静态ip,那就必须要配置gateway(网关)ip,否则不能连接外网;局域网使用是可以的 为什么默认不需要配置?因为默认网络是动态ip,是通过dhcp自动发现的,通过dhcp,linux就知道了该找哪个网关ip;静态ip则不然; 桥接模式也需要配置网关ip(通常为路由器ip),默认使用动态ip时,也是用dhcp自动发现的; 若要配置dns,则最好在/etc/network/interfaces中配置,虽然/etc/resolv.conf中也能配置,但是重启就会没了;而/etc/network/interfaces中配置的dns则会自动同步到resolv.conf,重启也不会消失 lo 的两行是非必须的,是回环地址(127.0.0.1),可以不用显式配置,linux会在开机后自动启用lo,可以用 ping 127.0.0.1 来检测 也可以设置: dns-search com # 设置DNS搜索域 ,见上面的 resolv.conf配置详解(就是这个resolv.conf里的 search, 会自动同步到resolv.conf) 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 37 38 39 40 41 42 43 44 45 46 47 48 su export PATH=$PATH:/usr/sbin cp /etc/network/interfaces /etc/network/interfaces.bak cat \u0026gt; /etc/network/interfaces \u0026lt;\u0026lt; EOF # This file describes the network interfaces available on your system # and how to activate them. For more information, see interfaces(5). source /etc/network/interfaces.d/* # The loopback network interface auto lo iface lo inet loopback # The primary network interface # allow-hotplug ens160 # iface ens160 inet dhcp auto ens160 iface ens160 inet static address 172.16.106.12 netmask 255.255.255.0 gateway 172.16.106.2 dns-nameservers 223.5.5.5 119.29.29.29 8.8.8.8 EOF cat /etc/network/interfaces #确认一下 systemctl restart networking.service ip addr ######### 安装resolvconf apt install resolvconf reboot systemctl status resolvconf.service # 服务状态为 active (exited)，说明它已经成功运行并退出（这类服务是一次性任务，在启动后完成工作便退出） cat /etc/resolv.conf ls -l /etc/resolv.conf systemctl start resolvconf.service systemctl stop resolvconf.service systemctl enable resolvconf.service systemctl disable resolvconf.service cat /etc/resolv.conf # 查看dns是否被自动更改 ls -l /etc/resolv.conf # 查看是否为软链接 # 关闭 quanx ping www.baidu.com wget www.baidu.com rm index.html dig www.baidu.com # SERVER: 223.5.5.5#53(223.5.5.5) (UDP) nslookup -debug www.baidu.com # Server:223.5.5.5 Copied! c、配置默认语言 尽管在安装时已经选择了默认语言(即安装的第一步,选择安装程序的显示语言,这个设置也会成为安装后系统的默认语言) 在系统安装完成后,可以通过命令重新配置; 如果安装时选择的英文, 安装后用命令把中文加为第二个语言,但是默认首选还是英文,只是添加了个中文环境信息,可以给其他软件使用;如: tldr -L zh tree ,这个命令,如果不添加中文语言环境的支持,是不会显示中文的 默认 tty界面(非远程连接、非图形化界面的系统自己的操作界面,那个黑窗口) 无论怎么配置,通常也是不支持中文的,除非安装 fbterm 这个第三方的tty 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 su export PATH=$PATH:/usr/sbin # 鼠标滚轮可以快速滚动 dpkg-reconfigure locales # 选中 en_US.UTF-8 和 zh_CN.UTF-8 # 用空格选中(多选),上下箭头移动, tab键移至 [ok] 回车确认; 提示选择 en_US.UTF-8 为首选默认语言,选择后回车确定 # 重新登录或重启 locale # 查看目前系统所使用的语言环境 locale -a # 查看系统已安装的所有语言 # 环境变量有: (默认都是 \u0026#34;en_US.UTF-8\u0026#34; ; 若修改默认的语言为中文则变为: \u0026#34;zn_CN.UTF-8\u0026#34; ) LANG # 系统的主要语言和地区设置，影响所有未单独设置的 LC_* 变量。 LANGUAGE # 定义语言优先顺序，主要用于翻译系统消息 (LC_MESSAGES)。 LC_ALL # 强制覆盖所有 LC_* 变量的值，优先级最高，通常用于调试。 LC_CTYPE # 定义字符分类和编码规则，影响字符输入和显示 (如 UTF-8)。 LC_NUMERIC # 控制数值格式，例如小数点符号、分组符号等。 LC_TIME # 定义日期和时间格式 (如美式日期 vs 欧式日期)。 LC_COLLATE # 控制字符串比较和排序规则，影响文件名排序等。 LC_MONETARY # 控制货币格式，例如货币符号、千分位分隔符等。 LC_MESSAGES # 定义系统消息的语言 (如错误提示、确认信息等)。 LC_PAPER # 控制默认纸张尺寸 (如 Letter vs A4)。 LC_NAME # 定义人名的显示格式 (如姓氏在前或名字在前)。 LC_ADDRESS # 控制地址格式和显示规则。 LC_TELEPHONE # 定义电话号码的格式 (如是否显示国家代码)。 LC_MEASUREMENT # 控制度量单位 (如公制 vs 英制)。 LC_IDENTIFICATION # 描述语言环境的特定信息，通常不直接影响用户体验。 # 不需要自己配置环境变量,直接使用系统默认的体验最佳 Copied! d 、重新配置键盘布局 装好系统后,默认不用修改 键盘布局就是,在 tty 界面,或者linux自己的图形化界面的键盘映射;其目的是让操作系统正确识别和映射键盘上的按键到字符或功能; 按键 Shift+2 输出 @（在美式键盘中）还是 \u0026quot; (在英式键盘中)某些键盘布局会增加对语言特定符号的支持，比如法语的 é 或德语的 ß 某些布局支持 AltGr（右 Alt 键）的特殊功能，用于输入更多字符 但是远程连接的话就没这个说法了,用的是我本地的电脑自己的键盘映射,传给linux的就是实际的字符信息了; 最佳选择: 美式键盘（US Layout）：标准 QWERTY 布局，符号按键位置国际通用; 中国市场的键盘以美式 QWERTY 布局为主,硬件上没有区别,中文输入完全依赖系统输入法 默认 tty界面(非远程连接、非图形化界面的系统自己的操作界面,那个黑窗口) 无论怎么配置,通常也是不支持中文的,除非安装 fbterm 这个第三方的tty 1 2 3 4 su export PATH=$PATH:/usr/sbin dpkg-reconfigure keyboard-configuration service keyboard-setup restart Copied! e、安装 sudo 1 2 3 4 5 6 7 8 9 10 11 12 13 # sudo命令需要用户需要输入自己的密码进行身份验证，而不是 root 密码。这是 sudo 的一大优点，可以避免暴露 root 密码 su export PATH=$PATH:/usr/sbin apt update apt list sudo ls /usr/sbin/sudo apt install sudo /usr/sbin/usermod -aG sudo $(whoami) #将helq用户加入sudo的用户组; # -a (append)：表示将用户添加到新的组时，保留其现有的组。如果不加 -a，用户将会退出其他组，仅加入指定的组 # -G(group)：后面跟一组组名，用逗号分隔，表示要添加用户的目标组 groups helq #确认用户已被添加到 sudo 组;输出中应该包含 sudo # 重新登录使更改生效 sudo echo \u0026#34;aaa\u0026#34; #测试 Copied! f、前置软件安装 1 2 3 4 5 6 7 8 9 10 11 su export PATH=$PATH:/usr/sbin apt update \u0026amp;\u0026amp; apt-get update apt install -y net-tools build-essential openssh-server curl unzip zip tree cmake jq # 安装zsh apt install zsh zsh --version sudo chsh -s $(which zsh) # 设置 zsh 为 root用户的 默认 shell reboot # 重启 chsh -s $(which zsh) # 重新为普通用户设置 Copied! f、允许root直接登录 默认情况下,是不允许 root直接登录的即: ssh -p 22 root@172.16.106.12 会失败\n1 2 echo \u0026#34;PermitRootLogin yes\u0026#34; | sudo tee -a /etc/ssh/sshd_config \u0026gt;/dev/null # 解决方式, 重启启动虚拟机就可以了 reboot # 重启 Copied! g 、虚拟机克隆的情况 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 # 执行下面这个命令,重新配置静态ip dns 和 hostname su -c \u0026#34;wget -q -O- --header=\u0026#39;Cache-Control: no-cache\u0026#39; \\\u0026#34;https://raw.githubusercontent.com/loganoxo/Config/master/linux/install/pre.sh?$(date +%s)\\\u0026#34; | bash -s -- \\\u0026#34;run\\\u0026#34; \\\u0026#34;$(whoami)\\\u0026#34; \\\u0026#34;clone\\\u0026#34; \u0026#34; ################# 重新生成 ssh host key ############################## # 因为 A、B 和 C 虚拟机的 SSH 主机密钥是克隆时复制过来的，三台虚拟机的密钥相同，所以 SSH 客户端会认为它们是同一台主机。 # ssh连接这三台机器时,本地~/.ssh/known_hosts 文件中这三台机器的指纹完全相同; # 当你尝试通过 SSH 连接到主机时，SSH 客户端会检查该主机的指纹是否与之前记录的匹配。如果三台虚拟机的指纹相同，当你切换连接到另一个虚拟机时，SSH 客户端会认为主机身份可能被篡改，提示警告 # 主机指纹的目的是确保客户端连接到正确的服务器。如果三台虚拟机的指纹相同，客户端无法区分它们。这可能会带来以下问题： # • 中间人攻击更容易成功，因为客户端无法验证主机的唯一性。 # • 如果某台虚拟机被攻破，攻击者可能利用相同的指纹冒充其他虚拟机 # 管理混乱: 在使用工具（如 Ansible、SSH 配置文件）管理多台主机时，相同的指纹可能导致配置错误或意外连接到错误的主机 # 尽量为每台虚拟机生成唯一的 SSH 主机密钥，确保指纹唯一性，以避免潜在问题并提高系统安全性。 # 在 克隆出来的虚拟机中执行: sudo rm -f /etc/ssh/ssh_host_* sudo ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N \u0026#34;\u0026#34; sudo ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key -N \u0026#34;\u0026#34; sudo ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N \u0026#34;\u0026#34; sudo systemctl restart ssh sudo reboot # ssh-keygen: 用于生成 SSH 密钥的工具; -t rsa: 指定生成密钥的类型为 RSA。RSA 是一种常用的公钥算法 # -f /etc/ssh/ssh_host_rsa_key : 指定生成的密钥文件的路径和文件名; 公钥会自动生成在相同路径，文件名为 /etc/ssh/ssh_host_rsa_key.pub ; 私钥存储在同目录 /etc/ssh/ssh_host_rsa_key# -N \u0026#34;\u0026#34; : 双引号不能去掉; 设置密钥的密码为空;空密码适用于 SSH 主机密钥，因为它们需要在没有人工干预的情况下由 SSH 服务自动使用 ############## 客户端中需要把之前在 ~/.ssh/known_hosts 中生成的 ip+指纹 删除 ssh-keygen -R \u0026lt;VM Ip\u0026gt; ssh-keygen -R \u0026#34;172.16.106.110\u0026#34; ssh-keygen -R \u0026#34;172.16.106.120\u0026#34; ssh-keygen -R \u0026#34;172.16.106.130\u0026#34; cat ~/.ssh/known_hosts # 重新用 SSH 连接; 会重新提示接受新的指纹 Copied! 三、以下配置可以用脚本执行 1、安装shell插件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 su helq export PATH=$PATH:/usr/sbin sudo apt update # 安装 git sudo apt install git git --version # 安装 ohmyzsh sh -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\u0026#34; git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting rm ~/.zshrc.pre-oh-my-zsh # 安装 ohmybash bash -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/ohmybash/oh-my-bash/master/tools/install.sh)\u0026#34; rm ~/.bashrc.omb-backup-* # 安装 starship curl -sS https://starship.rs/install.sh | sh Copied! 2、环境搭建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 su helq export PATH=$PATH:/usr/sbin/ # 临时解决debian系统的默认PATH没有sbin的问题 # git sudo apt install git git --version # ssh-github 私钥 ### 可选-上传 ~/.ssh/ 下的github私钥 到 虚拟机 ssh -T git@github.com # 若有权限太宽泛的问题 chmod 600 /home/helq/.ssh/loganoxo-GitHub ssh -T git@github.com # eval \u0026#34;$(ssh-agent -s)\u0026#34; cd ~ mkdir -p ~/.aria2 ~/.config ~/.ssh ~/.shell_bak ~/software ~/Data ~/.local/bin ~/.config/navi ~/.zoxide ~/.undodir ~/.vim ~/Temp ~/share git clone https://github.com/loganoxo/Config.git ~/Data/Config mv ~/.bashrc ~/.shell_bak/ \u0026amp;\u0026amp; mv ~/.profile ~/.shell_bak/ \u0026amp;\u0026amp; mv ~/.zshrc ~/.shell_bak/ bash ~/Data/Config/my-ln.sh sudo bash ~/Data/Config/linux/for_root/create_root_files.sh \u0026#34;$HOME\u0026#34; \u0026#34;$HOME/Data/Config/linux/for_root/template.sh\u0026#34; sudo ln -sf ~/Data/Config/vim/settings.vim /root/.vimrc source \u0026#34;$HOME/.zshrc\u0026#34; Copied! 3、安装必备工具 debian等linux系统,若是arm架构的,则不推荐装 homebrew(linuxbrew),有很多包没有通用二进制文件,只能在本地编译,很慢又容易出依赖的问题,不友好 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 # 安装防火墙 sudo apt install ufw -y sudo ufw status #inactive，说明 UFW 未启用 # 默认情况下，UFW 会阻止所有传入的网络流量，除非明确允许。例如，如果没有添加允许 SSH 的规则，远程登录将会被拒绝; # 默认情况下，UFW 会允许所有传出的网络流量，比如从本机访问互联网的请求 # UFW 的 allow 命令默认允许的是传入流量; ufw allow ssh 等效于 ufw allow in ssh ; 传出的用法如: ufw allow out 53 sudo ufw disable #禁用 sudo ufw default deny incoming \u0026amp;\u0026amp; sudo ufw default allow outgoing # enable之前先开放 ssh 端口, 否则远程连接会断开; 允许SSH（端口 22） HTTP（端口 80） HTTPS（端口 443） sudo ufw allow ssh \u0026amp;\u0026amp; sudo ufw allow http \u0026amp;\u0026amp; sudo ufw allow https \u0026amp;\u0026amp; sudo ufw allow 80 sudo ufw allow 6000:6007/tcp \u0026amp;\u0026amp; sudo ufw allow 6000:6007/udp #允许使用端口 6000-6007 的 连接 sudo ufw limit ssh # 限制ssh登录尝试的连接次数,防止暴力破解密码;每个ip每30秒最多尝试6次 sudo ufw enable #启用 sudo ufw status verbose #查看所有端口开放情况 sudo ufw reset #清空所有规则并恢复默认配置 sudo ufw delete allow ssh #删除某个规则 #根据规则编号删除 sudo ufw status numbered sudo ufw delete \u0026lt;编号\u0026gt; # 安装 nginx sudo apt install nginx -y sudo systemctl list-unit-files --state=enabled #查看所有自启动的软件 sudo systemctl disable nginx.service #禁止nginx开机自启 sudo systemctl status nginx sudo systemctl stop nginx sudo systemctl start nginx sudo systemctl status nginx curl http://127.0.0.1:80 #测试 # 在本地宿主机访问 http://172.16.106.12 ; http://127.0.0.1:12382/ # 安装 go sudo apt install golang-go go version Copied! 4、命令行工具 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 su helq # 安装 bat sudo apt install bat #这样安装的bat会因为避免名字冲突而让他的命令变为 batcat, 所以需要符号链接 # 切换到普通用户执行: mkdir -p ~/.local/bin ln -s /usr/bin/batcat ~/.local/bin/bat # 安装fzf # sudo apt install fzf #版本太低了 mkdir -p ~/software git clone https://github.com/junegunn/fzf.git ~/software/fzf wget -P \u0026#34;$HOME/software/fzf/\u0026#34; https://github.com/junegunn/fzf/releases/download/v0.56.3/fzf-0.56.3-linux_arm64.tar.gz tar -xzf ~/software/fzf/fzf-0.56.3-linux_arm64.tar.gz -C ~/software/fzf ln -sf ~/software/fzf/fzf ~/.local/bin/fzf # 安装fd sudo apt install fd-find ln -s $(which fdfind) ~/.local/bin/fd # 安装zoxide mkdir -p ~/.zoxide curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh # 安装vim mkdir -p ~/.undodir ~/.vim sudo apt install vim curl -fLo ~/.vim/autoload/plug.vim --create-dirs \\ https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim # 测试 :PlugStatus :PlugInstall :PlugClean vim -c \u0026#39;:PlugStatus\u0026#39; -c \u0026#39;:PlugInstall\u0026#39; -c \u0026#39;:PlugClean\u0026#39; -c \u0026#39;:qa!\u0026#39; # 安装sdkman curl -s \u0026#34;https://get.sdkman.io?rcupdate=false\u0026#34; | bash #不修改zshrc 和 bashrc sdk version sdk list java sdk install java 8.0.432.fx-zulu java -XshowSettings:properties -version #查看安装的jdk详细版本信息 sdk install java 11.0.25.fx-zulu sdk install java 17.0.13.fx-zulu sdk install java 17.0.13-zulu sdk install java 17.0.12-oracle #设为默认 sdk install java 17.0.13-tem sdk default java 17.0.12-oracle java -version sdk list maven sdk install maven 3.9.9 sdk default maven 3.9.9 # 重启虚拟机 mvn -version # 安装fnm curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell #不修改zshrc 和 bashrc ln -s ~/.local/share/fnm/fnm ~/.local/bin/fnm # eval \u0026#34;$(fnm completions --shell zsh)\u0026#34; #命令补全,没啥用,就只是补全命令,不会补全当前存在的node版本 fnm -V #查看fnm的版本 fnm ls #查看本地已安装的nodejs的版本 fnm current #打印当前使用的node版本 fnm ls-remote #通过网络查看所有已发布的nodejs版本 fnm ls-remote --lts #通过网络查看所有已发布的长期支持的nodejs版本 fnm ls-remote --sort \u0026lt;SORT\u0026gt; #默认asc升序,desc为倒序 fnm install --lts #安装最新的LTS版本,会自动加别名 default, lts-latest fnm install --latest #安装最新的版本,会自动加别名 latest # fnm install v18.3.0 #安装指定的版本 # fnm install 17 #部分版本匹配,从你的部分输入中猜测最新的可用版本,它将安装版本为v17.9.1 的节点 # fnm use \u0026lt;alias/version\u0026gt; #在当前shell中临时使用某个版本的node fnm use 22 fnm use lts-latest fnm use default # fnm default \u0026lt;alias/version\u0026gt; #将某个版本设为默认版本;即新shell中默认使用的node版本 fnm default lts-latest # fnm uninstall \u0026lt;alias/version\u0026gt; #卸载某个版本 # fnm 给某node版本起别名,用于让其他命令在使用时,将版本号用别名替换; # fnm alias [OPTIONS] \u0026lt;alias/version\u0026gt; \u0026lt;NAME\u0026gt; 设置别名; 别名唯一; # 若此次设置的别名 aaa 与其他版本的别名重复,则会自动取消之前的别名,给这个命令里的版本17设置别名 aaa # fnm alias 17 aaa fnm alias lts-latest lts # fnm unalias [OPTIONS] \u0026lt;alias_name\u0026gt; 取消别名 # fnm unalias lts which -a npm npm -g install nrm pm2 prettier yarn yrm npm list -g nrm ls nrm use taobao # 安装rust curl --proto \u0026#39;=https\u0026#39; --tlsv1.2 -sSf https://sh.rustup.rs | sh rustc --version # 安装navi tt # 方法一,用cargo cargo install --locked navi # 方法二,自己编译,有问题 git clone https://github.com/denisidoro/navi \u0026amp;\u0026amp; cd navi make BIN_DIR=/home/helq/.local/bin install # 方法三,用脚本,有问题,在debian上执行不了 BIN_DIR=/home/helq/.local/bin bash \u0026lt;(curl -sL https://raw.githubusercontent.com/denisidoro/navi/master/scripts/install) # tldr cargo install tealdeer tldr --update tldr bat tldr -L zh tree # glow mkdir -p ~/software wget -P \u0026#34;$HOME/software\u0026#34; https://github.com/charmbracelet/glow/releases/download/v2.0.0/glow_2.0.0_Linux_arm64.tar.gz tar xvzf ~/software/glow_2.0.0_Linux_arm64.tar.gz -C \u0026#34;$HOME/software/\u0026#34; mv ~/software/glow_2.0.0_Linux_arm64 ~/software/glow ln -s ~/software/glow/glow ~/.local/bin/glow # the_silver_searcher sudo apt install silversearcher-ag # trash-cli https://github.com/andreafrancia/trash-cli sudo apt install python3 python3-pip python3-venv sudo apt install trash-cli # 版本很低 sudo mkdir --parent /.Trash sudo chmod a+rw /.Trash sudo chmod +t /.Trash # +t 设置粘滞位 (sticky bit) 权限;只有文件或子目录的所有者，或目录本身的所有者（通常是管理员），才能删除或重命名该目录内的内容; # 其他用户即使有写权限，也无法删除非自己拥有的文件 # trash-cli https://github.com/andreafrancia/trash-cli # trash-put \u0026lt;file/directory\u0026gt; # trash files and directories. # trash-empty # 从垃圾箱中删除所有文件 # trash-empty \u0026lt;days\u0026gt; # 仅删除已删除时间超过 \u0026lt;days\u0026gt; 的文件 # trash-list | grep \u0026lt;path\u0026gt; # list trashed files. # trash-restore # 选择文件进行恢复, 若要覆盖同名文件则 加 --overwrite # trash-rm \\*.o # 从垃圾箱中删除与模式匹配的文件 # 从 home 分区回收的文件将被移动到此处 ~/.local/share/Trash/ # uv 包含了 pipx 的功能 https://docs.astral.sh/uv/ # 默认是managed: 最先找uv管理的python,其次找系统python(若此时在conda的某个环境中,conda该环境的python也会被找到),最后才下载;only-managed:只找uv管理的python,没有则下载; # # 安装选项:https://docs.astral.sh/uv/configuration/installer/#disabling-shell-modifications curl -LsSf https://astral.sh/uv/install.sh | sh # 默认在 ~/.local/share/uv/ # brew install uv export UV_PYTHON_PREFERENCE=\u0026#34;only-managed\u0026#34; uv python list uv python install # 默认在 ~/.local/share/uv/python/ uv python install 3.12 uv python uninstall 3.12 uv tool list uv tool install ruff # 安装命令行工具,默认在 ~/.local/bin/ ~/.local/share/uv/tools/ uv tool install ruff -p 3.12 # 指定安装的命令行工具的虚拟环境中的python版本 ruff --version ruff check a.py # 这个命令的功能: 检查python代码是否有问题 uv tool uninstall ruff # 命令补全: https://docs.astral.sh/uv/getting-started/installation/#upgrading-uv # 安装miniconda (超过200人公司使用收费) 免费替代: https://github.com/conda-forge/miniforge # https://docs.anaconda.com/miniconda/install/ # https://docs.conda.io/projects/conda/en/stable/user-guide/index.html # miniconda3 # 寻找适合的版本 https://repo.anaconda.com/miniconda/ 带py的指base环境的python版本 ########## mac 也可以用 brew install miniconda cd ~/Temp curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh -o ~/Temp/miniconda.sh shasum -a 256 ~/Temp/miniconda.sh # 验证文件的 sha256 的值 bash ~/Temp/miniconda.sh # 安装路径与mac同步为 ~/.miniconda3 rm ~/Temp/miniconda.sh conda init bash zsh ####### linux ### 静默安装 mkdir -p ~/Temp wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-aarch64.sh -O ~/Temp/miniconda.sh bash ~/Temp/miniconda.sh -b -u -p ~/.miniconda3 # -b:不对 shell 脚本进行 PATH 修改,以非交互模式（静默模式）运行安装; -u:如果指定的安装路径（通过 -p）已有 Miniconda 安装，它会更新而不是报错或覆盖安装; -p: 指定安装路径 rm ~/Temp/miniconda.sh ### 按照提示安装 mkdir -p ~/Temp wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-aarch64.sh -O ~/Temp/miniconda.sh bash ~/Temp/miniconda.sh # 安装路径与mac同步为 ~/.miniconda3 rm ~/Temp/miniconda.sh # 也可以用apt安装 https://docs.conda.io/projects/conda/en/stable/user-guide/install/rpm-debian.html # 静默安装后,可选择shell环境,会修改 .zshrc .bashrc conda init zsh conda init bash conda init --all # 在所有可用 shell 上初始化 conda conda --version conda list conda info # base环境 推荐 只能用于安装 anaconda、conda 和 conda 相关的软件包，例如`anaconda-client`或`conda-build` # 配置文件 : ~/.condarc conda config --set auto_activate_base false # 设置开启新shell的时候不自动进入conda的base环境 conda config --set changeps1 False # 抑制 conda 自己的提示修饰符 # conda config --add channels conda-forge 会加在第一个 conda config --append channels conda-forge conda config --remove channels conda-forge conda -V conda create -n env_test python=3.9 # -n 是创建的环境的名字 conda activate env_test python3 -V conda activate # 默认是base环境 python3 -V conda deactivate conda update conda # 更新自己 # 安装包或者命令行工具 conda install trash-cli # 官方的 channel 没有很多的开源包 conda install -c conda-forge trash-cli # 指定 conda-forge 的channel # install的命令行工具的执行文件放在当前环境的bin目录下,如 /home/helq/miniconda3/envs/env_test/bin/trash-put; 同时也会被pip管理 # install 也可以安装python库文件,可以在 Python 脚本或交互式环境中直接导入并使用 import numpy as np # 卸载conda rm -rf ~/conda rm -rf ~/.condarc ~/.conda ~/.continuum # 导出某个非base环境到yaml文件; 仅将 Anaconda 或 Miniconda 文件复制到新目录或另一台计算机不会重新创建环境。您必须将环境作为一个整体导出 conda activate env_test conda export -f env_test.yml --no-builds 或者 conda env export -f env_test.yml --no-builds # --override-channels 不导出.condarc里的channel; # --no-builds 不导出构建编号,当跨平台迁移的时候必加,因为同一个版本的包在不同平台上的构建编号肯定不同;如: mac导出到linux上的时候 # 导入 conda env create -f env_test.yml # docker # https://docs.docker.com/engine/install/debian/#install-using-the-repository sudo apt update for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get remove $pkg; done #卸载冲突的包 sudo apt autoremove # Set up Docker\u0026#39;s `apt` repository # Add Docker\u0026#39;s official GPG key: sudo apt-get update \u0026amp;\u0026amp; sudo apt-get install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Add the repository to Apt sources: echo \\ \u0026#34;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \\ $(. /etc/os-release \u0026amp;\u0026amp; echo \u0026#34;$VERSION_CODENAME\u0026#34;) stable\u0026#34; | \\ sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null sudo apt-get update # Install the Docker packages sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # Verify sudo docker run hello-world sudo docker images sudo docker container ls -a # 以非 root 用户身份管理 Docker; # 创建一个名为`docker`的 Unix 组并向其中添加用户。当 Docker 守护进程启动时，它会创建一个可供`docker`组成员访问的 Unix 套接字 sudo groupadd docker # 创建`docker`组 sudo usermod -aG docker $USER # 将您的用户添加到`docker`组 sudo reboot # 注销并重新登录，以便重新评估您的组成员身份 docker run hello-world # 验证您是否可以在没有`sudo`情况下运行`docker`命令 # 如果在分配用户组之前 用sudo权限 执行过 docker CLI 中的如 docker login 这类命令, 会创建 ~/.docker 目录; # 在上述的情况下, 分配用户组后, 用普通用户直接执行 docker 命令, 有可能会报错, 因为 ~/.docker 的权限是 root 用户的; 报错信息可能为: # WARNING: Error loading config file: /home/user/.docker/config.json -stat /home/user/.docker/config.json: permission denied # 解决方式一: 删除`~/.docker/`目录（它会自动重新创建，但所有自定义设置都会丢失） # 方式二: sudo chown \u0026#34;$USER\u0026#34;:\u0026#34;$USER\u0026#34; /home/\u0026#34;$USER\u0026#34;/.docker -R # 将这个文件夹的 拥有者 和 所属组 设为当前用户;-R：递归修改，即包括子目录和文件 sudo chmod g+rwx \u0026#34;$HOME/.docker\u0026#34; -R # 为 .docker 目录及其内容增加组权限，使所属组的成员可以读取（read）、写入（write）、执行（execute）文件。 # 在 Debian 和 Ubuntu 上，Docker 服务(守护进程)(不是指的容器) 默认在启动时启动 sudo systemctl status docker.service # 这是 Docker 的主服务，负责管理 Docker 守护进程（dockerd），提供核心功能，包括容器管理、镜像拉取和存储等 sudo systemctl status containerd.service # 这是 containerd 容器运行时服务，是一个独立的守护进程，用于管理容器的生命周期 sudo systemctl enable docker.service sudo systemctl enable containerd.service # 禁止开机启动;但是当使用docker命令时,这两个服务会自动启动 sudo systemctl disable docker.service sudo systemctl disable containerd.service # 其他安装 sudo apt install shellcheck shfmt tmux universal-ctags Copied! 5、文件上传下载服务 a、dufs 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 # https://github.com/sigoden/dufs # 安装 或者用docker直接使用就不用安装了 cargo install dufs # 使用1、用docker直接启动容器挂载当前目录 docker run -v `pwd`:/data -p 5000:5000 --rm sigoden/dufs /data -A # -A 是dufs的选项 # 默认情况下，容器即使停止后，仍然会保留在本地（可以用 docker ps -a 查看）。这些“停止的容器”会占用存储空间，并且需要手动清理 # --rm 容器停止时自动删除容器及其临时文件（例如挂载的匿名卷）;避免积累过多无用的停止容器，减少磁盘空间浪费。 # 使用2、用本地dufs命令 mkdir -p ~/share/dufs \u0026amp;\u0026amp; cd ~/share/dufs sudo ufw allow 5000 dufs # 以只读模式提供当前目录,只允许查看和下载;默认在前台执行 nohup dufs \u0026gt; output.log 2\u0026gt;\u0026amp;1 \u0026amp; # 后台启动 jobs -l # 查看后台启动程序; kill PID dufs -A # 允许所有操作，如上传/删除/搜索/创建/编辑 dufs --allow-upload # 只允许查看和下载和上传操作 # --allow-archive 允许文件夹打包下载; --allow-search 允许搜索 dufs ~/share/dufs # 指定某个目录 dufs linux-distro.iso # 指定单个文件 dufs -a admin:123@/:rw # 指定用户名admin/密码123 dufs -b 127.0.0.1 -p 80 # 监听特定ip和端口 dufs --hidden .git,.DS_Store,tmp # 隐藏目录列表中的路径 dufs --hidden \u0026#39;.*\u0026#39; # hidden dotfiles dufs --hidden \u0026#39;*/\u0026#39; # hidden all folders dufs --hidden \u0026#39;*.log,*.lock\u0026#39; # hidden by exts dufs --hidden \u0026#39;*.log\u0026#39; --hidden \u0026#39;*.lock\u0026#39; dufs --render-index # 使用index.html 提供静态网站 dufs --render-spa # 提供像 React/Vue 这样的单页应用程序 # 可以记录日志,见github # 可以将这些选项放在配置文件,见github # 命令行客户端访问 curl -T path-to-file http://127.0.0.1:5000/new-path/path-to-file # 上传文件 curl http://127.0.0.1:5000/path-to-file # download the file curl http://127.0.0.1:5000/path-to-file?hash # retrieve the sha256 hash of the file curl -C- -o file http://127.0.0.1:5000/file # 可断点下载 curl -o path-to-folder.zip http://127.0.0.1:5000/path-to-folder?zip # 将文件夹下载为 zip 文件 curl -X DELETE http://127.0.0.1:5000/path-to-file-or-folder # 删除文件/文件夹 curl -X MKCOL http://127.0.0.1:5000/path-to-folder # 创建目录 curl -X MOVE http://127.0.0.1:5000/path -H \u0026#34;Destination: http://127.0.0.1:5000/new-path\u0026#34; # 将文件/文件夹移动到新路径 # 列出/搜索目录内容 curl http://127.0.0.1:5000?q=Dockerfile # search for files, similar to `find -name Dockerfile` curl http://127.0.0.1:5000?simple # output names only, similar to `ls -1` curl http://127.0.0.1:5000?json # output paths in json format # 需要账户密码 curl http://127.0.0.1:5000/file --user user:pass # basic auth curl http://127.0.0.1:5000/file --user user:pass --digest # digest auth curl http://127.0.0.1:5000/__dufs__/health # 健康检查 # 可断点上传 upload_offset=$(curl -I -s http://127.0.0.1:5000/file | tr -d \u0026#39;\\r\u0026#39; | sed -n \u0026#39;s/content-length: //p\u0026#39;) dd skip=$upload_offset if=file status=none ibs=1 | \\ curl -X PATCH -H \u0026#34;X-Update-Range: append\u0026#34; --data-binary @- http://127.0.0.1:5000/file Copied! b、filebrowser 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 ############################ docker模式 mkdir -p ~/share/filebrowser mkdir -p ~/share/filebrowser/files touch ~/share/filebrowser/filebrowser.db touch ~/share/filebrowser/filebrowser.json cat \u0026gt; ~/share/filebrowser/filebrowser.json \u0026lt;\u0026lt; EOF { \u0026#34;port\u0026#34;: 80, \u0026#34;baseURL\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;address\u0026#34;: \u0026#34;\u0026#34;, \u0026#34;log\u0026#34;: \u0026#34;stdout\u0026#34;, \u0026#34;database\u0026#34;: \u0026#34;/database/filebrowser.db\u0026#34;, \u0026#34;root\u0026#34;: \u0026#34;/srv\u0026#34; } EOF sudo ufw allow 12786 docker run -d \\ -v ~/share/filebrowser/files:/srv \\ -v ~/share/filebrowser/filebrowser.db:/database/filebrowser.db \\ -v ~/share/filebrowser/filebrowser.json:/.filebrowser.json \\ -u $(id -u):$(id -g) \\ -p 12786:80 \\ filebrowser/filebrowser # 默认情况下，镜像里已经有一个包含一些默认值的配置文件，因此您只需挂载根目录和数据库即可; 默认配置文件: # https://github.com/filebrowser/filebrowser/blob/master/docker/root/defaults/settings.json ######################## 命令行模式 curl -fsSL https://raw.githubusercontent.com/filebrowser/get/master/get.sh | bash nohup filebrowser -r ~/share/filebrowser -a 0.0.0.0 \u0026gt; output.log 2\u0026gt;\u0026amp;1 \u0026amp; # Username: `admin` Password: `admin` Copied! c、开启 sftp 服务 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 # 确保系统已安装 OpenSSH 服务器 默认应该都存在并启动了sftp服务的 sudo apt update sudo apt install openssh-server # 检查 SSH 服务状态 sudo systemctl status ssh # 查看 /etc/ssh/sshd_config 这个文件内是否存在: # override default of no subsystems Subsystem\tsftp\t/usr/lib/openssh/sftp-server # 若存在则自动为所有创建的用户打开sftp服务,root能访问和修改所有文件; 普通用户能查看所有文件,但是只能修改自己有权限的文件 # 所以为了安全性,可以添加一个只能sftp连接不能ssh登录shell的用户,并限制这个用户 使用 SFTP 能使用的目录;主要目的是为了不暴露普通用户密码给别人 # ChrootDirectory 指定的目录及其父目录必须满足以下条件:1、由 root 拥有;2、不可被其他用户写入（即权限中不能有 write 权限分配给非 root 用户）;这意味着普通用户的主目录（比如 /home/sftpuser），无法直接作为 ChrootDirectory，因为主目录通常是由该用户自己拥有的，而不是 root # 解决办法:1、创建一个由 root 拥有的顶层目录（例如 /var/sftp），保证它不可写;2、在顶层目录下创建一个子目录（例如 /var/sftp/sftpuser），将该子目录的所有权赋予用户（例如 sftpuser），以允许用户上传文件 sudo useradd -s /sbin/nologin sftpuser # 创建一个新用户，该用户仅被授予对服务器的文件传输访问权限; 不能登录shell,没有home目录 # -m 会创建home目录; -d \u0026lt;path\u0026gt; 自定义home目录 sudo passwd sftpuser # 添加密码 123456 sudo mkdir -p /var/sftp/sftpuser sudo chown root:root /var/sftp sudo chmod 755 /var/sftp # root用户为7所有权限; 同组用户和不同组的用户为5只允许读和执行; 4:读 2:写 1:执行 sudo chown sftpuser:sftpuser /var/sftp/sftpuser # 将目录的所有权更改为您刚刚创建的用户 sudo chmod 775 /var/sftp/sftpuser # 同组用户可以读和写(目录必须有执行权限才能cd进入);其他用户只读 sudo usermod -aG sftpuser $(whoami) # 把当前用户加入 sftpuser 用户组,让当前用户可以操作`分享目录`,就让当前用户可以将自己的文件复制到这个`分享目录中`了 getent group sftpuser # 查看用户组中有哪些用户 # 权限组生效要重新登录 ## 修改 SSH 服务器配置以禁止sftpuser用户的终端访问，但允许文件传输访问 sudo vim /etc/ssh/sshd_config # 滚动到文件的最底部并添加以下配置片段; # 或者检查 /etc/ssh/sshd_config 中是否存在 Include /etc/ssh/sshd_config.d/*.conf 字样; 可如下方加一个新文件被引入 ################################ sudo touch /etc/ssh/sshd_config.d/sftp.conf su cat \u0026gt; /etc/ssh/sshd_config.d/sftp.conf \u0026lt;\u0026lt; EOF Match User sftpuser ForceCommand internal-sftp PasswordAuthentication yes ChrootDirectory /var/sftp PermitTunnel no AllowAgentForwarding no AllowTcpForwarding no X11Forwarding no EOF ################################ su helq sudo sshd -t # 测试 sudo systemctl restart sshd # 重启或重新加载 SSH 服务 # 到 其他机器上 : # 验证ssh是否关闭 ssh sftpuser@your_server_ip # 会收到 This service allows sftp connections only.表示连接失败,无法再使用 SSH 访问 shell # 验证sftp是否开启 sftp sftpuser@your_server_ip # 此命令将生成带有交互式提示的成功登录消息;可以在提示符中使用`ls`列出目录内容 ############## 或者可以通过 sftp客户端界面(Cyberduck或FileZilla等) 连接访问 # Match User sftpuser 告诉 SSH 服务器仅将以下命令应用于指定的用户 # ForceCommand internal-sftp 强制 SSH 服务器在登录时运行 SFTP 服务器，确保其只能上传/下载文件,禁止 shell 登录访问。 # PasswordAuthentication yes 允许该用户进行密码验证;不然可能需要用户使用基于密钥的验证 # ChrootDirectory /var/sftp/ 确保不允许用户访问/var/sftp目录之外的任何内容,/var/sftp 必须由 root 拥有，且不可被其他用户写入,在 /var/sftp 内，可以创建用户有写权限的子目录，如 /var/sftp/sftpuser # PermitTunnel 禁止 SSH 隧道功能,提高安全性，防止用户滥用隧道功能绕过网络限制 # AllowAgentForwarding 禁止 SSH 代理转发功能,端口转发、隧道和 X11 转发,进一步限制用户的功能，防止代理滥用 # AllowTcpForwarding 禁止 TCP 转发功能,防止用户通过 SSH 隧道代理访问内部网络或外部服务器 # X11Forwarding 禁止 X11 图形界面转发,减少不必要的功能支持，提高安全性 # 这组命令从Match User开始，也可以为不同的用户复制和重复。确保相应地修改Match User行中的用户名 Copied! d 、开启 ftp 服务 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 sudo apt update \u0026amp;\u0026amp; sudo apt install vsftpd # 默认应该都是没有安装的 # 开放端口;为 FTP 打开端口`20`和`21`;在启用 TLS 时打开端口`990`; pasv_max_port pasv_min_port 限制可用于被动 FTP 的端口范围40000到50000 sudo ufw status sudo ufw allow from any to any port 20,21,990,40000:50000 proto tcp sudo ufw status # 创建只用于 ftp 的用户;不允许登录shell # chroot_local_user=YES时, ftp要求用户要有home目录,并且home目录不可写 sudo useradd -s /sbin/nologin -d /var/ftp ftpuser # -m 会创建home目录; -d \u0026lt;path\u0026gt; 自定义home目录 sudo sh -c \u0026#39;echo \u0026#34;/sbin/nologin\u0026#34; \u0026gt;\u0026gt; /etc/shells\u0026#39; # ftp会检查登录的用户的shell是否正常; 这里让 /sbin/nologin 变为有效的登录 shell sudo mkdir -p /var/ftp/ftpuser sudo chmod 555 /var/ftp # 让home目录不可写; 4:读 2:写 1:执行 sudo chown nobody:nogroup /var/ftp # 限制权限，确保进程或服务无法访问不必要的系统资源 sudo passwd ftpuser # 添加密码 123456 sudo chown ftpuser:ftpuser /var/ftp/ftpuser # 将目录的所有权更改为您刚刚创建的用户 sudo chmod 775 /var/ftp/ftpuser # 同组用户可以读和写(目录必须有执行权限才能cd进入);其他用户只读 sudo usermod -aG ftpuser $(whoami) # 把当前用户加入 ftpuser 用户组,让当前用户可以操作`分享目录`,就让当前用户可以将自己的文件复制到这个`分享目录中`了 getent group ftpuser # 查看用户组中有哪些用户 sudo useradd -r -M -s /sbin/nologin ftpsecure # 创建一个供nopriv_user使用的低权限用户 # 权限组生效要重新登录 sudo cp /etc/vsftpd.conf /etc/vsftpd.conf.bak # 备份原始文件 su #################################################################### cat \u0026gt; /etc/vsftpd.conf \u0026lt;\u0026lt; EOF # NO: 表示 vsftpd 不会以独立的守护进程方式运行;用于资源受限的系统; 因为 inetd 或 xinetd 在没有连接时不会启动 FTP 服务,从而减少资源占用 # 当 listen=YES 时,vsftpd 只会在 IPv4 地址 上启动并监听 FTP 连接, listen_ipv6就必须为 NO listen=YES # 启用对 IPv6 的支持,同时接受来自 IPv4 和 IPv6 的连接 listen_ipv6=NO # 允许系统中的本地用户使用 FTP 登录 local_enable=YES # 启用此选项后,所有本地用户在登录时将被限制在他们的HOME目录内,他们无法访问其他系统目录 chroot_local_user=YES # 用于指定哪些本地用户可以不受 chroot_local_user 功能限制;可以在 chroot_list_file 中列出特定用户 # chroot_list_enable=YES # chroot_list_file=/etc/vsftpd.chroot_list # 允许用户使用写操作; 如 上传文件 删除文件 write_enable=YES # 实际权限 = 默认权限(文件的默认权限是666; 目录的默认权限是777) - umask local_umask=022 # 每当用户切换到某个目录时,vsftpd 会检查该目录下是否存在一个名为 .message 的文件;如果 .message 文件存在,其内容会显示给用户,作为该目录的欢迎消息或说明 dirmessage_enable=YES # 目录列表(例如 ls 命令)中的时间戳将显示为服务器本地时区时间 use_localtime=YES # 启用上传和下载操作的日志记录功能;默认在/var/log/vsftpd.log; 可由选项xferlog_file自定义 xferlog_enable=YES # 用于指定 vsftpd 的日志文件位置 # xferlog_file=/var/log/vsftpd.log # 如果启用此选项,日志将采用标准 xferlog 格式,通常与传统 FTP 服务的日志格式兼容;日志文件的默认存储路径会更改为 /var/log/xferlog # xferlog_std_format=YES # 指定 FTP 数据连接的来源端口为 20;(21 端口:用于控制连接;接收用户命令并返回响应) connect_from_port_20=YES # 默认开启被动模式;需要服务端开放40000-50000的端口,所以对客户端的防火墙很友好 pasv_enable=YES pasv_min_port=40000 pasv_max_port=50000 # 设置空闲会话的超时时间,单位为秒;如果客户端在此期间没有任何活动(如上传、下载、浏览目录等),则服务器会自动关闭该会话 idle_session_timeout=3600 # 设置数据连接的超时时间,单位为秒;指定了 vsftpd 等待数据连接(例如文件上传、下载等)时的最大空闲时间;如果在这个时间内没有数据传输,连接会被断开 data_connection_timeout=600 # 当 vsftpd 需要执行与 FTP 客户端相关的非特权操作时,它会以 nopriv_user 的身份运行子进程或线程,可以有效地减少可能的安全漏洞 nopriv_user=ftpsecure # 自定义的登录横幅信息 ftpd_banner=Welcome to blah FTP service. # 禁用匿名登录,只有本地用户可以登录 anonymous_enable=NO # 禁止匿名用户上传文件 anon_upload_enable=NO # 禁止匿名用户创建目录 anon_mkdir_write_enable=NO # 控制匿名用户上传的文件的所有权设置;用于加强安全性和便于管理;默认情况下,匿名用户上传的文件的所有者是 ftp 或 nobody;通过启用此选项,可以将文件的所有者更改为指定的用户 # chown_uploads=YES # chown_username=whoever # 指定一个空目录,作为 vsftpd 的 chroot() 监狱;chroot() 是一个将进程及其子进程的根目录改为指定目录的系统调用。 # 在 vsftpd 中,使用 chroot() 可以将某些用户限制在某个目录（或目录树）中,使得这些用户无法访问系统的其他部分 # 当 FTP 用户登录时,通常会被限制在他们的家目录内。如果启用了 chroot_local_user 或其他类似设置,FTP 用户将无法访问他们家目录以外的文件和目录 # 该目录并不是为用户上传和下载文件的目录,而是用于保护和增强安全性。在一些特定情况下,vsftpd 会切换到该目录,并限制其对文件系统的访问 secure_chroot_dir=/var/run/vsftpd/empty # 告诉 vsftpd 在进行用户登录认证时,使用 PAM(可插拔认证模块)来进行验证;简单来说,它指定了 FTP 服务器在验证用户身份时,应该使用什么样的认证规则 # PAM 就是一个管理系统登录、密码验证等认证工作的工具;vsftpd 需要验证用户是否能登录,pam_service_name=vsftpd 就是告诉它：在验证时,去找一个专门为 vsftpd 配置的认证规则文件,通常这个文件叫 vsftpd,会放在 /etc/pam.d/ 文件夹里 pam_service_name=vsftpd # 是否开启 FTPS rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key ssl_enable=NO # 使用 UTF-8 编码的文件系统 utf8_filesystem=YES # FTP 服务器将异步处理 ABOR 请求,提高处理效率,但可能存在安全风险 # async_abor_enable=YES # 专门用于文本文件,启用该选项后,FTP 服务器会在上传/下载文件时进行 ASCII 处理(即转换换行符),可能会增加 CPU 负担和安全隐患 # ascii_upload_enable=YES # ascii_download_enable=YES # 启用此选项后,vsftpd 将检查匿名 FTP 用户的电子邮件地址,如果该地址在禁止列表中,用户将无法登录 # deny_email_enable=YES # 这是存储被禁止的电子邮件地址列表的文件。vsftpd 会检查该文件中的电子邮件地址,并阻止这些地址的匿名用户登录。每行一个电子邮件地址 # banned_email_file=/etc/vsftpd.banned_emails # 用户使用 ls 命令时,FTP 服务器会列出当前目录及其所有子目录的内容,出于性能考虑,最好保持默认的禁用状态 # ls_recurse_enable=YES EOF ##################################################################### sudo systemctl restart vsftpd Copied! e、minio 1 2 3 4 5 6 7 8 mkdir -p ~/software/minio/share sudo ufw allow from any to any port 19000,19001 proto tcp docker run -d -p 19000:9000 -p 19001:9001 \\ -v ~/software/minio/share:/data \\ quay.io/minio/minio server /data --console-address \u0026#34;:9001\u0026#34; http://ip:19001 minioadmin:minioadmin Copied! 四、可选 a、pipx 1 2 3 4 5 6 7 8 9 # 可选 pipx 专门下载python命令行工具 到隔离环境的,用哪个用户下载的就在哪个用户目录下的 ~/.local/bin 中; 除非加了--global 会安装在全局的地方,但是版本低不支持 sudo apt update sudo apt install pipx pipx ensurepath # 就是设置环境变量中的 $PATH:~/.local/bin sudo pipx ensurepath --global # apt的版本太低不支持 pipx install trash-cli # 安装最新版本,但是默认只能一个用户使用 pipx list pipx uninstall-all pipx completions # 查看补全说明 Copied! b、clash 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 ##################### clash代理 ######################################################## mkdir -p cd /usr/local/clash-arm64 cd /usr/local/clash-arm64 ls clash-linux-arm64-v1.18.0.gz config.yaml Country.mmdb \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; gunzip clash-linux-arm64-v1.18.0.gz mv clash-linux-arm64-v1.18.0 clash chmod +x clash sudo mkdir /etc/clash sudo cp clash /usr/local/bin sudo cp config.yaml /etc/clash/ sudo cp Country.mmdb /etc/clash/ ls /etc/clash/ \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; sudo vim /etc/systemd/system/clash.service \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; [Unit] Description=Clash daemon, A rule-based proxy in Go. After=network.target [Service] Type=simple Restart=always ExecStart=/usr/local/bin/clash -d /etc/clash [Install] WantedBy=multi-user.target \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; sudo systemctl stop clash sudo systemctl enable clash sudo systemctl start clash sudo systemctl disable clash sudo systemctl status clash export http_proxy=http://127.0.0.1:25307 \u0026amp;\u0026amp; export https_proxy=http://127.0.0.1:25307 \u0026amp;\u0026amp; export all_proxy=socks5://127.0.0.1:25307 curl https://www.google.com curl https://github.com \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; mkdir -p /usr/local/my-functions/ \u0026amp;\u0026amp; cd /usr/local/my-functions/ echo \u0026#34;source /usr/local/my-functions/debian_proxy\u0026#34; \u0026gt;\u0026gt; /home/helq/.bashrc echo \u0026#34;source /usr/local/my-functions/debian_proxy\u0026#34; \u0026gt;\u0026gt; /root/.bashrc echo \u0026#34;alias ll=\u0026#39;ls -l\u0026#39;\u0026#34; \u0026gt;\u0026gt; /home/helq/.bashrc echo \u0026#34;alias ll=\u0026#39;ls -l\u0026#39;\u0026#34; \u0026gt;\u0026gt; /root/.bashrc cd .. #重新连接 sudo systemctl status clash getproxy setproxy term getproxy curl https://www.google.com ##################################### Copied! c、卸载 apache2 1 2 3 4 5 6 7 8 apt list apache which -a apache2 ls /usr/sbin/apache2 systemctl disable apache2.service #禁止自启动 apt-get --purge remove apache2 -y sudo apt autoremove -y Copied! 五、后续更新 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 安装 fastfetchmkdir -p \u0026#34;$HOME/software/fastfetch\u0026#34; wget -P \u0026#34;$HOME/software/fastfetch\u0026#34; https://github.com/fastfetch-cli/fastfetch/releases/download/2.31.0/fastfetch-linux-aarch64.deb sudo apt update sudo dpkg -i \u0026#34;$HOME/software/fastfetch/fastfetch-linux-aarch64.deb\u0026#34; fastfetch --version which -a fastfetch git -C \u0026#34;$HOME/Data/Config\u0026#34; pull # ssh 连接时,不要打印 系统版本和版权信息 touch \u0026#34;$HOME/.hushlogin\u0026#34; # ffmpeg 安装 sudo apt install ffmpeg # 下载 m3u8 视频 ffmpeg -i \u0026#34;https://aa.ww.bb/mixed.m3u8\u0026#34; -c copy -bsf:a aac_adtstoasc output.mp4 Copied! ","date":"2024-11-25T00:00:00+08:00","image":"https://logan.1357810.xyz/cover/debiancover.png","permalink":"https://qh.1357810.xyz/articles/linux/debian/install/","title":"debian的安装"},{"content":" 目录 操作备忘录 重复的威力 光标移动 插入模式 插入模式的命令 自动补全 文本编辑 文本对象 移动文本 文字排版 复制粘贴 撤销与恢复 查找替换 可视模式 注释命令 打开文件 保存退出 文件比较 文件操作 缓冲区 分屏窗口 标签页 Vim书签 文件浏览器 拼写检查 代码折叠 文档加解密 宏录制 其它命令 历史命令 寄存器 配置文件 常用插件 Vim模式 外部命令 GUI命令 自动命令 快速修复窗口 文件编码 帮助信息 有点意思 约定规范 按键说明 网络资源 使用建议 Vim键盘图 其他图 正则表达式的模式 Ctrl按键 Ctrl-X模式 操作备忘录 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 37 38 39 40 41 42 43 44 45 46 47 48 \u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34; \u0026#34; 操作备忘录 \u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34; \u0026#34; 0、vim -u NONE \u0026lt;file\u0026gt; , 不加载vimrc的配置打开vim, 测试干净环境下的vim \u0026#34; 1、删除（不剪切），不将其剪切内容放入默认寄存器，而是直接丢进黑洞寄存器 \u0026#34; \u0026lt;press==\u0026gt; \u0026#34;_dd \u0026#34; 2、放到无名寄存器(\u0026#34;)(vim默认的),在vim默认寄存器为系统剪切板时有用,不复制到系统剪切板,但是还能通过\u0026#34;\u0026#34;p找回 \u0026#34; \u0026lt;press==\u0026gt; \u0026#34;\u0026#34;dd \u0026#34; 3、删除并进入插入模式: s===cl===dli \u0026#34; 4、* : 向后搜索光标所在的单词并居中显示结果; # : 向前搜索光标所在的单词并居中显示结果 \u0026#34; 5、插入模式,\u0026lt;Ctrl-o\u0026gt;,临时执行一个普通模式命令，然后自动返回插入模式 \u0026#34; 6、gv 重选上次的选区 \u0026#34; 7、J 将多行合并为一行,行与行之间会加空格; gJ 将多行合并为一行，行与行之间不加空格 \u0026#34; 8、在一些情况下,normal模式使用不了,如:tab terminal ; 可以用: \u0026lt;C-w\u0026gt;N 或者 \u0026lt;C-\\\u0026gt;\u0026lt;C-n\u0026gt; 切回normal模式 \u0026#34; 9、插入上一次插入的文本(i文本`esc`a`C-a`) \u0026#34; 10、用\u0026lt;.\u0026gt;键可以重复上次的命令(编辑) \u0026#34; 11、选区上下移动 :\u0026lt;C-u\u0026gt;\u0026#39;\u0026lt;,\u0026#39;\u0026gt;m \u0026#39;\u0026gt;+1\u0026lt;CR\u0026gt;gv 整体向下移动一行; :\u0026lt;C-u\u0026gt;\u0026#39;\u0026lt;,\u0026#39;\u0026gt;m \u0026#39;\u0026lt;-2\u0026lt;CR\u0026gt;gv 整体向上移动一行 \u0026#34; 12、g\u0026#39; g` 标签跳转时的反引号和引号命令,前面加上g,可以不改变jumplist,即不改变跳转列表的顺序和位置 \u0026#34; 13、:h[elp] cursorcolumn 查看文档; :set cursorcolumn? 查看当前环境的某项配置的值 \u0026#34; 14、:verbose set cursorcolumn? 查看当前环境的某项配置的值,更详细,看到是在哪里被定义修改的 \u0026#34; 15、:map \u0026lt;C-c\u0026gt; :imap \u0026lt;C-c\u0026gt; :verbose map \u0026lt;C-c\u0026gt; :verbose imap \u0026lt;C-c\u0026gt; 查看按键映射; \u0026#34; 16、:echo hasmapto(\u0026#39;\u0026lt;Plug\u0026gt;Sneak_S\u0026#39;, \u0026#39;v\u0026#39;) 执行某个函数并打印值,hasmapto是vim自带的检查映射的函数; \u0026#34; 17、:call fun() 执行某个函数 \u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34; 代码折叠命令： ( close==创建折叠 )!= (open==展开折叠) \u0026#34; za 切换当前光标下的折叠的状态(open或close) == zc+zo \u0026#34; zA 递归切换当前折叠及其嵌套折叠的状态 == zC+zO \u0026#34; zr 减少折叠级别 \u0026#34; zm 增加折叠级别 \u0026#34; zj 定位到下一个折叠处 \u0026#34; zk 定位到上一个折叠处 \u0026#34; \u0026#34; zc 创建一个光标下的折叠(close)(从内向外) \u0026#34; zC 递归创建折叠及嵌套折叠(从内向外) \u0026#34; zo 展开一个光标下的折叠(open)(从外向内) \u0026#34; zO 递归打开当前折叠及其嵌套折叠,大写的字母O (从外向内) \u0026#34; zf 对所指定的文本范围进行折叠,只支持manual和marker \u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34; \u0026#34; 宏命令 \u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34; \u0026#34;-----------------{{{----------------- \u0026#34; A. 常规使用宏的流程： \u0026#34; 1. q 进入宏录制 + 寄存器字母(a-z) \u0026#34; 2. 录制宏内容 \u0026#34; 3. q 退出宏录制 \u0026#34; 4. @a-z 使用字母指定的宏 \u0026#34; 5. @@ 重复最近使用过宏 \u0026#34; ------------------------------------------------------------------------------- Copied! 重复的威力 1 2 . # 小数点，即重复（Dot）命令，重复执行上一次命令 N{command} # 重复某个命令 N 次，例如：10k，光标上移 10 行 Copied! 善用宏和正则表达式，同样可以达到减少重复操作的目的。\nnormal模式下键入5a文本\u0026lt;ESC\u0026gt; 会插入5次文本 光标移动 注意：普通（Normal）模式下，任意一个动作都可以重复。\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 # -------------------- 单位级移动 -------------------- h # 光标左移，等价于 \u0026lt;Left\u0026gt; 方向键（h 键位于左边，按该键光标左移） j # 光标下移，等价于 \u0026lt;Down\u0026gt; 方向键（j 键有向下的突起，按该键光标下移） k # 光标上移，等价于 \u0026lt;Up\u0026gt; 方向键 （k 键与 j 键相反，按该键光标上移） l # 光标右移，等价于 \u0026lt;Right\u0026gt; 方向键（l 键位于右边，按下该键光标向右移动） # -------------------- 单词级移动 -------------------- w # 移动到下一个标点或空格分隔的单词开头（w: word） W # 移动到下一个空格分隔的单词开头（W: Word） e # 移动到下一个标点或空格分隔的单词尾部（e: end） E # 移动到下一个空格分隔的单词尾部（E: End） b # 移动到上一个标点或空格分隔的单词开头（b: backward） B # 移动到上一个空格分隔的单词开头（B: Backward） # -------------------- 块级移动 ---------------------- 0 # 跳到行首，数字 0，等价于 \u0026lt;Home\u0026gt; 起始键 ^ # 跳到行首非空字符，可以使用 0w 代替 ^，按键更方便 $ # 跳到行尾，等价于 \u0026lt;End\u0026gt; 结尾键 ge # 向后移动到单词词尾 gE # 向后移动到空白隔开的单词词尾 gg # 跳到第一行，等价于 Ctrl+\u0026lt;Home\u0026gt; G # 跳到最后一行，等价于 Ctrl+\u0026lt;End\u0026gt; [N]G # 跳到第 N 行，例如 10G 是移动到第 10 行 :N # 跳到第 N 行，例如 :10\u0026lt;Enter\u0026gt; 是移动到第 10 行 N| # 移动到当前行的 N 列 {count}% # 移动到文件百分之 {count} 的位置，例如 10% 是移动到文件 10% 的位置 \u0026lt;Enter\u0026gt; # 移动到下一行首个非空字符 N\u0026lt;Enter\u0026gt; # 光标向下移动 N 行 # 句子的结尾是由句号 (.)、问号 (?) 或感叹号 (!) 后跟一个或多个空格、制表符或换行符标识的;没有分隔符时,不会拆分成不同的句子 ) # 移动到下一个句子的开头 ( # 移动到[上一个句子/这个句子] 的开头 } # 移动到下一个段落（空行分隔） { # 移动到上一个段落（空行分隔） + # 移动到下一行首个非空字符，等价于 \u0026lt;Enter\u0026gt; 回车键 - # 移动到上一行首个非空字符 H # 移动到屏幕上部（H: High） M # 移动到屏幕中部（M: Middle） L # 移动到屏幕下部（L: Low） gm # 移动到的行中间 gj # 光标向下移动一个屏幕行，非实际行，忽略自动换行 gk # 光标向上移动一个屏幕行，非实际行，忽略自动换行 \u0026lt;S+Up\u0026gt; # 按住 \u0026lt;Shift\u0026gt; 上档键再按 \u0026lt;Up\u0026gt; 方向键，向上翻页 \u0026lt;S+Down\u0026gt; # 按住 \u0026lt;Shift\u0026gt; 上档键再按 \u0026lt;Down\u0026gt; 方向键，向下翻页 \u0026lt;S+Left\u0026gt; # 按住 \u0026lt;Shift\u0026gt; 上档键再按 \u0026lt;Left\u0026gt; 方向键，向左移动一个单词 \u0026lt;S+Right\u0026gt; # 按住 \u0026lt;Shift\u0026gt; 上档键再按 \u0026lt;Right\u0026gt; 方向键，向右移动一个单词 :ju[mps] # 输出所有跳转 :cle[arjumps] # 清除所有跳转 # -------------------- 翻屏移动 ---------------------- zz # 调整光标所在行到屏幕中央 zt # 调整光标所在行到屏幕上部 zb # 调整光标所在行到屏幕下部 Ctrl+e # 向上滚动一行（e: extra line） Ctrl+y # 向下滚动一行 Ctrl+u # 向上滚动半屏（Move up 1/2 a screen） Ctrl+d # 向下滚动半屏（Move down 1/2 a screen） Ctrl+f # 向下滚动一屏（Move forward one full screen） Ctrl+b # 向上滚动一屏（Move back one full screen） # -------------------- 编程辅助移动 ------------------ % # 不仅匹配跳转到对应的 {} () []，而且能在 if、else、elseif 之间跳跃 gd # 跳转到局部变量定义处，即光标下的单词的定义 gD # 跳转到全局变量定义处，即光标下的单词的定义 gf # 打开名称为光标下文件名的文件 [m # 跳转到上一个成员函数开头(光标移动到{那里) ]m # 跳转到下一个成员函数开头(光标移动到{那里) ]M # 跳转到下一个成员函数结尾(光标移动到}那里) [M # 跳转到上一个成员函数结尾(光标移动到}那里) [{ # 跳转到上一处未匹配的 {,从代码块内部向外找{ ,平级的不会找 下面3个同理 ]} # 跳转到下一处未匹配的 },从代码块内部向外找} [( # 跳转到上一处未匹配的 (,从代码块内部向外找( ]) # 跳转到下一处未匹配的 ),从代码块内部向外找) [c # 跳转到上一个不同处（diff 时） ]c # 跳转到下一个不同处（diff 时） [[ # 跳转到上一个行首是{的那一行 ]] # 跳转到下一个行首是{的那一行,这两个都没啥用 [] # 跳转到上一个行首是}的那一行 ][ # 跳转到下一个行首是}的那一行 ########## 自定义: ################# # ]] 跳转到下一个{ # [[ 跳转到上一个{ # ][ 跳转到下一个},]开头都是下一个 # [] 跳转到上一个},[开头都是上一个 ################################## [/ # 跳转到当前/上一个注释块(/*)开始处 [* # 同上 ]/ # 跳转到当前/下一个注释块(*/)结尾处 ]* # 同上 ]# # 跳转到下一个 #if 或者 #else 处,只在C语言里有用 [# # 跳转到上一个 #else 或者 #endif 处,只在C语言里有用 /pattern # 从光标处向文件尾搜索 pattern ?pattern # 从光标处向文件头搜索 pattern n # 向同一方向执行上一次搜索 N # 向相反方向执行上一次搜索 % # 匹配括号移动，包括 ()，{}，[]。结合以下两个命令相当强大。前提：需要把光标先移到括号上 * # 向后搜索光标所在的单词 # # 向前搜索光标所在的单词 \u0026lt;Shift\u0026gt;* # 搜索光标所在位置的字符串，不用输入字符串，查询速度比 /pattern 快 f{char} # 向后搜索当前行第一个为 {char} 的字符，Nfv 可以找到第 N 个为 v 字符，下同（f: find） F{char} # 向前搜索当前行第一个为 {char} 的字符 t{char} # 向后搜索当前行第一个为 {char} 的字符前（t: to） T{char} # 向前搜索当前行第一个为 {char} 的字符前 ; # 重复上次的字符查找命令（f/t 命令） , # 反转方向查找上次的字符查找命令（f/t 命令） tx # 搜索当前行到指定 字符串 之前 fx # 搜索当前行到指定 字符串 之处 \u0026lt;Esc\u0026gt; # 放弃查找。例如，启动了 f 命令后发现想用的是 F 命令，\u0026lt;Esc\u0026gt; 退出键放弃查找 Copied! 注意：块级 移动要善于使用 Vim 的书签和标签页功能，实现文件内容及文件之间快速移动。\n插入模式 1 2 3 4 5 6 7 8 9 10 11 12 13 i # 在光标处进入插入模式（i: insert） I # 在行首进入插入模式 a # 在光标后进入插入模式（a: append） A # 在行尾进入插入模式 o # 在下一行插入新行并进入插入模式 O # 在上一行插入新行并进入插入模式 s # 删除光标所在的字符并进入插入模式 S # 删除当前行并插入文本 gi # 进入到上一次插入模式的位置 gI # 在当前行第 1 列插入 \u0026lt;Esc\u0026gt; # 退出插入模式 Ctrl+[ # 退出插入模式，等价于 \u0026lt;Esc\u0026gt; 退出键 Ctrl+C # 退出插入模式，等价于 \u0026lt;Esc\u0026gt; 和 Ctrl+[，但不检查缩写 Copied! 插入模式的命令 注意：由 i, I, a, A, o, O, s, S 等命令进入插入模式。\n1 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 \u0026lt;Up\u0026gt; # 光标向上移动 \u0026lt;Down\u0026gt; # 光标向下移动 \u0026lt;Left\u0026gt; # 光标向左移动 \u0026lt;Right\u0026gt; # 光标向右移动 \u0026lt;S+Up\u0026gt; # 按住 \u0026lt;Shift\u0026gt; 上档键再按 \u0026lt;Up\u0026gt; 方向键，向上翻页 \u0026lt;S+Down\u0026gt; # 按住 \u0026lt;Shift\u0026gt; 上档键再按 \u0026lt;Down\u0026gt; 方向键，向下翻页 \u0026lt;S+Left\u0026gt; # 按住 \u0026lt;Shift\u0026gt; 上档键再按 \u0026lt;Left\u0026gt; 方向键，向左移动一个单词 \u0026lt;S+Right\u0026gt; # 按住 \u0026lt;Shift\u0026gt; 上档键再按 \u0026lt;Right\u0026gt; 方向键，向右移动一个单词 \u0026lt;PageUp\u0026gt; # 向上翻页，\u0026lt;PageUp\u0026gt; 是向上翻页键 \u0026lt;PageDown\u0026gt; # 向下翻页，\u0026lt;PageDown\u0026gt; 是向下翻页键 \u0026lt;Delete\u0026gt; # 删除光标处字符，\u0026lt;Delete\u0026gt; 是删除键 \u0026lt;Backspace\u0026gt; # 退格键 \u0026lt;Backspace\u0026gt; 向后删除字符 \u0026lt;Home\u0026gt; # 光标跳转行首 \u0026lt;End\u0026gt; # 光标跳转行尾 Ctrl+d # 减少缩进光标所在行 Ctrl+f # 自动缩进光标所在行（相当于普通模式下的 `==` ） Ctrl+t # 增加缩进光标所在行 Ctrl+h # 删除前一个字符，等价于 \u0026lt;Backspace\u0026gt; 退格键 Ctrl+o # 临时退出插入模式，执行单条命令又返回插入模式 Ctrl+u # 当前行删除到行首所有字符 Ctrl+w # 删除光标前的一个单词 Ctrl+\\ Ctrl+O # 临时退出插入模式（光标保持），执行单条命令又返回插入模式 Ctrl+R 0 # 插入寄存器（内部 0 号剪贴板）内容，Ctrl+R 后可跟寄存器名 Ctrl+R \u0026#34; # 插入匿名寄存器内容，相当于插入模式下 p 粘贴 Ctrl+R = # 插入表达式计算结果，等号后面跟表达式 Ctrl+R : # 插入上一次命令行命令 Ctrl+R / # 插入上一次搜索的关键字 Ctrl+v {char} # 插入非数字的字面量 Ctrl+v {code} # 插入用三位数字表示的 ASCII/Unicode 字符编码，例如 Ctrl+v 065 Ctrl+v 065 # 插入 10 进制 ASCII 字符（两数字） 065 即 A 字符 Ctrl+v x41 # 插入 16 进制 ASCII 字符（三数字） x41 即 A 字符 Ctrl+v o101 # 插入 8 进制 ASCII 字符（三数字） o101 即 A 字符 Ctrl+v u1234 # 插入 16 进制 Unicode 字符（四数字） Ctrl+v U12345678 # 插入 16 进制 Unicode 字符（八数字） Ctrl+K {ch1} {ch2} # 插入 digraph（见 :h digraph），快速输入日文或符号等 Copied! 自动补全 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Ctrl+n # 插入模式下文字自动补全，最常用的补全 Ctrl+P # 插入模式下文字自动补全 Ctrl+e # 有补全列表时，终止这次补全，继续输入 Ctrl+X # 进入补全模式，注意：智能补全命令均以组合键 Ctrl+X 作为起始操作，下同 Ctrl+X Ctrl+L # 补全整行 Ctrl+X Ctrl+N # 插入模式下根据当前缓冲区关键字补全 Ctrl+X Ctrl+K # 根据字典补全 Ctrl+X Ctrl+T # 根据同义词字典补全 Ctrl+X Ctrl+F # 插入模式下补全文件名 Ctrl+X Ctrl+I # 根据头文件内关键字补全 Ctrl+X Ctrl+] # 标签文件关键词补全 Ctrl+X Ctrl+D # 补全宏定义 Ctrl+X Ctrl+V # 补全 Vim 命令 Ctrl+X Ctrl+U # 用户自定义补全方式 Ctrl+X Ctrl+S # 拼写建议，例如：一个英文单词 Ctrl+X Ctrl+O # 插入模式下全能 Omnifunc 补全 Copied! 文本编辑 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 r # 替换当前字符（r: replace） R # 进入替换模式，直至按 \u0026lt;Esc\u0026gt; 退出键离开 [N]s # 替换 N 个字符，即删除光标后 N 个字符并进入插入模式 [N]S # 替换 N 行，即删除 N 行并进入插入模式 [N]x # 剪切、删除光标右边 N 个字符，相当于d[N]l [n]X # 剪切、删除光标左边 N 个字符，相当于d[n]h cc # 改写当前行，即删除当前行并进入插入模式，等价于 S cw # 改写光标开始处的当前单词 ciw # 改写光标所处的单词 caw # 改写光标所处的单词，并且包括前后空格 c0 # 改写到行首 c^ # 改写到行首非空字符 c$ # 改写到行末 C # 改写到行尾，等价于 c$ ci\u0026#34; # 改写双引号中的内容，i 的含义下同（i: inner） ci\u0026#39; # 改写单引号中的内容 cib # 改写小括号中的内容 cab # 改写小括号中的内容，包含小括号本身 ci) # 改写小括号中的内容 ci] # 改写中括号中内容 ciB # 改写大括号中内容 caB # 改写大括号中的内容，包含大括号本身 ci} # 改写大括号中内容 cit # 改写 XML 中 tag 的内容 cis # 改写当前句子 c[N]w # 改写光标后 N 个单词 c[N]l # 改写光标后 N 个字母 c[N]h # 改写光标前 N 个字母 [N]cc # 修改当前 N 行 ct( # 改写到小括号前 dd # 删除（剪切）当前行，当前行会存到寄存器里（d: delete = cut） dd[N]p # 删除（剪切）当前行并加入 N-1 个当前行，复制空行时很有用 d0 # 删除（剪切）到行首 d^ # 删除（剪切）到行首非零字符 d$ # 删除（剪切）到行末 D # 删除（剪切）到行末，等价于 d$ dw # 删除（剪切）当前单词 diw # 删除（剪切）光标所处的单词（iw: inner word） daw # 删除（剪切）光标所处的单词，并包含前后空格 d2w # 删除（剪切）下 2 个单词 d[N]w # 删除（剪切）N 个单词，并包含前后空格 d[N]l # 删除（剪切）光标右边 N 个字符 d[N]h # 删除（剪切）光标左边 N 个字符 [N]dd # 删除（剪切）从当前行开始的 N 行 :Nd # 删除（剪切）第 N 行 :N,Md\u0026lt;CR\u0026gt; # 删除（剪切） N ~ M 行，其中 \u0026lt;CR\u0026gt; 为 \u0026lt;Enter\u0026gt; 回车键 di\u0026#34; # 删除（剪切）双引号中的内容 di\u0026#39; # 删除（剪切）单引号中的内容 dib # 删除（剪切）小括号中的内容 di) # 删除（剪切）小括号中的内容 dab # 删除（剪切）小括号内的内容，包含小括号本身 di] # 删除（剪切）中括号中内容 diB # 删除（剪切）大括号中内容 di} # 删除（剪切）大括号中内容 daB # 删除（剪切）大括号内的内容，包含大括号本身 dit # 删除（剪切） XML 中 tag 的内容 dis # 删除（剪切）当前句子 dt( # 删除（剪切）到小括号前 dgg # 删除（剪切）到文件头部 d1G # 删除（剪切）到文件头部，同上 dG # 删除（剪切）到文件尾部 d} # 删除（剪切）下一个段落 d{ # 删除（剪切）上一个段落 d/f\u0026lt;CR\u0026gt; # 比较高级的组合命令，它将删除 当前位置 到下一个字母 f 之间的内容，其中 \u0026lt;CR\u0026gt; 为 \u0026lt;Enter\u0026gt; 回车键 ~ # 转换大小写 g~iw # 替换当前单词的大小写 gUiw # 将单词转成大写 guiw # 将当前单词转成小写 guu # 全行转为小写 gUU # 全行转为大写 Ctrl+A # 增加数字 Ctrl+X # 减少数字 Copied! 文本对象 注意：只适用于可视模式或在操作符后，例如：操作包括 选择 v、删除 d、复制 y、修改 c 等。\n1 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 aw # 操作整个单词，不包括分隔符（aw: a word） aW # 操作整个单词，包括分隔符（aW: a Word） iw # 操作整个单词，不包括分隔符（iw: inner word） iW # 操作整个单词，包括分隔符（iW: inner Word） is # 操作整个句子，不包括分隔符 (s: sentence） ib # 操作内含块，从 [( 到 ])（b: block） iB # 操作内含大块，从 [{ 到 ]} （B: Block） ab # 操作一个块，从 [( 至 ])（b: block） aB # 操作一个大块，从 [{ 到 ]}（B: Block） ap # 操作一个段落（p: paragraph） ip # 操作内含段落 i) # 操作小括号字符串 a) # 操作小括号字符串，包含小括号本身 i] # 操作中括号字符串 a] # 操作中括号字符串，包含中括号本身 i} # 操作大括号字符串 a} # 操作大括号字符串，包含大括号本身 i\u0026#39; # 操作单引号字符串 a\u0026#39; # 操作单引号字符串，包含单引号本身 i\u0026#34; # 操作双引号字符串 a\u0026#34; # 操作双引号字符串，包含双引号本身 a` # 操作一个反引号字符串 i` # 操作内含反引号字符串 a\u0026gt; # 操作一个 \u0026lt;\u0026gt; 块 i\u0026gt; # 操作内含 \u0026lt;\u0026gt; 块 at # 操作一个标签块，例如 从 \u0026lt;aaa\u0026gt; 到 \u0026lt;/aaa\u0026gt;（t: tag） it # 操作内含标签块，例如 从 \u0026lt;aaa\u0026gt; 到 \u0026lt;/aaa\u0026gt; 2i) # 操作往外两层小括号内 2a) # 操作往外两层小括号内，包含小括号本身 [N]f) # 移动到第 N 个小括号处 [N]t) # 移动到第 N 个小括号前 Copied! 文本对象的配对括号、标点及配对标点内的内容的编辑修改对编程非常实用，可以简单总结为。\n1 2 3 4 5 6 7 8 ci\u0026#39;、ci\u0026#34;、ci(、ci[、ci{、ci\u0026lt; # 分别修改这些配对标点符号中的文本内容 ca\u0026#39;、ca\u0026#34;、ca(、ca[、ca{、ca\u0026lt; # 分别修改这些配对标点符号中的文本内容，包括 标点符号 本身 di\u0026#39;、di\u0026#34;、di(、dib、di[、di{、diB、di\u0026lt; # 分别删除这些配对标点符号中的文本内容 da\u0026#39;、da\u0026#34;、da(、dab、da[、da{、daB、da\u0026lt; # 分别删除这些配对标点符号中的文本内容，包括 标点符号 本身 yi\u0026#39;、yi\u0026#34;、yi(、yi[、yi{、yi\u0026lt; # 分别复制这些配对标点符号中的文本内容 ya\u0026#39;、ya\u0026#34;、ya(、ya[、ya{、ya\u0026lt; # 分别复制这些配对标点符号中的文本内容，包括 标点符号 本身 vi\u0026#39;、vi\u0026#34;、vi(、vi[、vi{、vi\u0026lt; # 分别选中这些配对标点符号中的文本内容 va\u0026#39;、va\u0026#34;、va(、va[、va{、va\u0026lt; # 分别选中这些配对标点符号中的文本内容，包括 标点符号 本身 Copied! 移动文本 移动文本命令格式。\n1 :[range]m[ove]{address} Copied! 参数说明：\n[range]：表示要移动的行范围。 {address}：表示移动的目标位置，这两个参数都可以缺省。 例如：\n1 2 3 4 5 6 7 :m+1 # 下移 1 行 :m-2 # 上移 1 行 :8,10m2 # 把当前打开文件的第 8~10 行内容移动到第 2 行下方 :8,10m+2 # 把当前打开文件的第 8~10 行内容移动到光标下面第 2 行下方 ## 选区上下移动 :\u0026lt;C-u\u0026gt;\u0026#39;\u0026lt;,\u0026#39;\u0026gt;m \u0026#39;\u0026gt;+1\u0026lt;CR\u0026gt;gv # 整体向下移动一行; :\u0026lt;C-u\u0026gt;\u0026#39;\u0026lt;,\u0026#39;\u0026gt;m \u0026#39;\u0026lt;-2\u0026lt;CR\u0026gt;gv # 整体向上移动一行 Copied! 文字排版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [N]\u0026gt;\u0026gt; # 向右缩进 N 行，单位为 shiftwidth [N]\u0026lt;\u0026lt; # 向左缩进 N 行，单位为 shiftwidth :ce[nter] # 本行文字居中 :le[ft] # 本行文字靠左 :ri[ght] # 本行文字靠右 :[range]ce[nter] [width] # 在 range 范围行的文字居中 :[range]le[ft] [indent] # 在 range 范围行的行文字靠左 :[range]ri[ght] [width] # 在 range 范围行的行文字靠右 gq # 对选中的文字重排，即对过长文字进行断行 gqq # 重排当前行 gq[N]q # 重排 N 行 gqap # 重排当前段落 gq[N]ap # 重排 N 个段落 gq[N]j # 重排当前行和下面 N 行 gqQ # 重排当前段落到文章末尾 J # 将多行合并为一行,行与行之间会加空格 gJ # 将多行合并为一行，行与行之间不加空格 == # 自动缩进，当前文件所有行自动缩进对齐使用 gg=G Copied! 复制粘贴 复制命令的格式。\n1 :[range]co[py]{address} Copied! 参数说明：\n[range]：表示要复制的行范围，其中 copy 可缩写为 :co 或 :t。 {address}：表示复制的目标位置，这两个参数都可以缺省，用于表示 Vim 光标所在当前行。 例如：\n1 2 3 4 5 6 :3copy. # 复制文件的第 3 行到当前行（当前行用 . 表示） :3,5t. # 把第 3 行到第 5 行的内容复制到当前行下方 :t5 # 把当前行复制到第 5 行下方 :t. # 复制当前行到当前行下方，等价于普通模式下的 yyp :t$ # 把当前行复制到文本结尾 :\u0026#39;\u0026lt;,\u0026#39;\u0026gt;t0 # 把高亮选中的行复制到文件开头 Copied! 常用复制粘贴命令。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 p # 粘贴到光标后（p: paste） P # 粘贴到光标前 y # 复制标记内容 y0 # 复制当前光标位置到行首的内容 y$ # 复制当前位置到本行结束的内容 yy # 复制当前行（Yank (copy) a line ） Y # 复制当前行，等价于 yy yiw # 复制当前单词 y[N]w # 复制 N 个单词 y[N]s # 复制 N 个句子 [N]yy # 复制光标下 N 行内容 ygg # 复制光标以上的所有行 y1G # 复制光标以上的所有行，同上 yG # 复制光标以下的所有行 yypVr{char} # 复制字符并替换为等长指定字符，Markdown 编辑时尤为好用 :[range]y # 复制范围，例如 :20,30y 是复制 20 到 30 行，:10y 是复制第 10 行 :[range]d # 删除范围，例如 :20,30d 是删除（剪切） 20 到 30 行，:10d 是删除（剪切）第 10 行 \u0026#34;_[command] # 使用 [command] 删除内容，并且不进行复制（不会污染寄存器） \u0026#34;*[command] # 使用 [command] 复制内容到系统剪贴板（需要 Vim 版本有 clipboard 支持） Copied! 撤销与恢复 1 2 3 4 5 6 [N]u # 撤销命令，N 为任意整数，表示撤销 N 步操作，下同（u: undo） [N]U # 撤销整行操作，N 为任意整数 Ctrl+r # 撤销上一次 u 命令（r: redo） Ctrl+R # 回退前一个命令 :earlier {N}s # 回退到 N 秒前的文件内容，其中 s 可替换为 m（分）、h（小时）、d（天） :later {N}s # 前进 N 秒，其中 s 可替换为 m（分）、h（小时）、d（天） Copied! 查找替换 查找命令 普通（Normal）模式下的查找命令（注意：Esc 退出键可以中止大部分命令）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /pattern # 从光标处向文件尾搜索 pattern ?pattern # 从光标处向文件头搜索 pattern n # 向同一方向执行上一次搜索 N # 向相反方向执行上一次搜索 % # 匹配括号移动，包括 ()，{}，[]。结合以下两个命令相当强大。前提：需要把光标先移到括号上 * # 向后搜索光标所在的单词 g*\t# 与*类似,但是不严格按照单词来分界;`aaa bbb`的aaa上用g* 能搜索到 aaaacccc # # 向前搜索光标所在的单词 g# # 与#类似,但是不严格按照单词来分界;`aaa bbb`的aaa上用g* 能搜索到 aaaacccc \u0026lt;Shift\u0026gt;* # 搜索光标所在位置的字符串，不用输入字符串，查询速度比 /pattern 快 f{char} # 向后搜索当前行第一个为 {char} 的字符，Nfv 可以找到第 N 个为 v 字符，下同（f: find） F{char} # 向前搜索当前行第一个为 {char} 的字符 t{char} # 向后搜索当前行第一个为 {char} 的字符前（t: to） T{char} # 向前搜索当前行第一个为 {char} 的字符前 ; # 重复上次的字符查找命令（f/t 命令） , # 反转方向查找上次的字符查找命令（f/t 命令） tx # 搜索当前行到指定 字符串 之前 fx # 搜索当前行到指定 字符串 之处 \u0026lt;Esc\u0026gt; # 放弃查找。例如，启动了 f 命令后发现想用的是 F 命令，\u0026lt;Esc\u0026gt; 退出键放弃查找 Copied! substitute 命令替换 普通（Normal）模式下的替换命令格式。\n1 :[range]s[ubstitute]/{pattern}/{string}/[flags] Copied! 参数说明：\n{pattern}：就是要被替换掉的字串，可以用 regexp 來表示。 {string}：將 pattern 由 string 所取代。 [range]：有以下一些取值。 [range]取值 含义 无 默认光标所在行 . 光标所在当前行 N 第 N 行 $ 最后一行 \u0026lsquo;a 标记 a 所在的行（之前要用 ma 做过标记） $-1 倒数第二行，可以对某一行加减某个数值获得确定的某行 1,10 第 1~10 行 1,$ 第一行到最后一行 1,. 第一行到当前行 .,$ 当前行到最后一行 \u0026lsquo;a,\u0026lsquo;b 标记 a 所在的行 到 标记 b 所在的行（之前要用 ma、mb 做过标记） % 所有行（和 1，$ 等价） ?str? 从当前位置向上搜索，找到第一个 str 的行（str 可以是正则） /str/ 从当前位置向下搜索，找到第一个 str 的行（str 可以是正则） 注意：上面的所有用于 range 的表示方法都可以通过 +、- 操作来设置相对偏移量。\n[flags]有以下一些取值： [flags]取值 含义 g 对指定范围内的所有匹配项（g: global）进行替换 c 在替换前请求用户进行确认（c: confirm） e 忽略执行过程中的错误（e: error） i 不区分大小写（i: ignore） 无 只在指定范围内的第一个匹配项进行替换 举例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 g\u0026amp; # 重复上一次 substitute 命令 :s/old/new/ # 当前行的第一个 old 替换为 new :s/old/new/g # 当前行的 old 全部替换为 new :s/old/\\U\u0026amp;/ # 当前行的 old 替换为大写的 OLD :N,Ms/old/new/g # 将第 N~M 行中所有的 old 全部替换为 new :%s/old/new/g # 当前文件中的 old 全部替换为 new :%s/old/new/gc # 将当前文件中的 old 全部替换为 new，并且每处询问你是否替换 :%s/^/xxx/g # 在每行行首插入 xxx，^ 表示行首，注释时非常有用 :%s/./# \u0026amp; # 在非空行的行首添加注释，\u0026amp; 代表前边匹配到非空行字符 :%s/$/xxx/g # 在每行行尾插入 xxx，$ 表示行尾 :%s/hello/\u0026amp;, world/ # 将会把 hello 替换成 hello, wolrd :%s/.*/(\u0026amp;)/ # 将会把所有行用 () 包含起来 :%s/\\s\\+$//e # 删除每行末尾的空格 :%s/1\\\\2\\/3/123/g # 将 \u0026#34;1\\2/3\u0026#34; 替换为 \u0026#34;123\u0026#34;，特殊字符使用反斜杠标注 :%s/\\r//g # 删除 DOS 换行符 ^M :%s///gn # 统计某个模式的匹配个数 :%s/^\\n$//gc/ # 替换多个空行为一个空行 :%s/\\n/\\r\\r/ # 每行后加入空行 :%s/^\\s*$\\n//g # 删除所有空白行 :%s/^M$//g # 删除文件中显式的 ^M 符号，即操作系统换行符问题 :%s/_\\(\\w\\)/\\u\\1/g # 将下划线转为驼峰式写法 :%s/^\\(\\w\\)/\\L\\1/g # 将首字母大写的切换成小写 :h[elp] s[ubstitute] # 查看 substitute 替换命令的帮助文档 Copied! global 命令替换 还有一种比替换更灵活的方式，它允许匹配到某个指定模式后执行指定的 Ex 命令，即 Vim 的 global 命令，其处理重复工作的效率极高。其命令格式为：\n1 :[range]g[lobal][!]/{pattern}/[cmd] Copied! 参数说明：\n[range]：表示操作范围，:global 命令的默认作用范围是整个文件 (用 % 表示)。 操作范围参考上面的 [range] 取值。 [!]：表示反转 :global 命令的行为，将在没有匹配到指定模式的行上执行 cmd。 {pattern}：指定 :global 命令要匹配的目标模式，若将该域留空，Vim 会自动使用当前(最近一次)的查找模式。 [cmd]：除 :global 命令之外的任何 Ex 命令，Vim 缺省使用 :print 命令，缩写为 :p。 例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 :g/pattern # 查找并显示文件中所有包含模式 pattern 的行，并移动到最后一个匹配处 :g/pattern/p # 查找并显示文件中所有包含模式 pattern 的行 :g/\\\u0026lt;pattern\\\u0026gt;/p # 查找并显示文件中所有精确匹配单词 pattern 的行 :g!/pattern/nu # 查找并显示文件中所有不包含模式 pattern 的行，并显示这些行号 :v/pattern/d # 删除所有不包含 pattern 的行 :g/.*/m0 # 将所有的行按相反的顺序排列。其中，查找模式 .* 将匹配所有行，m0 命令将每一行移动到 0 行之后 :g/^/t. # 重复每一行，其中 :t 或 :copy 为复制命令 :g/^/+1 d # 删除偶数行 :g/^/d|m. # 删除奇数行 :g/^$/d # 删除所有空白行 :g/^\\s*$/d # 删除所有空白行 :v/./d # 删除所有空白行，其中 . 用于匹配除换行符 \\n 外的任何单字符 :g/pattern/d_ # 删除大量匹配行，避免花费不必要的时间拷贝匹配行至默认寄存器，可以指定黑洞寄存器 _ :N,Mg/pattern/p # 查找并显示第 N 到 M 行之间所有包含模式 pattern 的行 :%g/^ xyz/normal dd # 表示对于以一个空格和 xyz 开头的行，执行 Normal 模式下的 dd 命令 :g/.\\n\\n\\@!/norm o # 非空行每行后加入空行，且多个空行合并为一个空行，\\n 末尾匹配换行符，\\n\\@! 表示 \\n 紧接着 \\n，则匹配失败 :h[elp] :g[lobal] # 查看 global 命令的帮助文档 Copied! 特别说明：\nsubstitute 与 global 形式很相似，都是要进行查找匹配，但 substitute 执行的是替换，而 global 执行的其它命令。 global 命令实际上是分成两步执行：首先扫描 [range] 指定范围内的所有行，给匹配 {pattern} 的行打上标记；然后依次对打有标记的行执行 [cmd] 命令，如果被标记的行在对之前匹配行的命令操作中被删除、移动或合并，则其标记自动消失，而不对该行执行 [cmd] 命令。例子可以参考上面的奇数行、偶数行删除命令！ 正则替换 高级的查找替换要用到正则表达式。详情可以查看下面的 正则表达式 命令的帮助文档。\n1 :h[elp] pattern # 查看 正则表达式 的帮助文档获取更多信息 Copied! 注意：以下非 Vim 的命令，只代表正则表达式。\n1 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 # ---------------- 表示【目标字符集】的元字符 --------------- [a-z] # 表示匹配方括号中列举的任意一个字符，即匹配 a-z 中的任意一个字符 [^a-z] # 表示匹配除 方括号中字符以外 的任意字符 . # 等价于 [^\\n]，表示匹配 任意一个除换行符 ( \\n ) 外的其他字符 \\l # 等价于 [a-z]，表示匹配 任意一个小写字母 \\L # 等价于 [^a-z]，表示匹配 任意一个除小写字母外的其他字符 \\u # 等价于 [A-Z]，表示匹配 任意一个大写字母 \\U # 等价于 [^A-Z]，表示匹配 任意一个除大写字母外的其他字符 \\w # 等价于 [0-9A-Za-z_]，表示匹配 任意一个单词字母 \\W # 等价于 [^0-9A-Za-z_]，表示匹配 任意一个除单词字母外的其他字符 \\d # 等价于 [0-9]，表示匹配 任意一个阿拉伯数字 \\D # 等价于 [^0-9]，表示匹配 任意一个除阿拉伯数字外的其他字符 \\x # 等价于 [0-9A-Fa-f]，表示匹配 任意一个十六进制数字 \\X # 等价于 [^0-9A-Fa-f]，表示匹配 任意一个除十六进制数字外的其他字符 # ---------------- 表示【次数】的元字符 --------------------- * # 表示匹配 0 个或者任意个目标字符 \\+ # 表示匹配 1 个或者任意个目标字符 \\? # 表示匹配 0 个或者1个目标字符 \\{N,M} # 表示匹配 N~M 个目标字符，即最少匹配 N 个目标字符，最多匹配 M-1 个目标字符 \\{N} # 表示匹配 N 个目标字符，即目标字符需连续出现 N 次 \\{N,} # 表示匹配 N 任意个目标字符，即最少匹配 N 个目标字符 \\{,M} # 表示匹配 0~M 个目标字符，即最多匹配 M-1 个目标字符，也可以不匹配字符 # ---------------- 表示【位置】的元字符 --------------------- ^ # 表示匹配 输入字符串的开始位置 (行首) $ # 表示匹配 输入字符串的结束位置 (行尾) \\\u0026lt; # 表示匹配 单词词首 \\\u0026gt; # 表示匹配 单词词尾 # ---------------- 表示【非打印字符】的元字符 ---------------- \\n # 表示匹配 一个换行符 \\r # 表示匹配 一个回车符 Enter 键 \\t # 表示匹配 一个制表符 Tab 键 \\s # 表示匹配 任意一个空白字符，包括空格、制表符、换页符等 \\S # 表示匹配 任意一个非空白字符 # ---------------- 表示【子模式】的元字符 ------------------- () # 任何 () 内部的匹配文本被称为 子匹配，会被自动保存到临时的仓库中以便后续引用，可以用 \\1-9 来依次引用子匹配 Copied! Magic 模式 1 :h[elp] /magic # 查看更多 Magic 模式帮助信息 Copied! Vim 正则表达式可以分为四种模式。\nmagic 模式：使用 \\m 前缀，其后模式的解释方式为 \u0026lsquo;magic\u0026rsquo; 选项。^，$，.，* 和 [] 等字符含有特殊意义；而 +、?、()、和 {} 等其它字符则按字面意义解释。magic为默认设置，表达式中的\\m前缀可以省略。 no magic 模式：使用 \\M 前缀，其后模式的解释方式为 \u0026rsquo;nomagic\u0026rsquo; 选项。除了 ^ 和 $ 之外的特殊字符，都将被视为普通文本。 very magic 模式：使用 \\v 前缀，其后模式中除 \u0026lsquo;0\u0026rsquo;-\u0026lsquo;9\u0026rsquo;，\u0026lsquo;a\u0026rsquo;-\u0026lsquo;z\u0026rsquo;，\u0026lsquo;A\u0026rsquo;-\u0026lsquo;Z\u0026rsquo; 和 \u0026lsquo;_\u0026rsquo; 之外的字符都当作特殊字符解释。 very nomagic 模式：使用 \\V 前缀，其后模式中只有反斜杠（\\）具有特殊意义。 不同模式之间的区别，在于哪些特殊字符需要使用反斜杠（\\）进行转义。例如星号（*），在 magic 和 very magic 模式下视为特殊修饰符；而在 no magic 和 very nomagic 模式下则被视为普通字符，必须使用 “*” 恢复其特殊作用。\n可视模式 Vim 可视模式下允许选中一块文本区域，并对文本区域进行 复制 y、剪切 d、删除 d、替换 r、改变大小写 等操作。\n1 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 v # 切换到面向字符的可视模式（v: visual） V # 切换到面向行的可视模式 Ctrl+v # 切换到面向列块的可视模式 V\u0026gt; # 向右增加缩进 V\u0026lt; # 向左减少缩进 v0 # 选中当前位置到行首 v$ # 选中当前位置到行末 viw # 选中当前单词 vib # 选中小括号内的内容 vi) # 选中小括号内的内容 vi] # 选中中括号内的内容 viB # 选中大括号内的内容 vi} # 选中大括号内的内容 vis # 选中句子中的内容 vab # 选中小括号内的内容，包含小括号本身 va) # 选中小括号内的内容，包含小括号本身 va] # 选中中括号内的内容，包含中括号本身 vaB # 选中大括号内的内容，包含大括号本身 va} # 选中大括号内的内容，包含大括号本身 v[N]wd # 删除（剪切）选中的 N 个单词 v[N]wc # 修改高亮选中的 N 个单词，并进入插入模式 v[N]w~ # 高亮选中的 N 个单词转换大小写 {visual}o # 跳转到可视模式选中区域的另一端（o: other end） {visual}O # 跳转到可视模式选中区域的另一端 {visual}u # 标记区转换为小写 {visual}U # 标记区转换为大写 gv # 重选上次的高亮选区 g Ctrl+G # 显示所选择区域的统计信息 ggVG # 选择全文 \u0026lt;Esc\u0026gt; # 按 \u0026lt;Esc\u0026gt; 退出键退出可视模式 Copied! 注释命令 1 2 3 4 5 6 7 Ctrl+v # 多行注释 步骤1：进入命令行模式，按 Ctrl+v 进入可视模式，然后按 j 或者 k 字母键选中多行，把需要注释的行标记起来 I # 多行注释 步骤2：按大写字母 I 字母键，再插入注释符，例如 #、// \u0026lt;Esc\u0026gt; # 多行注释 步骤3：按 \u0026lt;Esc\u0026gt; 退出键就会全部注释了 Ctrl+v # 取消多行注释 步骤1：进入命令模式，按 Ctrl+v 进入可视模式，按 l 字母键横向选中列的个数，例如 #、//，需要选中 2 列 j or k # 取消多行注释 步骤2：按字母 j 或者 k 键移动选中注释符号 d # 取消多行注释 步骤3：按 d 字母键就可全部取消注释 Copied! 复杂注释。\n1 2 3 4 5 6 7 8 9 10 11 :N,M s/^/ 注释符 /g # 在指定行 N ~ M 的行首添加注释（注意冒号） :N,M s/^ 注释符 //g # 在指定行 N ~ M 的行首取消注释（注意冒号） :3,5 s/^/#/g # 注释第 3 ~ 5 行 :3,5 s/^#//g # 解除 3 ~ 5 行的注释 :1,$ s/^/#/g # 注释整个文档 :1,$ s/^#//g # 取消注释整个文档 :%s/^/#/g # 注释整个文档，此法更快 :%s/^#//g # 取消注释整个文档 Copied! 打开文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 vim [options] # 启动 Vim 并开启一个空白缓冲区 vim [options] {file} .. # 启动并编辑一个或多个文件 vim [options] - # 从标准输入读入文件 vim [options] -t {tag} # 编辑与标签 tag 关联的文件 vim [options] -q [fname] # 以快速修复模式开始编辑并显示首个错误。options 选项均参考以下 vim . # 打开文件管理器，显示目录文件，通过选中文件编辑 vim {file} # 打开或新建文件，置于光标第一行首 vim + {file} # 打开文件，置光标于最后一行首 vim +[N] {file} # 打开文件，置光标于第 N 行首 vim -b {file} # 以二进制模式打开文件，该模式某些特殊字符 如换行符 ^M 都可以显示出来 vim -v {file} # Vi 模式，以普通模式启动 Ex vim -e {file} # Ex 模式，以 Ex 模式启动 vim vim -r {file} # 恢复上次异常退出的文件 vim -R {file} # 以只读的方式打开文件，但仍然可以使用 :wq! 写入 vim -M {file} # 以只读的方式打开文件，不可以强制保存 :wq! 写入 vim -x {file} # 以加密方式打开文件 vim -p {files} # 打开多个文件，每个文件占用一个标签页 vim -o {files} # 在水平分割的多个窗口中编辑多个文件 vim -O {files} # 在垂直分割的多个窗口中编辑多个文件 vim -c cmd {file} # 在打开文件 file 前，先执行指定命令 cmd；vim file.txt -c \u0026#34;e ++enc=UTF-8\u0026#34;：以指定的编码打开 file.txt 文件 vim +{cmd} {file} # 在打开文件 file 后，再执行命令 cmd vim +/string {file} # 打开文件 file，并将光标停留在第一个匹配的 string 上 vim -d {file1} {file2} # 同时打开 file1 和 file2 文件并 diff 两个文件的差异 Copied! 保存退出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 :w # 写入文件并保存，会修改文件的时间戳 :w[rite] {file} # 按名称 file 保存文件 :w !sudo tee % # 以超级用户权限保存文件，也可以这样 :w !sudo tee % \u0026gt; /dev/null :wa # 保存所有文件 :wa[ll] # 保存所有文件 :wqa[ll] # 保存所有文件并退出 :[N]wn[ext] # 保存当前文件，并编辑下 N 个文件 :[N]wp[revious] # 保存当前文件，并编辑上 N 个文件 :q # 关闭光标所在的窗口并退出 :q[uit] # 关闭光标所在的窗口并退出 :q! # 不保存文件并强制退出 :qa[ll] # 放弃所有文件操作并退出 :qa[ll]! # 放弃所有文件操作并强制退出 :x # 保存文件并退出，不会修改文件的时间戳 ZZ # 保存已改动的文件，并关闭退出窗口，等价于 :x ZQ # 不保存文件关闭窗口，等价于 :q! Copied! 文件比较 Vim 的 diff 模式是依赖于 diff 命令的，所以首先保证系统中的 diff 命令是可用的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 vimdiff lfile rfile # 纵向分割窗口比较文件 vim -d lfile rfile # 纵向分割窗口比较文件 vim -d -o lfile rfile # 横向分割窗口比较文件 :diffs[plit] {file} # 在当前窗口分割，载入另一个文件 file 进行文件比较 :vert diffs {file} # 在载入文件时要使用纵向分割 :difft[his] # 将当前文件加入 diff :diffp[atch] {patchfile} # 将 buffer 中的文件载入当前窗口进行文件比较 :diffu[pdate] # 文件改动后，刷新 diff :diffu[pdate]! # 对所有文件更新 diff :[range]diffpu[t] # 指定范围的合并，当前文件的指定范围内容复制到另一个文件里 :[range]diffg[et] # 指定范围的合并，把另一个文件的指定范围内容复制到当前行中 ]c # 在 diff 中的跳转到下一个不同 [c # 在 diff 中的跳转到上一个不同 :diffo[ff] # 将目前文件退出 diff 模式 :diffo[ff]! # 将目前窗口中的所有文件退出 diff 模式 :qa[ll] # 不保存文件修改并退出 :wa[ll] # 保存全部文件 :wqa[ll] # 修改合并后，保存全部文件并退出 Copied! 文件操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 :e[dit] {file} # 打开文件并编辑，通过文件的绝对或相对路径打开文件，Tab 制表键补全路径 :e[dit] . # 打开文件管理器，浏览当前目录下的文件，选中并编辑 :e[dit] # 重新载入当前文件 :e[dit]! # 放弃修改，重新回到文件打开时的状态 :E[xplore] # 打开文件管理器，并显示活动缓冲区所在的目录 :sav[eas] {file} # 另存为指定文件 file :o {file} # 在当前窗口打开另一个文件（o: open） :r {file} # 读取文件并将内容插入到光标后 :r !dir # 将 dir 命令的输出捕获并插入到光标后 :on[ly] # 关闭除光标所在的窗口之外的其他窗口，等价于 Ctrl+W o :clo[se] # 关闭光标所在窗口的文件，等价于 Ctrl+W c :cd {path} # 切换 Vim 当前路径至 path :cd - # 回到上一次当前目录 :pwd # 显示 Vim 当前路径 :n[ew] {file} # 打开一个新的窗口编辑新文件 file :[N]new # 打开 N 个新的窗口编辑新文件，N 为任意正整数 :ene[w] # 在当前窗口创建新文件 :[N]vne[w] # 纵向切分 N 个新窗口编辑新文件，N 为任意正整数 :tabnew # 在新的标签页中编辑新文件 :fin[d] {file} # 在 path 当中查找文件 file 并编辑 :f[ile] # 显示当前文件名及光标位置 :f[ile] {name} # 置当前文件名为 name :files # 查看缓冲区列表 Copied! 缓冲区 缓冲区（Buffer）是一块内存区域，用于存储着正在编辑的文件。在保存缓冲区并退出时，内容也随之被写回原始文件。切换缓冲区可以在多个文件中来回编辑，提高编辑效率。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 :ls # 查看缓冲区列表 :files # 查看缓冲区列表，同上 :buffers # 查看缓冲区列表，同上 :ls [flags] # 查看指定状态的缓冲区，其中 [flags] 参考下面列表取值 :ball # 为每个缓冲区打开一个窗口 :bad[d] {name} # 将名称为 name 的文件添加到缓冲区列表 :b[uffer] [N] # 打开指定缓存编号的缓冲区 :b[uffer] {name} # 打开名称为 name 的缓冲区 :sb[uffer] [N] # 纵向分割打开指定缓存编号的缓冲区 :sb[uffer] {name} # 纵向分割打开名称为 name 的缓冲区 :bn[ext] # 切换到下一个缓冲区 :bN[ext] # 切换到上一个缓冲区 :bp[revious] # 切换到上一个缓冲区，同上 :bf[irst] # 切换到第一个缓冲区 :bl[ast] # 切换到最后一个缓冲区 :bd[elete] [N] # 删除指定 N 编号的缓冲区 :N,Mbdelete # 删除指定范围的缓冲区，例如 :3,5bdelete 表示删除缓存编号在 3~5 范围的缓冲区 :bun[load][!] [N] # 卸载缓冲区，! 代表是否强制卸载缓冲区 N Ctrl+^ # 切换缓冲区，先输入缓存编号，再按 Ctrl+^ Copied! 查看缓冲区列表时，缓冲区状态包含以下几种：\n缓冲区状态 说明 + modified buffers，已更改的缓冲区 - buffers with \u0026lsquo;modifiable\u0026rsquo; off，禁用了 modifiable 选项，只读缓冲区 = readonly buffers，只读缓冲区 a active buffers，活动缓冲区，显示在当前屏幕上 u unlisted buffers (overrides the \u0026ldquo;!\u0026rdquo;) h hidden buffers，隐藏缓冲区，已载入但没显示在屏幕上 x buffers with a read error，读入时报错的缓冲区 % current buffer，当前缓冲区 # alternate buffer，交换缓冲区 R terminal buffers with a running job F terminal buffers with a finished job ? terminal buffers without a job: :terminal NONE t show time last used and sort buffers 分屏窗口 分屏窗口是基于 Ctrl+W 快捷键的，Ctrl 是控制功能键，W 是代表 Windom，Ctrl+W 代表控制窗口的意思。\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 \u0026#34; :sp[file] 上下分屏 :vsp[file] 左右分屏 \u0026#34; vim -o file1 file2 file3 上下分屏 \u0026#34; vim -O file1 file2 file3 左右分屏 \u0026#34; \u0026lt;C-w\u0026gt;h/j/k/l/w 在窗口间移动光标 \u0026#34; \u0026lt;C-w\u0026gt;H/J/K/L/r/R 移动窗口; \u0026lt;C-w\u0026gt;o 关闭其他窗口只保留当前的(:only); \u0026lt;C-w\u0026gt;T 将当前窗口移到新的标签页中 \u0026#34; \u0026lt;C-w\u0026gt;+-\u0026lt;\u0026gt; 增/减 窗口 高度/宽度 ; \u0026lt;C-w\u0026gt;= 设置所有窗口同宽同高 :sp[lit] {file} # 横向切分窗口并在新窗口打开文件 file :[N]sp[lit] # 横向切分 N 个当前窗口出来，内容同步，游标可以不同 :[N]new # 横向切分出一个 N 行高的窗口，并编辑一个新文件 :[N]sv[iew] {file} # 横向切分窗口并在新窗口打开文件 file，等价于 :split，区别在于当前窗口的内容 只读 :[N]sf[ind] {file} # 横向切分窗口，从 path 中找到文件 file 并编辑之 Ctrl+W s # 横向切分当前窗口（s: split） Ctrl+W f # 横向切分出一个窗口，并在新窗口打开名称为光标所在词的文件 :vs[plit] {file} # 纵向切分窗口并在新窗口打开文件 file（vs: vertical split） :[N]vsp[lit] # 纵向切分 N 个当前窗口出来，内容同步，游标可以不同 :[N]vne[w] # 纵向切分出一个新窗口 Ctrl+W v # 纵向切分当前窗口（v:vertical split） Ctrl+W n # 新建一个无文件窗口 Ctrl+W c # 关闭当前窗口，但不能关闭最后一个窗口，等价于 :clo[se] Ctrl+W o # 关闭其他窗口，只保留当前活动窗口，等价于 :on[ly] Ctrl+W q # 退出当前窗口，如果是最后一个窗口，则退出 Vim，等价于 :q[uit] Ctrl+W h # 跳转到左边的窗口（h 键位于左边，按该键光标左移） Ctrl+W j # 跳转到下边的窗口（j 键有向下的突起，按该键光标下移） Ctrl+W k # 跳转到上边的窗口（k 键与 j 键相反，按该键光标上移） Ctrl+W l # 跳转到右边的窗口（l 键位于右边，按下该键光标向右移动） Ctrl+W H # 移动当前窗口到最左边 Ctrl+W J # 移动当前窗口到最下边 Ctrl+W K # 移动当前窗口到最上边 Ctrl+W L # 移动当前窗口到最右边 Ctrl+W # 切换到下一个窗口（W: Window） Ctrl+W w # 循环切换到下一个窗口 Ctrl+W W # 循环切换到上一个窗口 Ctrl+W p # 切换至上个访问过的窗口 Ctrl+W r # 反转互换窗口（r: reverse） Ctrl+W T # 将当前窗口移到新的标签页中 ctrl+W t # 切换到最上面的窗口 ctrl+W b # 切换到最下面的窗口 Ctrl+W P # 跳转到预览窗口 Ctrl+W z # 关闭预览窗口 Ctrl+W = # 设置所有窗口同宽同高 Ctrl+W _ # 纵向最大化当前窗口 Ctrl+W | # 横向最大化当前窗口 Ctrl+W + # 增加当前窗口的行高，前面可以加数字 Ctrl+W - # 减少当前窗口的行高，前面可以加数字 Ctrl+W \u0026lt; # 减少当前窗口的列宽，前面可以加数字 Ctrl+W \u0026gt; # 增加当前窗口的列宽，前面可以加数字 :res[ize] \u0026lt;N\u0026gt; # 调整当前窗口的高度，增加 N 行 :res[ize] +\u0026lt;N\u0026gt; # 调整当前窗口的高度，增加 N 行 :res[ize] -\u0026lt;N\u0026gt; # 调整当前窗口的高度，减小 N 行 :vert[ical] res[ize] \u0026lt;N\u0026gt; # 调整当前窗口的宽度，增加 N 列 :vert[ical] res[ize] +\u0026lt;N\u0026gt; # 调整当前窗口的宽度，增加 N 列 :vert[ical] res[ize] -\u0026lt;N\u0026gt; # 调整当前窗口的宽度，减小 N 列 Copied! 标签页 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 :tabs # 显示所有标签页 :tabnew {file} # 在新标签页中编辑新的文件 file :tabe[dit] {file} # 在新标签页中打开并编辑文件 file :tabf[ind] {file} # 在当前目录搜索 file，并在新标签页中打开。请注意，此命令只能打开一个文件 :tab split # 在新的标签页中打开当前窗口里的文件 :tab ball # 将缓存中所有文件用标签页打开 :tab drop {file} # 如果文件已被其他标签页和窗口打开则跳过去，否则新标签打开 :tabc[lose] # 关闭当前标签页 :tabo[nly] # 关闭其他标签页 :tabn N # 切换到第 N 个标签页，例如：tabn 3 切换到第 3 个标签页 :tabm[ove] N # 将当前标签页移动到第 N 个标签页之后，标签页编号是从 0 开始计数的 :tabm[ove] +N # 标签页往右移 N 个位置 :tabm[ove] -N # 标签页往左移 N 个位置 :tabr[ewind] # 切换到第一个标签页，等价于 :tabfirst 命令 :tabfir[st] # 切换到第一个标签页 :tabl[ast] # 切换到最后一个标签页 :tabn[ext] # 切换到下一个标签页，等价于 gt :tabp[revious] # 切换到上一个标签页，等价于 gT gt # 切换到下一个标签页 gT # 切换到上一个标签页 [N]gt # 切换到第 N 个标签页，例如 2gt 将会切换到第 2 个标签页 :tabd[o] {cmd} # 同时在多个标签页中执行命令，例如 :tabdo %s/food/drink/g，一次完成对所有文件的替换操作 :tab help # 在标签页打开帮助 :h tabpage # 查看标签页帮助文档 Copied! vim书签 Vim 书签可以在文件内容及文件之间快速定位到指定位置。\n1 2 3 4 5 6 7 8 m{mark-name} # 创建名称为 mark-name 的书签 :marks # 查看并列出所有书签 :marks {mark-name} # 查看名称为 mark-name 书签的详情信息 `{mark-name} # 跳转到书签的确切位置。请注意，此字符是后退引号 \u0026#39;{mark-name} # 跳转到书签行的开头。请注意，此字符是单引号 :delm[arks] {mark-name} # 删除书签，可以批量删除 :delm[arks]! # 删除所有当前缓冲区的书签，但不包括 A-Z 和 0-9 的书签 :h marks # 查看书签帮助文档 Copied! 例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 :marks # 查看所有书签 m{a-zA-Z} # 创建书签，小写的是文件书签，大写的是全局书签，可以用 a-zA-Z 中的任何字母标记 ma # 保存当前位置到书签 a :marks a # 显示名称为 a 书签的详细信息 \u0026#39;a # 跳转到书签 a 所在的行 `a # 跳转到书签 a 所在位置 `` # 回到上次跳转的位置 \u0026#39;\u0026#39; # 回到上次跳转的位置 `. # 回到上次编辑的位置 \u0026#39;. # 回到上次编辑的位置 \u0026#39;A # 跳转到全局书签 A [\u0026#39; # 跳转到上一个书签 ]\u0026#39; # 跳转到下一个书签 \u0026#39;\u0026lt; # 跳转到上次可视模式选择区域的开始 \u0026#39;\u0026gt; # 跳转到上次可视模式选择区域的结束 :delm a b # 删除书签 a 和 b :delmarks p-z # 删除范围在 p ~ z 的书签 Copied! 文件浏览器 Vim 7.0 之后内置 Netrw 插件，提供文件浏览器功能，相比与 NERDTree 第三方插件来说速度更快，体量更轻，设计更简洁。\n1 2 3 4 5 6 7 8 9 10 :[N]E[xplore][!] [dir] # 当前窗口中打开文件浏览器 :[N]Hex[plore][!] [dir] # 水平分割窗口打开文件浏览器 :[N]Lex[plore][!] [dir] # 左边窗口打开文件浏览器 :[N]Sex[plore][!] [dir] # 水平分割窗口打开文件浏览器 :[N]Vex[plore][!] [dir] # 垂直分割窗口打开文件浏览器 :Tex[plore] [dir] # 新标签页打开文件浏览器 :Rex[plore] # 返回文件浏览器 :Nexplore # 定位到下一个匹配文件 :Pexplore # 定位到上一个匹配文件 :h[elp] netrw # 查看更多 Netrw 插件的帮助信息 Copied! 拼写检查 1 2 3 4 5 6 7 :set spell # 打开拼写检查 :set nospell # 关闭拼写检查 ]s # 下一处错误拼写的单词 [s # 上一处错误拼写的单词 zg # 加入单词到拼写词表中 zug # 撤销上一次加入的单词 z= # 拼写建议 Copied! 代码折叠 1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34;\u0026#34; 代码折叠命令： ( close==创建折叠 )!= (open==展开折叠) \u0026#34; za 切换当前光标下的折叠的状态(open或close) == zc+zo \u0026#34; zA 递归切换当前折叠及其嵌套折叠的状态 == zC+zO \u0026#34; zr 减少折叠级别 \u0026#34; zm 增加折叠级别 \u0026#34; zj 定位到下一个折叠处 \u0026#34; zk 定位到上一个折叠处 \u0026#34; \u0026#34; zc 创建一个光标下的折叠(close)(从内向外) \u0026#34; zC 递归创建折叠及嵌套折叠(从内向外) \u0026#34; zo 展开一个光标下的折叠(open)(从外向内) \u0026#34; zO 递归打开当前折叠及其嵌套折叠,大写的字母O (从外向内) \u0026#34; zf 对所指定的文本范围进行折叠,只支持manual和marker Copied! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 :h[elp] z # 查看 折叠 帮助文档 zf{motion} # 手动定义一个折叠（f:fold） :{range}fold # 将范围 {range} 包括的行定义为一个折叠 z= # 给出拼写建议 zf # 创建代码折叠 zF # 指定行数 N 创建折叠 za # 打开或关闭当前折叠 zA # 递归切换折叠，即递归打开一个关闭的折叠或关闭一个打开的折叠 zi # 切换折叠，切换 foldenable（i: invert） zc # 关闭光标下的一个折叠（c: close） zC # 递归关闭折叠（C: Close） zj # 定位到下一个折叠处 zk # 定位到上一个折叠处 zd # 删除光标下折叠（d: delete） zD # 递归删除折叠（D: Delete） zE # 删除所有折叠 zm # 收起嵌套的折行，减少 foldlevel（m: more） zM # 关闭所有折叠，置 foldlevel 为 0，设置 foldenable zn # 不折叠，重置 foldenable 并打开所有代码（n: none） zN # 正常折叠，重置 foldenable 并恢复所有折叠（N: Normal） zr # 打开嵌套的折行，增加 foldlevel（r: reduce） zR # 打开所有折叠，置 foldlevel 为最大值 zo # 打开光标下的折叠（o: open） zO # 递归打开折叠（O: Open） Copied! 文档加解密 1 2 3 4 5 6 vim -x {file} # 文档加密，输入加密密码并再次确认密码。注意：不修改内容也要保存，否则密码设定不会生效 :X # 文档加密，命令模式下输入加密密码并再次确认密码。注意：不修改内容也要保存，否则密码设定不会生效 :set key={password} # 文档加密，命令模式下输入加密密码并再次确认密码。注意：不修改内容也要保存，否则密码设定不会生效 :X # 文档解密，命令模式下直接按 \u0026lt;Enter\u0026gt; 回车键，表示密码为空。注意：不修改内容也要保存，否则解密设定不会生效 :set key= # 文档解密，命令模式下设置 key 的密码为空。注意：不修改内容也要保存，否则密码设定不会生效 Copied! 宏录制 宏是录制和播放功能，是一系列 Vim 命令操作的集成，利用宏可以减少很多重复的复杂操作。\n1 2 3 4 5 q{0-9a-zA-Z\u0026#34;} # 开始录制名字为 {0-9a-zA-Z\u0026#34;} 的宏，例如 qa 表示录制名字为 a 的宏 q # 结束录制宏 @{0-9a-z\u0026#34;.=*+} # 播放名字为 {0-9a-z\u0026#34;.=*+} 的宏，例如 @a 表示播放名字为 a 的宏 @@ # 播放上一个宏 @: # 重复上一个 Ex 命令，即冒号命令 Copied! 宏 举例：需要将以下多行文本的行首键入一个 Tab 制表键进行 行首缩进。\n1 2 3 4 5 6 7 set nu set tabstop=4 set shiftwidth=4 set softtabstop=4 set autoindent set wrap syntax on Copied! 录制宏 先将光标移动到第一行。 在 Normal 模式下，按 q 字母键加一个字母开始录制。例如按下 qa，将该宏注册为 a。 按下 I 字母键在行首插入，在编辑模式按下 Tab 制表键。按 退出键返回到 Normal 模式。 按下 j 字母键将光标移动到下一行。 按下 q 字母键完成录制。 使用宏 使用上面录制的宏 a，按下 @a，播放名字为 a 的宏。 Normal 模式下将光标移动到第二行，按下 @a，再使用了一次宏 a。 多次操作按下 N@a，其中 N 为正整数，代表执行 N 次宏。例如将光标移动到第 3 行，对余下的 5 行操作宏 a，按下 5@a。 以上 录制宏、使用宏 两个共同操作，完成多行文本的行首键入一个 Tab 制表键进行行首缩进！\n其它命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ga # 显示光标下字符的 ASCII 码或者 Unicode 编码 g8 # 显示光标下字符的 UTF-8 编码字节序 gi # 回到上次进入插入的地方，并切换到插入模式 gH # 启动选择行模式 K # 查询光标下单词的帮助 Ctrl+G # 显示正在编辑的文件名、文件大小和位置信息等信息 g Ctrl+G # 显示文件的大小、字符数、单词数和行数，可视模式下也可用 Ctrl+PgUp # 上个标签页，GVim OK，部分终端软件需设置对应键盘码 Ctrl+PgDown # 下个标签页，GVim OK，部分终端软件需设置对应键盘码 Ctrl+R Ctrl+W # 命令模式下插入光标下单词 Ctrl+Insert # 复制到系统剪贴板（GVim） Shift+Insert # 粘贴系统剪贴板的内容（GVim） Ctrl+X Ctrl+E # 插入模式下向上滚屏 Ctrl+X Ctrl+Y # 插入模式下向下滚屏 :.!date # 在当前窗口插入时间 :%!xxd # 开始二进制编辑 :%!xxd -r # 保存二进制编辑 :r !curl -sL {URL} # 读取 URL 内容添加到光标后 :v/./,/./-j # 压缩空行 :Man bash # 在 Vim 中查看 man，先调用 :runtime! ftplugin/man.vim 激活 /fred\\|joe # 搜索 fred 或者 joe /\\\u0026lt;\\d\\d\\d\\d\\\u0026gt; # 精确搜索四个数字 /^\\n\\{3} # 搜索连续三个空行 Copied! 历史命令 历史命令格式。\n1 :his[tory] [{name}] [{first}][, [{last}]] Copied! 参数说明：\n{name}：指定历史记录类型。 {first}：指定命令历史的起始位置，默认为第一条记录。 {last}：指定命令历史的终止位置，默认为最后一条记录。 在命令行模式下。\n1 2 3 4 5 6 :his[tory] # 查看所有命令行模式下输入的命令历史 :his[tory] all # 查看所有类型的历史记录 :history c 1,5 # 查看第一到第五条命令行历史 :history search 或 / 或 ? # 查看搜索历史 :call histdel(\u0026#34;\u0026#34;) # 删除历史记录 :help :history # 查看 :history 命令的帮助信息 Copied! 在普通模式下。\n1 2 3 q/ # 查看使用 q/ 输入的搜索历史 q? # 查看使用 q? 输入的搜索历史 q: # 查看命令行历史 Copied! 寄存器 Vim 寄存器是用于保存临时数据的地方。Vim 有多个寄存器，可当作多个剪贴板，在使用多个文件时，此功能非常有用，且活用多个寄存器可以显著提高数据的安全和可操作性。\n1 2 3 4 5 :reg[isters] # 查看所有寄存器的值 :reg[isters] {args} # 查看指定 {args} 中提到的寄存器值 \u0026#34;{register} # 普通模式下调取寄存器值 :Ctrl+r \u0026#34;{reg-name} # 命令模式下输入 Ctrl+r 后 Vim 会自动打出 \u0026#34; 寄存器引用符号 Ctrl+r {reg-name} # 插入模式下无需输入寄存器引用符号 \u0026#34; Copied! 例如：\n1 2 3 4 \u0026#34;?yy # 复制当前行到寄存器 ? ，问号代表 0 ~ 9 的寄存器名称 \u0026#34;?d3j # 删除光标下三行内容，并放到寄存器 ? ，问号代表 0 ~ 9 的寄存器名称 \u0026#34;?p # 将寄存器 ? 的内容粘贴到光标后 \u0026#34;?P # 将寄存器 ? 的内容粘贴到光标前 Copied! Vim 寄存器分类。\n寄存器名称 引用方式 说明 无名寄存器 \u0026quot;\u0026quot; 默认寄存器，所有的复制和修改操作（x、s、d、c、y）都会将该数据复制到无名寄存器 字母寄存器 \u0026ldquo;a-zA-Z {register} 只能是一位的 26 个英文字母，从 a-z，A-Z 寄存器内容将会合并到对应小写字母内容后边 复制专用寄存器 \u0026ldquo;0 仅当使用复制操作(y)时，该数据将会同时被复制到无名寄存器和复制专用寄存器 逐级临时缓存寄存器 \u0026ldquo;1 - \u0026ldquo;9 所有不带范围（‘(’，‘)’，‘{’，‘}’）、操作涉及 1 行以上的删除修改操作（x、s、d、c）的数据都会复制到逐级临时缓存寄存器，并在新的数据加入时，逐级先后推移。1 的数据复制到 2，2 到 3，最后的 9 寄存器内容将会被删除 黑洞寄存器 \u0026ldquo;_ 几乎所有的操作涉及的数据都会被复制到寄存器，如果想让操作的数据不经过寄存器，可以指定黑洞寄存器，数据到该寄存器就会消失掉，不能显示，也不存在 系统剪切板 \u0026ldquo;+ 或 \u0026ldquo;* 与 Vim 外部的 GUI 交互数据时，需要使用专用的系统剪切板 表达式寄存器 \u0026ldquo;= 所有寄存器里最特殊的一个，用于计算表达式。输入完该寄存器应用后，会在命令行里提示“=”，按需输入表达式，结果将会显示到光标处 其他寄存器 - - 配置文件 Vim 配置文件有全局和用户两种版本，且用户配置文件优先于全局系统配置文件。\n1 2 3 4 5 6 :ve[rsion] # 查看 Vim 版本，同时也查看 Vim 载入配置文件的优先顺序及所在位置 :echo $MYVIMRC # Vim 命令模式下使用该命令输出 Vim 配置文件的位置 :edit $MYVIMRC # Vim 命令模式下使用该命令打开 Vim 配置文件 :so[urce] $MYVIMRC # Vim 配置文件改动后，使用该命令加载新的配置选项，命令缩写为 :so % :echo $VIM # 输出全局 vimrc 配置文件位置，存放在 Vim 的安装目录中 :echo $HOME # 输出用户 vimrc 配置文件位置，存放在用户主目录中 Copied! 在命令行模式下单个设置选项，且选项只在当前窗口生效（命令前记得加上 “:” ，在 vimrc 配置文件 中则不需要）。\n1 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 :se[t][!] # 显示出所有与其默认值不同的选项，当 [!] 出现时，每个选项都显示在单独的行上 :se[t][!] all # 显示所有选项列表，当 [!] 出现时，每个选项都显示在单独的行上 :se[t] all\u0026amp; # 将所有的选项都重置为默认值 :se[t] {option}\u0026amp; # 将选项设置为默认值 :se[t] {option}? # 查看某个选项的当前值，屏幕底部将显示其返回值 :se[t] {option} # 打开某个设置选项， 选项大致可分为三种：布尔值选项、数值选项和字符串选项 :se[t] no{option} # 关闭某个设置选项 :se[t] {option}! # 反转设置选项 :se[t] inv{option} # 反转设置选项，同上 :se[t] {option}:{valus} # 设置选项的值 :se[t] {option}={valus} # 设置选项的值，同上 :se[t] {option}+={valus} # 在选项中增加数值或增加字符串 :se[t] {option}-={valus} # 在选项中减去数值或移除字符串 :se[t] {option}^={valus} # 将选项乘以数值或在选项开头增加字符串 :setl[ocal] # 等价于 :set，但对局部选项设定其局部值 :setg[lobal] # 等价于 :set，但对局部选项设定其全局值 :h[elp] se[t] # 查看命令的帮助信息 :map # 查看当前 Vim 配置的 map 映射快捷键 :inoremap # 查看当前 Vim 配置的 inoremap 映射快捷键 :nnoremap # 查看当前 Vim 配置的 nnoremap 映射快捷键 :unm[ap] {lhs} # 取消 {lhs} 选项的映射，例如 :unmap \u0026lt;F10\u0026gt; 表示取消 F10 的映射 :mapclear # 取消所有映射；请注意，该命令将会移除所有用户定义和系统默认的键盘映射，慎用 :mapc[lear]! # 清除插入及命令行模式下的映射 :imapc[lear] # 清除插入模式下的映射 :vmapc[lear] # 清除可视模式下的映射 :omapc[lear] # 清除操作符等待模式下的映射 :nmapc[lear] # 清除普通模式下的映射 :cmapc[lear] # 清除命令行模式下的映射 :scr[iptnames] # 查看 Vim 加载时加载了那些插件和脚本 Copied! 按键映射命令格式：\n1 [prefix]map {lhs} {rhs} # 将键 {lhs} 映射为 {rhs}，{rhs} 可进行映射扫描，也可递归映射 Copied! 参数说明：\n{lhs}：lhs 代表 left-hand-side，即左边参数。 {rhs}：rhs 代表 right-hand-side，即右边参数。 [prefix]：作用模式前缀，有以下取值。 前缀 作用模式 命令格式 命令缩写 \u0026lt;Space\u0026gt; 普通、可视、选择和操作符等待 :map {lhs} {rhs} 无 n 普通模式 :nmap {lhs} {rhs} :nm {lhs} {rhs} v 可视和选择模式 :vmap {lhs} {rhs} :vm {lhs} {rhs} s 选择模式 :smap {lhs} {rhs} 无 x 可视模式 :xmap {lhs} {rhs} :xm {lhs} {rhs} o 操作符等待 :omap {lhs} {rhs} :om {lhs} {rhs} ! 插入和命令行模式 :map! {lhs} {rhs} 无 i 插入模式 :imap {lhs} {rhs} :im {lhs} {rhs} I 插入、命令行和 Lang-Arg 模式 :lmap {lhs} {rhs} :lm {lhs} {rhs} c 命令行模式 :cmap {lhs} {rhs} :lm {lhs} {rhs} nore 不递归（no rerecursion）映射，和以上前缀自由搭配使用 :noremap {lhs} {rhs} :nor {lhs} {rhs} un 取消 :map 绑定的 {lhs} :unmap {lhs} 无 {rhs} 之前可能显示的特殊字符：\n特殊字符 意义 * 不可重映射 \u0026amp; 仅脚本的局部映射可以被重映射 @ 缓冲区的局部映射 特殊参数说明（特殊参数必须在映射命令的后边，{lhs} 参数的前面）：\n参数 说明 \u0026lt;buffer\u0026gt; 如果映射命令的第一个参数是 ，映射将只局限于当前缓冲区（即此时正在编辑的文件）内 \u0026lt;silent\u0026gt; 执行绑定键时不在命令行上回显按键映射的命令内容 \u0026lt;special\u0026gt; 一般用于定义特殊键怕有副作用的场合 \u0026lt;script\u0026gt; 该映射只使用通过以 \u0026ldquo;\u0026rdquo; 开头来定义的脚本局部映射来重映射 {rhs} 中的字符 \u0026lt;expr\u0026gt; 如果定义新映射的第一个参数是 ，那么参数会作为表达式来进行计算，计算结果作为实际使用的 \u0026lt;unique\u0026gt; 用于定义新的键映射或者缩写命令时检查是否该键已经被映射，如果该映射或者缩写已经存在，则该命令会失败 映射快捷键时常用的键表：\n键 键说明 组合键 \u0026lt;F1\u0026gt; ~ \u0026lt;F12\u0026gt; 功能键 F1 ～ F12 \u0026lt;K0\u0026gt; ~ \u0026lt;K9\u0026gt; 数值 0 到 9 \u0026lt;Shift\u0026gt; Shift 键 \u0026lt;S-\u0026hellip;\u0026gt; \u0026lt;s-\u0026hellip;\u0026gt; \u0026lt;Shift-\u0026hellip;\u0026gt; \u0026lt;Ctrl\u0026gt; Ctrl 键 \u0026lt;C-\u0026hellip;\u0026gt; \u0026lt;c-\u0026hellip;\u0026gt; \u0026lt;Ctrl-\u0026hellip;\u0026gt; \u0026lt;Alt\u0026gt; Alt 键 \u0026lt;A-\u0026hellip;\u0026gt; \u0026lt;a-\u0026hellip;\u0026gt; \u0026lt;Alt-\u0026hellip;\u0026gt; \u0026lt;Up\u0026gt;/\u0026lt;Down\u0026gt;/\u0026lt;Right\u0026gt;/\u0026lt;Left\u0026gt; 方向键 \u0026lt;Esc\u0026gt; Esc 键 \u0026lt;Leader\u0026gt; \\ 前缀键 \u0026lt;Tab\u0026gt; Tab 制表键 \u0026lt;CR\u0026gt; Enter 回车键 在 vimrc 配置文件 中可以批量设置选项，例如：\n1 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 37 38 39 40 41 42 43 44 syntax # 列出已经定义的语法项 syntax clear # 清除已定义的语法规则 syntax on # 允许语法高亮 syntax off # 禁止语法高亮 set nu # 设置显示行号，禁止显示行号可以用 :set nonu set number # 设置显示行号，禁止显示行号可以用 :set nonumber set list # 设置显示制表符和换行符 set history=200 # 记录 200 条历史命令 set bs=? # 设置 \u0026lt;Backspace\u0026gt; 退格键模式，现代编辑器为 :set bs=eol, start, indent set sw=4 # 设置缩进宽度为 4 set ts=4 # 设置制表符宽度为 4 set noet # 设置不展开 Tab 制表键成空格 set et # 设置展开 Tab 制表键成空格 set winaltkeys=no # 设置 GVim 下正常捕获 \u0026lt;Alt\u0026gt; 换挡键 set nowrap # 关闭自动换行 set ttimeout # 允许终端按键检测超时（终端下功能键为一串 Esc 开头的扫描码） set ttm=100 # 设置终端按键检测超时为 100 毫秒 set term=? # 设置终端类型，例如常见的 xterm set ignorecase # 设置搜索是否忽略大小写 set smartcase # 智能大小写，默认忽略大小写，除非搜索内容里包含大写字母 set relativenumber # 设置显示相对行号（其他行与当前行的距离） set paste # 打开粘贴模式（粘贴时禁用缩进等影响格式的内容） set nopaste # 关闭粘贴模式 set spell # 允许拼写检查 set hlsearch # 开启高亮查找 set nohlsearch # 关闭高亮查找 set termguicolors # Vim 开启真彩色 set ruler # 总是显示光标位置 set nocompatible # 设置不兼容原始 vi 模式，该设置必须在最开头 set incsearch # 查找输入时动态增量显示查找结果 set insertmode # Vim 始终处于插入模式下，使用 Ctrl+o 临时执行命令 set ff=unix # 设置换行为 unix set ff=dos # 设置换行为 dos set ff? # 查看换行设置 set hidden # 打开隐藏模式，未保存的缓存可以被切换走，或者关闭 set nohidden # 关闭隐藏模式，未保存的缓存不能被切换走，或者关闭 set termcap # 查看会从终端接收什么以及会发送给终端什么命令 set guicursor= # 解决 SecureCRT/PenguiNet 中 NeoVim 局部奇怪字符问题 set t_RS= t_SH= # 解决 SecureCRT/PenguiNet 中 Vim8.0 终端功能奇怪字符 set fo+=a # 开启文本段的实时自动格式化 set showtabline=? # 标签页显示方式，? 为 0 时隐藏标签页，1 会按需显示，2 会永久显示 inoremap vv \u0026lt;Esc\u0026gt; # 插入模式下的 vv 键为 Esc 退出键，退出插入模式 nnoremap gh ^ # 普通模式下按 gh 键进行行首跳转，代替数字 0 进行行首跳转 nnoremap gl $ # 普通模式下按 gl 键进行行尾跳转，代替数字 $ 进行行尾跳转 Copied! 常用插件 vim-commentary ：批量注释工具，可以注释多行和去除多行注释。 NERDTree ：插件用于列出当前路径的目录树。 asyncrun.vim ：插件使用 Vim 8、NeoVim 的异步机制，让你在后台运行 Shell 命令，并将结果实时显示到 Vim 的 Quickfix 窗口中。 vim模式 1 2 3 4 5 6 7 普通模式 # 按 \u0026lt;Esc\u0026gt; 退出键或 Ctrl+[ 进入普通模式，左下角显示文件名或为空 插入模式 # 按 i 字母键进入插入模式，左下角显示 --INSERT-- 可视模式 # 按 v 字母键进入可视模式，左下角显示 --VISUAL-- 选择模式 # 按 {visual}+Ctrl+g 组合键进入选择模式，左下角显示 --SELECT-- 替换模式 # 按 r 或 R 字母键开始替换模式，左下角显示 --REPLACE-- 命令行模式 # 按 : 或者 / 或者 ? 开始命令行模式，左下角无明显信息 Ex 模式 # 按 Q 字母键进入 Ex 模式，与命令行模式类似，执行完命令后，会继续停留在 Ex 模式，按 :vi[sual] 退出 Ex 模式 Copied! 外部命令 1 2 3 4 5 6 7 8 9 10 :!{command} # 执行一次性 Shell 命令，例如 :!pwd，输出当前 Vim 模式下所处目录路径 :!! # 重新执行最近一次运行过的命令 :sh[ell] # 启动一个交互的 Shell 执行多个命令，不需要退出Vim。exit 命令退出并返回 Vim :!ls # 运行外部命令 ls，并等待返回 :r !ls # 将外部命令 ls 的输出捕获，并插入到光标后 :w !sudo tee % # sudo 以后保存当前文件，也可以这样 :w !sudo tee % \u0026gt; /dev/null :call system(\u0026#39;ls\u0026#39;) # 调用 ls 命令，但是不显示返回内容 :!start notepad # Windows 下启动 Notepad，最前面可以加 silent :sil !start cmd # Windows 下当前目录打开 cmd :%!prog # 运行文字过滤程序，如整理 JSON 格式 :%!python -m json.tool Copied! gui命令 1 2 3 4 5 6 7 8 9 :gui # UNIX 启动 GUI :gui {fname} # 同上，并编辑 fname :menu # 列出所有菜单 :menu {mpath} # 列出 mpath 下的所有菜单 :menu {mpath} {rhs} # 把 rhs 加入菜单 mpath :menu {pri} {mpath} {rhs} # 同上，并带有优先权 pri :menu ToolBar.{name} {rhs} # 把 rhs 加入工具栏 :tmenu {mpath} {text} # 为菜单 mpath 加入工具提示 :unmenu {mpath} # 删除菜单 mpath Copied! 自动命令 自动命令，是在指定事件发生时自动执行的命令。利用自动命令可以将重复的手工操作自动化，以提高编辑效率并减少人为操作的差错。\n自动命令语法格式。\n1 :au[tocmd] [group] {event} {aupat} [++once] [++nested] {command} Copied! 参数说明：\ngroup：组名是可选项，用于分组管理多条自动命令。 event：事件参数 ，用于指明触发命令的一个或多个事件。 pattern：限定针对符合匹配模式的文件执行命令。 nested：嵌套标记是可选项，用于允许嵌套自动命令。 command：指明需要执行的命令、函数或脚本。 1 2 3 4 :au[tocmd] # 查看所有自动命令，既包括 vimrc 文件中自定义的自动命令，也包括了各种插件定义的自动命令 :au[tocmd]! # 删除所有自动命令；此操作也将删除插件所定义的自动命令，请谨慎操作 :help autocommand-events # 查看 自动命令事件 帮助文档 :help autocmd-patterns # 查看匹配模式帮助文档 Copied! 自动命令组\n1 :h[elp] aug[roup] # 查看自动命令组的帮助文档 Copied! 通过 :augroup 命令，可以将多个相关联的自动命令分组管理，以便于按组来查看或删除自动命令。例如以下命令，将 C 语言开发的相关自动命令，组织在 “cprogram” 组内。详情亦见 vimrc 配置文件 中的自动代码折叠。\n1 2 3 4 5 :augroup cprograms : autocmd! : autocmd FileReadPost *.c :set cindent : autocmd FileReadPost *.cpp :set cindent :augroup END Copied! 如果我们针对同样的文件和同样的事件定义了多条自动命令，那么当满足触发条件时将分别执行多条自动命令。因此，建议在自动命令组的开头增加 :autocmd! 命令，以确保没有重复的自动命令存在。\n快速修复窗口 Quickfix 插件提供的功能，对编译调试程序非常有用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 :cope[n] [height] # 打开 Quickfix 窗口以显示当前的错误列表，height 表示高度，单位为行 :lop[en] [height] # 打开 Quickfix 窗口以显示当前窗口的位置列表 :ccl[ose] # 关闭 Qiuckfix 窗口 :lcl[ose] # 关闭显示当前窗口位置列表的窗口 :cw[indow] [height] # 出现识别错误时打开 Quickfix 窗口；否则什么也不做 :lw[indow] [height] # 等价于 :cwindow，除了使用显示当前窗口的位置列表 :cbo[ttom] # 将光标放在 Quickfix 窗口的最后一行并滚动以使其可见 :lbo[ttom] # 等价于 :cbottom，除了使用显示当前位置列表的窗口 :copen 10 # 打开 Quickfix 窗口，并且设置高度为 10 :cfir[st] # 跳转到 Quickfix 窗口中第 1 个错误 :cla[st] # 跳转到 Quickfix 窗口中最后 1 个错误 :cl[ist] # 在 Quickfix 窗口中列出所有错误 :cc [N] # 显示第 N 个错误的详细信息 :cn[ext] # 定位到 Quickfix 窗口中下一个错误 :cp[rev] # 定位到 Quickfix 窗口中上一个错误 :cold[er] # 到前一个旧列表 :cnew # 到后一个新列表 Copied! 文件编码 1 2 3 4 5 6 7 8 9 :e ++enc=utf8 {file} # UTF-8 编码打开 file 文件。 :w ++enc=gbk # 不管当前文件什么编码，把它转存成 GBK 编码。 :set encoding # Vim 内部编码查看 :set encoding=UTF-8 # Vim 内部编码修改 :set fileencoding # 当前编辑的文件字符编码查看 :set fileencoding=UTF-8 # 当前编辑的文件字符编码修改 :set fileencodings # 查看 Vim 自动探测 fileencoding 的顺序列表 :set fileencodings=UTF-8 # 修改 Vim 自动探测 fileencoding 的顺序列表 :h[elp] mbyte-options # 查看 文件编码 帮助文档 Copied! 文件编码说明。\nencoding（缩写：enc）: Vim 内部使用的字符编码方式，包括 Vim 的缓冲区、菜单文本、消息文本等。这个值一般用户不要设置，另外打开 Vim 之后再设置这个值也是没有意义的。大家可以将这个值看作是 Vim 程序自己的变量，如果在工作中遇到文件的编码问题，和 encoding 这个变量是万万没有关系的。 fileencoding（缩写：fenc）: Vim 中当前编辑的文件的字符编码方式，Vim 保存文件时也会将文件保存为这种字符编码方式。 fileencodings（缩写：fencs）: Vim 自动探测 fileencodings 的顺序列表，启动时会按照它所列出的字符编码方式 从前到后，逐一探测 即将打开的文件的字符编码方式，并且将 fileencoding 设置为最终探测到的字符编码方式。因此最好将 Unicode 编码方式放到这个列表的最前面，将拉丁语系编码方式 latin1 放到最后面。 termencoding（缩写：tenc）: Vim 所工作的终端字符编码方式。如果在终端环境下使用 Vim，需要设置 termencoding 和终端所使用的编码一致。 文件编码种类。\nucs-bom：非常严格的编码，非该编码的文件几乎没有可能被误判为 ucs-bom，因此一般放在第一位。 utf-8：相当严格的编码，除了很短的文件之外也是几乎不可能被误判的，因此一般放在第二位。 chinese：相对宽松的编码，在 Unix 里表示 GB2312，在 Windows 里表示 cp936，也就是 GBK 的别名。 cp936：相对宽松的编码，cp936 是 GBK 的别名，是 GB2312 的超集，可以支持繁体汉字，也避免出现删除半个汉字的情况。 latin1：最为宽松的编码，则放在列表的最后。 如果编码被误判了，解码后的结果就会显示为无法识别的乱码了。此时，如果你知道这个文件的正确编码，可以把 fileencodings 改成只有这一种编码，阻止任何 fall-back 发生，然后重新打开这个文件。\n帮助信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 :h[elp] {command} # 显示相关命令的帮助，也可以就输入 :help 而不跟命令，退出帮助需要输入 :q :h tutor # 入门文档 :h quickref # 快速帮助 :h index # 查询 Vim 所有键盘命令定义 :h summary # 帮助你更好的使用内置帮助系统 :h Ctrl+H # 查询普通模式下 Ctrl+H 是干什么的 :h i_Ctrl+H # 查询插入模式下 Ctrl+H 是干什么的 :h i_\u0026lt;Up\u0026gt; # 查询插入模式下方向键上是干什么的 :h pattern.txt # 正则表达式帮助 :h eval # 脚本编写帮助 :h function-list # 查看 VimScript 的函数列表 :h windows.txt # 窗口使用帮助 :h tabpage.txt # 标签页使用帮助 :h +timers # 显示对 +timers 特性的帮助 :h :! # 查看如何运行外部命令 :h tips # 查看 Vim 内置的常用技巧文档 :h set-termcap # 查看如何设置按键扫描码 :viu[sage] # 显示普通命令的帮助。目的是为了模拟对应的 Nvi 命令 :exu[sage] # 显示 Ex 命令的帮助。目的是为了模拟对应的 Nvi 命令 :ve[rsion] # 查看 Vim 版本，同时也查看 Vim 载入配置文件的优先顺序及所在位置 # -------------------- 常用帮助信息 -------------------- :h aug[roup] # 查看自动命令组的帮助文档 :h internal-variables # 获取更多变量作用域的帮助信息 Copied! 有点意思 1 2 3 4 5 6 xp # 交换两个字符 ddp # 交换两行 bi # 单词前加入字符 ea # 单词后加入字符 [N]r\u0026lt;Enter\u0026gt; # 用一个换行符替换 N 个字符 g Ctrl+G # 单词统计，按 g 再同时按 Ctrl+g 组合键 Copied! 约定规范 本文档使用的各种特定字符，按照以下内容进行约定规范。\n1 2 3 4 5 6 7 8 9 [] # 表示方括号里的字符可选，可达到减少键盘输入及重复操作的目的 [count] # 可选的数值，可用在命令前以倍数或重复该命令 [\u0026#34;x] # 可选的用于存储文本的寄存器。参见 registers，x 代表从 \u0026#39;a-zA-z\u0026#39; 的英文字母 {} # 花括号里的内容是命令中必须出现的，但是可以取不同的值 {char1-char2} # 在 char1 到 char2 区间内的 1 个字符，例如 {a-z} 是一个小写字母 {motion} # 表示动作，移动光标的命令或动作 {operator} # 表示操作符，用于对文本进行删除或修改操作的命令 {visual} # 选中的文本区域，先用 v、V 或者 Ctrl+v 设定开始位置，然后用移动光标的命令来选定选择文本的另一端 Ctrl+{char} # 作为控制字符输入的 {char}；即按住 Ctrl 键再按 {char}，{char} 是大写字母还是小写字母都一样 Copied! 按键说明 下面这些按键的名称文档里会用到。它们也可以用在 :map 映射命令里，详情参见 vimrc 配置文件 。\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 标识符 # 含义 \u0026lt;Nul\u0026gt; # 数字 0 \u0026lt;BS\u0026gt; # 退格键 \u0026lt;Backspace\u0026gt; \u0026lt;Tab\u0026gt; # 制表符 \u0026lt;Tab\u0026gt; \u0026lt;NL\u0026gt; # 换行符 \u0026lt;FF\u0026gt; # 换页符 \u0026lt;CR\u0026gt; # 回车符 \u0026lt;Enter\u0026gt; \u0026lt;Return\u0026gt; # 同 \u0026lt;CR\u0026gt; 即 \u0026lt;Return\u0026gt; \u0026lt;Enter\u0026gt; # 同 \u0026lt;CR\u0026gt; 即 \u0026lt;Enter\u0026gt; \u0026lt;Esc\u0026gt; # 转义 \u0026lt;Space\u0026gt; # 空格键 \u0026lt;lt\u0026gt; # 小于号 \u0026lt; \u0026lt;Bslash\u0026gt; # 反斜杠 \\ \u0026lt;Bar\u0026gt; # 竖杠 | \u0026lt;Del\u0026gt; # 删除键 \u0026lt;CSI\u0026gt; # 命令序列引入 \u0026lt;xCSI\u0026gt; # 图形界面的 CSI \u0026lt;EOL\u0026gt; # 行尾 (可以是 \u0026lt;CR\u0026gt;、\u0026lt;NL\u0026gt; 或 \u0026lt;CR\u0026gt;\u0026lt;NL\u0026gt;，根据不同的系统和 fileformat 而定) \u0026lt;Up\u0026gt; # 光标上移键 \u0026lt;Down\u0026gt; # 光标下移键 \u0026lt;Left\u0026gt; # 光标左移键 \u0026lt;Right\u0026gt; # 光标右移键 \u0026lt;S-Up\u0026gt; # Shift ＋ 光标上移键 \u0026lt;S-Down\u0026gt; # Shift ＋ 光标下移键 \u0026lt;S-Left\u0026gt; # Shift ＋ 光标左移键 \u0026lt;S-Right\u0026gt; # Shift ＋ 光标右移键 \u0026lt;C-Left\u0026gt; # Ctrl ＋ 光标左移键 \u0026lt;C-Right\u0026gt; # Ctrl ＋ 光标右移键 \u0026lt;F1\u0026gt;-\u0026lt;F12\u0026gt; # 功能键 1 到 12 \u0026lt;S-F1\u0026gt;-\u0026lt;S-F12\u0026gt; # Shift ＋ 功能键 1 到 12 \u0026lt;Help\u0026gt; # 帮助键 \u0026lt;Undo\u0026gt; # 撤销键 \u0026lt;Insert\u0026gt; # Insert 键 \u0026lt;Home\u0026gt; # Home \u0026lt;End\u0026gt; # End \u0026lt;PageUp\u0026gt; # Page-up \u0026lt;PageDown\u0026gt; # Page-down \u0026lt;kHome\u0026gt; # 小键盘 Home (左上) \u0026lt;kEnd\u0026gt; # 小键盘 End (左下) \u0026lt;kPageUp\u0026gt; # 小键盘 Page-up (右上) \u0026lt;kPageDown\u0026gt; # 小键盘 Page-down (右下) \u0026lt;kPlus\u0026gt; # 小键盘 + \u0026lt;kMinus\u0026gt; # 小键盘 - \u0026lt;kMultiply\u0026gt; # 小键盘 * \u0026lt;kDivide\u0026gt; # 小键盘 / \u0026lt;kEnter\u0026gt; # 小键盘 Enter \u0026lt;kPoint\u0026gt; # 小键盘 小数点 \u0026lt;k0\u0026gt;-\u0026lt;k9\u0026gt; # 小键盘 0 到 9 \u0026lt;S-...\u0026gt; # Shift ＋ 其它键 \u0026lt;C-...\u0026gt; # Ctr ＋ 其它键 \u0026lt;M-...\u0026gt; # Alt ＋ 键 或 Meta ＋ 键 \u0026lt;A-...\u0026gt; # 同 \u0026lt;M-...\u0026gt; Copied! 使用建议 多使用 :h[elp] {command} 获取显示相关命令的帮助文档，提高相关命令的认识水平。 永远不要用 Ctrl+C 代替 完全不同的含义，容易错误中断运行的后台脚本。 很多人使用 Ctrl+[ 代替 ，左手小指 Ctrl，右手小指 [ 熟练后很方便。 某些终端中使用 Vim 8 内嵌终端如看到奇怪字符，使用 :set t_RS= t_SH= 解决。 某些终端中使用 NeoVim 如看到奇怪字符，使用 :set guicursor= 解决。 多使用 ciw, ci[, ci\u0026rdquo;, ci( 以及 diw, di[, di\u0026rdquo;, di( 命令来快速改写/删除文本。 在行内左右移动光标时，多使用 w b e 或 W B E，而不是 h l 或方向键，这样会快很多。 Shift 上档键相当于移动加速键， w b e 移动光标很慢，但是 W B E 走的很快。 自己要善于总结新技巧，例如移动到行首非空字符时用 0w 命令比 ^ 命令更容易输入。 在空白行使用 dip 命令可以删除所有临近的空白行，viw 可以选择连续空白。 缩进时使用 \u0026gt;8j \u0026gt;} ap =i} == 会方便很多；文档以 常规、6、9 个空格间隔缩进。 插入模式下，当你发现一个单词写错了，应该多用 Ctrl+W 这比 退格键快。 y d c 命令可以很好结合 f t 和 /X 例如 dt) 和 y/End。 c d x 命令会自动填充寄存器 \u0026ldquo;1 到 \u0026ldquo;9 ，y 命令会自动填充 \u0026ldquo;0 寄存器。 用 v 命令选择文本时，可以用 0 掉头选择，有时很有用。 写文章时，可以写一段代码块，然后选中后执行 :!python 代码块就会被替换成结果。 搜索后经常使用 :nohl 来消除高亮，使用很频繁，可以 map 到 上。 搜索时可以用 Ctrl+R Ctrl+W 插入光标下的单词，命令模式也能这么用。 映射按键时，应该默认使用 noremap ，只有特别需要的时候使用 map。 用 y 复制文本后，命令模式中 Ctrl+R 然后按双引号 0 可以插入之前复制内容。 Windows 下的 GVim 可以设置 set rop=type:directx, renmode:5 增强显示。 当你觉得做某事很低效时，你应该停下来，然后思考正确的高效方式来完成。 网络资源 最新版本 Vim：https://github.com/vim/vim Windows 版：https://github.com/vim/vim-win32-installer/releases 插件浏览：http://vimawesome.com 正确设置 Alt 换挡键：http://www.skywind.me/blog/archives/2021 视频教程：http://vimcasts.org/ 中文帮助：http://vimcdoc.sourceforge.net/doc/help.html 中文版入门到精通：https://github.com/wsdjeg/vim-galore-zh_cn 五分钟脚本入门：http://www.skywind.me/blog/archives/2193 脚本精通：http://learnvimscriptthehardway.stevelosh.com/ 十六年使用经验：http://zzapper.co.uk/vimtips.html 配色方案：http://vimcolors.com/ vim键盘图 其他图 正则表达式的模式 默认是 magic,正则表达式元字符如 .、*、? 等是有效的 nomagic 模式下，只有 ^ 和 $ 仍作为元字符，其它的元字符需要转义才能使用 very magic 模式下，几乎所有字符都被视为元字符，不需要转义 very nomagic 模式下，几乎所有字符都被视为普通字符，必须显式转义 Ctrl按键 CtrlKey Normal Insert Visual Command C-a 将光标下的数字加1 插入上一次插入的文本(i文本escaC-a) 将光标下的数字加1 C-b 向上滚动一页 光标左移一个字符（终端中的行为） C-c 退出插入模式 中断当前命令 C-d 向下滚动半页 减少缩进 C-e 向下滚动一行 插入一个来自插入点以下的字符(若光标所在列在下一行存在字符)（终端中的行为） C-f 向下滚动一页 光标右移一个字符（终端中的行为） C-g 显示文件信息 C-h 无功能 删除前一个字符（等同于退格键） C-i 跳转到下一个位置(前进) 插入一个制表符（等同于 Tab） C-j 无功能 插入新行（等同于 CR和C-m） C-k 无功能 无功能 C-l 无功能(清除并重绘屏幕) 无功能 C-m 插入新行（等同于 CR和C-j） C-n 选择补全菜单中的下一项 C-o 光标跳转后,回到原来的位置(后退) 插入模式中的单命令执行：在插入模式下按 Ctrl-O，你可以临时执行一个普通模式命令，然后自动返回插入模式 C-p 选择补全菜单中的上一项 C-q 进入可视块模式(等同于C-v)（在某些终端中） C-r 按下u撤销输入后, 重做 插入寄存器内容 插入寄存器内容 C-s 无默认行为（在某些终端中暂停输出） C-t 增加缩进 C-u 向上滚动半页 删除当前行之前的所有字符 删除当前行之前的所有字符 C-v 进入可视块模式 C-w 窗口命令前缀 删除光标前一个单词 C-x 进入 Ctrl-X 模式（用于补全） C-y 向上滚动一行 从上一行复制字符到光标处 C-z 挂起 Vim（在 Unix 系统中） C-[ 退出插入模式（等同于 Esc） C-\\ 进入插入模式的 Ctrl-\\ 模式 C-] 跳转到光标下标签定义处 C-^ 切换到上一个缓冲区 C-@ 插入 NUL 字符 C-0到 C-9 无默认行为 无默认行为 无默认行为 无默认行为 Ctrl-X模式 在 Vim 中，Ctrl-X 模式是插入模式下的一组补全命令，可以提供多种类型的补全。以下是常用的 Ctrl-X 模式补全类型及其作用：\n常见的 Ctrl-X 模式补全类型 文件名补全（Ctrl-X Ctrl-F） 快捷键：Ctrl-X Ctrl-F 功能：补全文件名。 用法：输入文件路径的部分内容，然后按 Ctrl-X Ctrl-F 来补全文件名。 关键字补全（Ctrl-N 和 Ctrl-P） 快捷键：Ctrl-N（下一个）、Ctrl-P（上一个） 功能：补全当前缓冲区中出现的关键字。 用法：输入关键字的部分内容，然后按 Ctrl-N 或 Ctrl-P 来补全。 字典补全（Ctrl-X Ctrl-K） 快捷键：Ctrl-X Ctrl-K 功能：根据字典文件补全单词。 用法：输入单词的部分内容，然后按 Ctrl-X Ctrl-K 来补全。 标签补全（Ctrl-X Ctrl-]） 快捷键：Ctrl-X Ctrl-] 功能：根据标签补全。 用法：输入标签的部分内容，然后按 Ctrl-X Ctrl-] 来补全。 文件中的定义补全（Ctrl-X Ctrl-I） 快捷键：Ctrl-X Ctrl-I 功能：补全当前文件中所有包含的定义。 用法：输入单词的部分内容，然后按 Ctrl-X Ctrl-I 来补全。 用户定义补全（Ctrl-X Ctrl-U） 快捷键：Ctrl-X Ctrl-U 功能：使用用户定义的补全方法。 用法：输入单词的部分内容，然后按 Ctrl-X Ctrl-U 来补全。 拼写建议补全（Ctrl-X Ctrl-S） 快捷键：Ctrl-X Ctrl-S 功能：提供拼写建议。 用法：输入单词的部分内容，然后按 Ctrl-X Ctrl-S 来补全。 完整的 Ctrl-X 补全菜单 Ctrl-X Ctrl-L：整行补全。 Ctrl-X Ctrl-V：补全 Vim 选项。 Ctrl-X Ctrl-D：宏定义补全。 Ctrl-X Ctrl-O：补全 Omni 语法（如 LSP 补全）。 示例操作 文件名补全 输入部分文件路径，例如 ~/do。 按 Ctrl-X Ctrl-F，Vim 会尝试补全文件名，例如补全为 ~/documents/。 关键字补全 输入部分单词，例如 var. 按 Ctrl-N 或 Ctrl-P，Vim 会在当前缓冲区中搜索关键字并补全，例如补全为 various。 标签补全(代码中的标识符，如函数名、变量名、类名等) 输入部分标签名，例如 fea。 按 Ctrl-X Ctrl-]，Vim 会根据标签补全，例如补全为 features。 ","date":"2024-06-03T03:49:51+08:00","image":"https://logan.1357810.xyz/cover/pic_105.jpg","permalink":"https://qh.1357810.xyz/articles/linux/vim/","title":"Vim使用教程"},{"content":" hugo博客 配置baseUrl的坑 本地启动 不管配置文件怎么改，默认的baseUrl都为localhost，bind的ip为127.0.0.1; 生成的public里的html中的url都不会是你配置文件里的baseUrl -D包含草稿 1 hugo server -D Copied! 除非在后面加上 \u0026ndash;baseURL 1 hugo server -D --baseURL http://www.xxx.com/ Copied! 启动局域网连接 1 hugo server -D --bind=0.0.0.0 --port=1313 Copied! 所以本地开发测试时，不需要关心baseUrl，server启动时就一定是localhost 本地或服务器打包 打包时，hugo会严格按照配置文件中的baseUrl创建静态页面，与--environment无关 --gc构建站点时会自动执行垃圾回收;--minify对输出的 HTML、CSS 和 JavaScript 进行压缩和优化 1 hugo -D --gc --minify Copied! 服务器多环境打包 我的博客，同一套代码在github pages和vercel上都有部署,分别是不同的域名，所以就需要区分两个环境的baseUrl,我是这样做的：\n把原本的hugo.yaml当作是vercel 的配置，复制原本的配置为hugo-git.yaml当作github pages的配置 两个文件只有baseUrl不同 在启动Hugo打包的时候，需要加具体的配置文件名参数 1 2 hugo -D --gc --minify --config hugo.yaml # 在vercel上使用，默认为vercel hugo -D --gc --minify --config hugo-git.yaml #在github action中使用 Copied! ","date":"2024-04-10T00:00:00+08:00","image":"https://logan.1357810.xyz/cover/pic_075.jpg","permalink":"https://qh.1357810.xyz/articles/with-hugo/hugo-baseurl/","title":"hugo博客-配置baseUrl的坑"},{"content":" 放在同一个目录下的图片，支持PhotoSwipe图册 图片自动在一个图册里,默认需要 回车 或者其他元素，改动gallery.ts，注释if (!isNewLineImage) continue; Photo by mymind and Luke Chesser on Unsplash 网络链接支持PhotoSwipe图册 static目录支持PhotoSwipe图册 assets目录支持PhotoSwipe图册 svg不支持PhotoSwipe图册 使用figure短代码引入图片 vh为相对于窗口高度的百分比 短代码的 页绑定图片 和 全局图片 都可以使用PhotoSwipe图册，网络链接不可以\n页绑定图片 全局图片 网络链接 可以写markdown 图片轮播 引入全局资源 图片轮播引入网络链接 img标签 引入图片 使用imgproc，不能使用网络链接 这是本地图片 用gallery短代码引入网络图片 ","date":"2024-04-20T00:00:00+08:00","image":"https://logan.1357810.xyz/cover/pic_105.jpg","permalink":"https://qh.1357810.xyz/articles/with-hugo/picture-example/","title":"图片相关模板"},{"content":" 原生代码块 1 2 3 4 5 window.addEventListener(\u0026#34;onColorSchemeChange\u0026#34;, (colorScheme) =\u0026gt; { yzhanweather.clear() // Stop and clear all animations //yzhanweather.destory() // Destory the instance and free up memory yzhanweather_fun(); }); Copied! highlight shortcode 高亮代码 42{{ range .Pages }} 43\u0026lt;h2\u0026gt;\u0026lt;a href=\u0026#34;{{ .RelPermalink }}\u0026#34;\u0026gt;{{ .LinkTitle }}\u0026lt;/a\u0026gt;\u0026lt;/h2\u0026gt; 44{{ end }} md高亮代码 1 echo \u0026#34;hello\u0026#34; Copied! Gitlab Snippets Shortcode Github Card 1 \\{\\{\u0026lt; github title=\u0026#34;gohugoio/hugo\u0026#34; \u0026gt;\\}\\} Copied! Quote Shortcode Stack adds a quote shortcode. For example:\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n― A famous person, The book they wrote bilibilibi Shortcode,1为自动播放，0为手动播放 网易云,1为自动播放，0为手动播放 获取电影数据，打包时需要魔法；把\\去掉 {{\u0026lt; neodb \u0026ldquo;https://neodb.social/book/5SJvkuHNGL4XhBddW2J4EJ\" \u0026gt;}} {{\u0026lt; neodb \u0026ldquo;https://neodb.social/movie/1bgVODaWCBKlCQ1AuGlLzC\" \u0026gt;}} {{\u0026lt; neodb \u0026ldquo;https://neodb.social/tv/season/5es8Us1HHOhVz3UlLmTspr\" \u0026gt;}} {{\u0026lt; neodb \u0026ldquo;https://neodb.social/game/5pvs201VxbkldH4LOEtDVt\" \u0026gt;}} {{\u0026lt; neodb \u0026ldquo;https://neodb.social/podcast/5tlY7lSI0WfXcoHstz7u4S\" \u0026gt;}}\n测试标题，一级(顶级) 奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会\n二级 奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会\n二级 奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会\n三级 奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会\n三级 奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会\n四级 奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会\n四级 奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会\n五级 奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会\n五级 奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会\n六级 奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会\n####### 七级 没有处理，没有样式 奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会\n时间轴 2023-09-10 聊天气泡 John Doe\u0026nbsp;\u0026nbsp;\u0026nbsp;2023-09-12 14:30 这是左边的消息内容。 2023-09-12 14:45\u0026nbsp;\u0026nbsp;\u0026nbsp;Alice 这是右边的消息内容，测试长长长长长长长长长长长长长长长长长长长长长长长长度。 文字渐变色 我挑的配色很好看吧！\n好喜欢蓝色（再次）（再次）\n但总之换行的话就加个空标签。\n文字高斯模糊 一些手动打码效果！\n但总之换行的话就加个空标签。\ngithub卡片 内容折叠 用法 在markdown里写下 键盘标签 在 Windows 操作系统中，「复制」功能的快捷键是：CTRL + C\n总星数 实际星数 卡片 可以在这里插入链接假装是卡片式链接。 好像不能插入图片？ 换行需要空标签。实际使用需要双括号。 代码折叠 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 37 38 39 40 41 42 43 44 45 46 package com.logan; import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer; import io.lettuce.core.ReadFrom; import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; /** * @author logan * @version 1.0 * @date 2022/5/24 */ @Configuration public class RedisConfig { /** * 配置redis集群的读写分离 */ @Bean public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer() { return clientConfigurationBuilder -\u0026gt; clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED); } /** * 集群客户端是不支持多数据库db的，只有一个数据库默认是SELECT 0; */ @Bean public RedisTemplate customRedisTemplate(LettuceConnectionFactory lettuceConnectionFactory) { //创建新的redisTemplate RedisTemplate\u0026lt;String, Object\u0026gt; redisTemplate = new RedisTemplate\u0026lt;\u0026gt;(); //设置key序列化 redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); //设置value序列化 redisTemplate.setValueSerializer(new GenericFastJsonRedisSerializer()); // redisTemplate.setValueSerializer(RedisSerializer.json()); redisTemplate.setHashValueSerializer(RedisSerializer.json()); //设置连接工厂 redisTemplate.setConnectionFactory(lettuceConnectionFactory); return redisTemplate; } } Copied! 卡片链接 hugo-theme-lunaA simple, performance-first, SEO-friendly Hugo theme hugo-theme-luna. https://github.com/Ice-Hazymoon/hugo-theme-luna ","date":"2024-04-10T00:00:00+08:00","image":"https://logan.1357810.xyz/cover/pic_057.jpg","permalink":"https://qh.1357810.xyz/articles/with-hugo/other-example/","title":"其他模板"},{"content":" Markdown语法指南 概述 Markdown 由 Daring Fireball 创建，原始指南在 这里 。但是，它的语法因不同的解析器或编辑器而异。Typora\n正在使用 [GitHub Flavored Markdown][GFM]。\n块元素 段落和换行符 在 普通文本中/普通段落/引用(quote) 中遵循以下规律;\n在代码块或者有序或者无序列表中,一个回车就是换行\n语法层面：\n使用两个以上空格加上回车\n注意某些非markdown的编辑器会自动删除行尾空格，比如使用了 EditorConfig 的, 需要设置:\n1 2 [*.md] # 对所有 md 文件生效 trim_trailing_whitespace = false # 保留行末的空格 Copied! 也可以在段落后面使用一个空行来表示重新开始一个段落\n在Typora中\n编辑-\u0026gt;空格与换行-\u0026gt;取消‘保留单换行符’， 即严格换行模式 您只需按下 Return 即可创建新段落。 按 Shift + Return 可创建单个换行符。但是，大多数 markdown 解析器将忽略单行中断，要使其他 markdown 解析器识别您的换行符，可以在行尾留下两个空格，或者插入 \u0026lt;br/\u0026gt;. 在Obsidian中\n使用easy-typing-obsidian 插件，可以保证在普通段落编辑时，每次按回车换行时，生成两个换行符 也可以使用 在行尾留下两个空格， 然后按回车， 来手动换行 按 Shift + Return 可创建单个换行符， 渲染时会变成同一行 标题 标题在行的开头使用1-6个＃字符，对应于标题级别1-6。例如：\n# 这是一级标题 ## 这是二级标题 ###### 这是六级标题 Copied! 在typora中，输入＃后跟标题内容，按下 Return 键将创建标题。\n引用文字 Markdown 使用 \u0026gt; 字符进行块引用。在typora中，只需输入\u0026rsquo;\u0026gt;\u0026lsquo;后跟引用内容即可生成块引用。Typora将为您插入正确的“\u0026gt;”或换行符。通过添加额外级别的“\u0026gt;”允许在块引用内嵌入另一个块引用。它们表示为：\n1 2 3 4 5 6 7 8 \u0026gt; 这是一级缩进 \u0026gt;\u0026gt; 这是二级缩进 \u0026gt;\u0026gt;\u0026gt; 这是三级缩进 \u0026gt; \u0026gt;\u0026gt; 再来一个二级缩进 \u0026gt;\u0026gt;\u0026gt; 再来一个三级缩进 \u0026gt;\u0026gt; 这是新的块引用 Copied! 效果如下：\n这是一级缩进\n这是二级缩进\n这是三级缩进\n再来一个二级缩进\n再来一个三级缩进\n这是新的块引用\n列表 输入 - list item 1 将创建一个无序列表，该 - 符号可以替换为 + 或 *.\n输入 1. list item 1 将创建一个有序列表，有序和无序列表都可以缩进, 其 markdown 源代码如下：\n#### 无序列表 - 红色 - 绿色 - 蓝色 #### 有序列表 1. 红色 1. 绿色 2. 蓝色 Copied! 效果：\n无序列表 红色 绿色 蓝色 有序列表 红色 绿色 蓝色 任务列表 任务列表是标记为[ ]或[x]（未完成或完成）的项目的列表。例如：\n- [ ] 这是一个任务列表项 - [ ] 需要在前面使用列表的语法 - [ ] normal **formatting**, @mentions, #1234 refs - [ ] 未完成 - [x] 完成 Copied! 效果：\n这是一个任务列表项 需要在前面使用列表的语法 normal formatting, @mentions, #1234 refs 未完成 完成 您可以通过单击项目前面的复选框来更改完成/未完成状态。\n（栅栏式）代码块 Typora仅支持 Github Flavored Markdown 中的栅栏式代码块。不支持 markdown 中的原始代码块。\n使用栅栏式代码块很简单：输入```之后输入一个可选的语言标识符，然后按return键后输入代码，我们将通过语法高亮显示它：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 这是一个例子： ``` function test() { console.log(\u0026#34;notice the blank line before this function?\u0026#34;); } ``` 语法高亮： ```javascript function test() { console.log(\u0026#34;notice the blank line before this function?\u0026#34;); } ``` Copied! 还可以给代码块设置额外的class、行号是否显示、缩进等\n1 2 3 4 5 ```javascript{class=\u0026#34;md-reference-block\u0026#34; lineNos=false tabWidth=2} function test() { console.log(\u0026#34;notice the blank line before this function?\u0026#34;); } ``` Copied! 数学公式块 您可以使用 MathJax 渲染 LaTeX 数学表达式。\ntypora中输入 $$, 然后按“return”键将触发一个接受Tex / LaTex源代码的输入区域。\n在 markdown 源文件中，数学公式块是由$$标记包装的 LaTeX 表达式：\n$$ \\mathbf{V}_1 \\times \\mathbf{V}_2 = \\begin{vmatrix} \\mathbf{i} \u0026amp; \\mathbf{j} \u0026amp; \\mathbf{k} \\\\ \\frac{\\partial X}{\\partial u} \u0026amp; \\frac{\\partial Y}{\\partial u} \u0026amp; 0 \\\\ \\frac{\\partial X}{\\partial v} \u0026amp; \\frac{\\partial Y}{\\partial v} \u0026amp; 0 \\\\ \\end{vmatrix} $$ Copied! 表格 输入 | First Header | Second Header | 并按下 return 键将创建一个包含两列的表。\n创建表后，焦点在该表上将弹出一个表格工具栏，您可以在其中调整表格，对齐或删除表格。您还可以使用上下文菜单来复制和添加/删除列/行。\n可以跳过以下描述，因为表格的 markdown 源代码是由typora自动生成的。\n在 markdown 源代码中，它们看起来像这样：\n1 2 3 4 | First Header | Second Header | | ------------- | ------------- | | Content Cell | Content Cell | | Content Cell | Content Cell | Copied! 效果：\nFirst Header Second Header Content Cell Content Cell Content Cell Content Cell 您还可以在表格中包括内联 Markdown 语法，例如链接，粗体，斜体或删除线。\n最后，通过在标题行中包含冒号：您可以将文本定义为左对齐，右对齐或居中对齐：\n1 2 3 4 5 | Left-Aligned | Center Aligned | Right Aligned | | :------------ |:---------------:| -----:| | col 3 is | some wordy text | $1600 | | col 2 is | centered | $12 | | zebra stripes | are neat | $1 | Copied! 效果：\nLeft-Aligned Center Aligned Right Aligned col 3 is some wordy text $1600 col 2 is centered $12 zebra stripes are neat $1 最左侧的冒号表示左对齐的列; 最右侧的冒号表示右对齐的列; 两侧的冒号表示中心对齐的列。\n脚注 您可以在任何位置像这样创建脚注，他会显示在文章结尾处：\n[^1]: 这是脚注的内容 Copied! 使用脚注：\n这是一个需要添加脚注的文本[^1]。 Copied! 效果：\n这是一个需要添加脚注的文本1。\n鼠标移动到‘1’上标中查看脚注的内容。\n水平线 输入 *** 或 --- 或\u0026lt;hr/\u0026gt;在空行上按 return 键将绘制一条水平线。\n效果：\n目录 (TOC) typora中输入 [toc] 然后按 Return 键将创建一个“目录”部分，自动从文档内容中提取所有标题，其内容会自动更新。\n缩进 1、图片缩进 1.1 在列表中 列表中某个层级按下回车后，继续按回车直到光标在无缩进的行首，再粘贴图片语句，移动光标到行首，按下tab键调整缩进，经渲染后图片会配合列表自动缩进； 如果是有序列表，直接按回车到行首会影响后面的前标，要不影响的话，可以使用shift+return，然后删除缩进到行首，然后再回车换行，插入语句，回车换行 但是在普通段落中，用tab键调整图片的缩进，是不可行的，必须要和列表配合; 图片的上下必须用空行与列表隔开； 如：\naaaaaa bbbbbb\ncccccc\ndddddd\neeeeee\n1.2 使用css 但是在某些渲染环境中不能正常显示（如github的渲染）\n这是图片的标题 2、代码块缩进 2.1 在列表中 列表中某个层级按下回车后，继续按回车直到光标在无缩进的行首，再粘贴代码块语句，选中整个代码块语句，按下tab键调整缩进，经渲染后代码块会配合列表自动缩进； 如果是有序列表，直接按回车到行首会影响后面的前标，要不影响的话，可以使用shift+return，然后删除缩进到行首，然后再回车换行，插入语句，回车换行 但是在普通段落中，用tab键调整代码块的缩进，是不可行的，必须要和列表配合; 代码块的上下必须用空行与列表隔开； 如：\naaaaaa bbbbbb\ncccccc\n1 2 3 function calculate(t, i) { return Math.random() * (i - t) + t } Copied! dddddd\neeeeee\n2.2 使用css 与图片用css缩进类似，在某些渲染环境中不能正常显示（如github的渲染）\nfunction calculate(t, i) { return Math.random() * (i - t) + t } 行元素 在您输入后行元素会被立即解析并呈现。在这些span元素上移动光标会将这些元素扩展为markdown源代码。以下将解释这些span元素的语法。\n链接 Markdown 支持两种类型的链接：内联和引用。\n在这两种样式中，链接文本都写在[方括号]内。\n要创建内联链接，请在链接文本的结束方括号后立即使用一组常规括号。在常规括号内，输入URL地址，以及可选的用引号括起来的链接标题。例如：\nThis is [an example](http://example.com/ \u0026#34;Title\u0026#34;) inline link. [This link](http://example.net/) has no title attribute. Copied! 效果：\nThis is an example inline link.\nThis link has no title attribute.\n内部链接 您可以将常规括号内的 href 设置为文档内的某一个标题，这将创建一个书签，允许您在单击后跳转到该部分。\ntypora中需要Command(在Windows上：Ctrl) + 单击 将跳转到标题 块元素处。\n[内部跳转](#脚注) [外部跳转](/articles/with-hugo/page-a#title_a) Copied! 效果：\n内部跳转 外部跳转 链接引用 这种方法使得你可以在文档中的多个地方引用同一个链接，而不需要重复输入链接的地址和标题。\n在文档中的任何位置,创建一个链接引用：\n[bd]: https://www.baidu.com\t\u0026#34;百度搜索\u0026#34; Copied! 使用方式：\n这是一个[百度][bd]链接。 Copied! 效果：\n这是一个百度 链接。\n隐式链接名称快捷方式允许您省略链接的名称，在这种情况下，链接文本本身将用作名称：\n创建： [Google]: https://www.google.com 效果： 这是一个[Google][]链接。 Copied! 效果：\n这是一个Google 链接。\nURL网址 Typora允许您将 URL 作为链接插入，用 \u0026lt;括号括起来\u0026gt;。\n\u0026lt;i@typora.io\u0026gt; 成为 i@typora.io .\nTypora也将自动链接标准URL。例如： www.google.com .\n图片 图像与链接类似， 但在链接语法之前需要添加额外的 ! 字符。 图像语法如下所示：\n![替代文字](/path/to/img.jpg) ![替代文字](/path/to/img.jpg \u0026#34;可选标题\u0026#34;) Copied! 在typora中您可以使用拖放操作从图像文件或浏览器来插入图像。并通过单击图像修改 markdown 源代码。如果图像在拖放时与当前编辑文档位于同一目录或子目录中，则将使用相对路径。\n强调（斜体） Markdown 将星号 (*) 和下划线(_) 视为强调的指示。用一个 * or _ 包裹文本将使用HTML \u0026lt;em\u0026gt; 标签包裹文本。用两个 * or _ 包裹文本将使用HTML \u0026lt;strong\u0026gt; 标签包裹文本。\n1 2 3 \u0026lt;em\u0026gt; 元素用于表示强调文本，通常以斜体样式显示。它的语义意义是“强调”或“重点”，而不是特定的样式。浏览器默认会以斜体显示 \u0026lt;em\u0026gt; 元素的内容，但是它的确切样式取决于 CSS 样式表中的定义。 \u0026lt;strong\u0026gt; 元素用于表示重要文本，通常以加粗样式显示。它的语义意义是“重要”，而不是特定的样式。浏览器默认会以加粗显示 \u0026lt;strong\u0026gt; 元素的内容，但是它的确切样式同样取决于 CSS 样式表中的定义。 Copied! 例如：\n1 2 3 4 5 6 7 *一个星号* _一个下划线_ **两个星号** __两个下划线__ Copied! 效果：\n一个星号\n一个下划线\n两个星号\n两个下划线\n块引用中将忽略单词中的下划线，这通常用在代码和名称中，如下所示：\nwow__great__stuff\ndo__this__and_do__that__and_another_thing.\n要在用作强调分隔符的位置生成文字星号或下划线，可以用反斜杠转义：\n**这个文字被文字星号包围**\n推荐使用*\n代码 要指示代码范围，请使用反引号（`）进行包裹。与预格式化的代码块不同，代码跨度表示正常段落中的代码。例如：\naa`bb`cc Copied! 效果：\naabbcc\n删除线 GFM通过添加语法来创建删除线文本，标准的Markdown中缺少该功能。\n~~错误的文字。~~ 变成 错误的文字。\n下划线 下划线由原始HTML提供支持。\n\u0026lt;u\u0026gt;下划线\u0026lt;/u\u0026gt; 变成 下划线\n表情符号 :smile: 输入表情符号的语法是 :smile:\n在typora中，用户可以通过 ESC 按键触发表情符号的自动完成建议，或者在偏好设置面板里启用后自动触发表情符号。此外，还支持直接从 Edit -\u0026gt; Emoji \u0026amp; Symbols 菜单栏输入UTF8表情符号字符。\n内联数学公式 在typora中，要使用此功能，首先，请在 偏好设置 面板 -\u0026gt; Markdown扩展语法 选项卡中启用它。然后使用 $ 来包裹TeX命令，例如： $\\lim_{x \\to \\infty} \\exp(-x) = 0$ 将呈现为LaTeX命令。\n要触发内联公式的预览提示功能：输入“$”, 然后按 ESC 键, 然后输入TeX命令, 预览工具提示将如下所示：\n下标 在typora中，要使用此功能，首先，请在 偏好设置 面板 -\u0026gt; Markdown扩展语法 选项卡中启用它。然后用 ~ 来包裹下标内容，例如： H~2~O, X~long\\ text~Y\n效果:\nH2O Xlong\\ textY\n上标 在typora中，要使用此功能，首先，请在 偏好设置 面板 -\u0026gt; Markdown扩展语法 选项卡中启用它。然后用 ^ 来包裹上标内容，例如： X^2^。\n效果:\nX^2^\n高亮 在typora中，要使用此功能，首先，请在 偏好设置 面板 -\u0026gt; Markdown扩展语法 选项卡中启用它。然后用 == 来包裹高亮内容，例如： ==highlight==。\n效果:\n==highlight==\nHTML 您可以使用HTML来设置纯 Markdown 不支持的内容，例如， \u0026lt;span style=\u0026quot;color:red\u0026quot;\u0026gt;this text is red\u0026lt;/span\u0026gt; 用于添加红色文本。\n嵌入内容 有些网站提供基于iframe的嵌入代码，您也可以将其粘贴到Markdown中，例如：\n\u0026lt;iframe height=\u0026#39;265\u0026#39; scrolling=\u0026#39;no\u0026#39; title=\u0026#39;Fancy Animated SVG Menu\u0026#39; src=\u0026#39;http://codepen.io/jeangontijo/embed/OxVywj/?height=265\u0026amp;theme-id=0\u0026amp;default-tab=css,result\u0026amp;embed-version=2\u0026#39; frameborder=\u0026#39;no\u0026#39; allowtransparency=\u0026#39;true\u0026#39; allowfullscreen=\u0026#39;true\u0026#39; style=\u0026#39;width: 100%;\u0026#39;\u0026gt;\u0026lt;/iframe\u0026gt; Copied! 视频 您可以使用 \u0026lt;video\u0026gt; HTML标记嵌入视频，例如：\n\u0026lt;video src=\u0026#34;xxx.mp4\u0026#34; /\u0026gt; Copied! 其他 HTML 支持 你可以在这里 找到细节。\n这是脚注的内容\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2024-05-10T00:00:00+08:00","image":"https://logan.1357810.xyz/cover/pic_043.jpg","permalink":"https://qh.1357810.xyz/articles/misc/md/markdown-reference/","title":"Markdown语法指南"},{"content":"http://heavy_code_industry.gitee.io/code_heavy_industry/ https://maven.apache.org/index.html https://mvnrepository.com/ https://www.bilibili.com/video/BV12q4y147e4?spm_id_from=333.337.search-card.all.click 1、maven介绍 ①、概述 Maven 是一款『构建管理』和『依赖管理』的工具。但事实上这只是 Maven 的一部分功能。Maven 本身的产品定位是一款『项目管理工具』\n②、maven工作机制 maven仓库里有：自己项目打包成的jar、第三方jar、maven核心程序需要用到的插件jar（clean、test、package）\n③、功能 从『项目管理』的角度来看，Maven 提供了如下这些功能：\n项目对象模型（POM）：将整个项目本身抽象、封装为应用程序中的一个对象，以便于管理和操作。 全局性构建逻辑重用：Maven 对整个构建过程进行封装之后，程序员只需要指定配置信息即可完成构建。让构建过程从 Ant 的『编程式』升级到了 Maven 的『声明式』。 构件的标准集合：在 Maven 提供的标准框架体系内，所有的构件都可以按照统一的规范生成和使用。 构件关系定义：Maven 定义了构件之间的三种基本关系，让大型应用系统可以使用 Maven 来进行管理 继承关系：通过从上到下的继承关系，将各个子构件中的重复信息提取到父构件中统一管理 聚合关系：将多个构件聚合为一个整体，便于统一操作 依赖关系：Maven 定义了依赖的范围、依赖的传递、依赖的排除、版本仲裁机制等一系列规范和标准，让大型项目可以有序容纳数百甚至更多依赖 插件目标系统：Maven 核心程序定义抽象的生命周期，然后将插件的目标绑定到生命周期中的特定阶段，实现了标准和具体实现解耦合，让 Maven 程序极具扩展性 项目描述信息的维护：我们不仅可以在 POM 中声明项目描述信息，更可以将整个项目相关信息收集起来生成 HTML 页面组成的一个可以直接访问的站点。这些项目描述信息包括： 公司或组织信息\torganization 项目许可证 licenses 开发成员信息 developers issue 管理信息 issueManagement SCM 信息 scm 远程仓库支持 ④、maven坐标 使用三个**『向量』在『Maven的仓库』中唯一的定位到一个『jar』**包。\ngroupId：公司或组织的 id artifactId：一个项目或者是项目中的一个模块的 id version：版本号 三个向量的取值方式\ngroupId：公司或组织域名的倒序，通常也会加上项目名称 例如：com.atguigu.maven artifactId：模块的名称，将来作为 Maven 工程的工程名 version：模块的版本号，根据自己的需要设定 例如：SNAPSHOT 表示快照版本，正在迭代过程中，不稳定的版本 例如：RELEASE 表示正式版本 2、POM介绍 ①、含义 POM：Project Object Model，项目对象模型。和 POM 类似的是：DOM（Document Object Model），文档对象模型。它们都是模型化思想的具体体现。\n②、模型化思想 POM 表示将工程抽象为一个模型，再用程序中的对象来描述这个模型。这样我们就可以用程序来管理项目了。我们在开发过程中，最基本的做法就是将现实生活中的事物抽象为模型，然后封装模型相关的数据作为一个对象，这样就可以在程序中计算与现实事物相关的数据。\n③、pom标签 POM 理念集中体现在 Maven 工程根目录下 pom.xml 这个配置文件中。所以这个 pom.xml 配置文件就是 Maven 工程的核心配置文件。其实学习 Maven 就是学这个文件怎么配置，各个配置有什么用。\n下面是 spring-boot-starter 的 POM 文件，可以看到：除了我们熟悉的坐标标签、dependencies 标签，还有 description、url、organization、licenses、developers、scm、issueManagement 等这些描述项目信息的标签。\n1 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 37 38 39 40 41 42 43 44 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34; xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.5.6\u0026lt;/version\u0026gt; \u0026lt;name\u0026gt;spring-boot-starter\u0026lt;/name\u0026gt; \u0026lt;description\u0026gt;Core starter, including auto-configuration support, logging and YAML\u0026lt;/description\u0026gt; \u0026lt;url\u0026gt;https://spring.io/projects/spring-boot\u0026lt;/url\u0026gt; \u0026lt;organization\u0026gt; \u0026lt;name\u0026gt;Pivotal Software, Inc.\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;https://spring.io\u0026lt;/url\u0026gt; \u0026lt;/organization\u0026gt; \u0026lt;licenses\u0026gt; \u0026lt;license\u0026gt; \u0026lt;name\u0026gt;Apache License, Version 2.0\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;https://www.apache.org/licenses/LICENSE-2.0\u0026lt;/url\u0026gt; \u0026lt;/license\u0026gt; \u0026lt;/licenses\u0026gt; \u0026lt;developers\u0026gt; \u0026lt;developer\u0026gt; \u0026lt;name\u0026gt;Pivotal\u0026lt;/name\u0026gt; \u0026lt;email\u0026gt;info@pivotal.io\u0026lt;/email\u0026gt; \u0026lt;organization\u0026gt;Pivotal Software, Inc.\u0026lt;/organization\u0026gt; \u0026lt;organizationUrl\u0026gt;https://www.spring.io\u0026lt;/organizationUrl\u0026gt; \u0026lt;/developer\u0026gt; \u0026lt;/developers\u0026gt; \u0026lt;scm\u0026gt; \u0026lt;connection\u0026gt;scm:git:git://github.com/spring-projects/spring-boot.git\u0026lt;/connection\u0026gt; \u0026lt;developerConnection\u0026gt;scm:git:ssh://git@github.com/spring-projects/spring-boot.git\u0026lt;/developerConnection\u0026gt; \u0026lt;url\u0026gt;https://github.com/spring-projects/spring-boot\u0026lt;/url\u0026gt; \u0026lt;/scm\u0026gt; \u0026lt;issueManagement\u0026gt; \u0026lt;system\u0026gt;GitHub\u0026lt;/system\u0026gt; \u0026lt;url\u0026gt;https://github.com/spring-projects/spring-boot/issues\u0026lt;/url\u0026gt; \u0026lt;/issueManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; …… \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; Copied! 3、POM的四个层次 ①、超级pom 经过我们前面的学习，我们看到 Maven 在构建过程中有很多默认的设定。例如：源文件存放的目录、测试源文件存放的目录、构建输出的目录……等等。但是其实这些要素也都是被 Maven 定义过的。定义的位置就是：超级 POM。\n关于超级 POM，Maven 官网是这样介绍的：\nThe Super POM is Maven\u0026rsquo;s default POM. All POMs extend the Super POM unless explicitly set, meaning the configuration specified in the Super POM is inherited by the POMs you created for your projects.\n译文：Super POM 是 Maven 的默认 POM。除非明确设置，否则所有 POM 都扩展 Super POM，这意味着 Super POM 中指定的配置由您为项目创建的 POM 继承。\n所以我们自己的 POM 即使没有明确指定一个父工程（父 POM），其实也默认继承了超级 POM。\n就好比一个 Java 类默认继承了 Object 类。\n位置：${MAVEN_HOME}/lib/maven-model-builder-3.5.0.jar里面 org\\apache\\maven\\model\\pom-4.0.0.xml\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --\u0026gt; \u0026lt;!-- START SNIPPET: superpom --\u0026gt; \u0026lt;project\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;repositories\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;central\u0026lt;/id\u0026gt; \u0026lt;name\u0026gt;Central Repository\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;https://repo.maven.apache.org/maven2\u0026lt;/url\u0026gt; \u0026lt;layout\u0026gt;default\u0026lt;/layout\u0026gt; \u0026lt;snapshots\u0026gt; \u0026lt;enabled\u0026gt;false\u0026lt;/enabled\u0026gt; \u0026lt;/snapshots\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;/repositories\u0026gt; \u0026lt;pluginRepositories\u0026gt; \u0026lt;pluginRepository\u0026gt; \u0026lt;id\u0026gt;central\u0026lt;/id\u0026gt; \u0026lt;name\u0026gt;Central Repository\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;https://repo.maven.apache.org/maven2\u0026lt;/url\u0026gt; \u0026lt;layout\u0026gt;default\u0026lt;/layout\u0026gt; \u0026lt;snapshots\u0026gt; \u0026lt;enabled\u0026gt;false\u0026lt;/enabled\u0026gt; \u0026lt;/snapshots\u0026gt; \u0026lt;releases\u0026gt; \u0026lt;updatePolicy\u0026gt;never\u0026lt;/updatePolicy\u0026gt; \u0026lt;/releases\u0026gt; \u0026lt;/pluginRepository\u0026gt; \u0026lt;/pluginRepositories\u0026gt; \u0026lt;build\u0026gt; \u0026lt;directory\u0026gt;${project.basedir}/target\u0026lt;/directory\u0026gt; \u0026lt;outputDirectory\u0026gt;${project.build.directory}/classes\u0026lt;/outputDirectory\u0026gt; \u0026lt;finalName\u0026gt;${project.artifactId}-${project.version}\u0026lt;/finalName\u0026gt; \u0026lt;testOutputDirectory\u0026gt;${project.build.directory}/test-classes\u0026lt;/testOutputDirectory\u0026gt; \u0026lt;sourceDirectory\u0026gt;${project.basedir}/src/main/java\u0026lt;/sourceDirectory\u0026gt; \u0026lt;scriptSourceDirectory\u0026gt;${project.basedir}/src/main/scripts\u0026lt;/scriptSourceDirectory\u0026gt; \u0026lt;testSourceDirectory\u0026gt;${project.basedir}/src/test/java\u0026lt;/testSourceDirectory\u0026gt; \u0026lt;resources\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;directory\u0026gt;${project.basedir}/src/main/resources\u0026lt;/directory\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;/resources\u0026gt; \u0026lt;testResources\u0026gt; \u0026lt;testResource\u0026gt; \u0026lt;directory\u0026gt;${project.basedir}/src/test/resources\u0026lt;/directory\u0026gt; \u0026lt;/testResource\u0026gt; \u0026lt;/testResources\u0026gt; \u0026lt;pluginManagement\u0026gt; \u0026lt;!-- NOTE: These plugins will be removed from future versions of the super POM --\u0026gt; \u0026lt;!-- They are kept for the moment as they are very unlikely to conflict with lifecycle mappings (MNG-4453) --\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;artifactId\u0026gt;maven-antrun-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.3\u0026lt;/version\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;artifactId\u0026gt;maven-assembly-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.2-beta-5\u0026lt;/version\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;artifactId\u0026gt;maven-dependency-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.8\u0026lt;/version\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;artifactId\u0026gt;maven-release-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.5.3\u0026lt;/version\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/pluginManagement\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;reporting\u0026gt; \u0026lt;outputDirectory\u0026gt;${project.build.directory}/site\u0026lt;/outputDirectory\u0026gt; \u0026lt;/reporting\u0026gt; \u0026lt;profiles\u0026gt; \u0026lt;!-- NOTE: The release profile will be removed from future versions of the super POM --\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;release-profile\u0026lt;/id\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;performRelease\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;true\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;inherited\u0026gt;true\u0026lt;/inherited\u0026gt; \u0026lt;artifactId\u0026gt;maven-source-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;attach-sources\u0026lt;/id\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;jar-no-fork\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;inherited\u0026gt;true\u0026lt;/inherited\u0026gt; \u0026lt;artifactId\u0026gt;maven-javadoc-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;attach-javadocs\u0026lt;/id\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;jar\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;inherited\u0026gt;true\u0026lt;/inherited\u0026gt; \u0026lt;artifactId\u0026gt;maven-deploy-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;updateReleaseInfo\u0026gt;true\u0026lt;/updateReleaseInfo\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/profile\u0026gt; \u0026lt;/profiles\u0026gt; \u0026lt;/project\u0026gt; \u0026lt;!-- END SNIPPET: superpom --\u0026gt; Copied! ②、父POM 和 Java 类一样，POM 之间其实也是单继承的。如果我们给一个 POM 指定了父 POM，那么继承关系如下图所示：\n③、有效 POM 概念\n有效 POM 英文翻译为 effective POM，它的概念是这样的——在 POM 的继承关系中，子 POM 可以覆盖父 POM 中的配置；如果子 POM 没有覆盖，那么父 POM 中的配置将会被继承。按照这个规则，继承关系中的所有 POM 叠加到一起，就得到了一个最终生效的 POM。显然 Maven 实际运行过程中，执行构建操作就是按照这个最终生效的 POM 来运行的。这个最终生效的 POM 就是有效 POM，英文叫effective POM。\n查看有效 POM\n​\tmvn help:effective-pom\n​\t控制台会打印出来有效pom\n④、小结 综上所述，平时我们使用和配置的 POM 其实大致是由四个层次组成的：\n超级 POM：所有 POM 默认继承，只是有直接和间接之分。 父 POM：这一层可能没有，可能有一层，也可能有很多层。 当前 pom.xml 配置的 POM：我们最多关注和最多使用的一层。 有效 POM：隐含的一层，但是实际上真正生效的一层。 4、生命周期 ①、构建过程 构建过程包含的主要的环节：\n清理：删除上一次构建的结果，为下一次构建做好准备 编译：Java 源程序编译成 *.class 字节码文件 测试：运行提前准备好的测试程序 报告：针对刚才测试的结果生成一个全面的信息 打包 Java工程：jar包 Web工程：war包 安装：把一个 Maven 工程经过打包操作生成的 jar 包或 war 包存入 Maven 仓库 部署 部署 jar 包：把一个 jar 包部署到 Nexus 私服服务器上 部署 war 包：借助相关 Maven 插件（例如 cargo），将 war 包部署到 Tomcat 服务器上 为了让构建过程自动化完成，Maven 设定了三个生命周期，生命周期中的每一个环节对应构建过程中的一个操作。\n②、三个生命周期 生命周期名称 作用 各个环节 Clean 清理操作相关 pre-clean clean post-clean Site 生成站点相关 pre-site site post-site\ndeploy-site Default 主要构建过程 validate generate-sources\nprocess-sources\ngenerate-resources\nprocess-resources 复制并处理资源文件，至目标目录，准备打包。 compile 编译项目 main 目录下的源代码。 process-classes generate-test-sources process-test-sources generate-test-resources process-test-resources 复制并处理资源文件，至目标测试目录。 test-compile 编译测试源代码。 process-test-classes test 使用合适的单元测试框架运行测试。这些测试代码不会被打包或部署。 prepare-package package 接受编译好的代码，打包成可发布的格式，如JAR。\npre-integration-test integration-test post-integration-test verify install将包安装至本地仓库，以让其它项目依赖。 deploy将最终的包复制到远程的仓库，以让其它开发人员共享；或者部署到服务器上运行（需借助插件，例如：cargo）。 ③、特点 前面三个生命周期彼此是独立的。 在任何一个生命周期内部，执行任何一个具体环节的操作，都是从本周期最初的位置开始执行，直到指定的地方。（本节记住这句话就行了，其他的都不需要记） Maven 之所以这么设计其实就是为了提高构建过程的自动化程度：让使用者只关心最终要干的即可，过程中的各个环节是自动执行的。\n5、仓库和镜像 ①、仓库 本地仓库：在当前电脑上，为电脑上所有 Maven 工程服务 远程仓库：需要联网 局域网：我们自己搭建的 Maven 私服，例如使用 Nexus 技术。 Internet 中央仓库 镜像仓库：内容和中央仓库保持一致，但是能够分担中央仓库的负载，同时让用户能够就近访问提高下载速度，例如：Nexus aliyun 建议：不要中央仓库和阿里云镜像混用，否则 jar 包来源不纯，彼此冲突。\n专门搜索 Maven 依赖信息的网站：https://mvnrepository.com/\n②、镜像 Maven 下载 jar 包默认访问境外的中央仓库，而国外网站速度很慢。改成阿里云提供的镜像仓库，访问国内网站，可以让 Maven 下载 jar 包的时候速度更快。配置的方式是：\n将下面 mirror 标签整体复制到 settings.xml 文件的 mirrors 标签的内部。\n1 2 3 4 5 6 \u0026lt;mirror\u0026gt; \u0026lt;id\u0026gt;nexus-aliyun\u0026lt;/id\u0026gt; \u0026lt;mirrorOf\u0026gt;central\u0026lt;/mirrorOf\u0026gt; \u0026lt;name\u0026gt;Nexus aliyun\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;http://maven.aliyun.com/nexus/content/groups/public\u0026lt;/url\u0026gt; \u0026lt;/mirror\u0026gt; Copied! 6、约定的目录结构 ①、各个目录的作用 另外还有一个 target 目录专门存放构建操作输出的结果。\n②、约定目录结构的意义 Maven 为了让构建过程能够尽可能自动化完成，所以必须约定目录结构的作用。例如：Maven 执行编译操作，必须先去 Java 源程序目录读取 Java 源代码，然后执行编译，最后把编译结果存放在 target 目录。\n③、约定大于配置 Maven 对于目录结构这个问题，没有采用配置的方式，而是基于约定。这样会让我们在开发过程中非常方便。如果每次创建 Maven 工程后，还需要针对各个目录的位置进行详细的配置，那肯定非常麻烦。\n目前开发领域的技术发展趋势就是：约定大于配置，配置大于编码。\n7、依赖的配置 ①、依赖的范围scope 标签的位置：dependencies/dependency/scope\n标签的可选值：compile/test/provided/system/runtime/import\ncompile 和 test 对比 main目录（空间） test目录（空间） 开发过程（时间） 部署到服务器（时间） compile 有效 有效 有效 有效 test 无效 有效 有效 无效 compile 和 provided 对比 main目录（空间） test目录（空间） 开发过程（时间） 部署到服务器（时间） compile 有效 有效 有效 有效 provided 有效 有效 有效 无效 结论 compile：通常使用的第三方框架的 jar 包这样在项目实际运行时真正要用到的 jar 包都是以 compile 范围进行依赖的。比如 SSM 框架所需jar包。\ntest：测试过程中使用的 jar 包，以 test 范围依赖进来。比如 junit。\nprovided：在开发过程中需要用到的“服务器上的 jar 包”通常以 provided 范围依赖进来。比如 servlet-api、jsp-api。而这个范围的 jar 包之所以不参与部署、不放进 war 包，就是避免和服务器上已有的同类 jar 包产生冲突，同时减轻服务器的负担。说白了就是：“服务器上已经有了，你就别带啦！”\ntest scope 在springboot里不会参与打包，runtime provided会打进包里\n②、依赖的传递性 概念\nA 依赖 B，B 依赖 C，那么在 A 没有配置对 C 的依赖的情况下，A 里面能不能直接使用 C？\n传递的原则：\n在 A 依赖 B，B 依赖 C 的前提下，C 是否能够传递到 A，取决于 B 依赖 C 时使用的依赖范围。\nB 依赖 C 时使用 compile 范围：可以传递\nB 依赖 C 时使用 test 或 provided 范围：不能传递，所以需要这样的 jar 包时，就必须在需要的地方明确配置依赖才可以。\n③、依赖的排除 概念\n当 A 依赖 B，B 依赖 C 而且 C 可以传递到 A 的时候，A 不想要 C，需要在 A 里面把 C 排除掉。而往往这种情况都是为了避免 jar 包之间的冲突。\n​\t所以配置依赖的排除其实就是阻止某些 jar 包的传递。因为这样的 jar 包传递过来会和其他 jar 包冲突。\n配置方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.atguigu.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;pro01-maven-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;compile\u0026lt;/scope\u0026gt; \u0026lt;!-- 使用excludes标签配置依赖的排除\t--\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;!-- 在exclude标签中配置一个具体的排除 --\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;!-- 指定要排除的依赖的坐标（不需要写version） --\u0026gt; \u0026lt;groupId\u0026gt;commons-logging\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;commons-logging\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; Copied! ④、scope-import 管理依赖最基本的办法是继承父工程，但是和 Java 类一样，Maven 也是单继承的。如果不同体系的依赖信息封装在不同 POM 中了，没办法继承多个父工程怎么办？这时就可以使用 import 依赖范围。\n典型案例当然是在项目中引入 SpringBoot、SpringCloud 依赖：\n1 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 \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- SpringCloud 依赖导入 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;Hoxton.SR9\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- SpringCloud Alibaba 依赖导入 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-alibaba-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.2.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- SpringBoot 依赖导入 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; Copied! import 依赖范围使用要求：\n打包类型必须是 pom 必须放在 dependencyManagement 中 官网说明如下：\nThis scope is only supported on a dependency of type pom in the \u0026lt;dependencyManagement\u0026gt;section. It indicates the dependency is to be replaced with the effective list of dependencies in the specified POM\u0026rsquo;s \u0026lt;dependencyManagement\u0026gt; section. Since they are replaced, dependencies with a scope of import do not actually participate in limiting the transitivity of a dependency.\n⑤、scope-system 以 Windows 系统环境下开发为例，假设现在 D:\\tempare\\atguigu-maven-test-aaa-1.0-SNAPSHOT.jar 想要引入到我们的项目中，此时我们就可以将依赖配置为 system 范围：\n1 2 3 4 5 6 7 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.atguigu.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;atguigu-maven-test-aaa\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;systemPath\u0026gt;D:\\tempare\\atguigu-maven-test-aaa-1.0-SNAPSHOT.jar\u0026lt;/systemPath\u0026gt; \u0026lt;scope\u0026gt;system\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 但是很明显：这样引入依赖完全不具有可移植性，所以不要使用。如果需要引入体系外 jar 包我们后面会讲专门的办法。\n⑥、scope-runtime 专门用于编译时不需要，但是运行时需要的 jar 包。比如：编译时我们根据接口调用方法，但是实际运行时需要的是接口的实现类。典型案例是：\n1 2 3 4 5 6 7 \u0026lt;!--热部署 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-devtools\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;runtime\u0026lt;/scope\u0026gt; \u0026lt;optional\u0026gt;true\u0026lt;/optional\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 会参与依赖传递 ⑦、optional 配置 1 2 3 4 5 6 7 \u0026lt;!--热部署 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-devtools\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;runtime\u0026lt;/scope\u0026gt; \u0026lt;optional\u0026gt;true\u0026lt;/optional\u0026gt; \u0026lt;/dependency\u0026gt; Copied! runtime true\n正常一个依赖加上这两个配置之后不影响springboot打包，但是springboot打包时没有包括spring-boot-devtools\n应该是springboot自己做了处理\noptional本质含义 可选其实就是『可有可无』。官网的解释是：\n其核心含义是：Project X 依赖 Project A，A 中一部分 X 用不到的代码依赖了 B，那么对 X 来说 B 就是『可有可无』的。\noptional不会参与依赖传递 ⑧、依赖的版本仲裁 最短路径优先 ​\t在下图的例子中，对模块 pro25-module-a 来说，Maven 会采纳 1.2.12 版本。\n路径相同时先声明者优先 此时 Maven 采纳哪个版本，取决于在 pro29-module-x 中，对 pro30-module-y 和 pro31-module-z 两个模块的依赖哪一个先声明。\n其实 Maven 的版本仲裁机制只是在没有人为干预的情况下，自主决定 jar 包版本的一个办法。而实际上我们要使用具体的哪一个版本，还要取决于项目中的实际情况。所以在项目正常运行的情况下，jar 包版本可以由 Maven 仲裁，不必我们操心；而发生冲突时 Maven 仲裁决定的版本无法满足要求，此时就应该由程序员明确指定 jar 包版本。\n8、继承 ①、概念 Maven工程之间，A 工程继承 B 工程\nB 工程：父工程 A 工程：子工程 本质上是 A 工程的 pom.xml 中的配置继承了 B 工程中 pom.xml 的配置。\n只有打包方式为pom的工程才能作为父工程 ②、作用 在父工程中统一管理项目中的依赖信息，具体来说是管理依赖信息的版本。\n它的背景是：\n对一个比较大型的项目进行了模块拆分。 一个 project 下面，创建了很多个 module。 每一个 module 都需要配置自己的依赖信息。 它背后的需求是：\n在每一个 module 中各自维护各自的依赖信息很容易发生出入，不易统一管理。 使用同一个框架内的不同 jar 包，它们应该是同一个版本，所以整个项目中使用的框架版本需要统一。 使用框架时所需要的 jar 包组合（或者说依赖信息组合）需要经过长期摸索和反复调试，最终确定一个可用组合。这个耗费很大精力总结出来的方案不应该在新的项目中重新摸索。 通过在父工程中为整个项目维护依赖信息的组合既保证了整个项目使用规范、准确的 jar 包；又能够将以往的经验沉淀下来，节约时间和精力。\n9、聚合 ①、聚合本身的含义 部分组成整体\n动画片《战神金刚》中的经典台词：“我来组成头部！我来组成手臂！”就是聚合关系最生动的体现。\n②、Maven 中的聚合 使用一个“总工程”将各个“模块工程”汇集起来，作为一个整体对应完整的项目。\n项目：整体 模块：部分 TIP\n概念的对应关系：\n从继承关系角度来看：\n父工程\n子工程\n从聚合关系角度来看：\n总工程 模块工程 ③、好处 一键执行 Maven 命令：很多构建命令都可以在“总工程”中一键执行。\n以 mvn install 命令为例：Maven 要求有父工程时先安装父工程；有依赖的工程时，先安装被依赖的工程。我们自己考虑这些规则会很麻烦。但是工程聚合之后，在总工程执行 mvn install 可以一键完成安装，而且会自动按照正确的顺序执行。\n配置聚合之后，各个模块工程会在总工程中展示一个列表，让项目中的各个模块一目了然。\n④、聚合的配置 在总工程中配置 modules 即可：\n1 2 3 4 5 \u0026lt;modules\u0026gt; \u0026lt;module\u0026gt;pro04-maven-module\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;pro05-maven-module\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;pro06-maven-module\u0026lt;/module\u0026gt; \u0026lt;/modules\u0026gt; Copied! ⑤、依赖循环问题 如果 A 工程依赖 B 工程，B 工程依赖 C 工程，C 工程又反过来依赖 A 工程，那么在执行构建操作时会报下面的错误：\nDANGER\n[ERROR] [ERROR] The projects in the reactor contain a cyclic reference:\n这个错误的含义是：循环引用。\n10、定义jdk版本和编码 ①、直接使用默认取值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;properties\u0026gt; \u0026lt;!-- maven-resources-plugin和maven-compiler-plugin插件的默认取值 --\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;!-- maven-site-plugin等插件的默认取值 --\u0026gt; \u0026lt;project.reporting.outputEncoding\u0026gt;UTF-8\u0026lt;/project.reporting.outputEncoding\u0026gt; \u0026lt;!-- maven-compiler-plugin插件的默认取值 --\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;!-- spring-boot-starter-parent里定义的的默认取值 --\u0026gt; \u0026lt;java.version\u0026gt;1.8\u0026lt;/java.version\u0026gt; \u0026lt;/properties\u0026gt; Copied! ②、或者在插件中定义 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 37 \u0026lt;project\u0026gt; [...] \u0026lt;build\u0026gt; [...] \u0026lt;plugins\u0026gt; \u0026lt;!-- JDK版本 --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.10.1\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;source\u0026gt;1.8\u0026lt;/source\u0026gt; \u0026lt;target\u0026gt;1.8\u0026lt;/target\u0026gt; \u0026lt;encoding\u0026gt;UTF-8\u0026lt;/encoding\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;!-- 编码 --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-resources-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.2.0\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; ... \u0026lt;encoding\u0026gt;UTF-8\u0026lt;/encoding\u0026gt; ... \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; [...] \u0026lt;/build\u0026gt; [...] \u0026lt;/project\u0026gt; Copied! ③、或写在超级pom里，全局设定 配置的方式是：将 profile 标签整个复制到 settings.xml 文件的 profiles 标签内。\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;jdk-1.8\u0026lt;/id\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;activeByDefault\u0026gt;true\u0026lt;/activeByDefault\u0026gt; \u0026lt;jdk\u0026gt;1.8\u0026lt;/jdk\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;maven.compiler.compilerVersion\u0026gt;1.8\u0026lt;/maven.compiler.compilerVersion\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/profile\u0026gt; Copied! 不推荐,仅在本地生效，如果脱离当前的settings.xml能够覆盖的范围，则无法生效\n11、属性的声明与引用 ①、help 插件的各个目标 官网说明地址：https://maven.apache.org/plugins/maven-help-plugin\n目标 说明 help:active-profiles 列出当前已激活的 profile help:all-profiles 列出当前工程所有可用 profile help:describe 描述一个插件和/或 Mojo 的属性 help:effective-pom 以 XML 格式展示有效 POM help:effective-settings 为当前工程以 XML 格式展示计算得到的 settings 配置 help:evaluate 计算用户在交互模式下给出的 Maven 表达式 help:system 显示平台详细信息列表，如系统属性和环境变量 ②、使用help:evaluate查看属性值 定义属性 1 2 3 \u0026lt;properties\u0026gt; \u0026lt;com.atguigu.hello\u0026gt;good morning maven\u0026lt;/com.atguigu.hello\u0026gt; \u0026lt;/properties\u0026gt; Copied! 运行 mvn help:evaluate ③、通过 Maven 访问系统属性 java属性一览 1 2 3 4 5 6 Properties properties = System.getProperties(); Set\u0026lt;Object\u0026gt; propNameSet = properties.keySet(); for (Object propName : propNameSet) { String propValue = properties.getProperty((String) propName); System.out.println(propName + \u0026#34; = \u0026#34; + propValue); } Copied! java.runtime.name = Java(TM) SE Runtime Environment sun.boot.library.path = D:\\software\\Java\\jre\\bin java.vm.version = 25.141-b15 java.vm.vendor = Oracle Corporation java.vendor.url = http://java.oracle.com/ …………\n使用 Maven 访问系统属性 访问系统环境变量 访问 project 属性 ​\t使用表达式 ${project.xxx} 可以访问当前 POM 中的元素值。\n​\t访问一级标签:\n​\t${project.标签名}\n​\t访问子标签:\n​\t${project.标签名.子标签名}\n​\t​\t访问列表标签:\n​\t${project.标签名[下标]}\n​\t访问 settings 全局配置 ​\t${settings.标签名} 可以访问 settings.xml 中配置的元素值。\n​\t④、用途 在当前 pom.xml 文件中引用属性\n资源过滤功能：在非 Maven 配置文件中引用属性，由 Maven 在处理资源时将引用属性的表达式替换为属性值\n创建待处理的资源文件\n1 2 3 4 dev.user=${dev.jdbc.user} dev.password=${dev.jdbc.password} dev.url=${dev.jdbc.url} dev.driver=${dev.jdbc.driver} Copied! 配置 profile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026lt;profiles\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;devJDBCProfile\u0026lt;/id\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;dev.jdbc.user\u0026gt;root\u0026lt;/dev.jdbc.user\u0026gt; \u0026lt;dev.jdbc.password\u0026gt;atguigu\u0026lt;/dev.jdbc.password\u0026gt; \u0026lt;dev.jdbc.url\u0026gt;http://localhost:3306/db_good\u0026lt;/dev.jdbc.url\u0026gt; \u0026lt;dev.jdbc.driver\u0026gt;com.mysql.jdbc.Driver\u0026lt;/dev.jdbc.driver\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;build\u0026gt; \u0026lt;resources\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;!-- 表示为这里指定的目录开启资源过滤功能 --\u0026gt; \u0026lt;directory\u0026gt;src/main/resources\u0026lt;/directory\u0026gt; \u0026lt;!-- 将资源过滤功能打开 --\u0026gt; \u0026lt;filtering\u0026gt;true\u0026lt;/filtering\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;/resources\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/profile\u0026gt; \u0026lt;/profiles\u0026gt; Copied! 执行处理资源命令 1 mvn clean resources:resources -PdevJDBCProfile Copied! 12、profile详解 ①、profile 概述 [1] 单词释义 这里我们可以对接 profile 这个单词中『侧面』这个含义：项目的每一个运行环境，相当于是项目整体的一个侧面。\n[2] 项目的不同运行环境 通常情况下，我们至少有三种运行环境：\n开发环境：供不同开发工程师开发的各个模块之间互相调用、访问；内部使用 测试环境：供测试工程师对项目的各个模块进行功能测试；内部使用 生产环境：供最终用户访问——所以这是正式的运行环境，对外提供服务 而我们这里的『环境』仍然只是一个笼统的说法，实际工作中一整套运行环境会包含很多种不同服务器：\nMySQL Redis ElasticSearch RabbitMQ FastDFS Nginx Tomcat …… 就拿其中的 MySQL 来说，不同环境下的访问参数肯定完全不同：\n开发环境 测试环境 生产环境 dev.driver=com.mysql.jdbc.Driver dev.url=jdbc:mysql://124.71.36.17:3306/db-sys dev.username=root dev.password=atguigu test.driver=com.mysql.jdbc.Driver test.url=jdbc:mysql://124.71.36.89:3306/db-sys test.username=dev-team test.password=atguigu product.driver=com.mysql.jdbc.Driver product.url=jdbc:mysql://39.107.88.164:3306/prod-db-sys product.username=root product.password=atguigu 可是代码只有一套。如果在 jdbc.properties 里面来回改，那就太麻烦了，而且很容易遗漏或写错，增加调试的难度和工作量。所以最好的办法就是把适用于各种不同环境的配置信息分别准备好，部署哪个环境就激活哪个配置。\n在 Maven 中，使用 profile 机制来管理不同环境下的配置信息。但是解决同类问题的类似机制在其他框架中也有，而且从模块划分的角度来说，持久化层的信息放在构建工具中配置也违反了『高内聚，低耦合』的原则。\n所以 Maven 的 profile 我们了解一下即可，不必深究。\n[3] profile 声明和使用的基本逻辑 首先为每一个环境声明一个 profile 环境 A：profile A 环境 B：profile B 环境 C：profile C …… 然后激活某一个 profile [4] 默认 profile 其实即使我们在 pom.xml 中不配置 profile 标签，也已经用到 profile了。为什么呢？因为根标签 project 下所有标签相当于都是在设定默认的 profile。这样一来我们也就很容易理解下面这句话：project 标签下除了 modelVersion 和坐标标签之外，其它标签都可以配置到 profile 中。\n②、profile 配置 [1] 外部视角：配置文件 从外部视角来看，profile 可以在下面两种配置文件中配置：\nsettings.xml：全局生效。其中我们最熟悉的就是配置 JDK 1.8。\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;jdk-1.8\u0026lt;/id\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;activeByDefault\u0026gt;true\u0026lt;/activeByDefault\u0026gt; \u0026lt;jdk\u0026gt;1.8\u0026lt;/jdk\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;maven.compiler.compilerVersion\u0026gt;1.8\u0026lt;/maven.compiler.compilerVersion\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/profile\u0026gt; Copied! pom.xml：当前 POM 生效\n[2] 内部实现：具体标签 从内部视角来看，配置 profile 有如下语法要求：\nprofiles/profile 标签\n由于 profile 天然代表众多可选配置中的一个所以由复数形式的 profiles 标签统一管理。\n由于 profile 标签覆盖了 pom.xml 中的默认配置，所以 profiles 标签通常是 pom.xml 中的最后一个标签。\nid 标签\n每个 profile 都必须有一个 id 标签，指定该 profile 的唯一标识。这个 id 标签的值会在命令行调用 profile 时被用到。这个命令格式是：-D。\n其它允许出现的标签\n一个 profile 可以覆盖项目的最终名称、项目依赖、插件配置等各个方面以影响构建行为。\nbuild defaultGoal finalName resources testResources plugins reporting modules dependencies dependencyManagement repositories pluginRepositories properties ③、激活 profile [1] 默认配置默认被激活 前面提到了，POM 中没有在 profile 标签里的就是默认的 profile，当然默认被激活。\n[2] 基于环境信息激活 环境信息包含：JDK 版本、操作系统参数、文件、属性等各个方面。一个 profile 一旦被激活，那么它定义的所有配置都会覆盖原来 POM 中对应层次的元素。可以参考下面的标签结构：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;dev\u0026lt;/id\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;!-- 配置是否默认激活 --\u0026gt; \u0026lt;activeByDefault\u0026gt;false\u0026lt;/activeByDefault\u0026gt; \u0026lt;!-- 配置激活条件 --\u0026gt; \u0026lt;jdk\u0026gt;1.5\u0026lt;/jdk\u0026gt; \u0026lt;os\u0026gt; \u0026lt;name\u0026gt;Windows XP\u0026lt;/name\u0026gt; \u0026lt;family\u0026gt;Windows\u0026lt;/family\u0026gt; \u0026lt;arch\u0026gt;x86\u0026lt;/arch\u0026gt; \u0026lt;version\u0026gt;5.1.2600\u0026lt;/version\u0026gt; \u0026lt;/os\u0026gt; \u0026lt;property\u0026gt; \u0026lt;name\u0026gt;mavenVersion\u0026lt;/name\u0026gt; \u0026lt;value\u0026gt;2.0.5\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;file\u0026gt; \u0026lt;exists\u0026gt;file2.properties\u0026lt;/exists\u0026gt; \u0026lt;missing\u0026gt;file1.properties\u0026lt;/missing\u0026gt; \u0026lt;/file\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;/profile\u0026gt; Copied! 这里有个问题是：多个激活条件之间是什么关系呢？\nMaven 3.2.2 之前：遇到第一个满足的条件即可激活——或的关系。 Maven 3.2.2 开始：各条件均需满足——且的关系。 下面我们来看一个具体例子。假设有如下 profile 配置，在 JDK 版本为 1.6 时被激活：\n1 2 3 4 5 6 7 8 9 10 \u0026lt;profiles\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;JDK1.6\u0026lt;/id\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;!-- 指定激活条件为：JDK 1.6 --\u0026gt; \u0026lt;jdk\u0026gt;1.6\u0026lt;/jdk\u0026gt; \u0026lt;/activation\u0026gt; …… \u0026lt;/profile\u0026gt; \u0026lt;/profiles\u0026gt; Copied! 这里需要指出的是：Maven 会自动检测当前环境安装的 JDK 版本，只要 JDK 版本是以 1.6 开头都算符合条件。下面几个例子都符合：\n1.6.0_03 1.6.0_02 …… [3] 命令行激活 列出活动的 profile 1 2 # 列出所有激活的 profile，以及它们在哪里定义 mvn help:active-profiles Copied! 指定某个具体 profile 1 mvn compile -P\u0026lt;profile id\u0026gt; Copied! ④、操作举例 [1] 编写 Lambda 表达式代码 Lambda 表达式代码要求 JDK 版本必须是 1.8，我们可以以此来判断某个指定更低 JDK 版本的 profile 是否被激活生效。\n1 2 3 4 5 6 @Test public void test() { new Thread(()-\u0026gt;{ System.out.println(Thread.currentThread().getName() + \u0026#34; is working\u0026#34;); }).start(); } Copied! 以目前配置运行这个测试方法：\n[2] 配置 profile 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 \u0026lt;profiles\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;myJDKProfile\u0026lt;/id\u0026gt; \u0026lt;!-- build 标签：意思是告诉 Maven，你的构建行为，我要开始定制了！ --\u0026gt; \u0026lt;build\u0026gt; \u0026lt;!-- plugins 标签：Maven 你给我听好了，你给我构建的时候要用到这些插件！ --\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;!-- plugin 标签：这是我要指定的一个具体的插件 --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;!-- 插件的坐标。此处引用的 maven-compiler-plugin 插件不是第三方的，是一个 Maven 自带的插件。 --\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1\u0026lt;/version\u0026gt; \u0026lt;!-- configuration 标签：配置 maven-compiler-plugin 插件 --\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 具体配置信息会因为插件不同、需求不同而有所差异 --\u0026gt; \u0026lt;source\u0026gt;1.6\u0026lt;/source\u0026gt; \u0026lt;target\u0026gt;1.6\u0026lt;/target\u0026gt; \u0026lt;encoding\u0026gt;UTF-8\u0026lt;/encoding\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/profile\u0026gt; \u0026lt;/profiles\u0026gt; Copied! [3] 执行构建命令 1 mvn clean test -PmyJDKProfile Copied! ⑤、资源属性过滤 [1] 简介 Maven 为了能够通过 profile 实现各不同运行环境切换，提供了一种『资源属性过滤』的机制。通过属性替换实现不同环境使用不同的参数。\n[2] 操作演示 配置 profile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026lt;profiles\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;devJDBCProfile\u0026lt;/id\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;dev.jdbc.user\u0026gt;root\u0026lt;/dev.jdbc.user\u0026gt; \u0026lt;dev.jdbc.password\u0026gt;atguigu\u0026lt;/dev.jdbc.password\u0026gt; \u0026lt;dev.jdbc.url\u0026gt;http://localhost:3306/db_good\u0026lt;/dev.jdbc.url\u0026gt; \u0026lt;dev.jdbc.driver\u0026gt;com.mysql.jdbc.Driver\u0026lt;/dev.jdbc.driver\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;build\u0026gt; \u0026lt;resources\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;!-- 表示为这里指定的目录开启资源过滤功能 --\u0026gt; \u0026lt;directory\u0026gt;src/main/resources\u0026lt;/directory\u0026gt; \u0026lt;!-- 将资源过滤功能打开 --\u0026gt; \u0026lt;filtering\u0026gt;true\u0026lt;/filtering\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;/resources\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/profile\u0026gt; \u0026lt;/profiles\u0026gt; Copied! 创建待处理的资源文件 1 2 3 4 dev.user=${dev.jdbc.user} dev.password=${dev.jdbc.password} dev.url=${dev.jdbc.url} dev.driver=${dev.jdbc.driver} Copied! 执行处理资源命令 1 mvn clean resources:resources -PdevJDBCProfile Copied! 找到处理得到的资源文件 延伸 ​\t我们时不时会在 resource 标签下看到 includes 和 excludes 标签。它们的作用是：\nincludes：指定执行 resource 阶段时要包含到目标位置的资源 excludes：指定执行 resource 阶段时要排除的资源 情看下面的例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;build\u0026gt; \u0026lt;resources\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;!-- 表示为这里指定的目录开启资源过滤功能 --\u0026gt; \u0026lt;directory\u0026gt;src/main/resources\u0026lt;/directory\u0026gt; \u0026lt;!-- 将资源过滤功能打开 --\u0026gt; \u0026lt;filtering\u0026gt;true\u0026lt;/filtering\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;*.properties\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;excludes\u0026gt; \u0026lt;exclude\u0026gt;happy.properties\u0026lt;/exclude\u0026gt; \u0026lt;/excludes\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;/resources\u0026gt; \u0026lt;/build\u0026gt; Copied! 执行处理资源命令：\n1 mvn clean resources:resources -PdevJDBCProfile Copied! 执行效果如下：\n当然我们这里只是以 properties 文件为例，并不是只能处理 properties 文件。\n","date":"2024-03-03T12:49:51+08:00","image":"https://logan.1357810.xyz/cover/pic_001.jpg","permalink":"https://qh.1357810.xyz/articles/maven/maven-summarize/","title":"maven概述"},{"content":" 一、创建 Maven 工程 1、使用命令生成Maven工程 在工作目录打开命令行\n运行 mvn archetype:generate 命令\n下面根据提示操作\nTIP\nChoose a number or apply filter (format: [groupId:]artifactId, case sensitive contains): 7:【直接回车，使用默认值】\nDefine value for property \u0026lsquo;groupId\u0026rsquo;: com.atguigu.maven\nDefine value for property \u0026lsquo;artifactId\u0026rsquo;: pro01-maven-java\nDefine value for property \u0026lsquo;version\u0026rsquo; 1.0-SNAPSHOT: :【直接回车，使用默认值】\nDefine value for property \u0026lsquo;package\u0026rsquo; com.atguigu.maven: :【直接回车，使用默认值】\nConfirm properties configuration: groupId: com.atguigu.maven artifactId: pro01-maven-java version: 1.0-SNAPSHOT package: com.atguigu.maven Y: :【直接回车，表示确认。如果前面有输入错误，想要重新输入，则输入 N 再回车。】\n2、调整 Maven 默认生成的工程，对 junit 依赖的是较低的 3.8.1 版本，我们可以改成较适合的 4.12 版本。\n自动生成的 App.java 和 AppTest.java 可以删除。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;!-- 依赖信息配置 --\u0026gt; \u0026lt;!-- dependencies复数标签：里面包含dependency单数标签 --\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- dependency单数标签：配置一个具体的依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;!-- 通过坐标来依赖其他jar包 --\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.12\u0026lt;/version\u0026gt; \u0026lt;!-- 依赖的范围 --\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; Copied! 3、自动生成的 pom.xml 解读 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 \u0026lt;!-- modelVersion 标签：从maven2 开始就是固定为 4.0.0，代表当前pom.xml所采用的标签结构 --\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;!-- 当前Maven工程的坐标 --\u0026gt; \u0026lt;groupId\u0026gt;com.atguigu.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;pro01-maven-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;!-- 当前Maven工程的打包方式，可选值有下面三种： --\u0026gt; \u0026lt;!-- jar：表示这个工程是一个Java工程 --\u0026gt; \u0026lt;!-- war：表示这个工程是一个Web工程 --\u0026gt; \u0026lt;!-- pom：表示这个工程是“管理其他工程”的工程 --\u0026gt; \u0026lt;packaging\u0026gt;jar\u0026lt;/packaging\u0026gt; \u0026lt;name\u0026gt;pro01-maven-java\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;http://maven.apache.org\u0026lt;/url\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;!-- 工程构建过程中读取源码时使用的字符集 --\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;!-- 当前工程所依赖的jar包 --\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- 使用dependency配置一个具体的依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;!-- 在dependency标签内使用具体的坐标依赖我们需要的一个jar包 --\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.12\u0026lt;/version\u0026gt; \u0026lt;!-- scope标签配置依赖的范围 --\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; Copied! 二、编写代码 1、主体程序 主体程序指的是被测试的程序，同时也是将来在项目中真正要使用的程序。\n1 2 3 4 5 6 7 8 9 package com.atguigu.maven; public class Calculator { public int sum(int i, int j){ return i + j; } } Copied! 2、测试程序 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 package com.atguigu.maven; import org.junit.Test; import com.atguigu.maven.Calculator; // 静态导入的效果是将Assert类中的静态资源导入当前类 // 这样一来，在当前类中就可以直接使用Assert类中的静态资源，不需要写类名 import static org.junit.Assert.*; public class CalculatorTest{ @Test public void testSum(){ // 1.创建Calculator对象 Calculator calculator = new Calculator(); // 2.调用Calculator对象的方法，获取到程序运行实际的结果 int actualResult = calculator.sum(5, 3); // 3.声明一个变量，表示程序运行期待的结果 int expectedResult = 8; // 4.使用断言来判断实际结果和期待结果是否一致 // 如果一致：测试通过，不会抛出异常 // 如果不一致：抛出异常，测试失败 assertEquals(expectedResult, actualResult); } } Copied! 三、执行maven构建 1、要求 运行 Maven 中和构建操作相关的命令时，必须进入到 pom.xml 所在的目录。如果没有在 pom.xml 所在的目录运行 Maven 的构建命令，那么会看到下面的错误信息：\nThe goal you specified requires a project to execute but there is no POM in this directory\nTIP mvn -v 命令和构建操作无关，只要正确配置了 PATH，在任何目录下执行都可以。而构建相关的命令要在 pom.xml 所在目录下运行——操作哪个工程，就进入这个工程的 pom.xml 目录。\n2、清理操作 mvn clean\n效果：删除 target 目录\n3、编译操作 主程序编译：mvn compile\n测试程序编译：mvn test-compile\n主体程序编译结果存放的目录：target/classes\n测试程序编译结果存放的目录：target/test-classes\n4、测试操作 mvn test\n测试的报告存放的目录：target/surefire-reports\n5、打包操作 mvn package\n打包的结果——jar 包，存放的目录：target\n并没有把依赖的包（maven dependency）一起打在生成的jar包里，这里的打jar包，实际maven认为是一个工具包，是一个后面被项目继续依赖的包，所以没必要把依赖的包打进去。\n如果需要生成一个可执行的包，则需要用maven插件： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-jar-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.4\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;archive\u0026gt; \u0026lt;manifest\u0026gt; \u0026lt;useUniqueVersions\u0026gt;false\u0026lt;/useUniqueVersions\u0026gt; \u0026lt;addClasspath\u0026gt;true\u0026lt;/addClasspath\u0026gt; \u0026lt;classpathPrefix\u0026gt;lib/\u0026lt;/classpathPrefix\u0026gt; \u0026lt;mainClass\u0026gt;com.wyz.Main\u0026lt;/mainClass\u0026gt; \u0026lt;/manifest\u0026gt; \u0026lt;/archive\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; Copied! 可以把第三方包下载到lib目录： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-dependency-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;copy-dependencies\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;package\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;copy-dependencies\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 拷贝项目依赖包到lib/目录下 --\u0026gt; \u0026lt;outputDirectory\u0026gt;${project.build.directory}/lib\u0026lt;/outputDirectory\u0026gt; \u0026lt;!-- 间接依赖也拷贝 --\u0026gt; \u0026lt;excludeTransitive\u0026gt;false\u0026lt;/excludeTransitive\u0026gt; \u0026lt;!-- 带上版本号 --\u0026gt; \u0026lt;stripVersion\u0026gt;false\u0026lt;/stripVersion\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; Copied! 把依赖也打进jar包：mainClass是jar包的main方法入口: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;artifactId\u0026gt;maven-assembly-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;archive\u0026gt; \u0026lt;manifest\u0026gt; \u0026lt;mainClass\u0026gt;com.wyz.Main\u0026lt;/mainClass\u0026gt; \u0026lt;/manifest\u0026gt; \u0026lt;/archive\u0026gt; \u0026lt;descriptorRefs\u0026gt; \u0026lt;descriptorRef\u0026gt;jar-with-dependencies\u0026lt;/descriptorRef\u0026gt; \u0026lt;/descriptorRefs\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; Copied! 6、安装操作 mvn install\n1 2 [INFO] Installing D:\\maven-workspace\\space201026\\pro01-maven-java\\target\\pro01-maven-java-1.0-SNAPSHOT.jar to D:\\maven-rep1026\\com\\atguigu\\maven\\pro01-maven-java\\1.0-SNAPSHOT\\pro01-maven-java-1.0-SNAPSHOT.jar [INFO] Installing D:\\maven-workspace\\space201026\\pro01-maven-java\\pom.xml to D:\\maven-rep1026\\com\\atguigu\\maven\\pro01-maven-java\\1.0-SNAPSHOT\\pro01-maven-java-1.0-SNAPSHOT.pom Copied! 安装的效果是将本地构建过程中生成的 jar 包存入 Maven 本地仓库。这个 jar 包在 Maven 仓库中的路径是根据它的坐标生成的。\n坐标信息如下：\n1 2 3 \u0026lt;groupId\u0026gt;com.atguigu.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;pro01-maven-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; Copied! 在 Maven 仓库中生成的路径如下：\nmaven\\repository\\com\\atguigu\\maven\\pro01-maven-java\\1.0-SNAPSHOT\\pro01-maven-java-1.0-SNAPSHOT.jar\n另外，安装操作还会将 pom.xml 文件转换为 XXX.pom 文件一起存入本地仓库。所以我们在 Maven 的本地仓库中想看一个 jar 包原始的 pom.xml 文件时，查看对应 XXX.pom 文件即可，它们是名字发生了改变，本质上是同一个文件。\n","date":"2024-03-03T13:49:51+08:00","image":"https://logan.1357810.xyz/cover/pic_018.jpg","permalink":"https://qh.1357810.xyz/articles/maven/maven-common/","title":"1普通工程-命令行"},{"content":" 一、创建web工程 1、说明 使用 mvn archetype:generate 命令生成 Web 工程时，需要使用一个专门的 archetype。这个专门生成 Web 工程骨架的 archetype 可以参照官网看到它的用法：\n参数 archetypeGroupId、archetypeArtifactId、archetypeVersion 用来指定现在使用的 maven-archetype-webapp 的坐标。\n2、操作 注意：如果在上一个工程的目录下执行 mvn archetype:generate 命令，那么 Maven 会报错：不能在一个非 pom 的工程下再创建其他工程。所以不要再刚才创建的工程里再创建新的工程，请回到工作空间根目录来操作。\n然后运行生成工程的命令：\n1 mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-webapp -DarchetypeVersion=1.4 Copied! 下面的操作按照提示执行：\nTIP Define value for property \u0026lsquo;groupId\u0026rsquo;: com.atguigu.maven Define value for property \u0026lsquo;artifactId\u0026rsquo;: pro02-maven-web Define value for property \u0026lsquo;version\u0026rsquo; 1.0-SNAPSHOT: :【直接回车，使用默认值】\nDefine value for property \u0026lsquo;package\u0026rsquo; com.atguigu.maven: :【直接回车，使用默认值】 Confirm properties configuration: groupId: com.atguigu.maven artifactId: pro02-maven-web version: 1.0-SNAPSHOT package: com.atguigu.maven Y: :【直接回车，表示确认】\n3、生成的pom.xml 确认打包的方式是war包形式\nwar\n4、生成的Web工程的目录结构 webapp 目录下有 index.jsp\nWEB-INF 目录下有 web.xml\n5、创建 Servlet ①在 main 目录下创建 java 目录 ②在 java 目录下创建 Servlet 类所在的包的目录 ③在包下创建 Servlet 类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.atguigu.maven; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.ServletException; import java.io.IOException; public class HelloServlet extends HttpServlet{ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.getWriter().write(\u0026#34;hello maven web\u0026#34;); } } Copied! ④在 web.xml 中注册 Servlet 1 2 3 4 5 6 7 8 \u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;helloServlet\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;com.atguigu.maven.HelloServlet\u0026lt;/servlet-class\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;helloServlet\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/helloServlet\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; Copied! ​\n6、在 index.jsp 页面编写超链接 1 2 3 4 5 6 \u0026lt;html\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h2\u0026gt;Hello World!\u0026lt;/h2\u0026gt; \u0026lt;a href=\u0026#34;helloServlet\u0026#34;\u0026gt;Access Servlet\u0026lt;/a\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Copied! TIP JSP全称是 Java Server Page，和 Thymeleaf 一样，是服务器端页面渲染技术。这里我们不必关心 JSP 语法细节，编写一个超链接标签即可。\n7、编译 此时直接执行 mvn compile 命令出错：\n1 2 3 4 5 6 7 8 9 10 DANGER 程序包 javax.servlet.http 不存在 程序包 javax.servlet 不存在 找不到符号 符号: 类 HttpServlet …… Copied! 上面的错误信息说明：我们的 Web 工程用到了 HttpServlet 这个类，而 HttpServlet 这个类属于 servlet-api.jar 这个 jar 包。此时我们说，Web 工程需要依赖 servlet-api.jar 包。\n8、配置对 servlet-api.jar 包的依赖 对于不知道详细信息的依赖可以到https://mvnrepository.com/网站查询。使用关键词搜索，然后在搜索结果列表中选择适合的使用。\n比如，我们找到的 servlet-api 的依赖信息：\n1 2 3 4 5 6 7 \u0026lt;!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.servlet\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javax.servlet-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 这样就可以把上面的信息加入 pom.xml。重新执行 mvn compile 命令。\n9、将 Web 工程打包为 war 包 运行 mvn package 命令，生成 war 包的位置如下图所示：\n这个打包就会把所有的第三方依赖一起打进war包里，和普通工程就不一样了\n10、将 war 包部署到 Tomcat 上运行 将 war 包复制到 Tomcat/webapps 目录下\n启动 Tomcat：\n通过浏览器尝试访问：http://localhost:8080/pro02-maven-web/index.jsp\n二、依赖一个jar包 1、观念 明确一个意识：从来只有 Web 工程依赖 Java 工程，没有反过来 Java 工程依赖 Web 工程。本质上来说，Web 工程依赖的 Java 工程其实就是 Web 工程里导入的 jar 包。最终 Java 工程会变成 jar 包，放在 Web 工程的 WEB-INF/lib 目录下\n2、操作 在 pro02-maven-web 工程的 pom.xml 中，找到 dependencies 标签，在 dependencies 标签中做如下配置：\n1 2 3 4 5 6 7 \u0026lt;!-- 配置对Java工程pro01-maven-java的依赖 --\u0026gt; \u0026lt;!-- 具体的配置方式：在dependency标签内使用坐标实现依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.atguigu.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;pro01-maven-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 3、执行maven命令 ①测试命令 mvn test\n说明：测试操作中会提前自动执行编译操作，测试成功就说明编译也是成功的。\n②打包命令 mvn package\n通过查看 war 包内的结构，我们看到被 Web 工程依赖的 Java 工程确实是会变成 Web 工程的 WEB-INF/lib 目录下的 jar 包。\n③查看当前 Web 工程所依赖的 jar 包的列表 mvn dependency:list\nTIP\n[INFO] The following files have been resolved: [INFO] org.hamcrest:hamcrest-core:jar:1.3:test [INFO] javax.servlet:javax.servlet-api:jar:3.1.0:provided [INFO] com.atguigu.maven:pro01-maven-java:jar:1.0-SNAPSHOT:compile [INFO] junit:junit:jar:4.12:test\n说明：javax.servlet:javax.servlet-api:jar:3.1.0:provided 格式显示的是一个 jar 包的坐标信息。格式是：\ngroupId:artifactId:打包方式:version:依赖的范围\n这样的格式虽然和我们 XML 配置文件中坐标的格式不同，但是本质上还是坐标信息，大家需要能够认识这样的格式，将来从 Maven 命令的日志或错误信息中看到这样格式的信息，就能够识别出来这是坐标。进而根据坐标到Maven 仓库找到对应的jar包，用这样的方式解决我们遇到的报错的情况。\n④以树形结构查看当前 Web 工程的依赖信息 mvn dependency:tree\nTIP\n[INFO] com.atguigu.maven:pro02-maven-web:war:1.0-SNAPSHOT [INFO] +- junit:junit:jar:4.12:test [INFO] | - org.hamcrest:hamcrest-core:jar:1.3:test [INFO] +- javax.servlet:javax.servlet-api:jar:3.1.0:provided [INFO] - com.atguigu.maven:pro01-maven-java:jar:1.0-SNAPSHOT:compile\n我们在 pom.xml 中并没有依赖 hamcrest-core，但是它却被加入了我们依赖的列表。原因是：junit 依赖了hamcrest-core，然后基于依赖的传递性，hamcrest-core 被传递到我们的工程了。\n","date":"2024-03-03T14:49:51+08:00","image":"https://logan.1357810.xyz/cover/pic_034.jpg","permalink":"https://qh.1357810.xyz/articles/maven/maven-web/","title":"2 web工程-命令行"},{"content":" 一、创建父工程 创建的过程和前面创建普通工程 一样。\n工程名称：pro03-maven-parent\n工程创建好之后，要修改它的打包方式：\n1 2 3 4 5 6 \u0026lt;groupId\u0026gt;com.atguigu.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;pro03-maven-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;!-- 当前工程作为父工程，它要去管理子工程，所以打包方式必须是 pom --\u0026gt; \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; Copied! 有打包方式为 pom 的 Maven 工程能够管理其他 Maven 工程。打包方式为 pom 的 Maven 工程中不写业务代码，它是专门管理其他 Maven 工程的工程。\n二、创建模块工程 模块工程类似于 IDEA 中的 module，所以需要进入 pro03-maven-parent 工程的根目录，然后运行 mvn archetype:generate 命令来创建模块工程。\n假设，我们创建三个模块工程：\n三、查看被添加新内容的父工程 pom.xml 下面 modules 和 module 标签是聚合功能的配置\n1 2 3 4 5 \u0026lt;modules\u0026gt; \u0026lt;module\u0026gt;pro04-maven-module\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;pro05-maven-module\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;pro06-maven-module\u0026lt;/module\u0026gt; \u0026lt;/modules\u0026gt; Copied! 四、解读子工程的pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;!-- 使用parent标签指定当前工程的父工程 --\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;!-- 父工程的坐标 --\u0026gt; \u0026lt;groupId\u0026gt;com.atguigu.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;pro03-maven-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;!-- 子工程的坐标 --\u0026gt; \u0026lt;!-- 如果子工程坐标中的groupId和version与父工程一致，那么可以省略 --\u0026gt; \u0026lt;!-- \u0026lt;groupId\u0026gt;com.atguigu.maven\u0026lt;/groupId\u0026gt; --\u0026gt; \u0026lt;artifactId\u0026gt;pro04-maven-module\u0026lt;/artifactId\u0026gt; \u0026lt;!-- \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; --\u0026gt; Copied! 五、在父工程中配置依赖的统一管理 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 \u0026lt;!-- 使用dependencyManagement标签配置对依赖的管理 --\u0026gt; \u0026lt;!-- 被管理的依赖并没有真正被引入到工程 --\u0026gt; \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.0.0.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-beans\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.0.0.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-context\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.0.0.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-expression\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.0.0.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-aop\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.0.0.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; Copied! 六、子工程中引用那些被父工程管理的依赖 关键点：省略版本号\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 \u0026lt;!-- 把版本号去掉就表示子工程中这个依赖的版本由父工程决定。 --\u0026gt; \u0026lt;!-- 具体来说是由父工程的dependencyManagement来决定。 --\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-core\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-beans\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-context\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-expression\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-aop\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; Copied! 七、在父工程中声明自定义属性 1 2 3 4 5 6 7 \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;!-- 自定义标签，维护Spring版本数据 --\u0026gt; \u0026lt;!-- 通过自定义属性，统一指定Spring的版本 --\u0026gt; \u0026lt;atguigu.spring.version\u0026gt;4.3.6.RELEASE\u0026lt;/atguigu.spring.version\u0026gt; \u0026lt;/properties\u0026gt; Copied! 在需要的地方使用${}的形式来引用自定义的属性名：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${atguigu.spring.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 真正实现“一处修改，处处生效”。\n八、实际意义 编写一套符合要求、开发各种功能都能正常工作的依赖组合并不容易。如果公司里已经有人总结了成熟的组合方案，那么再开发新项目时，如果不使用原有的积累，而是重新摸索，会浪费大量的时间。为了提高效率，我们可以使用工程继承的机制，让成熟的依赖组合方案能够保留下来。\n如上图所示，公司级的父工程中管理的就是成熟的依赖组合方案，各个新项目、子系统各取所需即可。\n实验 多重继承： 1、aaa项目： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;groupId\u0026gt;org.example\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aaa\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.javassist\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javassist\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.24.1-GA\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.google.guava\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;guava\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;24.0-jre\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; Copied! 2、bbb项目继承aaa项目： 重新定义 javassist的版本\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.example\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;aaa\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;artifactId\u0026gt;bbb\u0026lt;/artifactId\u0026gt; \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.javassist\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javassist\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.28.0-GA\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; Copied! 3、ccc项目继承bbb项目： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.example\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;bbb\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;artifactId\u0026gt;ccc\u0026lt;/artifactId\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.javassist\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javassist\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.google.guava\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;guava\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; Copied! 4、发现 这时 执行 mvn dependency:tree 发现：\n1、javassist的版本是bbb项目定义的版本，被覆盖了；\n2、guava的版本还是aaa项目的版本；\n3、并且当修改aaa项目的guava版本，aaa项目内 mvn install 后，在ccc项目中重新加载后，发现guava版本也随之更改，这时就跟bbb项目的状态无关。\ntip:\n在bbb项目虽然是pom的打包格式，但是也可以写dependencies，直接依赖jar包，和dependencyManagement分别做两件事，不会出错\n","date":"2024-03-03T15:49:51+08:00","image":"https://logan.1357810.xyz/cover/pic_033.jpg","permalink":"https://qh.1357810.xyz/articles/maven/maven-parent/","title":"3 父子工程-命令行"},{"content":" 一、模块划分 一个父工程，一个子工程微服务，一个普通maven子工程\n二、建父工程 1、github建仓库 2、idea新建maven项目（父工程） 3、idea工程和github建立联系 选中 scr和pom.xml ，git add\ngit commit\ngit fetch\n命令行输入：\n1 2 git branch --set-upstream-to=origin/master git merge origin/master --allow-unrelated-histories Copied! commit + push 推送远程仓库 接下来就可以直接开发了 三、建子工程 另一个子工程以同样方式创建 四、父子关系 三个工程的pom文件内容是这样的：\nspringboot-parent-child 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;org.example\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;springboot-parent-child\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; \u0026lt;modules\u0026gt; \u0026lt;module\u0026gt;user-service\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;base-entity\u0026lt;/module\u0026gt; \u0026lt;/modules\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;!-- maven-resources-plugin插件的默认取值 --\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;!-- maven-site-plugin等插件的默认取值 --\u0026gt; \u0026lt;project.reporting.outputEncoding\u0026gt;UTF-8\u0026lt;/project.reporting.outputEncoding\u0026gt; \u0026lt;!-- maven-compiler-plugin插件的默认取值 --\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;!-- spring-boot-starter-parent里定义的的默认取值 --\u0026gt; \u0026lt;java.version\u0026gt;1.8\u0026lt;/java.version\u0026gt; \u0026lt;spring-boot-version\u0026gt;2.6.3\u0026lt;/spring-boot-version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;!-- \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.6.3\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; --\u0026gt; \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- SpringBoot 依赖导入 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring-boot-version}\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- SpringCloud 依赖导入 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2021.0.1\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- SpringCloud Alibaba 依赖导入 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-alibaba-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2021.0.1.0\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; \u0026lt;!-- 1、这里显式指定插件版本，其实版本在springboot的pom中有，但这种引入方式会对插件不友好 2、maven打包时会出现找不到版本的警告，为了规避，所以在这里指定版本， 后面继承的子工程就可以直接依赖这个插件了 3、用spring-boot-starter-parent引入的，插件可以直接使用，没有警告\t--\u0026gt; \u0026lt;!-- build 标签：用来配置对构建过程的定制 --\u0026gt; \u0026lt;build\u0026gt; \u0026lt;pluginManagement\u0026gt; \u0026lt;!-- plugins 标签：定制化构建过程中所使用到的插件 --\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;!-- plugin 标签：一个具体插件 --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring-boot-version}\u0026lt;/version\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/pluginManagement\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt; Copied! user-service 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;springboot-parent-child\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;org.example\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;artifactId\u0026gt;user-service\u0026lt;/artifactId\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- Nacos 服务注册发现启动器 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-nacos-discovery\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- web启动器依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.example\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;base-entity\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;!-- build 标签：用来配置对构建过程的定制 --\u0026gt; \u0026lt;build\u0026gt; \u0026lt;!-- plugins 标签：定制化构建过程中所使用到的插件 --\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;!-- plugin 标签：一个具体插件 --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;!-- 1、对于用import引入的springboot，这里要指定这个目标，否则执行mvn package时，不会自动执行springboot:repackage 2、如果不这样指定的话，那打包命令需要显式指定插件，命令为： mvn clean package spring-boot:repackage -Dmaven.test.skip=true 3、repackage前面必须有已经存在的包，否则会出错，所以前面跟着一个package原生命令 --\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;repackage\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt; Copied! base-entity 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 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;springboot-parent-child\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;org.example\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;artifactId\u0026gt;base-entity\u0026lt;/artifactId\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- OpenFeign 专用依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-openfeign\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; Copied! 部署 1 nohup java -jar user-service-1.0-SNAPSHOT.jar\u0026gt;demo06.log 2\u0026gt;\u0026amp;1 \u0026amp; Copied! 注意 1、引入springboot的方式 不是每个人都喜欢从 spring-boot-starter-parent 继承 POM。您可能需要使用自己公司标准的父 POM，或者您可能只是希望明确地声明所有 Maven 配置。\n如果您不想使用 spring-boot-starter-parent，则仍然可以通过使用 scope=import 依赖来获得依赖管理（但不是插件管理）的好处：\n假如 A工程这样配置，这种方式只能在后面继承这个A工程的的子工程中去覆盖依赖的版本：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; ... ... \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- SpringBoot 依赖导入 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.6.3\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; Copied! 如果这样，是覆盖不了的： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; ... ... \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- SpringBoot 依赖导入 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.6.3\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.6.4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; Copied! 但是这样，可以覆盖某个依赖的版本： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; ... ... \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- SpringBoot 依赖导入 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.6.3\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 覆盖web的版本 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.6.4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; Copied! 这种方式引入，也会使spring-boot-maven-plugin 引入进来，但是打包时有warning ","date":"2024-03-03T16:49:51+08:00","image":"https://logan.1357810.xyz/cover/pic_005.jpg","permalink":"https://qh.1357810.xyz/articles/maven/maven-springboot/","title":"4 springboot项目"},{"content":" 一、插件和目标 1、插件 Maven 的核心程序仅仅负责宏观调度，不做具体工作。具体工作都是由 Maven 插件完成的。例如：编译就是由 maven-compiler-plugin-3.1.jar 插件来执行的。\n2、目标 一个插件可以对应多个目标，而每一个目标都和生命周期中的某一个环节对应。\nDefault 生命周期中有 compile 和 test-compile 两个和编译相关的环节，这两个环节对应 compile 和 test-compile 两个目标，而这两个目标都是由 maven-compiler-plugin-3.1.jar 插件来执行的。\n3、调用插件的方法 方式一：通过生命周期映射的方式，将插件的goal绑定到生命周期中的phase上，然后调用phase。例如：maven-jar-plugin插件提供了一个叫jar的goal，默认会绑定到生命周期的package阶段(phase)。调用mvn package就会自动调用maven-jar-plugin:jar。生命周期中所有前置的phase会先自动执行。\n1 package \u0026lt;==\u0026gt; maven-jar-plugin:jar Copied! 方式二：直接调用插件的某个功能(goal)。如mvn maven-jar-plugin:jar。maven有一个约定，如果插件的名字叫maven-xxxx-plugin或xxxx-maven-plugin的话。可以直接用mvn xxxx:goal的方式调用其提供的功能。所以前面这个命令就可以简写成：mvn jar:jar。这种方式只会执行指定的goal。\n调用goal完整的命令格式为：\n1 2 mvn \u0026lt;plugin-prefix\u0026gt;:\u0026lt;goal\u0026gt; mvn [\u0026lt;plugin-group-id\u0026gt;:]\u0026lt;plugin-artifact-id\u0026gt;[:\u0026lt;plugin-version\u0026gt;]:\u0026lt;goal\u0026gt; Copied! 可以通过命令查看插件的详细信息：\n1 mvn help:describe -Dplugin=org.apache.maven.plugins:maven-compile-plugin –Ddetail Copied! 二、build标签 1、介绍 在实际使用 Maven 的过程中，我们会发现 build 标签有时候有，有时候没，这是怎么回事呢？其实通过有效 POM 我们能够看到，build 标签的相关配置其实一直都在，只是在我们需要定制构建过程的时候才会通过配置 build 标签覆盖默认值或补充配置。这一点我们可以通过打印有效 POM 来看到。\n所以本质上来说：我们配置的 build 标签都是对超级 POM 配置的叠加。那我们又为什么要在默认配置的基础上叠加呢？很简单，在默认配置无法满足需求的时候定制构建过程。\n2、build标签组成 ①、定义约定的目录结构 参考示例中的如下部分：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;sourceDirectory\u0026gt;D:\\idea2019workspace\\atguigu-maven-test-prepare\\src\\main\\java\u0026lt;/sourceDirectory\u0026gt; \u0026lt;scriptSourceDirectory\u0026gt;D:\\idea2019workspace\\atguigu-maven-test-prepare\\src\\main\\scripts\u0026lt;/scriptSourceDirectory\u0026gt; \u0026lt;testSourceDirectory\u0026gt;D:\\idea2019workspace\\atguigu-maven-test-prepare\\src\\test\\java\u0026lt;/testSourceDirectory\u0026gt; \u0026lt;outputDirectory\u0026gt;D:\\idea2019workspace\\atguigu-maven-test-prepare\\target\\classes\u0026lt;/outputDirectory\u0026gt; \u0026lt;testOutputDirectory\u0026gt;D:\\idea2019workspace\\atguigu-maven-test-prepare\\target\\test-classes\u0026lt;/testOutputDirectory\u0026gt; \u0026lt;resources\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;directory\u0026gt;D:\\idea2019workspace\\atguigu-maven-test-prepare\\src\\main\\resources\u0026lt;/directory\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;/resources\u0026gt; \u0026lt;testResources\u0026gt; \u0026lt;testResource\u0026gt; \u0026lt;directory\u0026gt;D:\\idea2019workspace\\atguigu-maven-test-prepare\\src\\test\\resources\u0026lt;/directory\u0026gt; \u0026lt;/testResource\u0026gt; \u0026lt;/testResources\u0026gt; \u0026lt;directory\u0026gt;D:\\idea2019workspace\\atguigu-maven-test-prepare\\target\u0026lt;/directory\u0026gt; Copied! 我们能看到各个目录的作用如下：\n目录名 作用 sourceDirectory 主体源程序存放目录 scriptSourceDirectory 脚本源程序存放目录 testSourceDirectory 测试源程序存放目录 outputDirectory 主体源程序编译结果输出目录 testOutputDirectory 测试源程序编译结果输出目录 resources 主体资源文件存放目录 testResources 测试资源文件存放目录 directory 构建结果输出目录 ②、备用插件管理 pluginManagement 标签存放着几个极少用到的插件：\nmaven-antrun-plugin maven-assembly-plugin maven-dependency-plugin maven-release-plugin 通过 pluginManagement 标签管理起来的插件就像 dependencyManagement 一样，子工程使用时可以省略版本号，起到在父工程中统一管理版本的效果。情看下面例子：\n被 spring-boot-dependencies 管理的插件信息： 1 2 3 4 5 6 7 8 9 \u0026lt;pluginManagement\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.6.2\u0026lt;/version\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/pluginManagement\u0026gt; Copied! 子工程使用的插件信息： 1 2 3 4 5 6 7 8 \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; Copied! ③、插件结构 plugins 标签存放的是默认生命周期中实际会用到的插件，这些插件想必大家都不陌生，所以抛开插件本身不谈，我们来看看 plugin 标签的结构：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;plugin\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;default-compile\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;compile\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;compile\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;default-testCompile\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;test-compile\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;testCompile\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; Copied! [1] 坐标部分 artifactId 和 version 标签定义了插件的坐标，作为 Maven 的自带插件这里省略了 groupId。 [2] 执行部分 executions 标签内可以配置多个 execution 标签，execution 标签内：\nid：指定唯一标识 phase：关联的生命周期阶段 goals/goal：关联指定生命周期的目标 goals 标签中可以配置多个 goal 标签，表示一个生命周期环节可以对应当前插件的多个目标。 另外，插件目标的执行过程可以进行配置，例如 maven-site-plugin 插件的 site 目标： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;default-site\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;site\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;site\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;outputDirectory\u0026gt;D:\\idea2019workspace\\atguigu-maven-test-prepare\\target\\site\u0026lt;/outputDirectory\u0026gt; \u0026lt;reportPlugins\u0026gt; \u0026lt;reportPlugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-project-info-reports-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;/reportPlugin\u0026gt; \u0026lt;/reportPlugins\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; Copied! configuration 标签内进行配置时使用的标签是插件本身定义的。就以 maven-site-plugin 插件为例，它的核心类是 org.apache.maven.plugins.site.render.SiteMojo，在这个类中我们看到了 outputDirectory 属性： SiteMojo 的父类是：AbstractSiteRenderingMojo，在父类中我们看到 reportPlugins 属性： 结论：每个插件能够做哪些设置都是各个插件自己规定的，无法一概而论。 三、定义jdk版本和编码 1、直接使用默认取值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;properties\u0026gt; \u0026lt;!-- maven-resources-plugin和maven-compiler-plugin插件的默认取值 --\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;!-- maven-site-plugin等插件的默认取值 --\u0026gt; \u0026lt;project.reporting.outputEncoding\u0026gt;UTF-8\u0026lt;/project.reporting.outputEncoding\u0026gt; \u0026lt;!-- maven-compiler-plugin插件的默认取值 --\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;!-- spring-boot-starter-parent里定义的的默认取值 --\u0026gt; \u0026lt;java.version\u0026gt;1.8\u0026lt;/java.version\u0026gt; \u0026lt;/properties\u0026gt; Copied! 2、或者在插件中定义 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 37 38 39 40 41 42 \u0026lt;project\u0026gt; [...] \u0026lt;!-- build 标签：意思是告诉 Maven，你的构建行为，我要开始定制了！ --\u0026gt; \u0026lt;build\u0026gt; [...] \u0026lt;!-- plugins 标签：Maven 你给我听好了，你给我构建的时候要用到这些插件！ --\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;!-- JDK版本 --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;!-- 插件的坐标。此处引用的 maven-compiler-plugin 插件不是第三方的，是一个 Maven 自带的插件。 --\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.10.1\u0026lt;/version\u0026gt; \u0026lt;!-- configuration 标签：配置 maven-compiler-plugin 插件 --\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 具体配置信息会因为插件不同、需求不同而有所差异 --\u0026gt; \u0026lt;source\u0026gt;1.8\u0026lt;/source\u0026gt; \u0026lt;target\u0026gt;1.8\u0026lt;/target\u0026gt; \u0026lt;encoding\u0026gt;UTF-8\u0026lt;/encoding\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;!-- 编码 --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-resources-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.2.0\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; ... \u0026lt;encoding\u0026gt;UTF-8\u0026lt;/encoding\u0026gt; ... \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; [...] \u0026lt;/build\u0026gt; [...] \u0026lt;/project\u0026gt; Copied! source 标签含义\n翻译过来就是：调用 Java 编译器命令时传入的 -source 参数。那对编译器来说，-source 参数是啥意思呢？\n『提供与指定发行版的源兼容性』这句话我的理解是：\n我们写代码是按 JDK 1.8 写的——这就是『源兼容性』里的『源』。 指定发行版就是我们指定的 JDK 1.8。 『兼容性』是谁和谁兼容呢？现在源代码是既定的，所以就是要求编译器使用指定的 JDK 版本来兼容我们的源代码。 target 标签含义\n调用 Java 编译器命令时传入的 -target 参数\n『生成特定 VM 版本的类文件』这句话我的理解是：\nVM 指 JVM 类文件指 *.class 字节码文件 整体意思就是源文件编译后，生成的 *.class 字节码文件要符合指定的 JVM 版本 3、或写在超级pom里，全局设定 配置的方式是：将 profile 标签整个复制到 settings.xml 文件的 profiles 标签内。\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;jdk-1.8\u0026lt;/id\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;activeByDefault\u0026gt;true\u0026lt;/activeByDefault\u0026gt; \u0026lt;jdk\u0026gt;1.8\u0026lt;/jdk\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;maven.compiler.compilerVersion\u0026gt;1.8\u0026lt;/maven.compiler.compilerVersion\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/profile\u0026gt; Copied! 不推荐,仅在本地生效，如果脱离当前的settings.xml能够覆盖的范围，则无法生效\n四、help 插件的各个目标 官网说明地址：https://maven.apache.org/plugins/maven-help-plugin\n目标 说明 help:active-profiles 列出当前已激活的 profile help:all-profiles 列出当前工程所有可用 profile help:describe 描述一个插件和/或 Mojo 的属性 help:effective-pom 以 XML 格式展示有效 POM help:effective-settings 为当前工程以 XML 格式展示计算得到的 settings 配置 help:evaluate 计算用户在交互模式下给出的 Maven 表达式 help:system 显示平台详细信息列表，如系统属性和环境变量 五、springboot打包插件 1、打包 1 2 3 4 5 6 7 8 9 \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.5.5\u0026lt;/version\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; Copied! 很显然 spring-boot-maven-plugin 并不是 Maven 自带的插件，而是 SpringBoot 提供的，用来改变 Maven 默认的构建行为。具体来说是改变打包的行为。默认情况下 Maven 调用 maven-jar-plugin 插件的 jar 目标，生成普通的 jar 包。\n普通 jar 包没法使用 java -jar xxx.jar 这样的命令来启动、运行，但是 SpringBoot 的设计理念就是每一个『微服务』导出为一个 jar 包，这个 jar 包可以使用 java -jar xxx.jar 这样的命令直接启动运行。\n这样一来，打包的方式肯定要进行调整。所以 SpringBoot 提供了 spring-boot-maven-plugin 这个插件来定制打包行为。 2、插件的七个目标 目标名称 作用 spring-boot:build-image Package an application into a OCI image using a buildpack. spring-boot:build-info Generate a build-info.properties file based on the content of the current MavenProject. spring-boot:help Display help information on spring-boot-maven-plugin. Call mvn spring-boot:help -Ddetail=true -Dgoal= to display parameter details. spring-boot:repackage Repackage existing JAR and WAR archives so that they can be executed from the command line using java -jar. With layout=NONE can also be used simply to package a JAR with nested dependencies (and no main class, so not executable). spring-boot:run Run an application in place. spring-boot:start Start a spring application. Contrary to the run goal, this does not block and allows other goals to operate on the application. This goal is typically used in integration test scenario where the application is started before a test suite and stopped after. spring-boot:stop Stop an application that has been started by the \u0026lsquo;start\u0026rsquo; goal. Typically invoked once a test suite has completed. 六、自定义插件 1、创建工程 创建一个常规的maven工程\n然后修改打包方式：\n1 \u0026lt;packaging\u0026gt;maven-plugin\u0026lt;/packaging\u0026gt; Copied! 引入依赖（二选一）：\n[1] 将来在文档注释中使用注解\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-plugin-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Copied! [2] 将来直接使用注解\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugin-tools\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-plugin-annotations\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 创建 Mojo 类\nMojo 类是一个 Maven 插件的核心类。\nMojo 这个单词的意思是：Maven Old Java Object，其实 mojo 这个单词本身包含魔力;符咒(袋);护身符;(人的)魅力的含义，Maven 用 Mojo 是因为它是对 POJO 开的一个小玩笑。\n[1] Mojo 接口\n每一个 Mojo 都需要实现 org.apache.maven.plugin.Mojo 接口\n**[2] AbstractMojo 抽象\n我们实现 Mojo 接口比较困难，幸好可以继承 AbstractMojo，此时我们只要实现 execute() 这一个方法即可。\n1 2 3 4 5 6 public class MyHelloPlugin extends AbstractMojo { @Override public void execute() throws MojoExecutionException, MojoFailureException { getLog().info(\u0026#34;---\u0026gt; This is my first maven plugin. \u0026lt;---\u0026#34;); } } Copied! 2、插件配置 ①、Mojo 类中的配置 [1]文档注释中用注解 ​\t对应的 pom.xml 中的依赖： maven-plugin-api\n[2]直接在类上标记注解 ​\t对应 pom.xml 中的依赖：maven-plugin-annotations\n1 2 3 4 5 6 7 8 // name 属性：指定目标名称 @Mojo(name = \u0026#34;firstBlood\u0026#34;) public class MyPluginOfFistBlood extends AbstractMojo { @Override public void execute() throws MojoExecutionException, MojoFailureException { getLog().info(\u0026#34;---\u0026gt; first blood \u0026lt;---\u0026#34;); } } Copied! ②、安装插件 要在后续使用插件，就必须至少将插件安装到本地仓库。 mvn install\n③、注册插件 我们需要将插件坐标中的 groupId 部分注册到 settings.xml 中。\n1 2 3 4 5 6 7 \u0026lt;pluginGroups\u0026gt; \u0026lt;!-- pluginGroup | Specifies a further group identifier to use for plugin lookup. \u0026lt;pluginGroup\u0026gt;com.your.plugins\u0026lt;/pluginGroup\u0026gt; --\u0026gt; \u0026lt;pluginGroup\u0026gt;com.atguigu.maven\u0026lt;/pluginGroup\u0026gt; \u0026lt;/pluginGroups\u0026gt; Copied! 3、使用插件 ①、识别插件前缀 Maven 根据插件的 artifactId 来识别插件前缀。例如下面两种情况：\n​ [1]前置匹配\n匹配规则：${prefix}-maven-plugin artifactId：hello-maven-plugin 前缀：hello ​ [2]中间匹配\n匹配规则：maven-${prefix}-plugin artifactId：maven-good-plugin 前缀：good ②在命令行直接用 命令： 1 mvn hello:sayHello Copied! 效果：\n③、配置到 build 标签里用 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 \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;com.atguigu.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hello-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;hello\u0026lt;/id\u0026gt; \u0026lt;!-- 指定和目标关联的生命周期阶段 --\u0026gt; \u0026lt;phase\u0026gt;clean\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;sayHello\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;blood\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;validate\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;firstBlood\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; Copied! 执行已和插件目标绑定的生命周期：\n七、maven常用插件 1、maven-compiler-plugin 默认绑定到comile phase。用于编译项目源代码。 compile目标会编译src/main/java目录下的源代码。 testCompile目标会编译src/test/java目录下的测试代码。\nmaven compile编译源代码。 maven testCompile编译源代码+测试代码。\n配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.6.0\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;source\u0026gt;1.8\u0026lt;/source\u0026gt; \u0026lt;!-- 源代码使用jdk1.8支持的特性 --\u0026gt; \u0026lt;target\u0026gt;1.8\u0026lt;/target\u0026gt; \u0026lt;!-- 使用jvm1.8编译目标代码 --\u0026gt; \u0026lt;encoding\u0026gt;utf-8\u0026lt;/encoding\u0026gt; \u0026lt;!-- 编码 --\u0026gt; \u0026lt;skipTests\u0026gt;true\u0026lt;/skipTests\u0026gt; \u0026lt;!-- 是否跳过测试 --\u0026gt; \u0026lt;showWarnings\u0026gt;true\u0026lt;/showWarnings\u0026gt; \u0026lt;meminitial\u0026gt;128m\u0026lt;/meminitial\u0026gt; \u0026lt;!-- 编译器使用的初始内存 --\u0026gt; \u0026lt;maxmem\u0026gt;512m\u0026lt;/maxmem\u0026gt; \u0026lt;!-- 编译器使用的最大内存 --\u0026gt; \u0026lt;compilerArgs\u0026gt; \u0026lt;!-- 传递参数 --\u0026gt; \u0026lt;compilerArgs\u0026gt; \u0026lt;!-- 传递参数 --\u0026gt; \u0026lt;arg\u0026gt;-Xlint:unchecked\u0026lt;/arg\u0026gt;\u0026lt;!--启用对未经检查的转换的警告 Map map = new HashMap(); map.put(\u0026#34;a\u0026#34;, new A());--\u0026gt; \u0026lt;arg\u0026gt;-Xlint:deprecation\u0026lt;/arg\u0026gt;\u0026lt;!--显示关于使用了过时的 API 的详细信息--\u0026gt; \u0026lt;/compilerArgs\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; Copied! 用户可以通过两种方式调用Maven插件目标。\n第一种方式是将插件目标与生命周期阶段（lifecycle phase）绑定，这样用户在命令行只是输入生命周期阶段而已，例如Maven默认将maven-compiler-plugin的compile目标与compile生命周期阶段绑定，因此命令mvn compile实际上是先定位到compile这一生命周期阶段，然后再根据绑定关系调用maven-compiler-plugin的compile目标。\n第二种方式是直接在命令行指定要执行的插件目标，例如mvn archetype:generate 就表示调用maven-archetype-plugin的generate目标，这种带冒号的调用方式与生命周期无关。\n2、maven-resources-plugin ①、分环境配置 配置目录 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 37 38 39 40 41 42 43 44 45 46 \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-resources-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.5\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;resources\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;directory\u0026gt;src/main/resources\u0026lt;/directory\u0026gt; \u0026lt;!-- 先把所有环境的配置全部排除 --\u0026gt; \u0026lt;excludes\u0026gt; \u0026lt;exclude\u0026gt;dev/**\u0026lt;/exclude\u0026gt; \u0026lt;exclude\u0026gt;prod/**\u0026lt;/exclude\u0026gt; \u0026lt;/excludes\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;directory\u0026gt;src/main/resources/${active.profile}\u0026lt;/directory\u0026gt; \u0026lt;!-- 再把当前环境的配置引入 --\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;**/*.xml\u0026lt;/include\u0026gt; \u0026lt;include\u0026gt;**/*.properties\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;/resources\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;!-- 或者 --\u0026gt; \u0026lt;build\u0026gt; \u0026lt;resources\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;directory\u0026gt;src/main/resources\u0026lt;/directory\u0026gt; \u0026lt;!-- 先把所有环境的配置全部排除 --\u0026gt; \u0026lt;excludes\u0026gt; \u0026lt;exclude\u0026gt;dev/**\u0026lt;/exclude\u0026gt; \u0026lt;exclude\u0026gt;prod/**\u0026lt;/exclude\u0026gt; \u0026lt;/excludes\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;directory\u0026gt;src/main/resources/${active.profile}\u0026lt;/directory\u0026gt; \u0026lt;!-- 再把当前环境的配置引入 --\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;**/*.xml\u0026lt;/include\u0026gt; \u0026lt;include\u0026gt;**/*.properties\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;/resources\u0026gt; \u0026lt;/build\u0026gt; Copied! 配置profile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;profiles\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;dev\u0026lt;/id\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;activeByDefault\u0026gt;true\u0026lt;/activeByDefault\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;active.profile\u0026gt;dev\u0026lt;/active.profile\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/profile\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;prod\u0026lt;/id\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;active.profile\u0026gt;prod\u0026lt;/active.profile\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/profile\u0026gt; \u0026lt;/profiles\u0026gt; Copied! ②、打源码包 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-source-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.0.1\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;attach-sources\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;verify\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;jar-no-fork\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; Copied! 3、maven-surefire-plugin 用于跑测试用例\n此插件可以不在pom.xml里面声明，maven运行命令时，会自动调用该插件。 一些有用的配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-surefire-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;testFailureIgnore\u0026gt;true\u0026lt;/testFailureIgnore\u0026gt; \u0026lt;!--测试有失败用例时，是否继续构建--\u0026gt; \u0026lt;skipTests\u0026gt;true\u0026lt;/skipTests\u0026gt; \u0026lt;!--是否跳过测试阶段，方式1--\u0026gt; \u0026lt;skip\u0026gt;true\u0026lt;/skip\u0026gt; \u0026lt;!--是否跳过测试阶段，方式2--\u0026gt; \u0026lt;skipAfterFailureCount\u0026gt;1\u0026lt;/skipAfterFailureCount\u0026gt; \u0026lt;!-- 只要有一个用例测试失败，就立即停止。默认情况下会跑完所有测试用例 --\u0026gt; \u0026lt;rerunFailingTestsCount\u0026gt;2\u0026lt;/rerunFailingTestsCount\u0026gt; \u0026lt;!-- 失败重试次数 --\u0026gt; \u0026lt;parallel\u0026gt;methods\u0026lt;/parallel\u0026gt; \u0026lt;!-- 并发执行测试用例 --\u0026gt; \u0026lt;threadCount\u0026gt;10\u0026lt;/threadCount\u0026gt; \u0026lt;!-- 并发执行时的线程数量 --\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; Copied! 4、spring-boot-maven-plugin spring-boot开发必备插件。它能将spring-boot项目代码及其依赖jar打包成一个完整的可执行的jar包（fat jar）作用和插件maven-shade-plugin差不多。 repackage的goal绑定在生命周期的package阶段。 使用方式：\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;repackage\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; Copied! 如果指定了spring-boot作为parent，可以不指定版本和repackage goal：\n1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.3.8.RELEASE\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;mainClass\u0026gt;${start-class}\u0026lt;/mainClass\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;/plugin\u0026gt; Copied! spring-boot查找main文件的流程是：\n1、首先查看\u0026lt;mainClass\u0026gt;是否有值，如果有，直接拿标签内的类名作为入口。\n1 2 3 \u0026lt;properties\u0026gt; \u0026lt;start-class\u0026gt;com.example.Application\u0026lt;/start-class\u0026gt; \u0026lt;/properties\u0026gt; Copied! 2、如果没找到\u0026lt;start-class\u0026gt;标签，会遍历所有文件，找到注解了@SpringBootApplication并含有main方法的类，将其作为入口。 实践证明，如果没有定义\u0026lt;start-class\u0026gt;，查找入口类的方法也是非常快的。 在实际开发中，推荐手动定义\u0026lt;start-class\u0026gt;。这样在一个项目工程中可以有多个@SpringBootApplication注解的类，修改一下pom里的配置就能灵活切换入口了。\n5、maven-jar-plugin 这个是普通java项目（非java web项目和其他特殊类型的java项目）package阶段默认绑定的插件，能够将编译好的class和资源打成jar包。\n常用配置：打出可以运行的有主类的jar包 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 \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-jar-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.0.2\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;archive\u0026gt; \u0026lt;!--加描述--\u0026gt; \u0026lt;addMavenDescriptor\u0026gt;false\u0026lt;/addMavenDescriptor\u0026gt; \u0026lt;manifest\u0026gt; \u0026lt;!--是否要把第三方jar放到manifest的classpath中--\u0026gt; \u0026lt;addClasspath\u0026gt;true\u0026lt;/addClasspath\u0026gt; \u0026lt;!--生成的manifest中classpath的前缀，因为要把第三方jar放到lib目录下，不是代码里的目录--\u0026gt; \u0026lt;classpathPrefix\u0026gt;lib/\u0026lt;/classpathPrefix\u0026gt; \u0026lt;!-- 执行的主程序路径 --\u0026gt; \u0026lt;mainClass\u0026gt;com.meix.boot.Application\u0026lt;/mainClass\u0026gt; \u0026lt;/manifest\u0026gt; \u0026lt;/archive\u0026gt; \u0026lt;!-- 过滤掉不希望包含在jar中的文件 --\u0026gt; \u0026lt;excludes\u0026gt; \u0026lt;exclude\u0026gt;*.xml\u0026lt;/exclude\u0026gt; \u0026lt;exclude\u0026gt;spring/**\u0026lt;/exclude\u0026gt; \u0026lt;exclude\u0026gt;config/**\u0026lt;/exclude\u0026gt; \u0026lt;/excludes\u0026gt; \u0026lt;!-- 把配置的这个目录下的部分资源打包，其他目录的java类不打 --\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;**/api/*\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; Copied! 把maven依赖的jar复制到lib目录下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-dependency-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.10\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;copy-dependencies\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;package\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;copy-dependencies\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;outputDirectory\u0026gt;${project.build.directory}/lib\u0026lt;/outputDirectory\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; Copied! 打开 MENIFEST.MF 文件可以看到如下内容：\n1 2 3 4 5 6 7 8 9 10 11 Manifest-Version: 1.0 Archiver-Version: Plexus Archiver Built-By: sandy //表示依赖的第三方jar包在哪里，显然需要放在和 当前jar包同级目录下的 lib文件夹下 Class-Path: lib/gson-2.8.5.jar lib/guava-19.0.jar lib/slf4j-api-1.7.25 .jar lib/logback-core-1.2.3.jar lib/logback-access-1.2.3.jar lib/logb ack-classic-1.2.3.jar lib/lombok-1.18.2.jar lib/commons-lang3-3.8.1.j ar lib/commons-io-2.2.jar Created-By: Apache Maven 3.5.2 Build-Jdk: 1.8.0_121 Main-Class: com.example.demo.test.App //表示运行的主程序 Copied! 所以安装包的路径结构应该是：\n为此需要 通过 maven-assembly-plugin 插件来组装出安装包。\n针对本示例 对应的 maven-assembly-plugin 配置如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-assembly-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;make-assembly\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;package\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;single\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;descriptors\u0026gt; \u0026lt;!-- 和 pom.xml 同级目录下 --\u0026gt; \u0026lt;descriptor\u0026gt;assembly.xml\u0026lt;/descriptor\u0026gt; \u0026lt;/descriptors\u0026gt; \u0026lt;!-- assembly 组装出来的包会被放置在和 pom.xml同级目录下的output目录下 --\u0026gt; \u0026lt;outputDirectory\u0026gt;output\u0026lt;/outputDirectory\u0026gt; \u0026lt;!-- true：会在生成的zip包名后面加上 id--\u0026gt; \u0026lt;appendAssemblyId\u0026gt;false\u0026lt;/appendAssemblyId\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; Copied! assembly.xml 内容如下：\n1 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 37 38 39 \u0026lt;?xml version=\u0026#39;1.0\u0026#39; encoding=\u0026#39;UTF-8\u0026#39;?\u0026gt; \u0026lt;assembly xmlns=\u0026#34;http://maven.apache.org/ASSEMBLY/2.1.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd\u0026#34;\u0026gt; \u0026lt;id\u0026gt;test-maven\u0026lt;/id\u0026gt; \u0026lt;formats\u0026gt; \u0026lt;format\u0026gt;zip\u0026lt;/format\u0026gt; \u0026lt;/formats\u0026gt; \u0026lt;includeBaseDirectory\u0026gt;false\u0026lt;/includeBaseDirectory\u0026gt; \u0026lt;fileSets\u0026gt; \u0026lt;fileSet\u0026gt; \u0026lt;!-- ${project.build.directory} 指 target目录 --\u0026gt; \u0026lt;directory\u0026gt;${project.build.directory}\u0026lt;/directory\u0026gt; \u0026lt;!-- 表示输出到 pom 中配置的 output目录下 --\u0026gt; \u0026lt;outputDirectory\u0026gt;/\u0026lt;/outputDirectory\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;!-- 把jar放入zip包根目录 --\u0026gt; \u0026lt;include\u0026gt;test-maven*.jar\u0026lt;/include\u0026gt; \u0026lt;!-- dependency插件复制的依赖jar放入zip --\u0026gt; \u0026lt;include\u0026gt;lib/*\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;/fileSet\u0026gt; \u0026lt;fileSet\u0026gt; \u0026lt;directory\u0026gt;${project.build.directory}/classes\u0026lt;/directory\u0026gt; \u0026lt;!-- 表示输出到 pom 中配置的 output/config 目录下 --\u0026gt; \u0026lt;outputDirectory\u0026gt;/config\u0026lt;/outputDirectory\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;application.properties\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;/fileSet\u0026gt; \u0026lt;fileSet\u0026gt; \u0026lt;!-- ${project.basedir} 指 工程根目录 --\u0026gt; \u0026lt;directory\u0026gt;${project.basedir}\u0026lt;/directory\u0026gt; \u0026lt;outputDirectory\u0026gt;/\u0026lt;/outputDirectory\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;doc/*.*\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;/fileSet\u0026gt; \u0026lt;/fileSets\u0026gt; \u0026lt;/assembly\u0026gt; Copied! 要自定义 jar 包名称，可以直接在 pom.xml 中 build 结点下设置 finalName，如：\n1 2 3 \u0026lt;build\u0026gt; \u0026lt;finalName\u0026gt;p-test-tool\u0026lt;/finalName\u0026gt; \u0026lt;/build\u0026gt; Copied! 6、maven-assembly-plugin 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 \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-assembly-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;make-assembly\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;package\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;single\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;descriptorRefs\u0026gt; \u0026lt;!-- 使用官方的打包策略 --\u0026gt; \u0026lt;descriptorRef\u0026gt;jar-with-dependencies\u0026lt;/descriptorRef\u0026gt; \u0026lt;/descriptorRefs\u0026gt; \u0026lt;archive\u0026gt; \u0026lt;manifest\u0026gt; \u0026lt;mainClass\u0026gt;a.App\u0026lt;/mainClass\u0026gt; \u0026lt;/manifest\u0026gt; \u0026lt;/archive\u0026gt; \u0026lt;!-- assembly 组装出来的包会被放置在和 pom.xml同级目录下的output目录下 --\u0026gt; \u0026lt;outputDirectory\u0026gt;output\u0026lt;/outputDirectory\u0026gt; \u0026lt;!-- true：会在生成的zip包名后面加上 id--\u0026gt; \u0026lt;appendAssemblyId\u0026gt;false\u0026lt;/appendAssemblyId\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; Copied! 7、maven-shade-plugin 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-shade-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.3.0\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;phase\u0026gt;package\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;shade\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;transformers\u0026gt; \u0026lt;transformer implementation=\u0026#34;org.apache.maven.plugins.shade.resource.ManifestResourceTransformer\u0026#34;\u0026gt; \u0026lt;mainClass\u0026gt;org.sonatype.haven.HavenCli\u0026lt;/mainClass\u0026gt; \u0026lt;/transformer\u0026gt; \u0026lt;/transformers\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; Copied! ","date":"2024-03-03T17:49:51+08:00","image":"https://logan.1357810.xyz/cover/pic_006.jpg","permalink":"https://qh.1357810.xyz/articles/maven/maven-build/","title":"5 build标签和插件"},{"content":" 一、jar包冲突问题 1、谁需要面对 jar 包冲突？ 先给结论：编订依赖列表的程序员。初次设定一组依赖，因为尚未经过验证，所以确实有可能存在各种问题，需要做有针对性的调整。那么谁来做这件事呢？我们最不希望看到的就是：团队中每个程序员都需要自己去找依赖，即使是做同一个项目，每个模块也各加各的依赖，没有统一管理。那前人踩过的坑，后人还要再踩一遍。而且大家用的依赖有很多细节都不一样，版本更是五花八门，这就让事情变得更加复杂。\n所以虽然初期需要根据项目开发和实际运行情况对依赖配置不断调整，最终确定一个各方面都 OK 的版本。但是一旦确定下来，放在父工程中做依赖管理，各个子模块各取所需，这样基本上就能很好的避免问题的扩散。\n即使开发中遇到了新问题，也可以回到源头检查、调整 dependencyManagement 配置的列表——而不是每个模块都要改。所以学完这一节你应该就会对前面讲过的『继承』有了更深的理解。\n2、表现形式 由于实际开发时我们往往都会整合使用很多大型框架，所以一个项目中哪怕只是一个模块也会涉及到大量 jar 包。数以百计的 jar 包要彼此协调、精密配合才能保证程序正常运行。而规模如此庞大的 jar 包组合在一起难免会有磕磕碰碰。最关键的是由于 jar 包冲突所导致的问题非常诡异，这里我们只能罗列较为典型的问题，而没法保证穷举。\n但是我们仍然能够指出一点：一般来说，由于我们自己编写代码、配置文件写错所导致的问题通常能够在异常信息中看到我们自己类的全类名或配置文件的所在路径。如果整个错误信息中完全没有我们负责的部分，全部是框架、第三方工具包里面的类报错，这往往就是 jar 包的问题所引起的。\n而具体的表现形式中，主要体现为找不到类或找不到方法。\n①抛异常：找不到类 此时抛出的常见的异常类型：\njava.lang.ClassNotFoundException：编译过程中找不到类 java.lang.NoClassDefFoundError：运行过程中找不到类 java.lang.LinkageError：不同类加载器分别加载的多个类有相同的全限定名 我们来举个例子：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.httpcomponents\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;httpclient\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.x.x\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Copied! httpclient 这个 jar 包中有一个类：org.apache.http.conn.ssl.NoopHostnameVerifier。这个类在较低版本中没有，但在较高版本存在。比如：\njar 包版本 是否存在 4.3.6 否 4.4 是 那当我们确实需要用到 NoopHostnameVerifier 这个类，我们看到 Maven 通过依赖传递机制引入了这个 jar 包，所以没有明确地显式声明对这个 jar 包的依赖。可是 Maven 传递过来的 jar 包是 4.3.6 版本，里面没有包含我们需要的类，就会抛出异常。\n而『冲突』体现在：4.3.6 和 4.4 这两个版本的 jar 包都被框架所依赖的 jar 包给传递进来了，但是假设 Maven 根据**『版本仲裁』**规则实际采纳的是 4.3.6。\n②抛异常：找不到方法 程序找不到符合预期的方法。这种情况多见于通过反射调用方法，所以经常会导致：java.lang.NoSuchMethodError。比如 antlr:antlr:x.x.x 这个包中有一个接口：antlr.collections.AST\n版本 getLine()方法 2.7.2 无 2.7.6 有 ③没报错但结果不对 发生这种情况比较典型的原因是：两个 jar 包中的类分别实现了同一个接口，这本来是很正常的。但是问题在于：由于没有注意命名规范，两个不同实现类恰巧是同一个名字。\n具体例子是有的同学在实际工作中遇到过：项目中部分模块使用 log4j 打印日志；其它模块使用 logback，编译运行都不会冲突，但是会引起日志服务降级，让你的 log 配置文件失效。比如：你指定了 error 级别输出，但是冲突就会导致 info、debug 都在输出。\n3、本质 以上表现形式归根到底是两种基本情况导致的：\n①同一jar包的不同版本 ②不同jar包中包含同名类 这里我们拿 netty 来举个例子，netty 是一个类似 Tomcat 的 Servlet 容器。通常我们不会直接依赖它，所以基本上都是框架传递进来的。那么当我们用到的框架很多时，就会有不同的框架用不同的坐标导入 netty。大家可以参照下表对比一下两组坐标：\n截止到3.2.10.Final版本以前的坐标形式： 从3.3.0.Final版本开始以后的坐标形式： org.jboss.netty netty 3.2.10.Final io.netty netty 3.9.2.Final 但是偏偏这两个**『不同的包』里面又有很多『全限定名相同』**的类。例如：\norg.jboss.netty.channel.socket.ServerSocketChannelConfig.class org.jboss.netty.channel.socket.nio.NioSocketChannelConfig.class org.jboss.netty.util.internal.jzlib.Deflate.class org.jboss.netty.handler.codec.serialization.ObjectDecoder.class org.jboss.netty.util.internal.ConcurrentHashMap$HashIterator.class org.jboss.netty.util.internal.jzlib.Tree.class org.jboss.netty.util.internal.ConcurrentIdentityWeakKeyHashMap$Segment.class org.jboss.netty.handler.logging.LoggingHandler.class org.jboss.netty.channel.ChannelHandlerLifeCycleException.class org.jboss.netty.util.internal.ConcurrentIdentityHashMap$ValueIterator.class org.jboss.netty.util.internal.ConcurrentIdentityWeakKeyHashMap$Values.class org.jboss.netty.util.internal.UnterminatableExecutor.class org.jboss.netty.handler.codec.compression.ZlibDecoder.class org.jboss.netty.handler.codec.rtsp.RtspHeaders$Values.class org.jboss.netty.handler.codec.replay.ReplayError.class org.jboss.netty.buffer.HeapChannelBufferFactory.class\n……\n其实还有很多，这里列出的只是冰山一角。\n当然，如果全限定名相同，类中的代码也完全相同，那么用着也行。问题是如果**『全限定名相同』，但是『代码不同』**，那可太坑了。我们随便找一个来看看：\n坐标信息：org.jboss.netty:netty:jar:3.2.10.Final 代码截图： 坐标信息：io.netty:netty:jar:3.9.2.Final 代码截图： 4、解决办法 ①概述 很多情况下常用框架之间的整合容易出现的冲突问题都有人总结过了，拿抛出的异常搜索一下基本上就可以直接找到对应的 jar 包。我们接下来要说的是通用方法。\n不管具体使用的是什么工具，基本思路无非是这么两步：\n第一步：把彼此冲突的 jar 包找到 第二步：在冲突的 jar 包中选定一个。具体做法无非是通过 exclusions 排除依赖，或是明确声明依赖。 ②IDEA 的 Maven Helper 插件 这个插件是 IDEA 中安装的插件，不是 Maven 插件。它能够给我们罗列出来同一个 jar 包的不同版本，以及它们的来源。但是对不同 jar 包中同名的类没有办法。 基于 pom.xml 的依赖冲突分析 ③Maven 的 enforcer 插件 使用 Maven 的 enforcer 插件既可以检测同一个 jar 包的不同版本，又可以检测不同 jar 包中同名的类。\n引入 netty 依赖 这里我们引入两个对 netty 的依赖，展示不同 jar 包中有同名类的情况。\n1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.jboss.netty\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;netty\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.2.10.Final\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.netty\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;netty\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.9.2.Final\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; Copied! 配置 enforcer 插件 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 \u0026lt;build\u0026gt; \u0026lt;pluginManagement\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-enforcer-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.1\u0026lt;/version\u0026gt; \u0026lt;executions\u0026gt; \u0026lt;execution\u0026gt; \u0026lt;id\u0026gt;enforce-dependencies\u0026lt;/id\u0026gt; \u0026lt;phase\u0026gt;validate\u0026lt;/phase\u0026gt; \u0026lt;goals\u0026gt; \u0026lt;goal\u0026gt;display-info\u0026lt;/goal\u0026gt; \u0026lt;goal\u0026gt;enforce\u0026lt;/goal\u0026gt; \u0026lt;/goals\u0026gt; \u0026lt;/execution\u0026gt; \u0026lt;/executions\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.codehaus.mojo\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;extra-enforcer-rules\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-beta-4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;rules\u0026gt; \u0026lt;banDuplicateClasses\u0026gt; \u0026lt;findAllDuplicates\u0026gt;true\u0026lt;/findAllDuplicates\u0026gt; \u0026lt;/banDuplicateClasses\u0026gt; \u0026lt;/rules\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/pluginManagement\u0026gt; \u0026lt;/build\u0026gt; Copied! 测试 执行如下 Maven 命令：\n1 mvn clean package enforcer:enforce Copied! 部分运行结果：\n[INFO] \u0026mdash; maven-enforcer-plugin:1.4.1:enforce (default-cli) @ pro32-duplicate-class \u0026mdash; [WARNING] Rule 0: org.apache.maven.plugins.enforcer.BanDuplicateClasses failed with message: Duplicate classes found:\nFound in: io.netty:netty:jar:3.9.2.Final:compile org.jboss.netty:netty:jar:3.2.10.Final:compile Duplicate classes: org/jboss/netty/channel/socket/ServerSocketChannelConfig.class org/jboss/netty/channel/socket/nio/NioSocketChannelConfig.class org/jboss/netty/util/internal/jzlib/Deflate.class org/jboss/netty/handler/codec/serialization/ObjectDecoder.class org/jboss/netty/util/internal/ConcurrentHashMap$HashIterator.class\n……\n二、体系外jar包导入 1、提出问题 目前来说我们在 Maven 工程中用到的 jar 包都是通过 Maven 本身的机制导入进来的。\n而实际开发中确实有可能用到一些 jar 包并非是用 Maven 的方式发布，那自然也没法通过 Maven 导入。\n此时如果我们能够拿到该 jar 包的源码那还可以自己建一个 Maven 工程，自己打包。可是如果连源码都没有呢？\n这方面的例子包括一些人脸识别用的 jar 包、海康视频监控 jar 包等等。\n2、解决办法 ①准备一个体系外 jar 包 通过学 Maven 以前的方式创建一个 Java 工程，然后导出 jar 包即可用来测试。\n②将该 jar 包安装到 Maven 仓库 这里我们使用 install 插件的 install-file 目标：\n1 2 3 4 5 mvn install:install-file -Dfile=[体系外 jar 包路径] \\ -DgroupId=[给体系外 jar 包强行设定坐标] \\ -DartifactId=[给体系外 jar 包强行设定坐标] \\ -Dversion=1 \\ -Dpackage=jar Copied! 例如（Windows 系统下使用 ^ 符号换行；Linux 系统用 \\）：\n1 2 3 4 5 mvn install:install-file -Dfile=D:\\idea2019workspace\\atguigu-maven-outer\\out\\artifacts\\atguigu_maven_outer\\atguigu-maven-outer.jar ^ -DgroupId=com.atguigu.maven ^ -DartifactId=atguigu-maven-outer ^ -Dversion=1 ^ -Dpackaging=jar Copied! 执行结果：\n再看本地仓库中确实有：\n我们打开 POM 文件看看：\n1 2 3 4 5 6 7 8 9 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34; xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;com.atguigu.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;atguigu-maven-outer\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1\u0026lt;/version\u0026gt; \u0026lt;description\u0026gt;POM was created from install:install-file\u0026lt;/description\u0026gt; \u0026lt;/project\u0026gt; Copied! ③测试 在其它地方依赖这个 jar 包：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.atguigu.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;atguigu-maven-outer\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 创建对象、调用方法：\n三、Nexus私服 http://c.biancheng.net/nexus/ 1、安装 下载 ​\thttps://sonatype-download.global.ssl.fastly.net/repository/downloads-prod-group/3/nexus-3.38.1-01-unix.tar.gz\n​\thttps://download.sonatype.com/nexus/3/latest-unix.tar.gz\n上传到linux ​\t上传到 Linux 系统，解压后即可使用，不需要安装。但是需要注意：必须提前安装 JDK\njava环境 1 2 3 4 5 6 7 8 9 10 11 12 13 14 yum install -y java-1.8.0-openjdk* java -version which java ls -lr /usr/bin/java ls -lr /etc/alternatives/java vi /etc/profile source /etc/profile echo $JAVA_HOME # /etc/profile export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.322.b06-1.el7_9.x86_64 export JRE_HOME=$JAVA_HOME/jre export CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH Copied! 开放端口或者禁用防火墙 1 2 3 systemctl status firewalld.service systemctl stop firewalld.service systemctl disable firewalld.service Copied! 2、启动nexus 1 2 3 ./nexus start ./nexus status cat /usr/local/opt/sonatype-work/nexus3/admin.password Copied! [root@x ~]# /opt/nexus-3.37.0-01/bin/nexus start WARNING: ************************************************************ WARNING: Detected execution as \u0026ldquo;root\u0026rdquo; user. This is NOT recommended! WARNING: ************************************************************ Starting nexus [root@x ~]# /opt/nexus-3.37.0-01/bin/nexus status WARNING: ************************************************************ WARNING: Detected execution as \u0026ldquo;root\u0026rdquo; user. This is NOT recommended! WARNING: ************************************************************ nexus is running.\n查看端口 [root@x ~]# netstat -anp | grep java tcp 0 0 127.0.0.1:45614 0.0.0.0:* LISTEN 9872/java tcp 0 0 0.0.0.0:8081 0.0.0.0:* LISTEN 9872/java\n上面 45614 这个每次都不一样，不用管它。我们要访问的是 8081 这个端口。但是需要注意：8081 端口的这个进程要在启动 /opt/nexus-3.37.0-01/bin/nexus 这个主体程序一、两分钟后才会启动，请耐心等待。\n问题 解决： 1 2 3 4 vi /etc/security/limits.conf # 末尾加 @root - nofile 65536 # 重启nexus Copied! root为系统用户，前面要加上@\n3、访问 Nexus 首页 首页地址：http://[Linux 服务器地址]:8081/\n这里参考提示：\n用户名：admin 密码：查看 /opt/sonatype-work/nexus3/admin.password 文件 [root@hello ~]# cat /opt/sonatype-work/nexus3/admin.password ed5e96a8-67aa-4dca-9ee8-1930b1dd5415\n给 admin 用户指定新密码：\n匿名登录，启用还是禁用？由于启用匿名登录后，后续操作比较简单，这里我们演示禁用匿名登录的操作方式：\n4、nexus各种库 在仓库列表中，每个仓库都具有一系列属性：\nType：仓库的类型，Nexus 中有 4 中仓库类型：group（仓库组）、hosted（宿主仓库）、proxy（代理仓库）以及 virtual（虚拟仓库）。 Format：仓库的格式。 Policy：仓库的策略，表示该仓库是发布（Release）版本仓库还是快照（Snapshot）版本仓库。 Repository Status：仓库的状态。 Repository Path：仓库的路径。 仓库类型 说明 proxy 用来代理远程公共仓库，如 Maven 中央仓库、JBoss 远程仓库。 group 用来聚合代理仓库和宿主仓库，为这些仓库提供统一的服务地址，以便 Maven 可以更加方便地获得这些仓库中的构件。 hosted 存放：本团队其他开发人员部署到 Nexus 的 jar 包 仓库名称 说明 maven-central 该仓库用来代理 Maven 中央仓库，其策略为 Release，只会下载和缓存中央仓库中的发布版本的构件 maven-public 该仓库组将上述所有存储策略为 Release 的仓库聚合并通过统一的地址提供服务 maven-releasse 策略为 Release 的宿主仓库，用来部署公司或组织内部的发布版本构件。 maven-snapshots 策略为 Snapshot 的宿主仓库，用来部署公司或组织内部的快照版本构件。 初始状态下，这几个仓库都没有内容\n由上图可知：\nMaven 可以直接从宿主仓库中下载构件。 Maven 也可以从代理仓库中下载构件，代理仓库会从远程仓库下载并缓存构件。 Maven 还可以从仓库组中下载构件，仓库组会从其包含的宿主仓库和代理仓库中获取构件。 5、开启索引 Nexus 作为一款成熟的仓库管理工具，它通过维护仓库的索引提供了构件搜索功能，以便帮助用户方便快速地找到所需构件。\nNexus 索引下载功能默认是关闭的，如果想在 Nexus 中搜索远程仓库中的构件，就需要先开启索引下载功能。\nNexus 能够遍历仓库的所有内容，搜集它们的坐标，校验和以及所包含的 Java 类等信息，然后以索引（ nexus-indexer） 的形式保存起来。Nexus 索引保存在 Nexus 安装目录下 \\sonatype-work\\nexus\\indexer 目录中，该目录下每个子目录都代表 Nexus 中的一个仓库，用来存放各个仓库的索引，大多数的远程公共仓库（例如，中央仓库）都维护了一个这样的索引，因此本地的 Nexus 在下载到这个索引后，就能在此基础上为用户提供构件搜索和浏览等服务。需要注意的是，并不是所有的公共仓库都提供了索引 ，对于那些没有提供索引的仓库来说，我们是无法对其进行搜索的。\n搜索： 6、新建阿里云代理 修改maven-public 7、下载构件 两种方式：pom.xml 和 setting.xml\n①、在 pom.xml 中配置 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 37 38 39 40 \u0026lt;!--声明一个或多个远程仓库 --\u0026gt; \u0026lt;repositories\u0026gt; \u0026lt;!-- 声明一个 Nexus 私服上的仓库 --\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;!--仓库id --\u0026gt; \u0026lt;id\u0026gt;nexus\u0026lt;/id\u0026gt; \u0026lt;!-- 仓库的名称 --\u0026gt; \u0026lt;name\u0026gt;nexus\u0026lt;/name\u0026gt; \u0026lt;!--仓库的地址 --\u0026gt; \u0026lt;url\u0026gt;http://10.211.55.4:8081/repository/maven-public/\u0026lt;/url\u0026gt; \u0026lt;!-- 是否开启该仓库的 release 版本下载支持 --\u0026gt; \u0026lt;releases\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/releases\u0026gt; \u0026lt;!-- 是否开启该仓库的 snapshot 版本下载支持 --\u0026gt; \u0026lt;snapshots\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/snapshots\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;/repositories\u0026gt; \u0026lt;!-- 声明一个或多个远程插件仓库 --\u0026gt; \u0026lt;pluginRepositories\u0026gt; \u0026lt;!--声明一个 Nexus 私服上的插件仓库 --\u0026gt; \u0026lt;pluginRepository\u0026gt; \u0026lt;!--插件仓库 id --\u0026gt; \u0026lt;id\u0026gt;nexus\u0026lt;/id\u0026gt; \u0026lt;!--插件仓库 名称 --\u0026gt; \u0026lt;name\u0026gt;nexus\u0026lt;/name\u0026gt; \u0026lt;!-- 配置的插件仓库的地址 --\u0026gt; \u0026lt;url\u0026gt;http://10.211.55.4:8081/repository/maven-public/\u0026lt;/url\u0026gt; \u0026lt;!-- 是否开启该插件仓库的 release 版本下载支持 --\u0026gt; \u0026lt;releases\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/releases\u0026gt; \u0026lt;!-- 是否开启该插件仓库的 snapshot 版本下载支持 --\u0026gt; \u0026lt;snapshots\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/snapshots\u0026gt; \u0026lt;/pluginRepository\u0026gt; \u0026lt;/pluginRepositories\u0026gt; Copied! ②、在 setting.xml 中配置 Nexus 私服通常会与镜像（mirror）结合使用，使 Nexus 成为所有远程仓库的私服，这样不仅可以从 Nexus 中获取所有所需构件，还能将配置集中到 Nexus 私服中，简化 Maven 本身的配置。\n我们可以创建一个匹配任何仓库的镜像，镜像的地址为 Nexus 中仓库的地址，这样 Maven 对于任何构件的下载请求都会被拦截跳转到 Nexus 私服中，其具体配置如下。\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 \u0026lt;mirrors\u0026gt; \u0026lt;mirror\u0026gt; \u0026lt;id\u0026gt;nexus-mine\u0026lt;/id\u0026gt; \u0026lt;name\u0026gt;nexus name\u0026lt;/name\u0026gt; \u0026lt;mirrorOf\u0026gt;*\u0026lt;/mirrorOf\u0026gt; \u0026lt;url\u0026gt;http://localhost:8082/nexus/content/groups/bianchengbang_repository_group/\u0026lt;/url\u0026gt; \u0026lt;/mirror\u0026gt; \u0026lt;/mirrors\u0026gt; \u0026lt;profiles\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;nexus\u0026lt;/id\u0026gt; \u0026lt;repositories\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;central\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;http://localhost:8082/nexus/content/repositories/bianchengbang_central_proxy/\u0026lt;/url\u0026gt; \u0026lt;releases\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/releases\u0026gt; \u0026lt;snapshots\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/snapshots\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;/repositories\u0026gt; \u0026lt;pluginRepositories\u0026gt; \u0026lt;pluginRepository\u0026gt; \u0026lt;id\u0026gt;central\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;http://localhost:8082/nexus/content/repositories/bianchengbang_central_proxy/\u0026lt;/url\u0026gt; \u0026lt;releases\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/releases\u0026gt; \u0026lt;snapshots\u0026gt; \u0026lt;enabled\u0026gt;true\u0026lt;/enabled\u0026gt; \u0026lt;/snapshots\u0026gt; \u0026lt;/pluginRepository\u0026gt; \u0026lt;/pluginRepositories\u0026gt; \u0026lt;/profile\u0026gt; \u0026lt;/profiles\u0026gt; \u0026lt;activeProfiles\u0026gt; \u0026lt;activeProfile\u0026gt;nexus\u0026lt;/activeProfile\u0026gt; \u0026lt;/activeProfiles\u0026gt; \u0026lt;servers\u0026gt; \u0026lt;server\u0026gt; \u0026lt;id\u0026gt;nexus-mine\u0026lt;/id\u0026gt; \u0026lt;username\u0026gt;admin\u0026lt;/username\u0026gt; \u0026lt;password\u0026gt;password\u0026lt;/password\u0026gt; \u0026lt;/server\u0026gt; \u0026lt;/servers\u0026gt; Copied! 8、上传构件 首先pom.xml ： 1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;project\u0026gt; ... \u0026lt;distributionManagement\u0026gt; \u0026lt;repository\u0026gt; \u0026lt;id\u0026gt;my_releases\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;http://10.211.55.4:8081/repository/maven-releases/\u0026lt;/url\u0026gt; \u0026lt;/repository\u0026gt; \u0026lt;snapshotRepository\u0026gt; \u0026lt;id\u0026gt;my_snapshots\u0026lt;/id\u0026gt; \u0026lt;url\u0026gt;http://10.211.55.4:8081/repository/maven-snapshots/\u0026lt;/url\u0026gt; \u0026lt;/snapshotRepository\u0026gt; \u0026lt;/distributionManagement\u0026gt; \u0026lt;/project\u0026gt; Copied! 并且settings.xml ： 1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;servers\u0026gt; \u0026lt;server\u0026gt; \u0026lt;id\u0026gt;my_releases\u0026lt;/id\u0026gt; \u0026lt;username\u0026gt;admin\u0026lt;/username\u0026gt; \u0026lt;password\u0026gt;password\u0026lt;/password\u0026gt; \u0026lt;/server\u0026gt; \u0026lt;server\u0026gt; \u0026lt;id\u0026gt;my_snapshots\u0026lt;/id\u0026gt; \u0026lt;username\u0026gt;admin\u0026lt;/username\u0026gt; \u0026lt;password\u0026gt;password\u0026lt;/password\u0026gt; \u0026lt;/server\u0026gt; \u0026lt;/servers\u0026gt; Copied! 四、复制仓库 复制仓库，jar包的mirrorId不一样，所以不会用到本地包\n_remote.repositories: mine和central是mirrorId\n1 2 3 4 5 6 #NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice. #Mon Apr 11 19:18:31 CST 2022 hutool-all-4.5.4.jar\u0026gt;mine= hutool-all-4.5.4.pom\u0026gt;central= hutool-all-4.5.4.pom\u0026gt;mine= hutool-all-4.5.4.jar\u0026gt;central= Copied! 需要执行脚本 删除lastUpdated 和 _remote.repositories\n1 2 3 #!/bin/bash find $(cd $(dirname $0); pwd) -name \u0026#34;*.lastUpdated\u0026#34; -type f -print -exec rm -rf {} \\; find $(cd $(dirname $0); pwd) -name \u0026#34;_remote.repositories\u0026#34; -type f -print -exec rm -rf {} \\; Copied! ","date":"2024-03-03T18:49:51+08:00","image":"https://logan.1357810.xyz/cover/pic_008.jpg","permalink":"https://qh.1357810.xyz/articles/maven/maven-production/","title":"6 生产实践"},{"content":"1.准备\npom.xml 依赖如下：\n1 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 37 \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.11\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.18.22\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.7.22\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;ch.qos.logback\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;logback-classic\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.junit.jupiter\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit-jupiter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;RELEASE\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;compile\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; Copied! logback.xml 配置如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;configuration scan=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;appender name=\u0026#34;STDOUT\u0026#34; class=\u0026#34;ch.qos.logback.core.ConsoleAppender\u0026#34;\u0026gt; \u0026lt;encoder\u0026gt; \u0026lt;pattern\u0026gt;%date{HH:mm:ss} [%t] %logger - %m%n\u0026lt;/pattern\u0026gt; \u0026lt;/encoder\u0026gt; \u0026lt;/appender\u0026gt; \u0026lt;logger name=\u0026#34;c\u0026#34; level=\u0026#34;debug\u0026#34; additivity=\u0026#34;false\u0026#34;\u0026gt; \u0026lt;appender-ref ref=\u0026#34;STDOUT\u0026#34;/\u0026gt; \u0026lt;/logger\u0026gt; \u0026lt;root level=\u0026#34;ERROR\u0026#34;\u0026gt; \u0026lt;appender-ref ref=\u0026#34;STDOUT\u0026#34;/\u0026gt; \u0026lt;/root\u0026gt; \u0026lt;/configuration\u0026gt; Copied! 2.进程与线程 2.1 进程与线程 进程 程序由指令和数据组成，但这些指令要运行，数据要读写，就必须将指令加载至 CPU，数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的 。 当一个程序被运行，从磁盘加载这个程序的代码至内存，这时就开启了一个进程。 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程（例如记事本、画图、浏览器 等），也有的程序只能启动一个实例进程（例如网易云音乐、360 安全卫士等） 线程 一个进程之内可以分为一到多个线程。\n一个线程就是一个指令流，将指令流中的一条条指令以一定的顺序交给 CPU 执行\nJava 中，线程作为最小调度单位，进程作为资源分配的最小单位。 在 windows 中进程是不活动的，只是作 为线程的容器\n二者对比 进程基本上相互独立的，而线程存在于进程内，是进程的一个子集\n进程拥有共享的资源，如内存空间等，供其内部的线程共享\n进程间通信较为复杂\n同一台计算机的进程通信称为 IPC（Inter-process communication） 不同计算机之间的进程通信，需要通过网络，并遵守共同的协议，例如 HTTP 线程通信相对简单，因为它们共享进程内的内存，一个例子是多个线程可以访问同一个共享变量\n线程更轻量，线程上下文切换成本一般上要比进程上下文切换低\n2.2 并行与并发 单核cpu下，线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器，将 cpu 的时间片（windows 下时间片最小约为 15 毫秒）分给不同的程序使用，只是由于 cpu 在线程间（时间片很短）的切换非常快，人类感觉是同时运行的 。总结为一句话就是： 微观串行，宏观并行 。\n一般会将这种线程轮流使用 CPU 的做法称为并发， concurrent\nCPU 时间片 1 时间片 2 时间片 3 时间片 4 core 线程 1 线程 2 线程 3 线程 4 多核 cpu下，每个 核（core） 都可以调度运行线程，这时候线程可以是并行的。\nCPU 时间片 1 时间片 2 时间片 3 时间片 4 core1 线程 1 线程 2 线程 3 线程 4 core2 线程 4 线程 4 线程 2 线程 2 引用 Rob Pike 的一段描述：\n​\t并发（concurrent）是同一时间应对（dealing with）多件事情的能力 。\n​\t并行（parallel）是同一时间动手做（doing）多件事情的能力。\n2.3 应用 $\\textcolor{Green}{*应用之异步调用（案例1）} $ 需要等待结果 这时既可以使用同步处理，也可以使用异步来处理\njoin 实现（同步）\n1 2 3 4 5 6 7 8 9 10 11 12 13 static int result = 0; private static void test1() throws InterruptedException { log.debug(\u0026#34;开始\u0026#34;); Thread t1 = new Thread(() -\u0026gt; { log.debug(\u0026#34;开始\u0026#34;); sleep(1); log.debug(\u0026#34;结束\u0026#34;); result = 10; }, \u0026#34;t1\u0026#34;); t1.start(); t1.join(); log.debug(\u0026#34;结果为:{}\u0026#34;, result); } Copied! 输出\n1 2 3 4 20:30:40.453 [main] c.TestJoin - 开始 20:30:40.541 [Thread-0] c.TestJoin - 开始 20:30:41.543 [Thread-0] c.TestJoin - 结束 20:30:41.551 [main] c.TestJoin - 结果为:10 Copied! 评价\n需要外部共享变量，不符合面向对象封装的思想 必须等待线程结束，不能配合线程池使用 Future 实现（同步） 1 2 3 4 5 6 7 8 9 10 11 private static void test2() throws InterruptedException, ExecutionException { log.debug(\u0026#34;开始\u0026#34;); FutureTask\u0026lt;Integer\u0026gt; result = new FutureTask\u0026lt;\u0026gt;(() -\u0026gt; { log.debug(\u0026#34;开始\u0026#34;); sleep(1); log.debug(\u0026#34;结束\u0026#34;); return 10; }); new Thread(result, \u0026#34;t1\u0026#34;).start(); log.debug(\u0026#34;结果为:{}\u0026#34;, result.get()); } Copied! 输出\n1 2 3 4 10:11:57.880 c.TestSync [main] - 开始 10:11:57.942 c.TestSync [t1] - 开始 10:11:58.943 c.TestSync [t1] - 结束 10:11:58.943 c.TestSync [main] - 结果为:10 Copied! 评价\n规避了使用 join 之前的缺点 可以方便配合线程池使用 1 2 3 4 5 6 7 8 9 10 11 12 private static void test3() throws InterruptedException, ExecutionException { ExecutorService service = Executors.newFixedThreadPool(1); log.debug(\u0026#34;开始\u0026#34;); Future\u0026lt;Integer\u0026gt; result = service.submit(() -\u0026gt; { log.debug(\u0026#34;开始\u0026#34;); sleep(1); log.debug(\u0026#34;结束\u0026#34;); return 10; }); log.debug(\u0026#34;结果为:{}, result 的类型:{}\u0026#34;, result.get(), result.getClass()); service.shutdown(); } Copied! 输出\n1 2 3 4 10:17:40.090 c.TestSync [main] - 开始 10:17:40.150 c.TestSync [pool-1-thread-1] - 开始 10:17:41.151 c.TestSync [pool-1-thread-1] - 结束 10:17:41.151 c.TestSync [main] - 结果为:10, result 的类型:class java.util.concurrent.FutureTask Copied! 评价\n仍然是 main 线程接收结果 get 方法是让调用线程同步等待 自定义实现（同步） 见模式篇：保护性暂停模式\nCompletableFuture 实现（异步） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private static void test4() { // 进行计算的线程池 ExecutorService computeService = Executors.newFixedThreadPool(1); // 接收结果的线程池 ExecutorService resultService = Executors.newFixedThreadPool(1); log.debug(\u0026#34;开始\u0026#34;); CompletableFuture.supplyAsync(() -\u0026gt; { log.debug(\u0026#34;开始\u0026#34;); sleep(1); log.debug(\u0026#34;结束\u0026#34;); return 10; }, computeService).thenAcceptAsync((result) -\u0026gt; { log.debug(\u0026#34;结果为:{}\u0026#34;, result); }, resultService); } Copied! 输出\n1 2 3 4 10:36:28.114 c.TestSync [main] - 开始 10:36:28.164 c.TestSync [pool-1-thread-1] - 开始 10:36:29.165 c.TestSync [pool-1-thread-1] - 结束 10:36:29.165 c.TestSync [pool-2-thread-1] - 结果为:10 Copied! 评价\n可以让调用线程异步处理结果，实际是其他线程去同步等待 可以方便地分离不同职责的线程池 以任务为中心，而不是以线程为中心 BlockingQueue 实现（异步） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private static void test6() { ExecutorService consumer = Executors.newFixedThreadPool(1); ExecutorService producer = Executors.newFixedThreadPool(1); BlockingQueue\u0026lt;Integer\u0026gt; queue = new SynchronousQueue\u0026lt;\u0026gt;(); log.debug(\u0026#34;开始\u0026#34;); producer.submit(() -\u0026gt; { log.debug(\u0026#34;开始\u0026#34;); sleep(1); log.debug(\u0026#34;结束\u0026#34;); try { queue.put(10); } catch (InterruptedException e) { e.printStackTrace(); } }); consumer.submit(() -\u0026gt; { try { Integer result = queue.take(); log.debug(\u0026#34;结果为:{}\u0026#34;, result); } catch (InterruptedException e) { e.printStackTrace(); } }); } Copied! 不需等待结果 这时最好是使用异步来处理\n普通线程实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Slf4j(topic = \u0026#34;c.FileReader\u0026#34;) public class FileReader { public static void read(String filename) { int idx = filename.lastIndexOf(File.separator); String shortName = filename.substring(idx + 1); try (FileInputStream in = new FileInputStream(filename)) { long start = System.currentTimeMillis(); log.debug(\u0026#34;read [{}] start ...\u0026#34;, shortName); byte[] buf = new byte[1024]; int n = -1; do { n = in.read(buf); } while (n != -1); long end = System.currentTimeMillis(); log.debug(\u0026#34;read [{}] end ... cost: {} ms\u0026#34;, shortName, end - start); } catch (IOException e) { e.printStackTrace(); } } } Copied! 没有用线程时，方法的调用是同步的：\n1 2 3 4 5 6 7 8 @Slf4j(topic = \u0026#34;c.Sync\u0026#34;) public class Sync { public static void main(String[] args) { String fullPath = \u0026#34;E:\\\\1.mp4\u0026#34;; FileReader.read(fullPath); log.debug(\u0026#34;do other things ...\u0026#34;); } } Copied! 输出\n1 2 3 18:39:15 [main] c.FileReader - read [1.mp4] start ... 18:39:19 [main] c.FileReader - read [1.mp4] end ... cost: 4090 ms 18:39:19 [main] c.Sync - do other things ... Copied! 使用了线程后，方法的调用时异步的：\n1 2 3 4 private static void test1() { new Thread(() -\u0026gt; FileReader.read(Constants.MP4_FULL_PATH)).start(); log.debug(\u0026#34;do other things ...\u0026#34;); } Copied! 输出\n1 2 3 18:41:53 [main] c.Async - do other things ... 18:41:53 [Thread-0] c.FileReader - read [1.mp4] start ... 18:41:57 [Thread-0] c.FileReader - read [1.mp4] end ... cost: 4197 ms Copied! 线程池实现 1 2 3 4 5 6 private static void test2() { ExecutorService service = Executors.newFixedThreadPool(1); service.execute(() -\u0026gt; FileReader.read(Constants.MP4_FULL_PATH)); log.debug(\u0026#34;do other things ...\u0026#34;); service.shutdown(); } Copied! 输出\n1 2 3 11:03:31.245 c.TestAsyc [main] - do other things ... 11:03:31.245 c.FileReader [pool-1-thread-1] - read [1.mp4] start ... 11:03:33.479 c.FileReader [pool-1-thread-1] - read [1.mp4] end ... cost: 2235 ms Copied! CompletableFuture 实现 1 2 3 4 5 private static void test3() throws IOException { CompletableFuture.runAsync(() -\u0026gt; FileReader.read(Constants.MP4_FULL_PATH)); log.debug(\u0026#34;do other things ...\u0026#34;); System.in.read(); } Copied! 输出\n1 2 3 11:09:38.145 c.TestAsyc [main] - do other things ... 11:09:38.145 c.FileReader [ForkJoinPool.commonPool-worker-1] - read [1.mp4] start ... 11:09:40.514 c.FileReader [ForkJoinPool.commonPool-worker-1] - read [1.mp4] end ... cost: 2369 ms Copied! 以调用方角度来讲，\n如果 需要等待结果返回，才能继续运行就是同步 不需要等待结果返回，就能继续运行就是异步 1.设计\n多线程可以让方法执行变为异步的（即不要巴巴干等着）、比如说读取磁盘文件时，假设读取操作花费了 5 秒钟，如果没有线程调度机制，这 5 秒 cpu 什么都做不了，其它代码都得暂停\u0026hellip;\n2.结论\n比如在项目中，视频文件需要转换格式等操作比较费时，这时开一个新线程处理视频转换，避免阻塞主线程 tomcat 的异步 servlet 也是类似的目的，让用户线程处理耗时较长的操作，避免阻塞 tomcat 的工作线程 ui 程序中，开线程进行其他操作，避免阻塞 ui 线程 $\\textcolor{Green}{*应用之提高效率（案例1） }$ 充分利用多核 cpu 的优势，提高运行效率。想象下面的场景，执行 3 个计算，最后将计算结果汇总。\n1 2 3 4 计算 1 花费 10 ms 计算 2 花费 11 ms 计算 3 花费 9 ms 汇总需要 1 ms Copied! 如果是串行执行，那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms\n但如果是四核 cpu，各个核心分别使用线程 1 执行计算 1，线程 2 执行计算 2，线程 3 执行计算 3，那么 3 个 线程是并行的，花费时间只取决于最长的那个线程运行的时间，即 11ms 最后加上汇总时间只会花费 12ms\n注意：\n需要在多核 cpu 才能提高效率，单核仍然时是轮流执行\n1.设计 代码见【应用之效率-案例1】\n2.结论 单核 cpu 下，多线程不能实际提高程序运行效率，只是为了能够在不同的任务之间切换，不同线程轮流使用 cpu ，不至于一个线程总占用 cpu，别的线程没法干活 多核 cpu 可以并行跑多个线程，但能否提高程序运行效率还是要分情况的 有些任务，经过精心设计，将任务拆分，并行执行，当然可以提高程序的运行效率。但不是所有计算任 务都能拆分（参考后文的【阿姆达尔定律】） 也不是所有任务都需要拆分，任务的目的如果不同，谈拆分和效率没啥意义 IO 操作不占用 cpu，只是我们一般拷贝文件使用的是【阻塞 IO】，这时相当于线程虽然不用 cpu，但需要一 直等待 IO 结束，没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化。 3.Java 线程 3.1 创建和运行线程 方法一，直接使用 Thread 1 2 3 4 5 6 7 8 // 创建线程对象 Thread t = new Thread() { public void run() { // 要执行的任务 } }; // 启动线程 t.start(); Copied! 例如：\n1 2 3 4 5 6 7 8 9 // 构造方法的参数是给线程指定名字，推荐 Thread t1 = new Thread(\u0026#34;t1\u0026#34;) { @Override // run 方法内实现了要执行的任务 public void run() { log.debug(\u0026#34;hello\u0026#34;); } }; t1.start(); Copied! 输出：\n1 19:19:00 [t1] c.ThreadStarter - hello Copied! 方法二，使用 Runnable 配合 Thread 把【线程】和【任务】（要执行的代码）分开\nThread 代表线程 Runnable 可运行的任务（线程要执行的代码） 1 2 3 4 5 6 7 8 9 Runnable runnable = new Runnable() { public void run(){ // 要执行的任务 } }; // 创建线程对象 Thread t = new Thread( runnable ); // 启动线程 t.start(); Copied! 例如：\n1 2 3 4 5 6 7 8 9 10 // 创建任务对象 Runnable task2 = new Runnable() { @Override public void run() { log.debug(\u0026#34;hello\u0026#34;); } }; // 参数1 是任务对象; 参数2 是线程名字，推荐 Thread t2 = new Thread(task2, \u0026#34;t2\u0026#34;); t2.start(); Copied! 输出：\n1 9:19:00 [t2] c.ThreadStarter - hello Copied! Java 8 以后可以使用 lambda 精简代码\n1 2 3 4 5 // 创建任务对象 Runnable task2 = () -\u0026gt; log.debug(\u0026#34;hello\u0026#34;); // 参数1 是任务对象; 参数2 是线程名字，推荐 Thread t2 = new Thread(task2, \u0026#34;t2\u0026#34;); t2.start(); Copied! Thread 与 Runnable 的关系 分析 Thread 的源码，理清它与 Runnable 的关系\n1 2 3 4 //Runnable源码 public interface Runnable { public abstract void run(); } Copied! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //Thread源码（部分） public class Thread implements Runnable { /* What will be run. */ private Runnable target; public Thread(Runnable target) { init(null, target, \u0026#34;Thread-\u0026#34; + nextThreadNum(), 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { //... this.target = target; //... } @Override public void run() { if (target != null) { target.run(); } } Copied! 小结 方法1 是把线程和任务合并在了一起，方法2 是把线程和任务分开了 用 Runnable 更容易与线程池等高级API 配合 用 Runnable 让任务类脱离了 Thread 继承体系，更灵活 方法三，FutureTask 配合 Thread FutureTask 能够接收 Callable 类型的参数，用来处理有返回结果的情况\n1 2 3 4 5 6 7 8 9 10 // 创建任务对象 FutureTask\u0026lt;Integer\u0026gt; task3 = new FutureTask\u0026lt;\u0026gt;(() -\u0026gt; { log.debug(\u0026#34;hello\u0026#34;); return 100; }); // 参数1 是任务对象; 参数2 是线程名字，推荐 new Thread(task3, \u0026#34;t3\u0026#34;).start(); // 主线程阻塞，同步等待 task 执行完毕的结果 Integer result = task3.get(); log.debug(\u0026#34;结果是:{}\u0026#34;, result); Copied! 输出\n1 2 19:22:27 [t3] c.ThreadStarter - hello 19:22:27 [main] c.ThreadStarter - 结果是:100 Copied! 源码分析\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 //FutureTask源码（部分） public class FutureTask\u0026lt;V\u0026gt; implements RunnableFuture\u0026lt;V\u0026gt; { /** The underlying callable; nulled out after running */ private Callable\u0026lt;V\u0026gt; callable; /** The result to return or exception to throw from get() */ private Object outcome; // non-volatile, protected by state reads/writes public FutureTask(Callable\u0026lt;V\u0026gt; callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; this.state = NEW; // ensure visibility of callable } public void run() { //... try { Callable\u0026lt;V\u0026gt; c = callable; if (c != null \u0026amp;\u0026amp; state == NEW) { V result; boolean ran; try { result = c.call(); ran = true; } catch (Throwable ex) { result = null; ran = false; setException(ex); } if (ran) set(result); } } //... } protected void set(V v) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome = v; UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state finishCompletion(); } } public V get() throws InterruptedException, ExecutionException { int s = state; if (s \u0026lt;= COMPLETING) s = awaitDone(false, 0L); return report(s); } private V report(int s) throws ExecutionException { Object x = outcome; if (s == NORMAL) return (V)x; if (s \u0026gt;= CANCELLED) throw new CancellationException(); throw new ExecutionException((Throwable)x); } } Copied! 1 2 3 4 5 6 7 8 9 10 11 //Callable源码 @FunctionalInterface public interface Callable\u0026lt;V\u0026gt; { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; } Copied! 说明：\nFutureTask内置了一个Callable对象，初始化方法将指定的Callable赋给这个对象。 FutureTask实现了Runnable接口，并重写了Run方法，在Run方法中调用了Callable中的call方法，并将返回值赋值给outcome变量 get方法就是取出outcome的值。 3.2 观察多个线程同时运行 主要是理解\n交替执行 谁先谁后，不由我们控制 示例代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j(topic = \u0026#34;c.TestMultiThread\u0026#34;) public class TestMultiThread { public static void main(String[] args) { new Thread(() -\u0026gt; { while(true) { log.debug(\u0026#34;running\u0026#34;); } },\u0026#34;t1\u0026#34;).start(); new Thread(() -\u0026gt; { while(true) { log.debug(\u0026#34;running\u0026#34;); } },\u0026#34;t2\u0026#34;).start(); } } Copied! 运行结果：\n1 2 3 4 5 6 7 8 9 10 23:45:26.254 c.TestMultiThread [t2] - running 23:45:26.254 c.TestMultiThread [t2] - running 23:45:26.254 c.TestMultiThread [t2] - running 23:45:26.254 c.TestMultiThread [t2] - running 23:45:26.254 c.TestMultiThread [t1] - running 23:45:26.254 c.TestMultiThread [t1] - running 23:45:26.254 c.TestMultiThread [t1] - running 23:45:26.254 c.TestMultiThread [t1] - running 23:45:26.254 c.TestMultiThread [t1] - running 23:45:26.254 c.TestMultiThread [t1] - running Copied! 3.3 查看进程线程的方法 windows 任务管理器可以查看进程和线程数，也可以用来杀死进程 tasklist 查看进程 tasklist | findstr (查找关键字) taskkill 杀死进程 taskkill /F(彻底杀死）/PID(进程PID) Linux ps -fe 查看所有进程 ps -fT -p 查看某个进程（PID）的所有线程 kill 杀死进程 top 按大写 H 切换是否显示线程 top -H -p 查看某个进程（PID）的所有线程 Java jps 命令查看所有 Java 进程 jstack 查看某个 Java 进程（PID）的所有线程状态 jconsole 来查看某个 Java 进程中线程的运行情况（图形界面） jconsole 远程监控配置\n需要以如下方式运行你的 java 类\n1 2 3 java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote - Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 - Dcom.sun.management.jmxremote.authenticate=是否认证 java类 Copied! 关闭防火墙，允许端口\n修改 /etc/hosts 文件将 127.0.0.1 映射至主机名\n如果要认证访问，还需要做如下步骤\n复制 jmxremote.password 文件 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写 连接时填入 controlRole（用户名），R\u0026amp;D（密码） $\\textcolor{Blue}{3.4 * 原理之线程运行} $ 栈与栈帧 Java Virtual Machine Stacks （Java 虚拟机栈）\n我们都知道 JVM 中由堆、栈、方法区所组成，其中栈内存是给谁用的呢？其实就是线程，每个线程启动后，虚拟 机就会为其分配一块栈内存。\n每个栈由多个栈帧（Frame）组成，对应着每次方法调用时所占用的内存 每个线程只能有一个活动栈帧，对应着当前正在执行的那个方法 线程上下文切换（Thread Context Switch） 因为以下一些原因导致 cpu 不再执行当前的线程，转而执行另一个线程的代码\n线程的 cpu 时间片用完 垃圾回收 有更高优先级的线程需要运行 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法 当 Context Switch 发生时，需要由操作系统保存当前线程的状态，并恢复另一个线程的状态，Java 中对应的概念 就是程序计数器（Program Counter Register），它的作用是记住下一条 jvm 指令的执行地址，是线程私有的\n状态包括程序计数器、虚拟机栈中每个栈帧的信息，如局部变量、操作数栈、返回地址等 Context Switch 频繁发生会影响性能 3.5常见方法 方法 功能 说明 public void start() 启动一个新线程；Java虚拟机调用此线程的run方法 start 方法只是让线程进入就绪，里面代码不一定立刻 运行（CPU 的时间片还没分给它）。每个线程对象的 start方法只能调用一次，如果调用了多次会出现 IllegalThreadStateException public void run() 线程启动后调用该方法 如果在构造 Thread 对象时传递了 Runnable 参数，则 线程启动后会调用 Runnable 中的 run 方法，否则默 认不执行任何操作。但可以创建 Thread 的子类对象， 来覆盖默认行为 public void setName(String name) 给当前线程取名字 public void getName() 获取当前线程的名字。线程存在默认名称：子线程是Thread-索引，主线程是main public static Thread currentThread() 获取当前线程对象，代码在哪个线程中执行 public static void sleep(long time) 让当前线程休眠多少毫秒再继续执行。Thread.sleep(0) : 让操作系统立刻重新进行一次cpu竞争 public static native void yield() 提示线程调度器让出当前线程对CPU的使用 主要是为了测试和调试 public final int getPriority() 返回此线程的优先级 public final void setPriority(int priority) 更改此线程的优先级，常用1 5 10 java中规定线程优先级是1~10 的整数，较大的优先级 能提高该线程被 CPU 调度的机率 public void interrupt() 中断这个线程，异常处理机制 public static boolean interrupted() 判断当前线程是否被打断，清除打断标记 public boolean isInterrupted() 判断当前线程是否被打断，不清除打断标记 public final void join() 等待这个线程结束 public final void join(long millis) 等待这个线程死亡millis毫秒，0意味着永远等待 public final native boolean isAlive() 线程是否存活（还没有运行完毕） public final void setDaemon(boolean on) 将此线程标记为守护线程或用户线程 public long getId() 获取线程长整型 的 id id 唯一 public state getState() 获取线程状态 Java 中线程状态是用 6 个 enum 表示，分别为： NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED public boolean isInterrupted() 判断是否被打 断 不会清除 打断标记 3.6 start 与 run 调用 run 1 2 3 4 5 6 7 8 9 10 11 public static void main(String[] args) { Thread t1 = new Thread(\u0026#34;t1\u0026#34;) { @Override public void run() { log.debug(Thread.currentThread().getName()); FileReader.read(Constants.MP4_FULL_PATH); } }; t1.run(); log.debug(\u0026#34;do other things ...\u0026#34;); } Copied! 输出\n1 2 3 4 19:39:14 [main] c.TestStart - main 19:39:14 [main] c.FileReader - read [1.mp4] start ... 19:39:18 [main] c.FileReader - read [1.mp4] end ... cost: 4227 ms 19:39:18 [main] c.TestStart - do other things ... Copied! 程序仍在 main 线程运行， FileReader.read() 方法调用还是同步的\n调用start 将上述代码的 t1.run() 改为\n1 t1.start(); Copied! 输出\n1 2 3 4 19:41:30 [main] c.TestStart - do other things ... 19:41:30 [t1] c.TestStart - t1 19:41:30 [t1] c.FileReader - read [1.mp4] start ... 19:41:35 [t1] c.FileReader - read [1.mp4] end ... cost: 4542 ms Copied! 程序在 t1 线程运行， FileReader.read() 方法调用是异步的\n小结 直接调用 run 是在主线程中执行了 run，没有启动新的线程\n使用 start 是启动新的线程，通过新的线程间接执行 run 中的代码\n1 2 3 4 5 6 7 8 9 10 11 public static void main(String[] args) { Thread t1 = new Thread(\u0026#34;t1\u0026#34;) { @Override public void run() { log.debug(\u0026#34;running...\u0026#34;); } }; System.out.println(t1.getState()); t1.start(); System.out.println(t1.getState()); } Copied! 可以看见，start方法创建了一个新线程，将线程从就绪状态切换为Runnable\n1 2 3 NEW RUNNABLE 03:45:12.255 c.Test5 [t1] - running... Copied! 3.7 sleep 与 yield sleep 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态（阻塞）\n其它线程可以使用 interrupt 方法打断正在睡眠的线程，这时 sleep 方法会抛出 InterruptedException\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(\u0026#34;t1\u0026#34;) { @Override public void run() { log.debug(\u0026#34;enter sleep...\u0026#34;); try { Thread.sleep(2000); } catch (InterruptedException e) { log.debug(\u0026#34;wake up...\u0026#34;); e.printStackTrace(); } } }; t1.start(); Thread.sleep(1000); log.debug(\u0026#34;interrupt...\u0026#34;); t1.interrupt(); } Copied! 输出结果：\n1 2 3 4 5 6 03:47:18.141 c.Test7 [t1] - enter sleep... 03:47:19.132 c.Test7 [main] - interrupt... 03:47:19.132 c.Test7 [t1] - wake up... java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at cn.itcast.test.Test7$1.run(Test7.java:14) Copied! 睡眠结束后的线程未必会立刻得到执行\n建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 。其底层还是sleep方法。\n1 2 3 4 5 6 7 8 9 10 @Slf4j(topic = \u0026#34;c.Test8\u0026#34;) public class Test8 { public static void main(String[] args) throws InterruptedException { log.debug(\u0026#34;enter\u0026#34;); TimeUnit.SECONDS.sleep(1); log.debug(\u0026#34;end\u0026#34;); // Thread.sleep(1000); } } Copied! 在循环访问锁的过程中，可以加入sleep让线程阻塞时间，防止大量占用cpu资源。 yield 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态，然后调度执行其它线程 具体的实现依赖于操作系统的任务调度器 线程优先级 线程优先级会提示（hint）调度器优先调度该线程，但它仅仅是一个提示，调度器可以忽略它 如果 cpu 比较忙，那么优先级高的线程会获得更多的时间片，但 cpu 闲时，优先级几乎没作用 测试优先级和yield 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Slf4j(topic = \u0026#34;c.TestYield\u0026#34;) public class TestYield { public static void main(String[] args) { Runnable task1 = () -\u0026gt; { int count = 0; for (;;) { System.out.println(\u0026#34;----\u0026gt;1 \u0026#34; + count++); } }; Runnable task2 = () -\u0026gt; { int count = 0; for (;;) { // Thread.yield(); System.out.println(\u0026#34; ----\u0026gt;2 \u0026#34; + count++); } }; Thread t1 = new Thread(task1, \u0026#34;t1\u0026#34;); Thread t2 = new Thread(task2, \u0026#34;t2\u0026#34;); t1.setPriority(Thread.MIN_PRIORITY); t2.setPriority(Thread.MAX_PRIORITY); t1.start(); t2.start(); } } Copied! 测试结果：\n1 2 3 4 5 6 #优先级 ----\u0026gt;1 283500 ----\u0026gt;2 374389 #yield ----\u0026gt;1 119199 ----\u0026gt;2 101074 Copied! 可以看出，线程优先级和yield会对线程获取cpu时间片产生一定影响，但不会影响太大。\n$\\textcolor{Green}{* 应用之限制（案例1） } $ sleep 实现 在没有利用 cpu 来计算时，不要让 while(true) 空转浪费 cpu，这时可以使用 yield 或 sleep 来让出 cpu 的使用权 给其他程序\n1 2 3 4 5 6 7 while(true) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } Copied! 可以用 wait 或 条件变量达到类似的效果 不同的是，后两种都需要加锁，并且需要相应的唤醒操作，一般适用于要进行同步的场景 sleep 适用于无需锁同步的场景 wait 实现 1 2 3 4 5 6 7 8 9 10 synchronized(锁对象) { while(条件不满足) { try { 锁对象.wait(); } catch(InterruptedException e) { e.printStackTrace(); } } // do sth... } Copied! 条件变量实现 1 2 3 4 5 6 7 8 9 10 11 12 13 lock.lock(); try { while(条件不满足) { try { 条件变量.await(); } catch (InterruptedException e) { e.printStackTrace(); } } // do sth... } finally { lock.unlock(); } Copied! 3.8 join 方法详解 为什么需要 join 下面的代码执行，打印 r 是什么？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static int r = 0; public static void main(String[] args) throws InterruptedException { test1(); } private static void test1() throws InterruptedException { log.debug(\u0026#34;开始\u0026#34;); Thread t1 = new Thread(() -\u0026gt; { log.debug(\u0026#34;开始\u0026#34;); sleep(1); log.debug(\u0026#34;结束\u0026#34;); r = 10; }); t1.start(); log.debug(\u0026#34;结果为:{}\u0026#34;, r); log.debug(\u0026#34;结束\u0026#34;); } Copied! 分析\n因为主线程和线程 t1 是并行执行的，t1 线程需要 1 秒之后才能算出 r=10 而主线程一开始就要打印 r 的结果，所以只能打印出 r=0 解决方法\n用 sleep 行不行？为什么？ 用 join，加在 t1.start() 之后即可 $\\textcolor{green}{* 应用之同步（案例1）}$ 以调用方角度来讲，如果\n需要等待结果返回，才能继续运行就是同步 不需要等待结果返回，就能继续运行就是异步 等待多个结果\n问，下面代码 cost 大约多少秒？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static int r1 = 0; static int r2 = 0; public static void main(String[] args) throws InterruptedException { test2(); } private static void test2() throws InterruptedException { Thread t1 = new Thread(() -\u0026gt; { sleep(1); r1 = 10; }); Thread t2 = new Thread(() -\u0026gt; { sleep(2); r2 = 20; }); long start = System.currentTimeMillis(); t1.start(); t2.start(); t1.join(); t2.join(); long end = System.currentTimeMillis(); log.debug(\u0026#34;r1: {} r2: {} cost: {}\u0026#34;, r1, r2, end - start); } Copied! 分析如下\n第一个 join：等待 t1 时, t2 并没有停止, 而在运行 第二个 join：1s 后, 执行到此, t2 也运行了 1s, 因此也只需再等待 1s 如果颠倒两个 join 呢？\n最终都是输出\n1 20:45:43.239 [main] c.TestJoin - r1: 10 r2: 20 cost: 2005 Copied! 有时效的join 当线程执行时间没有超过join设定时间\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static int r1 = 0; static int r2 = 0; public static void main(String[] args) throws InterruptedException { test3(); } public static void test3() throws InterruptedException { Thread t1 = new Thread(() -\u0026gt; { sleep(1); r1 = 10; }); long start = System.currentTimeMillis(); t1.start(); // 线程执行结束会导致 join 结束 t1.join(1500); long end = System.currentTimeMillis(); log.debug(\u0026#34;r1: {} r2: {} cost: {}\u0026#34;, r1, r2, end - start); } Copied! 输出\n1 20:48:01.320 [main] c.TestJoin - r1: 10 r2: 0 cost: 1010 Copied! 当执行时间超时\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static int r1 = 0; static int r2 = 0; public static void main(String[] args) throws InterruptedException { test3(); } public static void test3() throws InterruptedException { Thread t1 = new Thread(() -\u0026gt; { sleep(2); r1 = 10; }); long start = System.currentTimeMillis(); t1.start(); // 线程执行结束会导致 join 结束 t1.join(1500); long end = System.currentTimeMillis(); log.debug(\u0026#34;r1: {} r2: {} cost: {}\u0026#34;, r1, r2, end - start); } Copied! 输出\n1 20:52:15.623 [main] c.TestJoin - r1: 0 r2: 0 cost: 1502 Copied! 3.9 interrupt方法详解 Interrupt说明 interrupt的本质是将线程的打断标记设为true，并调用线程的三个parker对象（C++实现级别）unpark该线程。\n基于以上本质，有如下说明：\n打断线程不等于中断线程，有以下两种情况： 打断正在运行中的线程并不会影响线程的运行，但如果线程监测到了打断标记为true，可以自行决定后续处理。 打断阻塞中的线程会让此线程产生一个InterruptedException异常，结束线程的运行。但如果该异常被线程捕获住，该线程依然可以自行决定后续处理（终止运行，继续运行，做一些善后工作等等） 打断 sleep，wait，join 的线程 这几个方法都会让线程进入阻塞状态\n打断 sleep 的线程, 会清空打断状态，以 sleep 为例\n1 2 3 4 5 6 7 8 9 private static void test1() throws InterruptedException { Thread t1 = new Thread(()-\u0026gt;{ sleep(1); }, \u0026#34;t1\u0026#34;); t1.start(); sleep(0.5); t1.interrupt(); log.debug(\u0026#34; 打断状态: {}\u0026#34;, t1.isInterrupted()); } Copied! 输出\n1 2 3 4 5 6 7 8 java.lang.InterruptedException: sleep interrupted at java.lang.Thread.sleep(Native Method) at java.lang.Thread.sleep(Thread.java:340) at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) at cn.itcast.n2.util.Sleeper.sleep(Sleeper.java:8) at cn.itcast.n4.TestInterrupt.lambda$test1$3(TestInterrupt.java:59) at java.lang.Thread.run(Thread.java:745) 21:18:10.374 [main] c.TestInterrupt - 打断状态: false Copied! 打断正常运行的线程 打断正常运行的线程, 不会清空打断状态\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private static void test2() throws InterruptedException { Thread t2 = new Thread(()-\u0026gt;{ while(true) { Thread current = Thread.currentThread(); boolean interrupted = current.isInterrupted(); if(interrupted) { log.debug(\u0026#34; 打断状态: {}\u0026#34;, interrupted); break; } } }, \u0026#34;t2\u0026#34;); t2.start(); sleep(0.5); t2.interrupt(); } Copied! 输出\n1 20:57:37.964 [t2] c.TestInterrupt - 打断状态: true Copied! * 模式之两阶段终止 Two Phase Termination 在一个线程 T1 中如何“优雅”终止线程 T2？这里的【优雅】指的是给 T2 一个料理后事的机会。\n错误思路 使用线程对象的 stop() 方法停止线程 stop 方法会真正杀死线程，如果这时线程锁住了共享资源，那么当它被杀死后就再也没有机会释放锁， 其它线程将永远无法获取锁 使用 System.exit(int) 方法停止线程 目的仅是停止一个线程，但这种做法会让整个程序都停止 两阶段终止模式 利用 isInterrupted interrupt 可以打断正在执行的线程，无论这个线程是在 sleep，wait，还是正常运行\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class TPTInterrupt { private Thread thread; public void start(){ thread = new Thread(() -\u0026gt; { while(true) { Thread current = Thread.currentThread(); if(current.isInterrupted()) { log.debug(\u0026#34;料理后事\u0026#34;); break; } try { Thread.sleep(1000); log.debug(\u0026#34;将结果保存\u0026#34;); } catch (InterruptedException e) { current.interrupt(); } // 执行监控操作 } },\u0026#34;监控线程\u0026#34;); thread.start(); } public void stop() { thread.interrupt(); } } Copied! 调用\n1 2 3 4 5 TPTInterrupt t = new TPTInterrupt(); t.start(); Thread.sleep(3500); log.debug(\u0026#34;stop\u0026#34;); t.stop(); Copied! 结果\n1 2 3 4 5 11:49:42.915 c.TwoPhaseTermination [监控线程] - 将结果保存 11:49:43.919 c.TwoPhaseTermination [监控线程] - 将结果保存 11:49:44.919 c.TwoPhaseTermination [监控线程] - 将结果保存 11:49:45.413 c.TestTwoPhaseTermination [main] - stop 11:49:45.413 c.TwoPhaseTermination [监控线程] - 料理后事 Copied! 利用停止标记 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 // 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性 // 我们的例子中，即主线程把它修改为 true 对 t1 线程可见 class TPTVolatile { private Thread thread; private volatile boolean stop = false; public void start(){ thread = new Thread(() -\u0026gt; { while(true) { Thread current = Thread.currentThread(); if(stop) { log.debug(\u0026#34;料理后事\u0026#34;); break; } try { Thread.sleep(1000); log.debug(\u0026#34;将结果保存\u0026#34;); } catch (InterruptedException e) { } // 执行监控操作 } },\u0026#34;监控线程\u0026#34;); thread.start(); } public void stop() { stop = true; thread.interrupt(); } } Copied! 调用\n1 2 3 4 5 TPTVolatile t = new TPTVolatile(); t.start(); Thread.sleep(3500); log.debug(\u0026#34;stop\u0026#34;); t.stop(); Copied! 结果\n1 2 3 4 5 11:54:52.003 c.TPTVolatile [监控线程] - 将结果保存 11:54:53.006 c.TPTVolatile [监控线程] - 将结果保存 11:54:54.007 c.TPTVolatile [监控线程] - 将结果保存 11:54:54.502 c.TestTwoPhaseTermination [main] - stop 11:54:54.502 c.TPTVolatile [监控线程] - 料理后事 Copied! 打断 park 线程 打断 park 线程, 不会清空打断状态\n1 2 3 4 5 6 7 8 9 10 11 private static void test3() throws InterruptedException { Thread t1 = new Thread(() -\u0026gt; { log.debug(\u0026#34;park...\u0026#34;); LockSupport.park(); log.debug(\u0026#34;unpark...\u0026#34;); log.debug(\u0026#34;打断状态：{}\u0026#34;, Thread.currentThread().isInterrupted()); }, \u0026#34;t1\u0026#34;); t1.start(); sleep(0.5); t1.interrupt(); } Copied! 输出\n1 2 3 21:11:52.795 [t1] c.TestInterrupt - park... 21:11:53.295 [t1] c.TestInterrupt - unpark... 21:11:53.295 [t1] c.TestInterrupt - 打断状态：true Copied! 如果打断标记已经是 true, 则 park 会失效\n1 2 3 4 5 6 7 8 9 10 11 12 private static void test4() { Thread t1 = new Thread(() -\u0026gt; { for (int i = 0; i \u0026lt; 5; i++) { log.debug(\u0026#34;park...\u0026#34;); LockSupport.park(); log.debug(\u0026#34;打断状态：{}\u0026#34;, Thread.currentThread().isInterrupted()); } }); t1.start(); sleep(1); t1.interrupt(); } Copied! 输出\n1 2 3 4 5 6 7 8 9 10 21:13:48.783 [Thread-0] c.TestInterrupt - park... 21:13:49.809 [Thread-0] c.TestInterrupt - 打断状态：true 21:13:49.812 [Thread-0] c.TestInterrupt - park... 21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态：true 21:13:49.813 [Thread-0] c.TestInterrupt - park... 21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态：true 21:13:49.813 [Thread-0] c.TestInterrupt - park... 21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态：true 21:13:49.813 [Thread-0] c.TestInterrupt - park... 21:13:49.813 [Thread-0] c.TestInterrupt - 打断状态：true Copied! 提示\n可以使用 Thread.interrupted() 清除打断状态\n3.10 不推荐的方法 还有一些不推荐使用的方法，这些方法已过时，容易破坏同步代码块，造成线程死锁\n方法名 static 功能说明 stop() 停止线程运行 suspend() 挂起（暂停）线程运行 resume() 恢复线程运行 3.11 主线程与守护线程 默认情况下，Java 进程需要等待所有线程都运行结束，才会结束。有一种特殊的线程叫做守护线程，只要其它非守护线程运行结束了，即使守护线程的代码没有执行完，也会强制结束。\n例:\n1 2 3 4 5 6 7 8 9 10 11 log.debug(\u0026#34;开始运行...\u0026#34;); Thread t1 = new Thread(() -\u0026gt; { log.debug(\u0026#34;开始运行...\u0026#34;); sleep(2); log.debug(\u0026#34;运行结束...\u0026#34;); }, \u0026#34;daemon\u0026#34;); // 设置该线程为守护线程 t1.setDaemon(true); t1.start(); sleep(1); log.debug(\u0026#34;运行结束...\u0026#34;); Copied! 输出：\n1 2 3 08:26:38.123 [main] c.TestDaemon - 开始运行... 08:26:38.213 [daemon] c.TestDaemon - 开始运行... 08:26:39.215 [main] c.TestDaemon - 运行结束... Copied! 注意\n垃圾回收器线程就是一种守护线程 Tomcat 中的 Acceptor 和 Poller 线程都是守护线程，所以 Tomcat 接收到 shutdown 命令后，不会等待它们处理完当前请求 3.12 五种状态 这是从 操作系统 层面来描述的\n【初始状态】仅是在语言层面创建了线程对象，还未与操作系统线程关联 【可运行状态】（就绪状态）指该线程已经被创建（与操作系统线程关联），可以由 CPU 调度执行 【运行状态】指获取了 CPU 时间片运行中的状态 当 CPU 时间片用完，会从【运行状态】转换至【可运行状态】，会导致线程的上下文切换 【阻塞状态】 如果调用了阻塞 API，如 BIO 读写文件，这时该线程实际不会用到 CPU，会导致线程上下文切换，进入 【阻塞状态】 等 BIO 操作完毕，会由操作系统唤醒阻塞的线程，转换至【可运行状态】 与【可运行状态】的区别是，对【阻塞状态】的线程来说只要它们一直不唤醒，调度器就一直不会考虑 调度它们 【终止状态】表示线程已经执行完毕，生命周期已经结束，不会再转换为其它状态 3.13 六种状态 这是从 Java API 层面来描述的\n根据 Thread.State 枚举，分为六种状态\nNEW 线程刚被创建，但是还没有调用 start() 方法 RUNNABLE 当调用了 start() 方法之后，注意，Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】（由于 BIO 导致的线程阻塞，在 Java 里无法区分，仍然认为 是可运行） BLOCKED ， WAITING ， TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分，后面会在状态转换一节 详述 TERMINATED 当线程代码运行结束 3.14 习题 阅读华罗庚《统筹方法》，给出烧水泡茶的多线程解决方案，提示\n参考图二，用两个线程（两个人协作）模拟烧水泡茶过程 文中办法乙、丙都相当于任务串行 而图一相当于启动了 4 个线程，有点浪费 用 sleep(n) 模拟洗茶壶、洗水壶等耗费的时间 $\\textcolor{green}{* 应用之统筹（烧水泡茶）}$ 解法1：join 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Thread t1 = new Thread(() -\u0026gt; { log.debug(\u0026#34;洗水壶\u0026#34;); sleep(1); log.debug(\u0026#34;烧开水\u0026#34;); sleep(15); }, \u0026#34;老王\u0026#34;); Thread t2 = new Thread(() -\u0026gt; { log.debug(\u0026#34;洗茶壶\u0026#34;); sleep(1); log.debug(\u0026#34;洗茶杯\u0026#34;); sleep(2); log.debug(\u0026#34;拿茶叶\u0026#34;); sleep(1); try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(\u0026#34;泡茶\u0026#34;); }, \u0026#34;小王\u0026#34;); t1.start(); t2.start(); Copied! 输出\n1 2 3 4 5 6 19:19:37.547 [小王] c.TestMakeTea - 洗茶壶 19:19:37.547 [老王] c.TestMakeTea - 洗水壶 19:19:38.552 [小王] c.TestMakeTea - 洗茶杯 19:19:38.552 [老王] c.TestMakeTea - 烧开水 19:19:40.553 [小王] c.TestMakeTea - 拿茶叶 19:19:53.553 [小王] c.TestMakeTea - 泡茶 Copied! 解法1 的缺陷：\n上面模拟的是小王等老王的水烧开了，小王泡茶，如果反过来要实现老王等小王的茶叶拿来了，老王泡茶 呢？代码最好能适应两种情况 上面的两个线程其实是各执行各的，如果要模拟老王把水壶交给小王泡茶，或模拟小王把茶叶交给老王泡茶 呢 解法2：wait/notify 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 class S2 { static String kettle = \u0026#34;冷水\u0026#34;; static String tea = null; static final Object lock = new Object(); static boolean maked = false; public static void makeTea() { new Thread(() -\u0026gt; { log.debug(\u0026#34;洗水壶\u0026#34;); sleep(1); log.debug(\u0026#34;烧开水\u0026#34;); sleep(5); synchronized (lock) { kettle = \u0026#34;开水\u0026#34;; lock.notifyAll(); while (tea == null) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (!maked) { log.debug(\u0026#34;拿({})泡({})\u0026#34;, kettle, tea); maked = true; } } }, \u0026#34;老王\u0026#34;).start(); new Thread(() -\u0026gt; { log.debug(\u0026#34;洗茶壶\u0026#34;); sleep(1); log.debug(\u0026#34;洗茶杯\u0026#34;); sleep(2); log.debug(\u0026#34;拿茶叶\u0026#34;); sleep(1); synchronized (lock) { tea = \u0026#34;花茶\u0026#34;; lock.notifyAll(); while (kettle.equals(\u0026#34;冷水\u0026#34;)) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } if (!maked) { log.debug(\u0026#34;拿({})泡({})\u0026#34;, kettle, tea); maked = true; } } }, \u0026#34;小王\u0026#34;).start(); } } Copied! 输出\n1 2 3 4 5 6 20:04:48.179 c.S2 [小王] - 洗茶壶 20:04:48.179 c.S2 [老王] - 洗水壶 20:04:49.185 c.S2 [老王] - 烧开水 20:04:49.185 c.S2 [小王] - 洗茶杯 20:04:51.185 c.S2 [小王] - 拿茶叶 20:04:54.185 c.S2 [老王] - 拿(开水)泡(花茶) Copied! 解法2 解决了解法1 的问题，不过老王和小王需要相互等待，不如他们只负责各自的任务，泡茶交给第三人来做\n1 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 37 38 39 40 41 class S3 { static String kettle = \u0026#34;冷水\u0026#34;; static String tea = null; static final Object lock = new Object(); public static void makeTea() { new Thread(() -\u0026gt; { log.debug(\u0026#34;洗水壶\u0026#34;); sleep(1); log.debug(\u0026#34;烧开水\u0026#34;); sleep(5); synchronized (lock) { kettle = \u0026#34;开水\u0026#34;; lock.notifyAll(); } }, \u0026#34;老王\u0026#34;).start(); new Thread(() -\u0026gt; { log.debug(\u0026#34;洗茶壶\u0026#34;); sleep(1); log.debug(\u0026#34;洗茶杯\u0026#34;); sleep(2); log.debug(\u0026#34;拿茶叶\u0026#34;); sleep(1); synchronized (lock) { tea = \u0026#34;花茶\u0026#34;; lock.notifyAll(); } }, \u0026#34;小王\u0026#34;).start(); new Thread(() -\u0026gt; { synchronized (lock) { while (kettle.equals(\u0026#34;冷水\u0026#34;) || tea == null) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug(\u0026#34;拿({})泡({})\u0026#34;, kettle, tea); } }, \u0026#34;王夫人\u0026#34;).start(); } } Copied! 输出\n1 2 3 4 5 6 20:13:18.202 c.S3 [小王] - 洗茶壶 20:13:18.202 c.S3 [老王] - 洗水壶 20:13:19.206 c.S3 [小王] - 洗茶杯 20:13:19.206 c.S3 [老王] - 烧开水 20:13:21.206 c.S3 [小王] - 拿茶叶 20:13:24.207 c.S3 [王夫人] - 拿(开水)泡(花茶) Copied! 解法3：第三者协调 本章小结 本章的重点在于掌握\n线程创建 线程重要 api，如 start，run，sleep，join，interrupt 等 线程状态 应用方面 异步调用：主线程执行期间，其它线程异步执行耗时操作 提高效率：并行计算，缩短运算时间 同步等待：join 统筹规划：合理使用线程，得到最优效果 原理方面 线程运行流程：栈、栈帧、上下文切换、程序计数器 Thread 两种创建方式 的源码 模式方面 终止模式之两阶段终止 4.共享模型之管程 4.1共享带来的问题 Java代码示例 两个线程对初始值为 0 的静态变量一个做自增，一个做自减，各做 5000 次，结果是 0 吗？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static int counter = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -\u0026gt; { for (int i = 0; i \u0026lt; 5000; i++) { counter++; } }, \u0026#34;t1\u0026#34;); Thread t2 = new Thread(() -\u0026gt; { for (int i = 0; i \u0026lt; 5000; i++) { counter--; } }, \u0026#34;t2\u0026#34;); t1.start(); t2.start(); t1.join(); t2.join(); log.debug(\u0026#34;{}\u0026#34;,counter); } Copied! 问题分析 以上的结果可能是正数、负数、零。为什么呢？因为 Java 中对静态变量的自增，自减并不是原子操作，要彻底理解，必须从字节码来进行分析\n例如对于 i++ 而言（i 为静态变量），实际会产生如下的 JVM 字节码指令：\n1 2 3 4 getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 iadd // 自增 putstatic i // 将修改后的值存入静态变量i Copied! 而对应 i\u0026ndash; 也是类似：\n1 2 3 4 getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 isub // 自减 putstatic i // 将修改后的值存入静态变量i Copied! 而 Java 的内存模型如下，完成静态变量的自增，自减需要在主存和工作内存中进行数据交换：\n如果是单线程以上 8 行代码是顺序执行（不会交错）没有问题：\n但多线程下这 8 行代码可能交错运行： 出现负数的情况：\n出现正数的情况：\n临界区 Critical Section 一个程序运行多个线程本身是没有问题的\n问题出在多个线程访问共享资源\n多个线程读共享资源其实也没有问题\n在多个线程对共享资源读写操作时发生指令交错，就会出现问题\n一段代码块内如果存在对共享资源的多线程读写操作，称这段代码块为临界区\n1 2 3 4 5 6 7 8 9 10 11 static int counter = 0; static void increment() // 临界区 { counter++; } static void decrement() // 临界区 { counter--; } Copied! 竞态条件 Race Condition 多个线程在临界区内执行，由于代码的执行序列不同而导致结果无法预测，称之为发生了竞态条件\n4.2 synchronized 解决方案 *$\\textcolor{green}{应用之互斥}$ 为了避免临界区的竞态条件发生，有多种手段可以达到目的。\n阻塞式的解决方案：synchronized，Lock 非阻塞式的解决方案：原子变量 本次课使用阻塞式的解决方案：synchronized，来解决上述问题，即俗称的【对象锁】，它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】，其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码，不用担心线程上下文切换\n注意\n虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成，但它们还是有区别的：\n互斥是保证临界区的竞态条件发生，同一时刻只能有一个线程执行临界区代码 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点 synchronized 语法\n1 2 3 4 synchronized(对象) // 线程1， 线程2(blocked) { 临界区 } Copied! 解决\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static int counter = 0; static final Object room = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -\u0026gt; { for (int i = 0; i \u0026lt; 5000; i++) { synchronized (room) { counter++; } } }, \u0026#34;t1\u0026#34;); Thread t2 = new Thread(() -\u0026gt; { for (int i = 0; i \u0026lt; 5000; i++) { synchronized (room) { counter--; } } }, \u0026#34;t2\u0026#34;); t1.start(); t2.start(); t1.join(); t2.join(); log.debug(\u0026#34;{}\u0026#34;,counter); } Copied! 图示流程\n思考\nsynchronized 实际是用对象锁保证了临界区内代码的原子性，临界区内的代码对外是不可分割的，不会被线程切 换所打断。\n为了加深理解，请思考下面的问题\n如果把 synchronized(obj) 放在 for 循环的外面，如何理解？\u0026ndash; 原子性 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作？\u0026ndash; 锁对象 如果 t1 synchronized(obj) 而 t2 没有加会怎么样？如何理解？\u0026ndash; 锁对象 面向对象改进 把需要保护的共享变量放入一个类\n1 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 37 38 39 40 41 class Room { int value = 0; public void increment() { synchronized (this) { value++; } } public void decrement() { synchronized (this) { value--; } } public int get() { synchronized (this) { return value; } } } @Slf4j public class Test1 { public static void main(String[] args) throws InterruptedException { Room room = new Room(); Thread t1 = new Thread(() -\u0026gt; { for (int j = 0; j \u0026lt; 5000; j++) { room.increment(); } }, \u0026#34;t1\u0026#34;); Thread t2 = new Thread(() -\u0026gt; { for (int j = 0; j \u0026lt; 5000; j++) { room.decrement(); } }, \u0026#34;t2\u0026#34;); t1.start(); t2.start(); t1.join(); t2.join(); log.debug(\u0026#34;count: {}\u0026#34; , room.get()); } } Copied! 4.3 方法上的 synchronized 1 2 3 4 5 6 7 8 9 10 11 12 13 class Test{ public synchronized void test() { } } //等价于 class Test{ public void test() { synchronized(this) { } } } Copied! 1 2 3 4 5 6 7 8 9 10 11 12 class Test{ public synchronized static void test() { } } 等价于 class Test{ public static void test() { synchronized(Test.class) { } } } Copied! 不加 synchronized 的方法 不加 synchronzied 的方法就好比不遵守规则的人，不去老实排队（好比翻窗户进去的）\n所谓的“线程八锁” 其实就是考察 synchronized 锁住的是哪个对象\n情况1：12 或 21\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Slf4j(topic = \u0026#34;c.Number\u0026#34;) class Number{ public synchronized void a() { log.debug(\u0026#34;1\u0026#34;); } public synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(()-\u0026gt;{ n1.a(); }).start(); new Thread(()-\u0026gt;{ n1.b(); }).start(); } Copied! 情况2：1s后12，或 2 1s后 1\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Slf4j(topic = \u0026#34;c.Number\u0026#34;) class Number{ public synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(()-\u0026gt;{ n1.a(); }).start(); new Thread(()-\u0026gt;{ n1.b(); }).start(); } Copied! 情况3：3 1s 12 或 23 1s 1 或 32 1s 1\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Slf4j(topic = \u0026#34;c.Number\u0026#34;) class Number{ public synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } public void c() { log.debug(\u0026#34;3\u0026#34;); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(()-\u0026gt;{ n1.a(); }).start(); new Thread(()-\u0026gt;{ n1.b(); }).start(); new Thread(()-\u0026gt;{ n1.c(); }).start(); } Copied! 情况4：2 1s 后 1\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j(topic = \u0026#34;c.Number\u0026#34;) class Number{ public synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); new Thread(()-\u0026gt;{ n1.a(); }).start(); new Thread(()-\u0026gt;{ n2.b(); }).start(); } Copied! 情况5：2 1s 后 1\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Slf4j(topic = \u0026#34;c.Number\u0026#34;) class Number{ public static synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(()-\u0026gt;{ n1.a(); }).start(); new Thread(()-\u0026gt;{ n1.b(); }).start(); } Copied! 情况6：1s 后12， 或 2 1s后 1\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Slf4j(topic = \u0026#34;c.Number\u0026#34;) class Number{ public static synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public static synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(String[] args) { Number n1 = new Number(); new Thread(()-\u0026gt;{ n1.a(); }).start(); new Thread(()-\u0026gt;{ n1.b(); }).start(); } Copied! 情况7：2 1s 后 1\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j(topic = \u0026#34;c.Number\u0026#34;) class Number{ public static synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); new Thread(()-\u0026gt;{ n1.a(); }).start(); new Thread(()-\u0026gt;{ n2.b(); }).start(); } Copied! 情况8：1s 后12， 或 2 1s后 1\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Slf4j(topic = \u0026#34;c.Number\u0026#34;) class Number{ public static synchronized void a() { sleep(1); log.debug(\u0026#34;1\u0026#34;); } public static synchronized void b() { log.debug(\u0026#34;2\u0026#34;); } } public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); new Thread(()-\u0026gt;{ n1.a(); }).start(); new Thread(()-\u0026gt;{ n2.b(); }).start(); } Copied! 4.4 变量的线程安全分析 成员变量和静态变量是否线程安全？ 如果它们没有共享，则线程安全 如果它们被共享了，根据它们的状态是否能够改变，又分两种情况 如果只有读操作，则线程安全 如果有读写操作，则这段代码是临界区，需要考虑线程安全 局部变量是否线程安全？ 局部变量是线程安全的 但局部变量引用的对象则未必 如果该对象没有逃离方法的作用访问，它是线程安全的 如果该对象逃离方法的作用范围，需要考虑线程安全 局部变量线程安全分析 1 2 3 4 public static void test1() { int i = 10; i++; } Copied! 每个线程调用 test1() 方法时局部变量 i，会在每个线程的栈帧内存中被创建多份，因此不存在共享\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static void test1(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=0 0: bipush 10 2: istore_0 3: iinc 0, 1 6: return LineNumberTable: line 10: 0 line 11: 3 line 12: 6 LocalVariableTable: Start Length Slot Name Signature 3 4 0 i I Copied! 如图\n局部变量的引用稍有不同\n先看一个成员变量的例子\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class ThreadUnsafe { ArrayList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); public void method1(int loopNumber) { for (int i = 0; i \u0026lt; loopNumber; i++) { // { 临界区, 会产生竞态条件 method2(); method3(); // } 临界区 } } private void method2() { list.add(\u0026#34;1\u0026#34;); } private void method3() { list.remove(0); } } Copied! 执行\n1 2 3 4 5 6 7 8 9 10 static final int THREAD_NUMBER = 2; static final int LOOP_NUMBER = 200; public static void main(String[] args) { ThreadUnsafe test = new ThreadUnsafe(); for (int i = 0; i \u0026lt; THREAD_NUMBER; i++) { new Thread(() -\u0026gt; { test.method1(LOOP_NUMBER); }, \u0026#34;Thread\u0026#34; + i).start(); } } Copied! 其中一种情况是，如果线程2 还未 add，线程1 remove 就会报错：\n1 2 3 4 5 6 7 Exception in thread \u0026#34;Thread1\u0026#34; java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.remove(ArrayList.java:496) at cn.itcast.n6.ThreadUnsafe.method3(TestThreadSafe.java:35) at cn.itcast.n6.ThreadUnsafe.method1(TestThreadSafe.java:26) at cn.itcast.n6.TestThreadSafe.lambda$main$0(TestThreadSafe.java:14) at java.lang.Thread.run(Thread.java:748) Copied! 分析：\n无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量 method3 与 method2 分析相同 将 list 修改为局部变量\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class ThreadSafe { public final void method1(int loopNumber) { ArrayList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; loopNumber; i++) { method2(list); method3(list); } } private void method2(ArrayList\u0026lt;String\u0026gt; list) { list.add(\u0026#34;1\u0026#34;); } private void method3(ArrayList\u0026lt;String\u0026gt; list) { list.remove(0); } } Copied! 那么就不会有上述问题了\n分析：\nlist 是局部变量，每个线程调用时会创建其不同实例，没有共享 而 method2 的参数是从 method1 中传递过来的，与 method1 中引用同一个对象 method3 的参数分析与 method2 相同 方法访问修饰符带来的思考，如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题？\n情况1：有其它线程调用 method2 和 method3 情况2：在 情况1 的基础上，为 ThreadSafe 类添加子类，子类覆盖 method2 或 method3 方法， 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class ThreadSafe { public final void method1(int loopNumber) { ArrayList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; loopNumber; i++) { method2(list); method3(list); } } private void method2(ArrayList\u0026lt;String\u0026gt; list) { list.add(\u0026#34;1\u0026#34;); } private void method3(ArrayList\u0026lt;String\u0026gt; list) { list.remove(0); } } class ThreadSafeSubClass extends ThreadSafe{ @Override public void method3(ArrayList\u0026lt;String\u0026gt; list) { new Thread(() -\u0026gt; { list.remove(0); }).start(); } } Copied! 从这个例子可以看出 private 或 final 提供【安全】的意义所在，请体会开闭原则中的【闭】\n常见线程安全类 String Integer StringBuffer Random Vector Hashtable java.util.concurrent 包下的类 这里说它们是线程安全的是指，多个线程调用它们同一个实例的某个方法时，是线程安全的。也可以理解为\n1 2 3 4 5 6 7 Hashtable table = new Hashtable(); new Thread(()-\u0026gt;{ table.put(\u0026#34;key\u0026#34;, \u0026#34;value1\u0026#34;); }).start(); new Thread(()-\u0026gt;{ table.put(\u0026#34;key\u0026#34;, \u0026#34;value2\u0026#34;); }).start(); Copied! 它们的每个方法是原子的 但注意它们多个方法的组合不是原子的，见后面分析 线程安全类方法的组合\n分析下面代码是否线程安全？\n1 2 3 4 5 Hashtable table = new Hashtable(); // 线程1，线程2 if( table.get(\u0026#34;key\u0026#34;) == null) { table.put(\u0026#34;key\u0026#34;, value); } Copied! 不可变类线程安全性\nString、Integer 等都是不可变类，因为其内部的状态不可以改变，因此它们的方法都是线程安全的 有同学或许有疑问，String 有 replace，substring 等方法【可以】改变值啊，那么这些方法又是如何保证线程安 全的呢？\n1 2 3 4 5 6 7 8 9 public class Immutable{ private int value = 0; public Immutable(int value){ this.value = value; } public int getValue(){ return this.value; } } Copied! 如果想增加一个增加的方法呢？\n1 2 3 4 5 6 7 8 9 10 11 12 13 public class Immutable{ private int value = 0; public Immutable(int value){ this.value = value; } public int getValue(){ return this.value; } public Immutable add(int v){ return new Immutable(this.value + v); } } Copied! 实例分析 例1：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class MyServlet extends HttpServlet { // 是否安全？ Map\u0026lt;String,Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); // 是否安全？ String S1 = \u0026#34;...\u0026#34;; // 是否安全？ final String S2 = \u0026#34;...\u0026#34;; // 是否安全？ Date D1 = new Date(); // 是否安全？ final Date D2 = new Date(); public void doGet(HttpServletRequest request, HttpServletResponse response) { // 使用上述变量 } } Copied! 例2：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class MyServlet extends HttpServlet { // 是否安全？ private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { // 记录调用次数 private int count = 0; public void update() { // ... count++; } } Copied! 例3：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Aspect @Component public class MyAspect { // 是否安全？ private long start = 0L; @Before(\u0026#34;execution(* *(..))\u0026#34;) public void before() { start = System.nanoTime(); } @After(\u0026#34;execution(* *(..))\u0026#34;) public void after() { long end = System.nanoTime(); System.out.println(\u0026#34;cost time:\u0026#34; + (end-start)); } } Copied! 例4:\n1 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 public class MyServlet extends HttpServlet { // 是否安全 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { // 是否安全 private UserDao userDao = new UserDaoImpl(); public void update() { userDao.update(); } } public class UserDaoImpl implements UserDao { public void update() { String sql = \u0026#34;update user set password = ? where username = ?\u0026#34;; // 是否安全 try (Connection conn = DriverManager.getConnection(\u0026#34;\u0026#34;,\u0026#34;\u0026#34;,\u0026#34;\u0026#34;)){ // ... } catch (Exception e) { // ... } } } Copied! 例5:\n1 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 class MyServlet extends HttpServlet { // 是否安全 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { // 是否安全 private UserDao userDao = new UserDaoImpl(); public void update() { userDao.update(); } } public class UserDaoImpl implements UserDao { // 是否安全 private Connection conn = null; public void update() throws SQLException { String sql = \u0026#34;update user set password = ? where username = ?\u0026#34;; conn = DriverManager.getConnection(\u0026#34;\u0026#34;,\u0026#34;\u0026#34;,\u0026#34;\u0026#34;); // ... conn.close(); } } Copied! 例6：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class MyServlet extends HttpServlet { // 是否安全 private UserService userService = new UserServiceImpl(); public void doGet(HttpServletRequest request, HttpServletResponse response) { userService.update(...); } } public class UserServiceImpl implements UserService { public void update() { UserDao userDao = new UserDaoImpl(); userDao.update(); } } public class UserDaoImpl implements UserDao { // 是否安全 private Connection = null; public void update() throws SQLException { String sql = \u0026#34;update user set password = ? where username = ?\u0026#34;; conn = DriverManager.getConnection(\u0026#34;\u0026#34;,\u0026#34;\u0026#34;,\u0026#34;\u0026#34;); // ... conn.close(); } } Copied! 例7:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public abstract class Test { public void bar() { // 是否安全 SimpleDateFormat sdf = new SimpleDateFormat(\u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34;); foo(sdf); } public abstract foo(SimpleDateFormat sdf); public static void main(String[] args) { new Test().bar(); } } Copied! 其中 foo 的行为是不确定的，可能导致不安全的发生，被称之为外星方法\n1 2 3 4 5 6 7 8 9 10 11 12 public void foo(SimpleDateFormat sdf) { String dateStr = \u0026#34;1999-10-11 00:00:00\u0026#34;; for (int i = 0; i \u0026lt; 20; i++) { new Thread(() -\u0026gt; { try { sdf.parse(dateStr); } catch (ParseException e) { e.printStackTrace(); } }).start(); } } Copied! 请比较 JDK 中 String 类的实现\n例8：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private static Integer i = 0; public static void main(String[] args) throws InterruptedException { List\u0026lt;Thread\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); for (int j = 0; j \u0026lt; 2; j++) { Thread thread = new Thread(() -\u0026gt; { for (int k = 0; k \u0026lt; 5000; k++) { synchronized (i) { i++; } } }, \u0026#34;\u0026#34; + j); list.add(thread); } list.stream().forEach(t -\u0026gt; t.start()); list.stream().forEach(t -\u0026gt; { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); log.debug(\u0026#34;{}\u0026#34;, i); } Copied! 4.5 习题 卖票练习 测试下面代码是否存在线程安全问题，并尝试改正\n将sell方法声明为synchronized即可 注意只将对count进行修改的一行代码用synchronized括起来也不行。对count大小的判断也必须是为原子操作的一部分，否则也会导致count值异常。 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 public class ExerciseSell { public static void main(String[] args) { TicketWindow ticketWindow = new TicketWindow(2000); List\u0026lt;Thread\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); // 用来存储买出去多少张票 List\u0026lt;Integer\u0026gt; sellCount = new Vector\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; 2000; i++) { Thread t = new Thread(() -\u0026gt; { // 分析这里的竞态条件 int count = ticketWindow.sell(randomAmount()); sellCount.add(count); }); list.add(t); t.start(); } list.forEach((t) -\u0026gt; { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); // 买出去的票求和 log.debug(\u0026#34;selled count:{}\u0026#34;,sellCount.stream().mapToInt(c -\u0026gt; c).sum()); // 剩余票数 log.debug(\u0026#34;remainder count:{}\u0026#34;, ticketWindow.getCount()); } // Random 为线程安全 static Random random = new Random(); // 随机 1~5 public static int randomAmount() { return random.nextInt(5) + 1; } } class TicketWindow { private int count; public TicketWindow(int count) { this.count = count; } public int getCount() { return count; } //在方法上加一个synchronized即可 public int sell(int amount) { if (this.count \u0026gt;= amount) { this.count -= amount; return amount; } else { return 0; } } } Copied! 另外，用下面的代码行不行，为什么？\n不行，因为sellCount会被多个线程共享，必须使用线程安全的实现类。 1 List\u0026lt;Integer\u0026gt; sellCount = new ArrayList\u0026lt;\u0026gt;(); Copied! 测试脚本\n1 for /L %n in (1,1,10) do java -cp \u0026#34;.;C:\\Users\\manyh\\.m2\\repository\\ch\\qos\\logback\\logback\u0002classic\\1.2.3\\logback-classic-1.2.3.jar;C:\\Users\\manyh\\.m2\\repository\\ch\\qos\\logback\\logback\u0002core\\1.2.3\\logback-core-1.2.3.jar;C:\\Users\\manyh\\.m2\\repository\\org\\slf4j\\slf4j\u0002api\\1.7.25\\slf4j-api-1.7.25.jar\u0026#34; cn.itcast.n4.exercise.ExerciseSell Copied! 说明：\n两段没有前后因果关系的临界区代码，只需要保证各自的原子性即可，不需要括起来。 转账练习 测试下面代码是否存在线程安全问题，并尝试改正\n将transfer方法的方法体用同步代码块包裹，将当Account.class设为锁对象。 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 37 38 39 40 41 42 43 44 45 46 public class ExerciseTransfer { public static void main(String[] args) throws InterruptedException { Account a = new Account(1000); Account b = new Account(1000); Thread t1 = new Thread(() -\u0026gt; { for (int i = 0; i \u0026lt; 1000; i++) { a.transfer(b, randomAmount()); } }, \u0026#34;t1\u0026#34;); Thread t2 = new Thread(() -\u0026gt; { for (int i = 0; i \u0026lt; 1000; i++) { b.transfer(a, randomAmount()); } }, \u0026#34;t2\u0026#34;); t1.start(); t2.start(); t1.join(); t2.join(); // 查看转账2000次后的总金额 log.debug(\u0026#34;total:{}\u0026#34;,(a.getMoney() + b.getMoney())); } // Random 为线程安全 static Random random = new Random(); // 随机 1~100 public static int randomAmount() { return random.nextInt(100) +1; } } class Account { private int money; public Account(int money) { this.money = money; } public int getMoney() { return money; } public void setMoney(int money) { this.money = money; } public void transfer(Account target, int amount) { if (this.money \u0026gt; amount) { this.setMoney(this.getMoney() - amount); target.setMoney(target.getMoney() + amount); } } } Copied! 这样改正行不行，为什么？\n不行，因为不同线程调用此方法，将会锁住不同的对象 1 2 3 4 5 6 public synchronized void transfer(Account target, int amount) { if (this.money \u0026gt; amount) { this.setMoney(this.getMoney() - amount); target.setMoney(target.getMoney() + amount); } } Copied! 4.6 Monitor 概念 Java 对象头 以 32 位虚拟机为例\n普通对象\n1 2 3 4 5 |--------------------------------------------------------------| | Object Header (64 bits) | |------------------------------------|-------------------------| | Mark Word (32 bits) | Klass Word (32 bits) | |------------------------------------|-------------------------| Copied! 数组对象\n1 2 3 4 5 |---------------------------------------------------------------------------------| | Object Header (96 bits) | |--------------------------------|-----------------------|------------------------| | Mark Word(32bits) | Klass Word(32bits) | array length(32bits) | |--------------------------------|-----------------------|------------------------| Copied! 其中 Mark Word 结构为\n1 2 3 4 5 6 7 8 9 10 11 12 13 |-------------------------------------------------------|--------------------| | Mark Word (32 bits) | State | |-------------------------------------------------------|--------------------| | hashcode:25 | age:4 | biased_lock:0 | 01 | Normal | |-------------------------------------------------------|--------------------| |thread:23|epoch:2| age:4 | biased_lock:1 | 01 | Biased | |-------------------------------------------------------|--------------------| | ptr_to_lock_record:30 | 00 | Lightweight Locked | |-------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked | |-------------------------------------------------------|--------------------| | | 11 | Marked for GC | |-------------------------------------------------------|--------------------| Copied! 64 位虚拟机 Mark Word\n1 2 3 4 5 6 7 8 9 10 11 12 13 |--------------------------------------------------------------------|--------------------| | Mark Word (64 bits) | State | |--------------------------------------------------------------------|--------------------| | unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal | |--------------------------------------------------------------------|--------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased | |--------------------------------------------------------------------|--------------------| | ptr_to_lock_record:62 | 00 | Lightweight Locked | |--------------------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked | |--------------------------------------------------------------------|--------------------| | | 11 | Marked for GC | |--------------------------------------------------------------------|--------------------| Copied! 参考资料\nhttps://stackoverflow.com/questions/26357186/what-is-in-java-object-header * 原理之Monitor(锁) Monitor 被翻译为监视器或管程\n每个 Java 对象都可以关联一个 Monitor 对象，如果使用 synchronized 给对象上锁（重量级）之后，该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针\nMonitor 结构如下\n刚开始 Monitor 中 Owner 为 null 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2，Monitor中只能有一 个 Owner 在 Thread-2 上锁的过程中，如果 Thread-3，Thread-4，Thread-5 也来执行 synchronized(obj)，就会进入 EntryList BLOCKED Thread-2 执行完同步代码块的内容，然后唤醒 EntryList 中等待的线程来竞争锁，竞争的时是非公平的 图中 WaitSet 中的 Thread-0，Thread-1 是之前获得过锁，但条件不满足进入 WAITING 状态的线程，后面讲 wait-notify 时会分析 注意：\nsynchronized 必须是进入同一个对象的 monitor 才有上述的效果 不加 synchronized 的对象不会关联监视器，不遵从以上规则 * 原理之 synchronized 1 2 3 4 5 6 7 static final Object lock = new Object(); static int counter = 0; public static void main(String[] args) { synchronized (lock) { counter++; } } Copied! 对应的字节码为\n1 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 37 38 39 40 41 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: getstatic #2 // \u0026lt;- lock引用 （synchronized开始） 3: dup 4: astore_1 // lock引用 -\u0026gt; slot 1 5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针 6: getstatic #3 // \u0026lt;- i 9: iconst_1 // 准备常数 1 10: iadd // +1 11: putstatic #3 // -\u0026gt; i 14: aload_1 // \u0026lt;- lock引用 15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList 16: goto 24 19: astore_2 // e -\u0026gt; slot 2 20: aload_1 // \u0026lt;- lock引用 21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList 22: aload_2 // \u0026lt;- slot 2 (e) 23: athrow // throw e 24: return Exception table: from to target type 6 16 19 any 19 22 19 any LineNumberTable: line 8: 0 line 9: 6 line 10: 14 line 11: 24 LocalVariableTable: Start Length Slot Name Signature 0 25 0 args [Ljava/lang/String; StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 19 locals = [ class \u0026#34;[Ljava/lang/String;\u0026#34;, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 Copied! 注意\n方法级别的 synchronized 不会在字节码指令中有所体现\n* 原理之 synchronized 进阶 轻量级锁 轻量级锁的使用场景：如果一个对象虽然有多线程要加锁，但加锁的时间是错开的（也就是没有竞争），那么可以使用轻量级锁来优化。\n轻量级锁对使用者是透明的，即语法仍然是 synchronized\n假设有两个方法同步块，利用同一个对象加锁\n1 2 3 4 5 6 7 8 9 10 11 12 static final Object obj = new Object(); public static void method1() { synchronized( obj ) { // 同步块 A method2(); } } public static void method2() { synchronized( obj ) { // 同步块 B } } Copied! 创建锁记录（Lock Record）对象，每个线程都的栈帧都会包含一个锁记录的结构，内部可以存储锁定对象的 Mark Word\n让锁记录中 Object reference 指向锁对象，并尝试用 cas 替换 Object 的 Mark Word，将 Mark Word 的值存 入锁记录\n如果 cas 替换成功，对象头中存储了 锁记录地址和状态 00 ，表示由该线程给对象加锁，这时图示如下\n如果 cas 失败，有两种情况\n如果是其它线程已经持有了该 Object 的轻量级锁，这时表明有竞争，进入锁膨胀过程 如果是自己执行了 synchronized 锁重入，那么再添加一条 Lock Record 作为重入的计数 当退出 synchronized 代码块（解锁时）如果有取值为 null 的锁记录，表示有重入，这时重置锁记录，表示重 入计数减一\n当退出 synchronized 代码块（解锁时）锁记录的值不为 null，这时使用cas将Mark Word的值恢复给对象头\n成功，则解锁成功 失败，说明轻量级锁进行了锁膨胀或已经升级为重量级锁，进入重量级锁解锁流程 锁膨胀 如果在尝试加轻量级锁的过程中，CAS 操作无法成功，这时一种情况就是有其它线程为此对象加上了轻量级锁（有 竞争），这时需要进行锁膨胀，将轻量级锁变为重量级锁。\n1 2 3 4 5 6 static Object obj = new Object(); public static void method1() { synchronized( obj ) { // 同步块 } } Copied! 当 Thread-1 进行轻量级加锁时，Thread-0 已经对该对象加了轻量级锁 这时 Thread-1 加轻量级锁失败，进入锁膨胀流程 即为 Object 对象申请 Monitor 锁，让 Object 指向重量级锁地址 然后自己进入 Monitor 的 EntryList BLOCKED 当 Thread-0 退出同步块解锁时，使用 cas 将 Mark Word 的值恢复给对象头，失败。这时会进入重量级解锁 流程，即按照 Monitor 地址找到 Monitor 对象，设置 Owner 为 null，唤醒 EntryList 中 BLOCKED 线程 自旋优化 重量级锁竞争的时候，还可以使用自旋来进行优化，如果当前线程自旋成功（即这时候持锁线程已经退出了同步 块，释放了锁），这时当前线程就可以避免阻塞。\n自旋重试成功的情况\n线程1 ( core 1上) 对象Mark 线程2 ( core 2上) - 10（重量锁） - 访问同步块，获取monitor 10（重量锁）重量锁指针 - 成功（加锁） 10（重量锁）重量锁指针 - 执行同步块 10（重量锁）重量锁指针 - 执行同步块 10(重量锁）重量锁指针 访问同步块，获取 monitor 执行同步块 10（重量锁）重量锁指针 自旋重试 执行完毕 10（重量锁）重量锁指针 自旋重试 成功（解锁） 01（无锁） 自旋重试 - 10（重量锁）重量锁指针 成功（加锁) - 10（重量锁）重量锁指针 执行同步块 - \u0026hellip; \u0026hellip; 自旋重试失败的情况\n线程1 ( core 1上) 对象Mark 线程2( core 2上) - 10（重量锁） - 访问同步块，获取monitor 10（重量锁）重量锁指针 - 成功（加锁) 10（重量锁）重量锁指针 - 执行同步块 10（重量锁）重量锁指针 - 执行同步块 10（重量锁）重量锁指针 访问同步块，获取monitor 执行同步块 10（重量锁）重量锁指针 自旋重试 执行同步块 10（重量锁）重量锁指针 自旋重试 执行同步块 10（重量锁）重量锁指针 自旋重试 执行同步块 10（重量锁）重量锁指针 阻塞 - \u0026hellip; \u0026hellip; 自旋会占用 CPU 时间，单核 CPU 自旋就是浪费，多核 CPU 自旋才能发挥优势。 在 Java 6 之后自旋锁是自适应的，比如对象刚刚的一次自旋操作成功过，那么认为这次自旋成功的可能性会 高，就多自旋几次；反之，就少自旋甚至不自旋，总之，比较智能。 Java 7 之后不能控制是否开启自旋功能 偏向锁 轻量级锁在没有竞争时（就自己这个线程），每次重入仍然需要执行 CAS 操作。\nJava 6 中引入了偏向锁来做进一步优化：只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头，之后发现 这个线程 ID 是自己的就表示没有竞争，不用重新 CAS。以后只要不发生竞争，这个对象就归该线程所有\n例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static final Object obj = new Object(); public static void m1() { synchronized( obj ) { // 同步块 A m2(); } } public static void m2() { synchronized( obj ) { // 同步块 B m3(); } } public static void m3() { synchronized( obj ) { // 同步块 C } } Copied! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 graph LR subgraph 偏向锁 t5(\u0026#34;m1内调用synchronized(obj)\u0026#34;) t6(\u0026#34;m2内调用synchronized(obj)\u0026#34;) t7(\u0026#34;m2内调用synchronized(obj)\u0026#34;) t8(对象) t5 -.用ThreadID替换MarkWord.-\u0026gt; t8 t6 -.检查ThreadID是否是自己.-\u0026gt; t8 t7 -.检查ThreadID是否是自己.-\u0026gt; t8 end subgraph 轻量级锁 t1(\u0026#34;m1内调用synchronized(obj)\u0026#34;) t2(\u0026#34;m2内调用synchronized(obj)\u0026#34;) t3(\u0026#34;m2内调用synchronized(obj)\u0026#34;) t1 -.生成锁记录.-\u0026gt; t1 t2 -.生成锁记录.-\u0026gt; t2 t3 -.生成锁记录.-\u0026gt; t3 t4(对象) t1 -.用锁记录替换markword.-\u0026gt; t4 t2 -.用锁记录替换markword.-\u0026gt; t4 t3 -.用锁记录替换markword.-\u0026gt; t4 end Copied! 偏向状态 回忆一下对象头格式\n1 2 3 4 5 6 7 8 9 10 11 12 13 |--------------------------------------------------------------------|--------------------| | Mark Word (64 bits) | State | |--------------------------------------------------------------------|--------------------| | unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal | |--------------------------------------------------------------------|--------------------| | thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased | |--------------------------------------------------------------------|--------------------| | ptr_to_lock_record:62 | 00 | Lightweight Locked | |--------------------------------------------------------------------|--------------------| | ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked | |--------------------------------------------------------------------|--------------------| | | 11 | Marked for GC | |--------------------------------------------------------------------|--------------------| Copied! 一个对象创建时：\n如果开启了偏向锁（默认开启），那么对象创建后，markword 值为 0x05 即最后 3 位为 101，这时它的 thread、epoch、age 都为 0 偏向锁是默认是延迟的，不会在程序启动时立即生效，如果想避免延迟，可以加 VM 参数- XX:BiasedLockingStartupDelay=0来禁用延迟 如果没有开启偏向锁，那么对象创建后，markword 值为 0x01 即最后 3 位为 001，这时它的 hashcode、 age 都为 0，第一次用到 hashcode 时才会赋值 1） 测试延迟特性\n2） 测试偏向锁\n1 class Dog {} Copied! 利用 jol 第三方工具来查看对象头信息（注意这里我扩展了 jol 让它输出更为简洁）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 添加虚拟机参数 -XX:BiasedLockingStartupDelay=0 public static void main(String[] args) throws IOException { Dog d = new Dog(); ClassLayout classLayout = ClassLayout.parseInstance(d); new Thread(() -\u0026gt; { log.debug(\u0026#34;synchronized 前\u0026#34;); System.out.println(classLayout.toPrintableSimple(true)); synchronized (d) { log.debug(\u0026#34;synchronized 中\u0026#34;); System.out.println(classLayout.toPrintableSimple(true)); } log.debug(\u0026#34;synchronized 后\u0026#34;); System.out.println(classLayout.toPrintableSimple(true)); }, \u0026#34;t1\u0026#34;).start(); } Copied! 1 2 3 4 5 6 11:08:58.117 c.TestBiased [t1] - synchronized 前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 11:08:58.121 c.TestBiased [t1] - synchronized 中 00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 11:08:58.121 c.TestBiased [t1] - synchronized 后 00000000 00000000 00000000 00000000 00011111 11101011 11010000 00000101 Copied! 注意\n处于偏向锁的对象解锁后，线程 id 仍存储于对象头中\n3）测试禁用\n在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁\n输出\n1 2 3 4 5 6 11:13:10.018 c.TestBiased [t1] - synchronized 前 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 11:13:10.021 c.TestBiased [t1] - synchronized 中 00000000 00000000 00000000 00000000 00100000 00010100 11110011 10001000 11:13:10.021 c.TestBiased [t1] - synchronized 后 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 Copied! 4)测试 hashCode\n正常状态对象一开始是没有 hashCode 的，第一次调用才生成 撤销 - 调用对象 hashCode 调用了对象的 hashCode，但偏向锁的对象 MarkWord 中存储的是线程 id，如果调用 hashCode 会导致偏向锁被 撤销\n轻量级锁会在锁记录中记录 hashCode 重量级锁会在 Monitor 中记录 hashCode 在调用 hashCode 后使用偏向锁，记得去掉-XX:-UseBiasedLocking 输出\n1 2 3 4 5 6 7 11:22:10.386 c.TestBiased [main] - 调用 hashCode:1778535015 11:22:10.391 c.TestBiased [t1] - synchronized 前 00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001 11:22:10.393 c.TestBiased [t1] - synchronized 中 00000000 00000000 00000000 00000000 00100000 11000011 11110011 01101000 11:22:10.393 c.TestBiased [t1] - synchronized 后 00000000 00000000 00000000 01101010 00000010 01001010 01100111 00000001 Copied! 撤销 - 其它线程使用对象 当有其它线程使用偏向锁对象时，会将偏向锁升级为轻量级锁\n1 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 private static void test2() throws InterruptedException { Dog d = new Dog(); Thread t1 = new Thread(() -\u0026gt; { synchronized (d) { log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); } synchronized (TestBiased.class) { TestBiased.class.notify(); } // 如果不用 wait/notify 使用 join 必须打开下面的注释 // 因为：t1 线程不能结束，否则底层线程可能被 jvm 重用作为 t2 线程，底层线程 id 是一样的 /*try { System.in.read(); } catch (IOException e) { e.printStackTrace(); }*/ }, \u0026#34;t1\u0026#34;); t1.start(); Thread t2 = new Thread(() -\u0026gt; { synchronized (TestBiased.class) { try { TestBiased.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); } log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); }, \u0026#34;t2\u0026#34;); t2.start(); } Copied! 输出\n1 2 3 4 [t1] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101 [t2] - 00000000 00000000 00000000 00000000 00011111 01000001 00010000 00000101 [t2] - 00000000 00000000 00000000 00000000 00011111 10110101 11110000 01000000 [t2] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 Copied! 撤销 - 调用 wait/notify 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 public static void main(String[] args) throws InterruptedException { Dog d = new Dog(); Thread t1 = new Thread(() -\u0026gt; { log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); try { d.wait(); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(ClassLayout.parseInstance(d).toPrintableSimple(true)); } }, \u0026#34;t1\u0026#34;); t1.start(); new Thread(() -\u0026gt; { try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (d) { log.debug(\u0026#34;notify\u0026#34;); d.notify(); } }, \u0026#34;t2\u0026#34;).start(); } Copied! 输出\n1 2 3 4 [t1] - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101 [t1] - 00000000 00000000 00000000 00000000 00011111 10110011 11111000 00000101 [t2] - notify [t1] - 00000000 00000000 00000000 00000000 00011100 11010100 00001101 11001010 Copied! 批量重偏向 如果对象虽然被多个线程访问，但没有竞争，这时偏向了线程 T1 的对象仍有机会重新偏向 T2，重偏向会重置对象 的 Thread ID\n当撤销偏向锁阈值超过 20 次后，jvm 会这样觉得，我是不是偏向错了呢，于是会在给这些对象加锁时重新偏向至 加锁线程\n1 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 private static void test3() throws InterruptedException { Vector\u0026lt;Dog\u0026gt; list = new Vector\u0026lt;\u0026gt;(); Thread t1 = new Thread(() -\u0026gt; { for (int i = 0; i \u0026lt; 30; i++) { Dog d = new Dog(); list.add(d); synchronized (d) { log.debug(i + \u0026#34;\\t\u0026#34; + ClassLayout.parseInstance(d).toPrintableSimple(true)); } } synchronized (list) { list.notify(); } }, \u0026#34;t1\u0026#34;); t1.start(); Thread t2 = new Thread(() -\u0026gt; { synchronized (list) { try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug(\u0026#34;===============\u0026gt; \u0026#34;); for (int i = 0; i \u0026lt; 30; i++) { Dog d = list.get(i); log.debug(i + \u0026#34;\\t\u0026#34; + ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(i + \u0026#34;\\t\u0026#34; + ClassLayout.parseInstance(d).toPrintableSimple(true)); } log.debug(i + \u0026#34;\\t\u0026#34; + ClassLayout.parseInstance(d).toPrintableSimple(true)); } }, \u0026#34;t2\u0026#34;); t2.start(); } Copied! 输出\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 [t1] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t1] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - ===============\u0026gt; [t2] - 0 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 0 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 0 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 1 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 1 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 1 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 2 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 2 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 2 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 3 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 3 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 3 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 4 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 4 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 4 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 5 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 5 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 6 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 6 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 6 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 7 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 7 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 7 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 8 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 8 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 9 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 9 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 9 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 10 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 10 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 10 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 11 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 11 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 11 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 12 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 12 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 12 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 13 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 13 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 13 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 14 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 14 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 14 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 15 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 15 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 15 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 16 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 16 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 16 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 17 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 17 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 17 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 18 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 18 00000000 00000000 00000000 00000000 00100000 01011000 11110111 00000000 [t2] - 18 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 [t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 19 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 20 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 21 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 22 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 23 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 24 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 25 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 26 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 27 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 28 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11100000 00000101 [t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 [t2] - 29 00000000 00000000 00000000 00000000 00011111 11110011 11110001 00000101 Copied! 批量撤销 当撤销偏向锁阈值超过 40 次后，jvm 会这样觉得，自己确实偏向错了，根本就不该偏向。于是整个类的所有对象 都会变为不可偏向的，新建的对象也是不可偏向的\n1 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 37 38 39 40 41 42 43 44 45 static Thread t1,t2,t3; private static void test4() throws InterruptedException { Vector\u0026lt;Dog\u0026gt; list = new Vector\u0026lt;\u0026gt;(); int loopNumber = 39; t1 = new Thread(() -\u0026gt; { for (int i = 0; i \u0026lt; loopNumber; i++) { Dog d = new Dog(); list.add(d); synchronized (d) { log.debug(i + \u0026#34;\\t\u0026#34; + ClassLayout.parseInstance(d).toPrintableSimple(true)); } } LockSupport.unpark(t2); }, \u0026#34;t1\u0026#34;); t1.start(); t2 = new Thread(() -\u0026gt; { LockSupport.park(); log.debug(\u0026#34;===============\u0026gt; \u0026#34;); for (int i = 0; i \u0026lt; loopNumber; i++) { Dog d = list.get(i); log.debug(i + \u0026#34;\\t\u0026#34; + ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(i + \u0026#34;\\t\u0026#34; + ClassLayout.parseInstance(d).toPrintableSimple(true)); } log.debug(i + \u0026#34;\\t\u0026#34; + ClassLayout.parseInstance(d).toPrintableSimple(true)); } LockSupport.unpark(t3); }, \u0026#34;t2\u0026#34;); t2.start(); t3 = new Thread(() -\u0026gt; { LockSupport.park(); log.debug(\u0026#34;===============\u0026gt; \u0026#34;); for (int i = 0; i \u0026lt; loopNumber; i++) { Dog d = list.get(i); log.debug(i + \u0026#34;\\t\u0026#34; + ClassLayout.parseInstance(d).toPrintableSimple(true)); synchronized (d) { log.debug(i + \u0026#34;\\t\u0026#34; + ClassLayout.parseInstance(d).toPrintableSimple(true)); } log.debug(i + \u0026#34;\\t\u0026#34; + ClassLayout.parseInstance(d).toPrintableSimple(true)); } }, \u0026#34;t3\u0026#34;); t3.start(); t3.join(); log.debug(ClassLayout.parseInstance(new Dog()).toPrintableSimple(true)); } Copied! 参考资料\nhttps://github.com/farmerjohngit/myblog/issues/12 https://www.cnblogs.com/LemonFive/p/11246086.html https://www.cnblogs.com/LemonFive/p/11248248.html [偏向锁论文](Eliminating Synchronization-Related Atomic Operations with Biased Locking and Bulk Rebiasing (oracle.com) )\n锁消除 锁消除\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Fork(1) @BenchmarkMode(Mode.AverageTime) @Warmup(iterations=3) @Measurement(iterations=5) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class MyBenchmark { static int x = 0; @Benchmark public void a() throws Exception { x++; } @Benchmark public void b() throws Exception { Object o = new Object(); synchronized (o) { x++; } } } Copied! java -jar benchmarks.jar\n1 2 3 Benchmark Mode Samples Score Score error Units c.i.MyBenchmark.a avgt 5 1.542 0.056 ns/op c.i.MyBenchmark.b avgt 5 1.518 0.091 ns/op Copied! java -XX:-EliminateLocks -jar benchmarks.jar\n1 2 3 Benchmark Mode Samples Score Score error Units c.i.MyBenchmark.a avgt 5 1.507 0.108 ns/op c.i.MyBenchmark.b avgt 5 16.976 1.572 ns/op Copied! 锁粗化\n对相同对象多次加锁，导致线程发生多次重入，可以使用锁粗化方式来优化，这不同于之前讲的细分锁的粒度。\n4.7 wait notify * 原理之 wait / notify Owner 线程发现条件不满足，调用 wait 方法，即可进入 WaitSet 变为 WAITING 状态 BLOCKED 和 WAITING 的线程都处于阻塞状态，不占用 CPU 时间片 BLOCKED 线程会在 Owner 线程释放锁时唤醒 WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒，但唤醒后并不意味者立刻获得锁，仍需进入 EntryList 重新竞争 API 介绍 obj.wait() 让进入 object 监视器的线程到 waitSet 等待 obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒 obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒 它们都是线程之间进行协作的手段，都属于 Object 对象的方法。必须获得此对象的锁，才能调用这几个方法\n1 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 final static Object obj = new Object(); public static void main(String[] args) { new Thread(() -\u0026gt; { synchronized (obj) { log.debug(\u0026#34;执行....\u0026#34;); try { obj.wait(); // 让线程在obj上一直等待下去 } catch (InterruptedException e) { e.printStackTrace(); } log.debug(\u0026#34;其它代码....\u0026#34;); } }).start(); new Thread(() -\u0026gt; { synchronized (obj) { log.debug(\u0026#34;执行....\u0026#34;); try { obj.wait(); // 让线程在obj上一直等待下去 } catch (InterruptedException e) { e.printStackTrace(); } log.debug(\u0026#34;其它代码....\u0026#34;); } }).start(); // 主线程两秒后执行 sleep(2); log.debug(\u0026#34;唤醒 obj 上其它线程\u0026#34;); synchronized (obj) { obj.notify(); // 唤醒obj上一个线程 // obj.notifyAll(); // 唤醒obj上所有等待线程 } } Copied! notify 的一种结果\n1 2 3 4 20:00:53.096 [Thread-0] c.TestWaitNotify - 执行.... 20:00:53.099 [Thread-1] c.TestWaitNotify - 执行.... 20:00:55.096 [main] c.TestWaitNotify - 唤醒 obj 上其它线程 20:00:55.096 [Thread-0] c.TestWaitNotify - 其它代码.... Copied! notifyAll 的结果\n1 2 3 4 5 19:58:15.457 [Thread-0] c.TestWaitNotify - 执行.... 19:58:15.460 [Thread-1] c.TestWaitNotify - 执行.... 19:58:17.456 [main] c.TestWaitNotify - 唤醒 obj 上其它线程 19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代码.... 19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代码.... Copied! wait() 方法会释放对象的锁，进入 WaitSet 等待区，从而让其他线程就机会获取对象的锁。无限制等待，直到 notify 为止\nwait(long n) 有时限的等待, 到 n 毫秒后结束等待，或是被 notify\n4.8 wait notify 的正确姿势 开始之前先看看\nsleep(long n) 和 wait(long n) 的区别 sleep 是 Thread 方法，而 wait 是 Object 的方法 sleep 不需要强制和 synchronized 配合使用，但 wait 需要 和 synchronized 一起用 sleep 在睡眠的同时，不会释放对象锁的，但 wait 在等待的时候会释放对象锁 它们 状态 TIMED_WAITING step 1 1 2 3 static final Object room = new Object(); static boolean hasCigarette = false; static boolean hasTakeout = false; Copied! 思考下面的解决方案好不好，为什么？\n1 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 new Thread(() -\u0026gt; { synchronized (room) { log.debug(\u0026#34;有烟没？[{}]\u0026#34;, hasCigarette); if (!hasCigarette) { log.debug(\u0026#34;没烟，先歇会！\u0026#34;); sleep(2); } log.debug(\u0026#34;有烟没？[{}]\u0026#34;, hasCigarette); if (hasCigarette) { log.debug(\u0026#34;可以开始干活了\u0026#34;); } } }, \u0026#34;小南\u0026#34;).start(); for (int i = 0; i \u0026lt; 5; i++) { new Thread(() -\u0026gt; { synchronized (room) { log.debug(\u0026#34;可以开始干活了\u0026#34;); } }, \u0026#34;其它人\u0026#34;).start(); } sleep(1); new Thread(() -\u0026gt; { // 这里能不能加 synchronized (room)？ hasCigarette = true; log.debug(\u0026#34;烟到了噢！\u0026#34;); }, \u0026#34;送烟的\u0026#34;).start(); Copied! 输出\n1 2 3 4 5 6 7 8 9 10 20:49:49.883 [小南] c.TestCorrectPosture - 有烟没？[false] 20:49:49.887 [小南] c.TestCorrectPosture - 没烟，先歇会！ 20:49:50.882 [送烟的] c.TestCorrectPosture - 烟到了噢！ 20:49:51.887 [小南] c.TestCorrectPosture - 有烟没？[true] 20:49:51.887 [小南] c.TestCorrectPosture - 可以开始干活了 20:49:51.887 [其它人] c.TestCorrectPosture - 可以开始干活了 20:49:51.887 [其它人] c.TestCorrectPosture - 可以开始干活了 20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了 20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了 20:49:51.888 [其它人] c.TestCorrectPosture - 可以开始干活了 Copied! 其它干活的线程，都要一直阻塞，效率太低 小南线程必须睡足 2s 后才能醒来，就算烟提前送到，也无法立刻醒来 加了 synchronized (room) 后，就好比小南在里面反锁了门睡觉，烟根本没法送进门，main 没加 synchronized 就好像 main 线程是翻窗户进来的 解决方法，使用 wait - notify 机制 step 2 思考下面的实现行吗，为什么？\n1 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 new Thread(() -\u0026gt; { synchronized (room) { log.debug(\u0026#34;有烟没？[{}]\u0026#34;, hasCigarette); if (!hasCigarette) { log.debug(\u0026#34;没烟，先歇会！\u0026#34;); try { room.wait(2000); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug(\u0026#34;有烟没？[{}]\u0026#34;, hasCigarette); if (hasCigarette) { log.debug(\u0026#34;可以开始干活了\u0026#34;); } } }, \u0026#34;小南\u0026#34;).start(); for (int i = 0; i \u0026lt; 5; i++) { new Thread(() -\u0026gt; { synchronized (room) { log.debug(\u0026#34;可以开始干活了\u0026#34;); } }, \u0026#34;其它人\u0026#34;).start(); } sleep(1); new Thread(() -\u0026gt; { synchronized (room) { hasCigarette = true; log.debug(\u0026#34;烟到了噢！\u0026#34;); room.notify(); } }, \u0026#34;送烟的\u0026#34;).start(); Copied! 解决了其它干活的线程阻塞的问题 但如果有其它线程也在等待条件呢？ step 3 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 37 38 39 40 41 42 43 44 45 46 47 new Thread(() -\u0026gt; { synchronized (room) { log.debug(\u0026#34;有烟没？[{}]\u0026#34;, hasCigarette); if (!hasCigarette) { log.debug(\u0026#34;没烟，先歇会！\u0026#34;); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug(\u0026#34;有烟没？[{}]\u0026#34;, hasCigarette); if (hasCigarette) { log.debug(\u0026#34;可以开始干活了\u0026#34;); } else { log.debug(\u0026#34;没干成活...\u0026#34;); } } }, \u0026#34;小南\u0026#34;).start(); new Thread(() -\u0026gt; { synchronized (room) { Thread thread = Thread.currentThread(); log.debug(\u0026#34;外卖送到没？[{}]\u0026#34;, hasTakeout); if (!hasTakeout) { log.debug(\u0026#34;没外卖，先歇会！\u0026#34;); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug(\u0026#34;外卖送到没？[{}]\u0026#34;, hasTakeout); if (hasTakeout) { log.debug(\u0026#34;可以开始干活了\u0026#34;); } else { log.debug(\u0026#34;没干成活...\u0026#34;); } } }, \u0026#34;小女\u0026#34;).start(); sleep(1); new Thread(() -\u0026gt; { synchronized (room) { hasTakeout = true; log.debug(\u0026#34;外卖到了噢！\u0026#34;); room.notify(); } }, \u0026#34;送外卖的\u0026#34;).start(); Copied! 输出\n1 2 3 4 5 6 7 20:53:12.173 [小南] c.TestCorrectPosture - 有烟没？[false] 20:53:12.176 [小南] c.TestCorrectPosture - 没烟，先歇会！ 20:53:12.176 [小女] c.TestCorrectPosture - 外卖送到没？[false] 20:53:12.176 [小女] c.TestCorrectPosture - 没外卖，先歇会！ 20:53:13.174 [送外卖的] c.TestCorrectPosture - 外卖到了噢！ 20:53:13.174 [小南] c.TestCorrectPosture - 有烟没？[false] 20:53:13.174 [小南] c.TestCorrectPosture - 没干成活... Copied! notify 只能随机唤醒一个 WaitSet 中的线程，这时如果有其它线程也在等待，那么就可能唤醒不了正确的线 程，称之为【虚假唤醒】 解决方法，改为 notifyAll step 4 1 2 3 4 5 6 7 new Thread(() -\u0026gt; { synchronized (room) { hasTakeout = true; log.debug(\u0026#34;外卖到了噢！\u0026#34;); room.notifyAll(); } }, \u0026#34;送外卖的\u0026#34;).start(); Copied! 输出\n1 2 3 4 5 6 7 8 9 20:55:23.978 [小南] c.TestCorrectPosture - 有烟没？[false] 20:55:23.982 [小南] c.TestCorrectPosture - 没烟，先歇会！ 20:55:23.982 [小女] c.TestCorrectPosture - 外卖送到没？[false] 20:55:23.982 [小女] c.TestCorrectPosture - 没外卖，先歇会！ 20:55:24.979 [送外卖的] c.TestCorrectPosture - 外卖到了噢！ 20:55:24.979 [小女] c.TestCorrectPosture - 外卖送到没？[true] 20:55:24.980 [小女] c.TestCorrectPosture - 可以开始干活了 20:55:24.980 [小南] c.TestCorrectPosture - 有烟没？[false] 20:55:24.980 [小南] c.TestCorrectPosture - 没干成活... Copied! 用 notifyAll 仅解决某个线程的唤醒问题，但使用 if + wait 判断仅有一次机会，一旦条件不成立，就没有重新判断的机会了 解决方法，用 while + wait，当条件不成立，再次 wait step 5 将 if 改为 while\n1 2 3 4 5 6 7 8 if (!hasCigarette) { log.debug(\u0026#34;没烟，先歇会！\u0026#34;); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } Copied! 改动后\n1 2 3 4 5 6 7 8 while (!hasCigarette) { log.debug(\u0026#34;没烟，先歇会！\u0026#34;); try { room.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } Copied! 输出\n1 2 3 4 5 6 7 8 20:58:34.322 [小南] c.TestCorrectPosture - 有烟没？[false] 20:58:34.326 [小南] c.TestCorrectPosture - 没烟，先歇会！ 20:58:34.326 [小女] c.TestCorrectPosture - 外卖送到没？[false] 20:58:34.326 [小女] c.TestCorrectPosture - 没外卖，先歇会！ 20:58:35.323 [送外卖的] c.TestCorrectPosture - 外卖到了噢！ 20:58:35.324 [小女] c.TestCorrectPosture - 外卖送到没？[true] 20:58:35.324 [小女] c.TestCorrectPosture - 可以开始干活了 20:58:35.324 [小南] c.TestCorrectPosture - 没烟，先歇会！ Copied! 1 2 3 4 5 6 7 8 9 10 synchronized(lock) { while(条件不成立) { lock.wait(); } // 干活 } //另一个线程 synchronized(lock) { lock.notifyAll(); } Copied! $\\textcolor{orange}{*模式之保护性暂停}$ 1.定义 即 Guarded Suspension，用在一个线程等待另一个线程的执行结果\n要点\n有一个结果需要从一个线程传递到另一个线程，让他们关联同一个 GuardedObject 如果有结果不断从一个线程到另一个线程那么可以使用消息队列（见生产者/消费者） JDK 中，join 的实现、Future 的实现，采用的就是此模式 因为要等待另一方的结果，因此归类到同步模式 2.实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class GuardedObject { private Object response; private final Object lock = new Object(); public Object get() { synchronized (lock) { // 条件不满足则等待 while (response == null) { try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } return response; } } public void complete(Object response) { synchronized (lock) { // 条件满足，通知等待线程 this.response = response; lock.notifyAll(); } } } Copied! 测试\n一个线程等待另一个线程的执行结果\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Override public void run() { GuardedObject guardedObject = MailBoxes.getGuardedObject(id); guardedObject.set(mail); log.debug(\u0026#34;送信成功,id={},内容：{}\u0026#34;,id,mail); } public static void main(String[] args) { GuardedObject guardedObject = new GuardedObject(); new Thread(() -\u0026gt; { try { // 子线程执行下载 List\u0026lt;String\u0026gt; response = download(); log.debug(\u0026#34;download complete...\u0026#34;); guardedObject.complete(response); } catch (IOException e) { e.printStackTrace(); } }).start(); log.debug(\u0026#34;waiting...\u0026#34;); // 主线程阻塞等待 Object response = guardedObject.get(); log.debug(\u0026#34;get response: [{}] lines\u0026#34;, ((List\u0026lt;String\u0026gt;) response).size()); } Copied! 执行结果\n1 2 3 08:42:18.568 [main] c.TestGuardedObject - waiting... 08:42:23.312 [Thread-0] c.TestGuardedObject - download complete... 08:42:23.312 [main] c.TestGuardedObject - get response: [3] lines Copied! 3.带超时版 GuardedObject 如果要控制超时时间呢\n1 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 37 38 39 class GuardedObjectV2 { private Object response; private final Object lock = new Object(); public Object get(long millis) { synchronized (lock) { // 1) 记录最初时间 long begin = System.currentTimeMillis(); // 2) 已经经历的时间 long timePassed = 0; while (response == null) { // 4) 假设 millis 是 1000，结果在 400 时唤醒了，那么还有 600 要等 long waitTime = millis - timePassed; log.debug(\u0026#34;waitTime: {}\u0026#34;, waitTime); if (waitTime \u0026lt;= 0) { log.debug(\u0026#34;break...\u0026#34;); break; } try { lock.wait(waitTime); } catch (InterruptedException e) { e.printStackTrace(); } // 3) 如果提前被唤醒，这时已经经历的时间假设为 400 timePassed = System.currentTimeMillis() - begin; log.debug(\u0026#34;timePassed: {}, object is null {}\u0026#34;, timePassed, response == null); } return response; } } public void complete(Object response) { synchronized (lock) { // 条件满足，通知等待线程 this.response = response; log.debug(\u0026#34;notify...\u0026#34;); lock.notifyAll(); } } } Copied! 测试，没有超时\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main(String[] args) { GuardedObjectV2 v2 = new GuardedObjectV2(); new Thread(() -\u0026gt; { sleep(1); v2.complete(null); sleep(1); v2.complete(Arrays.asList(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;)); }).start(); Object response = v2.get(2500); if (response != null) { log.debug(\u0026#34;get response: [{}] lines\u0026#34;, ((List\u0026lt;String\u0026gt;) response).size()); } else { log.debug(\u0026#34;can\u0026#39;t get response\u0026#34;); } } Copied! 输出\n1 2 3 4 5 6 7 08:49:39.917 [main] c.GuardedObjectV2 - waitTime: 2500 08:49:40.917 [Thread-0] c.GuardedObjectV2 - notify... 08:49:40.917 [main] c.GuardedObjectV2 - timePassed: 1003, object is null true 08:49:40.917 [main] c.GuardedObjectV2 - waitTime: 1497 08:49:41.918 [Thread-0] c.GuardedObjectV2 - notify... 08:49:41.918 [main] c.GuardedObjectV2 - timePassed: 2004, object is null false 08:49:41.918 [main] c.TestGuardedObjectV2 - get response: [3] lines Copied! 测试超时\n1 2 // 等待时间不足 List\u0026lt;String\u0026gt; lines = v2.get(1500); Copied! 输出\n1 2 3 4 5 6 7 8 9 08:47:54.963 [main] c.GuardedObjectV2 - waitTime: 1500 08:47:55.963 [Thread-0] c.GuardedObjectV2 - notify... 08:47:55.963 [main] c.GuardedObjectV2 - timePassed: 1002, object is null true 08:47:55.963 [main] c.GuardedObjectV2 - waitTime: 498 08:47:56.461 [main] c.GuardedObjectV2 - timePassed: 1500, object is null true 08:47:56.461 [main] c.GuardedObjectV2 - waitTime: 0 08:47:56.461 [main] c.GuardedObjectV2 - break... 08:47:56.461 [main] c.TestGuardedObjectV2 - can\u0026#39;t get response 08:47:56.963 [Thread-0] c.GuardedObjectV2 - notify... Copied! $\\textcolor{blue}{* 原理之 join}$ 是调用者轮询检查线程 alive 状态\n1 t1.join(); Copied! 等价于下面的代码\n1 2 3 4 5 6 synchronized (t1) { // 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束 while (t1.isAlive()) { t1.wait(0); } } Copied! 注意\njoin 体现的是【保护性暂停】模式，请参考之\n源码：\n1 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 //不带参 public final void join() throws InterruptedException { join(0); } //带参 //等待时长的实现类似于之前的保护性暂停 public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis \u0026lt; 0) { throw new IllegalArgumentException(\u0026#34;timeout value is negative\u0026#34;); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay \u0026lt;= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } } Copied! 4.多任务版GuardedObject 图中 Futures 就好比居民楼一层的信箱（每个信箱有房间编号），左侧的 t0，t2，t4 就好比等待邮件的居民，右 侧的 t1，t3，t5 就好比邮递员 。\n如果需要在多个类之间使用 GuardedObject 对象，作为参数传递不是很方便，因此设计一个用来解耦的中间类， 这样不仅能够解耦【结果等待者】和【结果生产者】，还能够同时支持多个任务的管理。\n新增 id 用来标识 Guarded Object\n1 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 37 38 39 40 41 42 43 44 45 46 class GuardedObject { // 标识 Guarded Object private int id; public GuardedObject(int id) { this.id = id; } public int getId() { return id; } // 结果 private Object response; // 获取结果 // timeout 表示要等待多久 2000 public Object get(long timeout) { synchronized (this) { // 开始时间 15:00:00 long begin = System.currentTimeMillis(); // 经历的时间 long passedTime = 0; while (response == null) { // 这一轮循环应该等待的时间 long waitTime = timeout - passedTime; // 经历的时间超过了最大等待时间时，退出循环 if (timeout - passedTime \u0026lt;= 0) { break; } try { this.wait(waitTime); // 虚假唤醒 15:00:01 } catch (InterruptedException e) { e.printStackTrace(); } // 求得经历时间 passedTime = System.currentTimeMillis() - begin; // 15:00:02 1s } return response; } } // 产生结果 public void complete(Object response) { synchronized (this) { // 给结果成员变量赋值 this.response = response; this.notifyAll(); } } } Copied! 中间解耦类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Mailboxes { private static Map\u0026lt;Integer, GuardedObject\u0026gt; boxes = new Hashtable\u0026lt;\u0026gt;(); private static int id = 1; // 产生唯一 id，方法必须声明为synchronized private static synchronized int generateId() { return id++; } public static GuardedObject getGuardedObject(int id) { return boxes.remove(id); } public static GuardedObject createGuardedObject() { GuardedObject go = new GuardedObject(generateId()); boxes.put(go.getId(), go); return go; } public static Set\u0026lt;Integer\u0026gt; getIds() { return boxes.keySet(); } } Copied! 业务相关类\n1 2 3 4 5 6 7 8 9 10 class People extends Thread{ @Override public void run() { // 收信 GuardedObject guardedObject = Mailboxes.createGuardedObject(); log.debug(\u0026#34;开始收信 id:{}\u0026#34;, guardedObject.getId()); Object mail = guardedObject.get(5000); log.debug(\u0026#34;收到信 id:{}, 内容:{}\u0026#34;, guardedObject.getId(), mail); } } Copied! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Postman extends Thread { private int id; private String mail; public Postman(int id, String mail) { this.id = id; this.mail = mail; } @Override public void run() { GuardedObject guardedObject = Mailboxes.getGuardedObject(id); log.debug(\u0026#34;送信 id:{}, 内容:{}\u0026#34;, id, mail); guardedObject.complete(mail); } } Copied! 测试\n1 2 3 4 5 6 7 8 9 public static void main(String[] args) throws InterruptedException { for (int i = 0; i \u0026lt; 3; i++) { new People().start(); } Sleeper.sleep(1); for (Integer id : Mailboxes.getIds()) { new Postman(id, \u0026#34;内容\u0026#34; + id).start(); } } Copied! 某次运行结果\n1 2 3 4 5 6 7 8 9 10:35:05.689 c.People [Thread-1] - 开始收信 id:3 10:35:05.689 c.People [Thread-2] - 开始收信 id:1 10:35:05.689 c.People [Thread-0] - 开始收信 id:2 10:35:06.688 c.Postman [Thread-4] - 送信 id:2, 内容:内容2 10:35:06.688 c.Postman [Thread-5] - 送信 id:1, 内容:内容1 10:35:06.688 c.People [Thread-0] - 收到信 id:2, 内容:内容2 10:35:06.688 c.People [Thread-2] - 收到信 id:1, 内容:内容1 10:35:06.688 c.Postman [Thread-3] - 送信 id:3, 内容:内容3 10:35:06.689 c.People [Thread-1] - 收到信 id:3, 内容:内容3 Copied! $\\textcolor{orange}{* 模式之生产者消费者}$ 1.定义 要点\n与前面的保护性暂停中的 GuardObject 不同，不需要产生结果和消费结果的线程一一对应 消费队列可以用来平衡生产和消费的线程资源 生产者仅负责产生结果数据，不关心数据该如何处理，而消费者专心处理结果数据 消息队列是有容量限制的，满时不会再加入数据，空时不会再消耗数据 JDK 中各种阻塞队列，采用的就是这种模式 2.实现 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class Message { private int id; private Object message; public Message(int id, Object message) { this.id = id; this.message = message; } public int getId() { return id; } public Object getMessage() { return message; } } class MessageQueue { private LinkedList\u0026lt;Message\u0026gt; queue; private int capacity; public MessageQueue(int capacity) { this.capacity = capacity; queue = new LinkedList\u0026lt;\u0026gt;(); } public Message take() { synchronized (queue) { while (queue.isEmpty()) { log.debug(\u0026#34;没货了, wait\u0026#34;); try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } Message message = queue.removeFirst(); queue.notifyAll(); return message; } } public void put(Message message) { synchronized (queue) { while (queue.size() == capacity) { log.debug(\u0026#34;库存已达上限, wait\u0026#34;); try { queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } queue.addLast(message); queue.notifyAll(); } } } Copied! 测试\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 MessageQueue messageQueue = new MessageQueue(2); // 4 个生产者线程, 下载任务 for (int i = 0; i \u0026lt; 4; i++) { int id = i; new Thread(() -\u0026gt; { try { log.debug(\u0026#34;download...\u0026#34;); List\u0026lt;String\u0026gt; response = Downloader.download(); log.debug(\u0026#34;try put message({})\u0026#34;, id); messageQueue.put(new Message(id, response)); } catch (IOException e) { e.printStackTrace(); } }, \u0026#34;生产者\u0026#34; + i).start(); } // 1 个消费者线程, 处理结果 new Thread(() -\u0026gt; { while (true) { Message message = messageQueue.take(); List\u0026lt;String\u0026gt; response = (List\u0026lt;String\u0026gt;) message.getMessage(); log.debug(\u0026#34;take message({}): [{}] lines\u0026#34;, message.getId(), response.size()); } }, \u0026#34;消费者\u0026#34;).start(); Copied! 某次运行结果\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 10:48:38.070 [生产者3] c.TestProducerConsumer - download... 10:48:38.070 [生产者0] c.TestProducerConsumer - download... 10:48:38.070 [消费者] c.MessageQueue - 没货了, wait 10:48:38.070 [生产者1] c.TestProducerConsumer - download... 10:48:38.070 [生产者2] c.TestProducerConsumer - download... 10:48:41.236 [生产者1] c.TestProducerConsumer - try put message(1) 10:48:41.237 [生产者2] c.TestProducerConsumer - try put message(2) 10:48:41.236 [生产者0] c.TestProducerConsumer - try put message(0) 10:48:41.237 [生产者3] c.TestProducerConsumer - try put message(3) 10:48:41.239 [生产者2] c.MessageQueue - 库存已达上限, wait 10:48:41.240 [生产者1] c.MessageQueue - 库存已达上限, wait 10:48:41.240 [消费者] c.TestProducerConsumer - take message(0): [3] lines 10:48:41.240 [生产者2] c.MessageQueue - 库存已达上限, wait 10:48:41.240 [消费者] c.TestProducerConsumer - take message(3): [3] lines 10:48:41.240 [消费者] c.TestProducerConsumer - take message(1): [3] lines 10:48:41.240 [消费者] c.TestProducerConsumer - take message(2): [3] lines 10:48:41.240 [消费者] c.MessageQueue - 没货了, wait Copied! 结果解读\n4.9 Park \u0026amp; Unpark 基本使用 它们是 LockSupport 类中的方法\n1 2 3 4 // 暂停当前线程 LockSupport.park(); // 恢复某个线程的运行 LockSupport.unpark(暂停线程对象) Copied! 先 park 再 unpark\n1 2 3 4 5 6 7 8 9 10 11 Thread t1 = new Thread(() -\u0026gt; { log.debug(\u0026#34;start...\u0026#34;); sleep(1); log.debug(\u0026#34;park...\u0026#34;); LockSupport.park(); log.debug(\u0026#34;resume...\u0026#34;); },\u0026#34;t1\u0026#34;); t1.start(); sleep(2); log.debug(\u0026#34;unpark...\u0026#34;); LockSupport.unpark(t1); Copied! 输出\n1 2 3 4 18:42:52.585 c.TestParkUnpark [t1] - start... 18:42:53.589 c.TestParkUnpark [t1] - park... 18:42:54.583 c.TestParkUnpark [main] - unpark... 18:42:54.583 c.TestParkUnpark [t1] - resume... Copied! 先 unpark 再 park\n1 2 3 4 5 6 7 8 9 10 11 Thread t1 = new Thread(() -\u0026gt; { log.debug(\u0026#34;start...\u0026#34;); sleep(2); log.debug(\u0026#34;park...\u0026#34;); LockSupport.park(); log.debug(\u0026#34;resume...\u0026#34;); }, \u0026#34;t1\u0026#34;); t1.start(); sleep(1); log.debug(\u0026#34;unpark...\u0026#34;); LockSupport.unpark(t1); Copied! 输出\n1 2 3 4 18:43:50.765 c.TestParkUnpark [t1] - start... 18:43:51.764 c.TestParkUnpark [main] - unpark... 18:43:52.769 c.TestParkUnpark [t1] - park... 18:43:52.769 c.TestParkUnpark [t1] - resume... Copied! 特点 与 Object 的 wait \u0026amp; notify 相比\nwait，notify 和 notifyAll 必须配合 Object Monitor 一起使用，而 park，unpark 不必 park \u0026amp; unpark 是以线程为单位来【阻塞】和【唤醒】线程，而 notify 只能随机唤醒一个等待线程，notifyAll 是唤醒所有等待线程，就不那么【精确】 park \u0026amp; unpark 可以先 unpark，而 wait \u0026amp; notify 不能先 notify $\\textcolor{blue}{* 原理之park和unpark}$ 每个线程都有自己的一个 Parker 对象(由C++编写，java中不可见)，由三部分组成 _counter ， _cond 和 _mutex 打个比喻\n线程就像一个旅人，Parker 就像他随身携带的背包，条件变量就好比背包中的帐篷。_counter 就好比背包中 的备用干粮（0 为耗尽，1 为充足） 调用 park 就是要看需不需要停下来歇息 如果备用干粮耗尽，那么钻进帐篷歇息 如果备用干粮充足，那么不需停留，继续前进 调用 unpark，就好比令干粮充足 如果这时线程还在帐篷，就唤醒让他继续前进 如果这时线程还在运行，那么下次他调用 park 时，仅是消耗掉备用干粮，不需停留继续前进 因为背包空间有限，多次调用 unpark 仅会补充一份备用干粮 当前线程调用 Unsafe.park() 方法 检查 _counter ，本情况为 0，这时，获得 _mutex 互斥锁 线程进入 _cond 条件变量阻塞 设置 _counter = 0 调用 Unsafe.unpark(Thread_0) 方法，设置 _counter 为 1 唤醒 _cond 条件变量中的 Thread_0 Thread_0 恢复运行 设置 _counter 为 0 调用 Unsafe.unpark(Thread_0) 方法，设置 _counter 为 1 当前线程调用 Unsafe.park() 方法 检查 _counter ，本情况为 1，这时线程无需阻塞，继续运行 设置 _counter 为 0 4.10 重新理解线程状态转换 假设有线程 Thread t\n情况 1 NEW --\u0026gt; RUNNABLE 当调用 t.start() 方法时，由 NEW \u0026ndash;\u0026gt; RUNNABLE 情况 2 RUNNABLE \u0026lt;--\u0026gt; WAITING t 线程用 synchronized(obj) 获取了对象锁后\n调用 obj.wait() 方法时，t 线程从 RUNNABLE --\u0026gt; WAITING 调用 obj.notify() ， obj.notifyAll() ， t.interrupt() 时 竞争锁成功，t 线程从 WAITING --\u0026gt; RUNNABLE 竞争锁失败，t 线程从 WAITING --\u0026gt; BLOCKED 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 public class TestWaitNotify { final static Object obj = new Object(); public static void main(String[] args) { new Thread(() -\u0026gt; { synchronized (obj) { log.debug(\u0026#34;执行....\u0026#34;); try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(\u0026#34;其它代码....\u0026#34;); // 断点 } },\u0026#34;t1\u0026#34;).start(); new Thread(() -\u0026gt; { synchronized (obj) { log.debug(\u0026#34;执行....\u0026#34;); try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(\u0026#34;其它代码....\u0026#34;); // 断点 } },\u0026#34;t2\u0026#34;).start(); sleep(0.5); log.debug(\u0026#34;唤醒 obj 上其它线程\u0026#34;); synchronized (obj) { obj.notifyAll(); // 唤醒obj上所有等待线程 断点 } } } Copied! 情况 3 RUNNABLE \u0026lt;--\u0026gt; WAITING 当前线程调用 t.join() 方法时，当前线程从 RUNNABLE --\u0026gt; WAITING 注意是当前线程在t 线程对象的监视器上等待 t 线程运行结束，或调用了当前线程的 interrupt() 时，当前线程从 WAITING --\u0026gt; RUNNABLE 情况 4 RUNNABLE \u0026lt;--\u0026gt; WAITING 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --\u0026gt; WAITING 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ，会让目标线程从 WAITING --\u0026gt; RUNNABLE 情况 5 RUNNABLE \u0026lt;--\u0026gt; TIMED_WAITING t 线程用 synchronized(obj) 获取了对象锁后\n调用 obj.wait(long n) 方法时，t 线程从 RUNNABLE --\u0026gt; TIMED_WAITING t 线程等待时间超过了 n 毫秒，或调用 obj.notify() ， obj.notifyAll() ，t.interrupt()时 竞争锁成功，t 线程从 TIMED_WAITING --\u0026gt; RUNNABLE 竞争锁失败，t 线程从 TIMED_WAITING --\u0026gt; BLOCKED 情况 6 RUNNABLE \u0026lt;--\u0026gt; TIMED_WAITING 当前线程调用 t.join(long n) 方法时，当前线程从 RUNNABLE --\u0026gt; TIMED_WAITING 注意是当前线程在t 线程对象的监视器上等待 当前线程等待时间超过了 n 毫秒，或t 线程运行结束，或调用了当前线程的 interrupt() 时，当前线程从 TIMED_WAITING --\u0026gt; RUNNABLE 情况 7 RUNNABLE \u0026lt;--\u0026gt; TIMED_WAITING 当前线程调用 Thread.sleep(long n) ，当前线程从 RUNNABLE --\u0026gt; TIMED_WAITING 当前线程等待时间超过了 n 毫秒，当前线程从 TIMED_WAITING --\u0026gt; RUNNABLE 情况 8 RUNNABLE \u0026lt;--\u0026gt; TIMED_WAITING 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时，当前线程从 RUNNABLE --\u0026gt; TIMED_WAITING 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ，或是等待超时，会让目标线程从 TIMED_WAITING--\u0026gt; RUNNABLE 情况 9 RUNNABLE \u0026lt;--\u0026gt; BLOCKED t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败，从 RUNNABLE --\u0026gt; BLOCKED 持 obj 锁线程的同步代码块执行完毕，会唤醒该对象上所有 BLOCKED 的线程重新竞争，如果其中 t 线程竞争 成功，从 BLOCKED --\u0026gt; RUNNABLE ，其它失败的线程仍然 BLOCKED 情况 10 RUNNABLE \u0026lt;--\u0026gt; TERMINATED 当前线程所有代码运行完毕，进入 TERMINATED 4.11 多把锁 多把不相干的锁\n一间大屋子有两个功能：睡觉、学习，互不相干。\n现在小南要学习，小女要睡觉，但如果只用一间屋子（一个对象锁）的话，那么并发度很低\n解决方法是准备多个房间（多个对象锁）\n例如\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 class BigRoom { public void sleep() { synchronized (this) { log.debug(\u0026#34;sleeping 2 小时\u0026#34;); Sleeper.sleep(2); } } public void study() { synchronized (this) { log.debug(\u0026#34;study 1 小时\u0026#34;); Sleeper.sleep(1); } } } Copied! 执行\n1 2 3 4 5 6 7 BigRoom bigRoom = new BigRoom(); new Thread(() -\u0026gt; { bigRoom.compute(); },\u0026#34;小南\u0026#34;).start(); new Thread(() -\u0026gt; { bigRoom.sleep(); },\u0026#34;小女\u0026#34;).start(); Copied! 结果\n1 2 12:13:54.471 [小南] c.BigRoom - study 1 小时 12:13:55.476 [小女] c.BigRoom - sleeping 2 小时 Copied! 改进\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class BigRoom { private final Object studyRoom = new Object(); private final Object bedRoom = new Object(); public void sleep() { synchronized (bedRoom) { log.debug(\u0026#34;sleeping 2 小时\u0026#34;); Sleeper.sleep(2); } } public void study() { synchronized (studyRoom) { log.debug(\u0026#34;study 1 小时\u0026#34;); Sleeper.sleep(1); } } } Copied! 某次执行结果\n1 2 12:15:35.069 [小南] c.BigRoom - study 1 小时 12:15:35.069 [小女] c.BigRoom - sleeping 2 小时 Copied! 将锁的粒度细分\n好处，是可以增强并发度 坏处，如果一个线程需要同时获得多把锁，就容易发生死锁 前提：两把锁锁住的两段代码互不相关 4.12 活跃性 死锁 有这样的情况：一个线程需要同时获取多把锁，这时就容易发生死锁\nt1 线程 获得 A对象 锁，接下来想获取 B对象 的锁 t2 线程 获得 B对象 锁，接下来想获取 A对象 的锁 例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Object A = new Object(); Object B = new Object(); Thread t1 = new Thread(() -\u0026gt; { synchronized (A) { log.debug(\u0026#34;lock A\u0026#34;); sleep(1); synchronized (B) { log.debug(\u0026#34;lock B\u0026#34;); log.debug(\u0026#34;操作...\u0026#34;); } } }, \u0026#34;t1\u0026#34;); Thread t2 = new Thread(() -\u0026gt; { synchronized (B) { log.debug(\u0026#34;lock B\u0026#34;); sleep(0.5); synchronized (A) { log.debug(\u0026#34;lock A\u0026#34;); log.debug(\u0026#34;操作...\u0026#34;); } } }, \u0026#34;t2\u0026#34;); t1.start(); t2.start(); Copied! 结果\n1 2 12:22:06.962 [t2] c.TestDeadLock - lock B 12:22:06.962 [t1] c.TestDeadLock - lock A Copied! 解决方式：\nReentrantLock 定位死锁 检测死锁可以使用 jconsole工具，或者使用 jps 定位进程 id，再用 jstack 定位死锁：\n1 2 3 4 5 6 7 cmd \u0026gt; jps Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 12320 Jps 22816 KotlinCompileDaemon 33200 TestDeadLock // JVM 进程 11508 Main 28468 Launcher Copied! 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 cmd \u0026gt; jstack 33200 Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 2018-12-29 05:51:40 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b14 mixed mode): \u0026#34;DestroyJavaVM\u0026#34; #13 prio=5 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE \u0026#34;Thread-1\u0026#34; #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry [0x000000001f54f000] java.lang.Thread.State: BLOCKED (on object monitor) at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28) - waiting to lock \u0026lt;0x000000076b5bf1c0\u0026gt; (a java.lang.Object) - locked \u0026lt;0x000000076b5bf1d0\u0026gt; (a java.lang.Object) at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) \u0026#34;Thread-0\u0026#34; #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry [0x000000001f44f000] java.lang.Thread.State: BLOCKED (on object monitor) at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15) - waiting to lock \u0026lt;0x000000076b5bf1d0\u0026gt; (a java.lang.Object) - locked \u0026lt;0x000000076b5bf1c0\u0026gt; (a java.lang.Object) at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) // 略去部分输出 Found one Java-level deadlock: ============================= \u0026#34;Thread-1\u0026#34;: waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object), which is held by \u0026#34;Thread-0\u0026#34; \u0026#34;Thread-0\u0026#34;: waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object), which is held by \u0026#34;Thread-1\u0026#34; Java stack information for the threads listed above: =================================================== \u0026#34;Thread-1\u0026#34;: at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28) - waiting to lock \u0026lt;0x000000076b5bf1c0\u0026gt; (a java.lang.Object) - locked \u0026lt;0x000000076b5bf1d0\u0026gt; (a java.lang.Object) at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) \u0026#34;Thread-0\u0026#34;: at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15) - waiting to lock \u0026lt;0x000000076b5bf1d0\u0026gt; (a java.lang.Object) - locked \u0026lt;0x000000076b5bf1c0\u0026gt; (a java.lang.Object) at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source) at java.lang.Thread.run(Thread.java:745) Found 1 deadlock. Copied! 避免死锁要注意加锁顺序 另外如果由于某个线程进入了死循环，导致其它线程一直等待，对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程，再利用 top -Hp 进程id 来定位是哪个线程，最后再用 jstack 排查 活锁 活锁出现在两个线程互相改变对方的结束条件，最后谁也无法结束，例如\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class TestLiveLock { static volatile int count = 10; static final Object lock = new Object(); public static void main(String[] args) { new Thread(() -\u0026gt; { // 期望减到 0 退出循环 while (count \u0026gt; 0) { sleep(0.2); count--; log.debug(\u0026#34;count: {}\u0026#34;, count); } }, \u0026#34;t1\u0026#34;).start(); new Thread(() -\u0026gt; { // 期望超过 20 退出循环 while (count \u0026lt; 20) { sleep(0.2); count++; log.debug(\u0026#34;count: {}\u0026#34;, count); } }, \u0026#34;t2\u0026#34;).start(); } } Copied! 解决方式：\n错开线程的运行时间，使得一方不能改变另一方的结束条件。 将睡眠时间调整为随机数。 饥饿 很多教程中把饥饿定义为，一个线程由于优先级太低，始终得不到 CPU 调度执行，也不能够结束，饥饿的情况不易演示，讲读写锁时会涉及饥饿问题\n下面我讲一下我遇到的一个线程饥饿的例子，先来看看使用顺序加锁的方式解决之前的死锁问题\n顺序加锁的解决方案\n说明：\n顺序加锁可以解决死锁问题，但也会导致一些线程一直得不到锁，产生饥饿现象。 解决方式：ReentrantLock 4.13 ReentrantLock 相对于 synchronized 它具备如下特点\n可中断 可以设置超时时间 可以设置为公平锁 支持多个条件变量 与 synchronized 一样，都支持可重入\n基本语法\n1 2 3 4 5 6 7 8 // 获取锁 reentrantLock.lock(); try { // 临界区 } finally { // 释放锁 reentrantLock.unlock(); } Copied! 可重入 可重入是指同一个线程如果首次获得了这把锁，那么因为它是这把锁的拥有者，因此有权利再次获取这把锁 如果是不可重入锁，那么第二次获得锁时，自己也会被锁挡住。\n1 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 static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { method1(); } public static void method1() { lock.lock(); try { log.debug(\u0026#34;execute method1\u0026#34;); method2(); } finally { lock.unlock(); } } public static void method2() { lock.lock(); try { log.debug(\u0026#34;execute method2\u0026#34;); method3(); } finally { lock.unlock(); } } public static void method3() { lock.lock(); try { log.debug(\u0026#34;execute method3\u0026#34;); } finally { lock.unlock(); } } Copied! 输出\n1 2 3 17:59:11.862 [main] c.TestReentrant - execute method1 17:59:11.865 [main] c.TestReentrant - execute method2 17:59:11.865 [main] c.TestReentrant - execute method3 Copied! 可打断 可打断指的是处于阻塞状态等待锁的线程可以被打断等待。注意lock.lockInterruptibly()和lock.trylock()方法是可打断的,lock.lock()不是。可打断的意义在于避免得不到锁的线程无限制地等待下去，防止死锁的一种方式。\n示例\n1 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 ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -\u0026gt; { log.debug(\u0026#34;启动...\u0026#34;); try { lock.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); log.debug(\u0026#34;等锁的过程中被打断\u0026#34;); return; } try { log.debug(\u0026#34;获得了锁\u0026#34;); } finally { lock.unlock(); } }, \u0026#34;t1\u0026#34;); lock.lock(); log.debug(\u0026#34;获得了锁\u0026#34;); t1.start(); try { sleep(1); t1.interrupt(); log.debug(\u0026#34;执行打断\u0026#34;); } finally { lock.unlock(); } Copied! 输出\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 18:02:40.520 [main] c.TestInterrupt - 获得了锁 18:02:40.524 [t1] c.TestInterrupt - 启动... 18:02:41.530 [main] c.TestInterrupt - 执行打断 java.lang.InterruptedException at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchr onizer.java:898) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchron izer.java:1222) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) at cn.itcast.n4.reentrant.TestInterrupt.lambda$main$0(TestInterrupt.java:17) at java.lang.Thread.run(Thread.java:748) 18:02:41.532 [t1] c.TestInterrupt - 等锁的过程中被打断 Copied! 注意如果是不可中断模式，那么即使使用了 interrupt 也不会让等待中断\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -\u0026gt; { log.debug(\u0026#34;启动...\u0026#34;); lock.lock(); try { log.debug(\u0026#34;获得了锁\u0026#34;); } finally { lock.unlock(); } }, \u0026#34;t1\u0026#34;); lock.lock(); log.debug(\u0026#34;获得了锁\u0026#34;); t1.start(); try { sleep(1); t1.interrupt(); log.debug(\u0026#34;执行打断\u0026#34;); sleep(1); } finally { log.debug(\u0026#34;释放了锁\u0026#34;); lock.unlock(); } Copied! 输出\n1 2 3 4 5 18:06:56.261 [main] c.TestInterrupt - 获得了锁 18:06:56.265 [t1] c.TestInterrupt - 启动... 18:06:57.266 [main] c.TestInterrupt - 执行打断 // 这时 t1 并没有被真正打断, 而是仍继续等待锁 18:06:58.267 [main] c.TestInterrupt - 释放了锁 18:06:58.267 [t1] c.TestInterrupt - 获得了锁 Copied! 锁超时 立刻失败\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -\u0026gt; { log.debug(\u0026#34;启动...\u0026#34;); if (!lock.tryLock()) { log.debug(\u0026#34;获取立刻失败，返回\u0026#34;); return; } try { log.debug(\u0026#34;获得了锁\u0026#34;); } finally { lock.unlock(); } }, \u0026#34;t1\u0026#34;); lock.lock(); log.debug(\u0026#34;获得了锁\u0026#34;); t1.start(); try { sleep(2); } finally { lock.unlock(); } Copied! 输出\n1 2 3 18:15:02.918 [main] c.TestTimeout - 获得了锁 18:15:02.921 [t1] c.TestTimeout - 启动... 18:15:02.921 [t1] c.TestTimeout - 获取立刻失败，返回 Copied! 超时失败\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -\u0026gt; { log.debug(\u0026#34;启动...\u0026#34;); try { if (!lock.tryLock(1, TimeUnit.SECONDS)) { log.debug(\u0026#34;获取等待 1s 后失败，返回\u0026#34;); return; } } catch (InterruptedException e) { e.printStackTrace(); } try { log.debug(\u0026#34;获得了锁\u0026#34;); } finally { lock.unlock(); } }, \u0026#34;t1\u0026#34;); lock.lock(); log.debug(\u0026#34;获得了锁\u0026#34;); t1.start(); try { sleep(2); } finally { lock.unlock(); } Copied! 输出\n1 2 3 18:19:40.537 [main] c.TestTimeout - 获得了锁 18:19:40.544 [t1] c.TestTimeout - 启动... 18:19:41.547 [t1] c.TestTimeout - 获取等待 1s 后失败，返回 Copied! 使用 tryLock 解决哲学家就餐问题\n1 2 3 4 5 6 7 8 9 10 class Chopstick extends ReentrantLock { String name; public Chopstick(String name) { this.name = name; } @Override public String toString() { return \u0026#34;筷子{\u0026#34; + name + \u0026#39;}\u0026#39;; } } Copied! 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 class Philosopher extends Thread { Chopstick left; Chopstick right; public Philosopher(String name, Chopstick left, Chopstick right) { super(name); this.left = left; this.right = right; } @Override public void run() { while (true) { // 尝试获得左手筷子 if (left.tryLock()) { try { // 尝试获得右手筷子 if (right.tryLock()) { try { eat(); } finally { right.unlock(); } } } finally { left.unlock(); } } } } private void eat() { log.debug(\u0026#34;eating...\u0026#34;); Sleeper.sleep(1); } } Copied! 公平锁 ReentrantLock 默认是不公平的\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ReentrantLock lock = new ReentrantLock(false); lock.lock(); for (int i = 0; i \u0026lt; 500; i++) { new Thread(() -\u0026gt; { lock.lock(); try { System.out.println(Thread.currentThread().getName() + \u0026#34; running...\u0026#34;); } finally { lock.unlock(); } }, \u0026#34;t\u0026#34; + i).start(); } // 1s 之后去争抢锁 Thread.sleep(1000); new Thread(() -\u0026gt; { System.out.println(Thread.currentThread().getName() + \u0026#34; start...\u0026#34;); lock.lock(); try { System.out.println(Thread.currentThread().getName() + \u0026#34; running...\u0026#34;); } finally { lock.unlock(); } }, \u0026#34;强行插入\u0026#34;).start(); lock.unlock(); Copied! 强行插入，有机会在中间输出\n注意：该实验不一定总能复现\n1 2 3 4 5 6 7 8 9 10 11 12 t39 running... t40 running... t41 running... t42 running... t43 running... 强行插入 start... 强行插入 running... t44 running... t45 running... t46 running... t47 running... t49 running... Copied! 改为公平锁后\n1 ReentrantLock lock = new ReentrantLock(true); Copied! 强行插入，总是在最后输出\n1 2 3 4 5 6 7 8 9 10 t465 running... t464 running... t477 running... t442 running... t468 running... t493 running... t482 running... t485 running... t481 running... 强行插入 running... Copied! 公平锁一般没有必要，会降低并发度，后面分析原理时会讲解\n条件变量 synchronized 中也有条件变量，就是我们讲原理时那个 waitSet 休息室，当条件不满足时进入 waitSet 等待\nReentrantLock 的条件变量比 synchronized 强大之处在于，它是支持多个条件变量的，这就好比\nsynchronized 是那些不满足条件的线程都在一间休息室等消息 而 ReentrantLock 支持多间休息室，有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤 醒 使用要点 await 前需要获得锁 await 执行后，会释放锁，进入 conditionObject 等待 await 的线程被唤醒（或打断、或超时）取重新竞争 lock 锁 竞争 lock 锁成功后，从 await 后继续执行 详细API 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 public interface Condition { void await() throws InterruptedException; void awaitUninterruptibly(); /* * \u0026lt;pre\u0026gt; {@code * boolean aMethod(long timeout, TimeUnit unit) { * long nanos = unit.toNanos(timeout); * lock.lock(); * try { * while (!conditionBeingWaitedFor()) { * if (nanos \u0026lt;= 0L) * return false; * nanos = theCondition.awaitNanos(nanos); * } * // ... * } finally { * lock.unlock(); * } * }}\u0026lt;/pre\u0026gt; * * @param nanosTimeout the maximum time to wait, in nanoseconds * @return an estimate of the {@code nanosTimeout} value minus * the time spent waiting upon return from this method. * A positive value may be used as the argument to a * subsequent call to this method to finish waiting out * the desired time. A value less than or equal to zero * indicates that no time remains. * @throws InterruptedException if the current thread is interrupted * (and interruption of thread suspension is supported) */ long awaitNanos(long nanosTimeout) throws InterruptedException; /** * Causes the current thread to wait until it is signalled or interrupted, * or the specified waiting time elapses. This method is behaviorally * equivalent to: * \u0026lt;pre\u0026gt; {@code awaitNanos(unit.toNanos(time)) \u0026gt; 0}\u0026lt;/pre\u0026gt; * * @param time the maximum time to wait * @param unit the time unit of the {@code time} argument * @return {@code false} if the waiting time detectably elapsed * before return from the method, else {@code true} * @throws InterruptedException if the current thread is interrupted * (and interruption of thread suspension is supported) */ boolean await(long time, TimeUnit unit) throws InterruptedException; /** * Causes the current thread to wait until it is signalled or interrupted, * or the specified deadline elapses. * * \u0026lt;pre\u0026gt; {@code * boolean aMethod(Date deadline) { * boolean stillWaiting = true; * lock.lock(); * try { * while (!conditionBeingWaitedFor()) { * if (!stillWaiting) * return false; * stillWaiting = theCondition.awaitUntil(deadline); * } * // ... * } finally { * lock.unlock(); * } * }}\u0026lt;/pre\u0026gt; * @param deadline the absolute time to wait until * @return {@code false} if the deadline has elapsed upon return, else * {@code true} * @throws InterruptedException if the current thread is interrupted * (and interruption of thread suspension is supported) */ boolean awaitUntil(Date deadline) throws InterruptedException; /** * Wakes up one waiting thread. */ void signal(); /** * Wakes up all waiting threads. */ void signalAll(); } Copied! 例子：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 static ReentrantLock lock = new ReentrantLock(); static Condition waitCigaretteQueue = lock.newCondition(); static Condition waitbreakfastQueue = lock.newCondition(); static volatile boolean hasCigrette = false; static volatile boolean hasBreakfast = false; public static void main(String[] args) { new Thread(() -\u0026gt; { try { lock.lock(); while (!hasCigrette) { try { waitCigaretteQueue.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug(\u0026#34;等到了它的烟\u0026#34;); } finally { lock.unlock(); } }).start(); new Thread(() -\u0026gt; { try { lock.lock(); while (!hasBreakfast) { try { waitbreakfastQueue.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug(\u0026#34;等到了它的早餐\u0026#34;); } finally { lock.unlock(); } }).start(); sleep(1); sendBreakfast(); sleep(1); sendCigarette(); } private static void sendCigarette() { lock.lock(); try { log.debug(\u0026#34;送烟来了\u0026#34;); hasCigrette = true; waitCigaretteQueue.signal(); } finally { lock.unlock(); } } private static void sendBreakfast() { lock.lock(); try { log.debug(\u0026#34;送早餐来了\u0026#34;); hasBreakfast = true; waitbreakfastQueue.signal(); } finally { lock.unlock(); } } Copied! 输出\n1 2 3 4 18:52:27.680 [main] c.TestCondition - 送早餐来了 18:52:27.682 [Thread-1] c.TestCondition - 等到了它的早餐 18:52:28.683 [main] c.TestCondition - 送烟来了 18:52:28.683 [Thread-0] c.TestCondition - 等到了它的烟 Copied! * 同步模式之顺序控制 固定运行顺序 比如，必须先 2 后 1 打印\nwait notify 版\n1 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 // 用来同步的对象 static Object obj = new Object(); // t2 运行标记， 代表 t2 是否执行过 static boolean t2runed = false; public static void main(String[] args) { Thread t1 = new Thread(() -\u0026gt; { synchronized (obj) { // 如果 t2 没有执行过 while (!t2runed) { try { // t1 先等一会 obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } System.out.println(1); }); Thread t2 = new Thread(() -\u0026gt; { System.out.println(2); synchronized (obj) { // 修改运行标记 t2runed = true; // 通知 obj 上等待的线程（可能有多个，因此需要用 notifyAll） obj.notifyAll(); } }); t1.start(); t2.start(); } Copied! Park Unpark 版\n可以看到，实现上很麻烦：\n首先，需要保证先 wait 再 notify，否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该 wait 第二，如果有些干扰线程错误地 notify 了 wait 线程，条件不满足时还要重新等待，使用了 while 循环来解决 此问题 最后，唤醒对象上的 wait 线程需要使用 notifyAll，因为『同步对象』上的等待线程可能不止一个 可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目：\n1 2 3 4 5 6 7 8 9 10 11 12 13 Thread t1 = new Thread(() -\u0026gt; { try { Thread.sleep(1000); } catch (InterruptedException e) { } // 当没有『许可』时，当前线程暂停运行；有『许可』时，用掉这个『许可』，当前线程恢复运行 LockSupport.park(); System.out.println(\u0026#34;1\u0026#34;); }); Thread t2 = new Thread(() -\u0026gt; { System.out.println(\u0026#34;2\u0026#34;); // 给线程 t1 发放『许可』（多次连续调用 unpark 只会发放一个『许可』） LockSupport.unpark(t1); }); t1.start(); t2.start(); Copied! park 和 unpark 方法比较灵活，他俩谁先调用，谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』， 不需要『同步对象』和『运行标记』\n交替输出 线程 1 输出 a 5 次，线程 2 输出 b 5 次，线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现\nwait notify 版\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class SyncWaitNotify { private int flag; private int loopNumber; public SyncWaitNotify(int flag, int loopNumber) { this.flag = flag; this.loopNumber = loopNumber; } public void print(int waitFlag, int nextFlag, String str) { for (int i = 0; i \u0026lt; loopNumber; i++) { synchronized (this) { while (this.flag != waitFlag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(str); flag = nextFlag; this.notifyAll(); } } } } Copied! 1 2 3 4 5 6 7 8 9 10 SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5); new Thread(() -\u0026gt; { syncWaitNotify.print(1, 2, \u0026#34;a\u0026#34;); }).start(); new Thread(() -\u0026gt; { syncWaitNotify.print(2, 3, \u0026#34;b\u0026#34;); }).start(); new Thread(() -\u0026gt; { syncWaitNotify.print(3, 1, \u0026#34;c\u0026#34;); }).start(); Copied! Lock 条件变量版\n1 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 class AwaitSignal extends ReentrantLock { public void start(Condition first) { this.lock(); try { log.debug(\u0026#34;start\u0026#34;); first.signal(); } finally { this.unlock(); } } public void print(String str, Condition current, Condition next) { for (int i = 0; i \u0026lt; loopNumber; i++) { this.lock(); try { current.await(); log.debug(str); next.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { this.unlock(); } } } // 循环次数 private int loopNumber; public AwaitSignal(int loopNumber) { this.loopNumber = loopNumber; } } Copied! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 AwaitSignal as = new AwaitSignal(5); Condition aWaitSet = as.newCondition(); Condition bWaitSet = as.newCondition(); Condition cWaitSet = as.newCondition(); new Thread(() -\u0026gt; { as.print(\u0026#34;a\u0026#34;, aWaitSet, bWaitSet); }).start(); new Thread(() -\u0026gt; { as.print(\u0026#34;b\u0026#34;, bWaitSet, cWaitSet); }).start(); new Thread(() -\u0026gt; { as.print(\u0026#34;c\u0026#34;, cWaitSet, aWaitSet); }).start(); as.start(aWaitSet); Copied! 注意\n该实现没有考虑 a，b，c 线程都就绪再开始\nPark Unpark 版\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class SyncPark { private int loopNumber; private Thread[] threads; public SyncPark(int loopNumber) { this.loopNumber = loopNumber; } public void setThreads(Thread... threads) { this.threads = threads; } public void print(String str) { for (int i = 0; i \u0026lt; loopNumber; i++) { LockSupport.park(); System.out.print(str); LockSupport.unpark(nextThread()); } } private Thread nextThread() { Thread current = Thread.currentThread(); int index = 0; for (int i = 0; i \u0026lt; threads.length; i++) { if(threads[i] == current) { index = i; break; } } if(index \u0026lt; threads.length - 1) { return threads[index+1]; } else { return threads[0]; } } public void start() { for (Thread thread : threads) { thread.start(); } LockSupport.unpark(threads[0]); } } SyncPark syncPark = new SyncPark(5); Thread t1 = new Thread(() -\u0026gt; { syncPark.print(\u0026#34;a\u0026#34;); }); Thread t2 = new Thread(() -\u0026gt; { syncPark.print(\u0026#34;b\u0026#34;); }); Thread t3 = new Thread(() -\u0026gt; { syncPark.print(\u0026#34;c\\n\u0026#34;); }); syncPark.setThreads(t1, t2, t3); syncPark.start(); Copied! 本章小结 本章我们需要重点掌握的是\n分析多线程访问共享资源时，哪些代码片段属于临界区 使用 synchronized 互斥解决临界区的线程安全问题 掌握 synchronized 锁对象语法 掌握 synchronzied 加载成员方法和静态方法语法 掌握 wait/notify 同步方法 使用 lock 互斥解决临界区的线程安全问题 掌握 lock 的使用细节：可打断、锁超时、公平锁、条件变量 学会分析变量的线程安全性、掌握常见线程安全类的使用 线程安全类的方法是原子性的，但方法之间的组合要具体分析。 了解线程活跃性问题：死锁、活锁、饥饿。 解决死锁、饥饿的方式：ReentranLock 应用方面 互斥：使用 synchronized 或 Lock 达到共享资源互斥效果 同步：使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果 原理方面 monitor、synchronized 、wait/notify 原理 synchronized 进阶原理 park \u0026amp; unpark 原理 模式方面 同步模式之保护性暂停 异步模式之生产者消费者 同步模式之顺序控制 5.共享模型之内存 5.1 Java 内存模型 JMM 即 Java Memory Model，它定义了主存、工作内存抽象概念，底层对应着 CPU 寄存器、缓存、硬件内存、 CPU 指令优化等。\nJMM的意义\n计算机硬件底层的内存结构过于复杂，JMM的意义在于避免程序员直接管理计算机底层内存，用一些关键字synchronized、volatile等可以方便的管理内存。 JMM 体现在以下几个方面\n原子性 - 保证指令不会受到线程上下文切换的影响 可见性 - 保证指令不会受 cpu 缓存的影响 有序性 - 保证指令不会受 cpu 指令并行优化的影响 5.2 可见性 退不出的循环 先来看一个现象，main 线程对 run 变量的修改对于 t 线程不可见，导致了 t 线程无法停止：\n1 2 3 4 5 6 7 8 9 10 11 static boolean run = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(()-\u0026gt;{ while(run){ // .... } }); t.start(); sleep(1); run = false; // 线程t不会如预想的停下来 } Copied! 为什么呢？分析一下：\n初始状态， t 线程刚开始从主内存读取了 run 的值到工作内存。 因为 t 线程要频繁从主内存中读取 run 的值，JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中， 减少对主存中 run 的访问，提高效率 1 秒之后，main 线程修改了 run 的值，并同步至主存，而 t 是从自己工作内存中的高速缓存中读取这个变量 的值，结果永远是旧值 解决方法 volatile（易变关键字）\n它可以用来修饰成员变量和静态成员变量，他可以避免线程从自己的工作缓存中查找变量的值，必须到主存中获取 它的值，线程操作 volatile 变量都是直接操作主存\n可见性 vs 原子性 前面例子体现的实际就是可见性，它保证的是在多个线程之间，一个线程对 volatile 变量的修改对另一个线程可 见， 不能保证原子性，仅用在一个写线程，多个读线程的情况： 上例从字节码理解是这样的：\n1 2 3 4 5 6 getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true getstatic run // 线程 t 获取 run true putstatic run // 线程 main 修改 run 为 false， 仅此一次 getstatic run // 线程 t 获取 run false Copied! 比较一下之前我们将线程安全时举的例子：两个线程一个 i++ 一个 i\u0026ndash; ，只能保证看到最新值，不能解决指令交错\n1 2 3 4 5 6 7 8 9 // 假设i的初始值为0 getstatic i // 线程2-获取静态变量i的值 线程内i=0 getstatic i // 线程1-获取静态变量i的值 线程内i=0 iconst_1 // 线程1-准备常量1 iadd // 线程1-自增 线程内i=1 putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1 iconst_1 // 线程2-准备常量1 isub // 线程2-自减 线程内i=-1 putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1 Copied! 注意\nsynchronized 语句块既可以保证代码块的原子性，也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作，性能相对更低 。\nJMM关于synchronized的两条规定：\n1）线程解锁前，必须把共享变量的最新值刷新到主内存中\n2）线程加锁时，将清空工作内存中共享变量的值，从而使用共享变量时需要从主内存中重新获取最新的值\n（注意：加锁与解锁需要是同一把锁）\n通过以上两点，可以看到synchronized能够实现可见性。同时，由于synchronized具有同步锁，所以它也具有原子性\n如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符，线程 t 也能正确看到 对 run 变量的修改了，想一想为什么？(println方法中有synchronized代码块保证了可见性)\nsynchronized关键字不能阻止指令重排，但在一定程度上能保证有序性（如果共享变量没有逃逸出同步代码块的话）。因为在单线程的情况下指令重排不影响结果，相当于保障了有序性。\n* 模式之两阶段终止 Two Phase Termination\n在一个线程 T1 中如何“优雅”终止线程 T2？这里的【优雅】指的是给 T2 一个料理后事的机会。\n1.错误思路 使用线程对象的 stop() 方法停止线程 stop 方法会真正杀死线程，如果这时线程锁住了共享资源，那么当它被杀死后就再也没有机会释放锁， 其它线程将永远无法获取锁 使用 System.exit(int) 方法停止线程 目的仅是停止一个线程，但这种做法会让整个程序都停止 2.两阶段终止模式 利用 isInterrupted\ninterrupt 可以打断正在执行的线程，无论这个线程是在 sleep，wait，还是正常运行\n1 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 class TPTInterrupt { private Thread thread; public void start(){ thread = new Thread(() -\u0026gt; { while(true) { Thread current = Thread.currentThread(); if(current.isInterrupted()) { log.debug(\u0026#34;料理后事\u0026#34;); break; } try { Thread.sleep(1000); log.debug(\u0026#34;将结果保存\u0026#34;); } catch (InterruptedException e) { //打断sleep线程会清除打断标记，所以要添加标记 current.interrupt(); } // 执行监控操作 } },\u0026#34;监控线程\u0026#34;); thread.start(); } public void stop() { thread.interrupt(); } } Copied! 调用\n1 2 3 4 5 TPTInterrupt t = new TPTInterrupt(); t.start(); Thread.sleep(3500); log.debug(\u0026#34;stop\u0026#34;); t.stop(); Copied! 结果\n1 2 3 4 5 11:49:42.915 c.TwoPhaseTermination [监控线程] - 将结果保存 11:49:43.919 c.TwoPhaseTermination [监控线程] - 将结果保存 11:49:44.919 c.TwoPhaseTermination [监控线程] - 将结果保存 11:49:45.413 c.TestTwoPhaseTermination [main] - stop 11:49:45.413 c.TwoPhaseTermination [监控线程] - 料理后事 Copied! 利用volatile修饰的停止标记 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 // 停止标记用 volatile 是为了保证该变量在多个线程之间的可见性 // 我们的例子中，即主线程把它修改为 true 对 t1 线程可见 class TPTVolatile { private Thread thread; private volatile boolean stop = false; public void start(){ thread = new Thread(() -\u0026gt; { while(true) { Thread current = Thread.currentThread(); if(stop) { log.debug(\u0026#34;料理后事\u0026#34;); break; } try { Thread.sleep(1000); log.debug(\u0026#34;将结果保存\u0026#34;); } catch (InterruptedException e) { } // 执行监控操作 } },\u0026#34;监控线程\u0026#34;); thread.start(); } public void stop() { stop = true; //让线程立即停止而不是等待sleep结束 thread.interrupt(); } } Copied! 调用\n1 2 3 4 5 TPTVolatile t = new TPTVolatile(); t.start(); Thread.sleep(3500); log.debug(\u0026#34;stop\u0026#34;); t.stop(); Copied! 结果\n1 2 3 4 5 11:54:52.003 c.TPTVolatile [监控线程] - 将结果保存 11:54:53.006 c.TPTVolatile [监控线程] - 将结果保存 11:54:54.007 c.TPTVolatile [监控线程] - 将结果保存 11:54:54.502 c.TestTwoPhaseTermination [main] - stop 11:54:54.502 c.TPTVolatile [监控线程] - 料理后事 Copied! * 模式之 Balking 1.定义 Balking （犹豫）模式用在一个线程发现另一个线程或本线程已经做了某一件相同的事，那么本线程就无需再做 了，直接结束返回\n2.实现 例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class MonitorService { // 用来表示是否已经有线程已经在执行启动了 private volatile boolean starting; public void start() { log.info(\u0026#34;尝试启动监控线程...\u0026#34;); synchronized (this) { if (starting) { return; } starting = true; } //其实synchronized外面还可以再套一层if，或者改为if(!starting)，if框后直接return // 真正启动监控线程... } } Copied! 当前端页面多次点击按钮调用 start 时\n输出\n1 2 3 4 5 [http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(false) [http-nio-8080-exec-1] cn.itcast.monitor.service.MonitorService - 监控线程已启动... [http-nio-8080-exec-2] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true) [http-nio-8080-exec-3] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true) [http-nio-8080-exec-4] cn.itcast.monitor.service.MonitorService - 该监控线程已启动?(true) Copied! 它还经常用来实现线程安全的单例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public final class Singleton { private Singleton() { } private static Singleton INSTANCE = null; public static synchronized Singleton getInstance() { if (INSTANCE != null) { return INSTANCE; } INSTANCE = new Singleton(); return INSTANCE; } } Copied! 对比一下保护性暂停模式：保护性暂停模式用在一个线程等待另一个线程的执行结果，当条件不满足时线程等待。\n5.3 有序性 JVM 会在不影响正确性的前提下，可以调整语句的执行顺序，思考下面一段代码\n1 2 3 4 5 static int i; static int j; // 在某个线程内执行如下赋值操作 i = ...; j = ...; Copied! 可以看到，至于是先执行 i 还是 先执行 j ，对最终的结果不会产生影响。所以，上面代码真正执行时，既可以是\n1 2 i = ...; j = ...; Copied! 也可以是\n1 2 j = ...; i = ...; Copied! 这种特性称之为『指令重排』，多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢？从 CPU 执行指令的原理来理解一下吧\n* 原理之指令级并行 名词 Clock Cycle Time\n主频的概念大家接触的比较多，而 CPU 的 Clock Cycle Time（时钟周期时间），等于主频的倒数，意思是 CPU 能 够识别的最小时间单位，比如说 4G 主频的 CPU 的 Clock Cycle Time 就是 0.25 ns，作为对比，我们墙上挂钟的 Cycle Time 是 1s\n例如，运行一条加法指令一般需要一个时钟周期时间\nCPI\n有的指令需要更多的时钟周期时间，所以引出了 CPI （Cycles Per Instruction）指令平均时钟周期数\nIPC\nIPC（Instruction Per Clock Cycle） 即 CPI 的倒数，表示每个时钟周期能够运行的指令数\nCPU 执行时间\n程序的 CPU 执行时间，即我们前面提到的 user + system 时间，可以用下面的公式来表示\n1 程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time Copied! 指令重排序优化 事实上，现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢？可以想到指令 还可以再划分成一个个更小的阶段，例如，每条指令都可以分为： 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段\n术语参考：\ninstruction fetch (IF) instruction decode (ID) execute (EX) memory access (MEM) register write back (WB) 在不改变程序结果的前提下，这些指令的各个阶段可以通过重排序和组合来实现指令级并行，这一技术在 80\u0026rsquo;s 中 叶到 90\u0026rsquo;s 中叶占据了计算架构的重要地位。\n提示：\n分阶段，分工是提升效率的关键！\n指令重排的前提是，重排指令不能影响结果，例如\n1 2 3 4 5 6 7 // 可以重排的例子 int a = 10; // 指令1 int b = 20; // 指令2 System.out.println( a + b ); // 不能重排的例子 int a = 10; // 指令1 int b = a - 5; // 指令2 Copied! 参考：\nScoreboarding and the Tomasulo algorithm (which is similar to scoreboarding but makes use of register renaming )are two of the most common techniques for implementing out-of-order execution and instruction-level parallelism.\n支持流水线的处理器 现代 CPU 支持多级指令流水线，例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理 器，就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内，同时运行五条指令的不同阶段（相当于一 条执行时间最长的复杂指令），IPC = 1，本质上，流水线技术并不能缩短单条指令的执行时间，但它变相地提高了 指令地吞吐率。\n提示：\n奔腾四（Pentium 4）支持高达 35 级流水线，但由于功耗太高被废弃\nSuperScalar 处理器 大多数处理器包含多个执行单元，并不是所有计算功能都集中在一起，可以再细分为整数运算单元、浮点数运算单 元等，这样可以把多条指令也可以做到并行获取、译码等，CPU 可以在一个时钟周期内，执行多于一条指令，IPC \u0026gt; 1\n诡异的结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int num = 0; boolean ready = false; // 线程1 执行此方法 public void actor1(I_Result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } // 线程2 执行此方法 public void actor2(I_Result r) { num = 2; ready = true; } Copied! I_Result 是一个对象，有一个属性 r1 用来保存结果，问，可能的结果有几种？\n有同学这么分析\n情况1：线程1 先执行，这时 ready = false，所以进入 else 分支结果为 1\n情况2：线程2 先执行 num = 2，但没来得及执行 ready = true，线程1 执行，还是进入 else 分支，结果为1\n情况3：线程2 执行到 ready = true，线程1 执行，这回进入 if 分支，结果为 4（因为 num 已经执行过了）\n但我告诉你，结果还有可能是 0 😁😁😁，信不信吧！\n这种情况下是：线程2 执行 ready = true，切换到线程1，进入 if 分支，相加为 0，再切回线程2 执行 num = 2\n相信很多人已经晕了 😵😵😵\n这种现象叫做指令重排，是 JIT 编译器在运行时的一些优化，这个现象需要通过大量测试才能复现：\n借助 java 并发压测工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress 1 2 3 mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress - DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast - DartifactId=ordering -Dversion=1.0 Copied! 创建 maven 项目，提供如下测试类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @JCStressTest @Outcome(id = {\u0026#34;1\u0026#34;, \u0026#34;4\u0026#34;}, expect = Expect.ACCEPTABLE, desc = \u0026#34;ok\u0026#34;) @Outcome(id = \u0026#34;0\u0026#34;, expect = Expect.ACCEPTABLE_INTERESTING, desc = \u0026#34;!!!!\u0026#34;) @State public class ConcurrencyTest { int num = 0; boolean ready = false; @Actor public void actor1(I_Result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } @Actor public void actor2(I_Result r) { num = 2; ready = true; } } Copied! 执行\n1 2 mvn clean install java -jar target/jcstress.jar Copied! 会输出我们感兴趣的结果，摘录其中一次结果：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 *** INTERESTING tests Some interesting behaviors observed. This is for the plain curiosity. 2 matching test results. [OK] test.ConcurrencyTest (JVM args: [-XX:-TieredCompilation]) Observed state Occurrences Expectation Interpretation 0 1,729 ACCEPTABLE_INTERESTING !!!! 1 42,617,915 ACCEPTABLE ok 4 5,146,627 ACCEPTABLE ok [OK] test.ConcurrencyTest (JVM args: []) Observed state Occurrences Expectation Interpretation 0 1,652 ACCEPTABLE_INTERESTING !!!! 1 46,460,657 ACCEPTABLE ok 4 4,571,072 ACCEPTABLE ok Copied! 可以看到，出现结果为 0 的情况有 638 次，虽然次数相对很少，但毕竟是出现了。\n解决方法 volatile 修饰的变量，可以禁用指令重排\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @JCStressTest @Outcome(id = {\u0026#34;1\u0026#34;, \u0026#34;4\u0026#34;}, expect = Expect.ACCEPTABLE, desc = \u0026#34;ok\u0026#34;) @Outcome(id = \u0026#34;0\u0026#34;, expect = Expect.ACCEPTABLE_INTERESTING, desc = \u0026#34;!!!!\u0026#34;) @State public class ConcurrencyTest { int num = 0; volatile boolean ready = false; @Actor public void actor1(I_Result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } @Actor public void actor2(I_Result r) { num = 2; ready = true; } } Copied! 结果为：\n1 2 3 *** INTERESTING tests Some interesting behaviors observed. This is for the plain curiosity. 0 matching test results. Copied! * 原理之 volatile volatile 的底层实现原理是内存屏障，Memory Barrier（Memory Fence）\n对 volatile 变量的写指令后会加入写屏障 对 volatile 变量的读指令前会加入读屏障 如何保证可见性 写屏障（sfence）保证在该屏障之前的，对共享变量的改动，都同步到主存当中\n1 2 3 4 5 public void actor2(I_Result r) { num = 2; ready = true; // ready 是 volatile 赋值带写屏障 // 写屏障 } Copied! 而读屏障（lfence）保证在该屏障之后，对共享变量的读取，加载的是主存中最新数据\n1 2 3 4 5 6 7 8 9 public void actor1(I_Result r) { // 读屏障 // ready 是 volatile 读取值带读屏障 if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } Copied! 如何保证有序性 写屏障会确保指令重排序时，不会将写屏障之前的代码排在写屏障之后\n1 2 3 4 5 public void actor2(I_Result r) { num = 2; ready = true; // ready 是 volatile 赋值带写屏障 // 写屏障 } Copied! 读屏障会确保指令重排序时，不会将读屏障之后的代码排在读屏障之前\n1 2 3 4 5 6 7 8 9 public void actor1(I_Result r) { // 读屏障 // ready 是 volatile 读取值带读屏障 if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } Copied! 还是那句话，不能解决指令交错：\n写屏障仅仅是保证之后的读能够读到最新的结果，但不能保证读跑到它前面去 而有序性的保证也只是保证了本线程内相关代码不被重排序 double-checked locking 问题 以著名的 double-checked locking 单例模式为例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public final class Singleton { private Singleton() { } private static Singleton INSTANCE = null; public static Singleton getInstance() { if(INSTANCE == null) { // t2 // 首次访问会同步，而之后的使用没有 synchronized synchronized(Singleton.class) { if (INSTANCE == null) { // t1 INSTANCE = new Singleton(); } } } return INSTANCE; } } Copied! 以上的实现特点是：\n懒惰实例化 首次使用 getInstance() 才使用 synchronized 加锁，后续使用时无需加锁 有隐含的，但很关键的一点：第一个 if 使用了 INSTANCE 变量，是在同步块之外 但在多线程环境下，上面的代码是有问题的，getInstance 方法对应的字节码为：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 3: ifnonnull 37 6: ldc #3 // class cn/itcast/n5/Singleton 8: dup 9: astore_0 10: monitorenter 11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 14: ifnonnull 27 17: new #3 // class cn/itcast/n5/Singleton 20: dup 21: invokespecial #4 // Method \u0026#34;\u0026lt;init\u0026gt;\u0026#34;:()V 24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 27: aload_0 28: monitorexit 29: goto 37 32: astore_1 33: aload_0 34: monitorexit 35: aload_1 36: athrow 37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 40: areturn Copied! 其中\n17 表示创建对象，将对象引用入栈 // new Singleton 20 表示复制一份对象引用 // 引用地址 21 表示利用一个对象引用，调用构造方法 24 表示利用一个对象引用，赋值给 static INSTANCE 也许 jvm 会优化为：先执行 24，再执行 21。如果两个线程 t1，t2 按如下时间序列执行：\n关键在于 0: getstatic 这行代码在 monitor 控制之外，它就像之前举例中不守规则的人，可以越过 monitor 读取 INSTANCE 变量的值\n这时 t1 还未完全将构造方法执行完毕，如果在构造方法中要执行很多初始化操作，那么 t2 拿到的是将是一个未初 始化完毕的单例\n对 INSTANCE 使用 volatile 修饰即可，可以禁用指令重排，但要注意在 JDK 5 以上的版本的 volatile 才会真正有效\ndouble-checked locking 解决 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public final class Singleton { private Singleton() { } private static volatile Singleton INSTANCE = null; public static Singleton getInstance() { // 实例没创建，才会进入内部的 synchronized代码块 if (INSTANCE == null) { synchronized (Singleton.class) { // t2 // 也许有其它线程已经创建实例，所以再判断一次 if (INSTANCE == null) { // t1 INSTANCE = new Singleton(); } } } return INSTANCE; } } Copied! 字节码上看不出来 volatile 指令的效果\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // -------------------------------------\u0026gt; 加入对 INSTANCE 变量的读屏障 0: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 3: ifnonnull 37 6: ldc #3 // class cn/itcast/n5/Singleton 8: dup 9: astore_0 10: monitorenter -----------------------\u0026gt; 保证原子性、可见性 11: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 14: ifnonnull 27 17: new #3 // class cn/itcast/n5/Singleton 20: dup 21: invokespecial #4 // Method \u0026#34;\u0026lt;init\u0026gt;\u0026#34;:()V 24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; // -------------------------------------\u0026gt; 加入对 INSTANCE 变量的写屏障 27: aload_0 28: monitorexit ------------------------\u0026gt; 保证原子性、可见性 29: goto 37 32: astore_1 33: aload_0 34: monitorexit 35: aload_1 36: athrow 37: getstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton; 40: areturn Copied! 如上面的注释内容所示，读写 volatile 变量时会加入内存屏障（Memory Barrier（Memory Fence）），保证下面 两点：\n可见性 写屏障（sfence）保证在该屏障之前的 t1 对共享变量的改动，都同步到主存当中 而读屏障（lfence）保证在该屏障之后 t2 对共享变量的读取，加载的是主存中最新数据 有序性 写屏障会确保指令重排序时，不会将写屏障之前的代码排在写屏障之后 读屏障会确保指令重排序时，不会将读屏障之后的代码排在读屏障之前 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性 happens-before happens-before 规定了对共享变量的写操作对其它线程的读操作可见，它是可见性与有序性的一套规则总结，抛 开以下 happens-before 规则，JMM 并不能保证一个线程对共享变量的写，对于其它线程对该共享变量的读可见\n线程解锁 m 之前对变量的写，对于接下来对 m 加锁的其它线程对该变量的读可见(synchronized关键字的可见性、监视器规则)\n1 2 3 4 5 6 7 8 9 10 11 12 static int x; static Object m = new Object(); new Thread(()-\u0026gt;{ synchronized(m) { x = 10; } },\u0026#34;t1\u0026#34;).start(); new Thread(()-\u0026gt;{ synchronized(m) { System.out.println(x); } },\u0026#34;t2\u0026#34;).start(); Copied! 线程对 volatile 变量的写，对接下来其它线程对该变量的读可见(volatile关键字的可见性、volatile规则)\n1 2 3 4 5 6 7 volatile static int x; new Thread(()-\u0026gt;{ x = 10; },\u0026#34;t1\u0026#34;).start(); new Thread(()-\u0026gt;{ System.out.println(x); },\u0026#34;t2\u0026#34;).start(); Copied! 线程 start 前对变量的写，对该线程开始后对该变量的读可见(程序顺序规则+线程启动规则)\n1 2 3 4 5 static int x; x = 10; new Thread(()-\u0026gt;{ System.out.println(x); },\u0026#34;t2\u0026#34;).start(); Copied! 线程结束前对变量的写，对其它线程得知它结束后的读可见（比如其它线程调用 t1.isAlive() 或 t1.join()等待 它结束）(线程终止规则)\n1 2 3 4 5 6 7 static int x; Thread t1 = new Thread(()-\u0026gt;{ x = 10; },\u0026#34;t1\u0026#34;); t1.start(); t1.join(); System.out.println(x); Copied! 线程 t1 打断 t2（interrupt）前对变量的写，对于其他线程得知 t2 被打断后对变量的读可见（通过 t2.interrupted 或 t2.isInterrupted）（线程中断机制）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static int x; public static void main(String[] args) { Thread t2 = new Thread(()-\u0026gt;{ while(true) { if(Thread.currentThread().isInterrupted()) { System.out.println(x); break; } } },\u0026#34;t2\u0026#34;); t2.start(); new Thread(()-\u0026gt;{ sleep(1); x = 10; t2.interrupt(); },\u0026#34;t1\u0026#34;).start(); while(!t2.isInterrupted()) { Thread.yield(); } System.out.println(x); } Copied! 对变量默认值（0，false，null）的写，对其它线程对该变量的读可见\n具有传递性，如果 x hb-\u0026gt; y 并且 y hb-\u0026gt; z 那么有 x hb-\u0026gt; z ，配合 volatile 的防指令重排，有下面的例子\n1 2 3 4 5 6 7 8 9 10 volatile static int x; static int y; new Thread(()-\u0026gt;{ y = 10; x = 20; },\u0026#34;t1\u0026#34;).start(); new Thread(()-\u0026gt;{ // x=20 对 t2 可见, 同时 y=10 也对 t2 可见 System.out.println(x); },\u0026#34;t2\u0026#34;).start(); Copied! 变量都是指成员变量或静态成员变量\n参考： 第17页\n在JMM中有一个很重要的概念对于我们了解JMM有很大的帮助，那就是happens-before规则。happens-before规则非常重要，它是判断数据是否存在竞争、线程是否安全的主要依据。JSR-133S使用happens-before概念阐述了两个操作之间的内存可见性。在JMM中，如果一个操作的结果需要对另一个操作可见，那么这两个操作则存在happens-before关系。\n那什么是happens-before呢？在JSR-133中，happens-before关系定义如下：\n如果一个操作happens-before另一个操作，那么意味着第一个操作的结果对第二个操作可见，而且第一个操作的执行顺序将排在第二个操作的前面。 两个操作之间存在happens-before关系，并不意味着Java平台的具体实现必须按照happens-before关系指定的顺序来执行。如果重排序之后的结果，与按照happens-before关系来执行的结果一致，那么这种重排序并不非法（也就是说，JMM允许这种重排序） happens-before规则如下：\n程序顺序规则：一个线程中的每一个操作，happens-before于该线程中的任意后续操作。 监视器规则：对一个锁的解锁，happens-before于随后对这个锁的加锁。 volatile规则：对一个volatile变量的写，happens-before于任意后续对一个volatile变量的读。 传递性：若果A happens-before B，B happens-before C，那么A happens-before C。 线程启动规则：Thread对象的start()方法，happens-before于这个线程的任意后续操作。 线程终止规则：线程中的任意操作，happens-before于该线程的终止监测。我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。 线程中断操作：对线程interrupt()方法的调用，happens-before于被中断线程的代码检测到中断事件的发生，可以通过Thread.interrupted()方法检测到线程是否有中断发生。 对象终结规则：一个对象的初始化完成，happens-before于这个对象的finalize()方法的开始。 参考链接：happens-before规则解析 - 知乎 (zhihu.com) 习题 balking 模式习题 希望 doInit() 方法仅被调用一次，下面的实现是否有问题，为什么？\n1 2 3 4 5 6 7 8 9 10 11 12 public class TestVolatile { volatile boolean initialized = false; void init() { if (initialized) { return; } doInit(); initialized = true; } private void doInit() { } } Copied! 线程安全单例习题 单例模式有很多实现方法，饿汉、懒汉、静态内部类、枚举类，试分析每种实现下获取单例对象（即调用 getInstance）时的线程安全，并思考注释中的问题\n饿汉式：类加载就会导致该单实例对象被创建\n懒汉式：类加载不会导致该单实例对象被创建，而是首次使用该对象时才会创建\n实现1(饿汉式)：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 问题1：为什么加 final(防止被子类继承从而重写方法改写单例) // 问题2：如果实现了序列化接口, 还要做什么来防止反序列化破坏单例(重写readResolve方法) public final class Singleton implements Serializable { // 问题3：为什么设置为私有? 是否能防止反射创建新的实例?(防止外部调用构造方法创建多个实例；不能) private Singleton() {} // 问题4：这样初始化是否能保证单例对象创建时的线程安全?(能，线程安全性由类加载器保障) private static final Singleton INSTANCE = new Singleton(); // 问题5：为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由(可以保证instance的安全性，也能方便实现一些附加逻辑) public static Singleton getInstance() { return INSTANCE; } public Object readResolve() { return INSTANCE; } } Copied! 实现2(枚举类)：\n1 2 3 4 5 6 7 8 9 // 问题1：枚举单例是如何限制实例个数的 (枚举类会按照声明的个数在类加载时实例化对象) // 问题2：枚举单例在创建时是否有并发问题(没有，由类加载器保障安全性) // 问题3：枚举单例能否被反射破坏单例(不能) // 问题4：枚举单例能否被反序列化破坏单例(不能) // 问题5：枚举单例属于懒汉式还是饿汉式(饿汉) // 问题6：枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做(写构造方法) enum Singleton { INSTANCE; } Copied! 实现3(synchronized方法)：\n1 2 3 4 5 6 7 8 9 10 11 12 public final class Singleton { private Singleton() { } private static Singleton INSTANCE = null; // 分析这里的线程安全, 并说明有什么缺点(没有线程安全问题，同步代码块粒度太大，性能差) public static synchronized Singleton getInstance() { if( INSTANCE != null ){ return INSTANCE; } INSTANCE = new Singleton(); return INSTANCE; } } Copied! 实现4：DCL+volatile\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public final class Singleton { private Singleton() { } // 问题1：解释为什么要加 volatile ?(防止putstatic和invokespecial重排导致的异常) private static volatile Singleton INSTANCE = null; // 问题2：对比实现3, 说出这样做的意义 (缩小了锁的粒度，提高了性能) public static Singleton getInstance() { if (INSTANCE != null) { return INSTANCE; } synchronized (Singleton.class) { // 问题3：为什么还要在这里加为空判断, 之前不是判断过了吗 if (INSTANCE != null) { // t2 return INSTANCE; } INSTANCE = new Singleton(); return INSTANCE; } } } Copied! 实现5(内部类初始化)：\n1 2 3 4 5 6 7 8 9 10 11 public final class Singleton { private Singleton() { } // 问题1：属于懒汉式还是饿汉式 private static class LazyHolder { static final Singleton INSTANCE = new Singleton(); } // 问题2：在创建时是否有并发问题 public static Singleton getInstance() { return LazyHolder.INSTANCE; } } Copied! 本章小结 本章重点讲解了 JMM 中的\n可见性 - 由 JVM 缓存优化引起 有序性 - 由 JVM 指令重排序优化引起 happens-before 规则 原理方面 CPU 指令并行 volatile 模式方面 两阶段终止模式的 volatile 改进 同步模式之 balking 6.共享模型之无锁 6.1 问题提出 (应用之互斥) 有如下需求，保证 account.withdraw 取款方法的线程安全\n1 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 package cn.itcast; import java.util.ArrayList; import java.util.List; interface Account { // 获取余额 Integer getBalance(); // 取款 void withdraw(Integer amount); /** * 方法内会启动 1000 个线程，每个线程做 -10 元 的操作 * 如果初始余额为 10000 那么正确的结果应当是 0 */ static void demo(Account account) { List\u0026lt;Thread\u0026gt; ts = new ArrayList\u0026lt;\u0026gt;(); long start = System.nanoTime(); for (int i = 0; i \u0026lt; 1000; i++) { ts.add(new Thread(() -\u0026gt; { account.withdraw(10); })); } ts.forEach(Thread::start); ts.forEach(t -\u0026gt; { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println(account.getBalance() + \u0026#34; cost: \u0026#34; + (end-start)/1000_000 + \u0026#34; ms\u0026#34;); } } Copied! 原有实现并不是线程安全的\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 class AccountUnsafe implements Account { private Integer balance; public AccountUnsafe(Integer balance) { this.balance = balance; } @Override public Integer getBalance() { return balance; } @Override public void withdraw(Integer amount) { balance -= amount; } } Copied! 执行测试代码\n1 2 3 public static void main(String[] args) { Account.demo(new AccountUnsafe(10000)); } Copied! 某次的执行结果\n1 330 cost: 306 ms Copied! 为什么不安全 withdraw 方法\n1 2 3 public void withdraw(Integer amount) { balance -= amount; } Copied! 对应的字节码\n1 2 3 4 5 6 7 8 9 ALOAD 0 // \u0026lt;- this ALOAD 0 GETFIELD cn/itcast/AccountUnsafe.balance : Ljava/lang/Integer; // \u0026lt;- this.balance INVOKEVIRTUAL java/lang/Integer.intValue ()I // 拆箱 ALOAD 1 // \u0026lt;- amount INVOKEVIRTUAL java/lang/Integer.intValue ()I // 拆箱 ISUB // 减法 INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; // 结果装箱 PUTFIELD cn/itcast/AccountUnsafe.balance : Ljava/lang/Integer; // -\u0026gt; this.balance Copied! 多线程执行\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ALOAD 0 // thread-0 \u0026lt;- this ALOAD 0 GETFIELD cn/itcast/AccountUnsafe.balance // thread-0 \u0026lt;- this.balance INVOKEVIRTUAL java/lang/Integer.intValue // thread-0 拆箱 ALOAD 1 // thread-0 \u0026lt;- amount INVOKEVIRTUAL java/lang/Integer.intValue // thread-0 拆箱 ISUB // thread-0 减法 INVOKESTATIC java/lang/Integer.valueOf // thread-0 结果装箱 PUTFIELD cn/itcast/AccountUnsafe.balance // thread-0 -\u0026gt; this.balance ALOAD 0 // thread-1 \u0026lt;- this ALOAD 0 GETFIELD cn/itcast/AccountUnsafe.balance // thread-1 \u0026lt;- this.balance INVOKEVIRTUAL java/lang/Integer.intValue // thread-1 拆箱 ALOAD 1 // thread-1 \u0026lt;- amount INVOKEVIRTUAL java/lang/Integer.intValue // thread-1 拆箱 ISUB // thread-1 减法 INVOKESTATIC java/lang/Integer.valueOf // thread-1 结果装箱 PUTFIELD cn/itcast/AccountUnsafe.balance // thread-1 -\u0026gt; this.balance Copied! 原因：Integer虽然是不可变类，其方法是线程安全的，但是以上操作涉及到了多个方法的组合，等价于以下代码：\nbalance = new Integer(Integer.valueOf(balance) - amount);\n前一个方法(valueOf)的结果决定后一个方法(构造方法)，这种组合在多线程环境下线程不安全。\n解决思路-锁（悲观互斥） 首先想到的是给 Account 对象加锁\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 class AccountUnsafe implements Account { private Integer balance; public AccountUnsafe(Integer balance) { this.balance = balance; } @Override public synchronized Integer getBalance() { return balance; } @Override public synchronized void withdraw(Integer amount) { balance -= amount; } } Copied! 结果为\n1 0 cost: 399 ms Copied! 解决思路-无锁（乐观重试） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class AccountSafe implements Account { private AtomicInteger balance; public AccountSafe(Integer balance) { this.balance = new AtomicInteger(balance); } @Override public Integer getBalance() { return balance.get(); } @Override public void withdraw(Integer amount) { while (true) { int prev = balance.get(); int next = prev - amount; if (balance.compareAndSet(prev, next)) { break; } } // 可以简化为下面的方法 // balance.addAndGet(-1 * amount); } } Copied! 执行测试代码\n1 2 3 public static void main(String[] args) { Account.demo(new AccountSafe(10000)); } Copied! 某次的执行结果\n1 0 cost: 302 ms Copied! 6.2 CAS 与 volatile 前面看到的 AtomicInteger 的解决方法，内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void withdraw(Integer amount) { while(true) { // 需要不断尝试，直到成功为止 while (true) { // 比如拿到了旧值 1000 int prev = balance.get(); // 在这个基础上 1000-10 = 990 int next = prev - amount; /* compareAndSet 正是做这个检查，在 set 前，先比较 prev 与当前值 - 不一致了，next 作废，返回 false 表示失败 比如，别的线程已经做了减法，当前值已经被减成了 990 那么本线程的这次 990 就作废了，进入 while 下次循环重试 - 一致，以 next 设置为新值，返回 true 表示成功 */ if (balance.compareAndSet(prev, next)) { break; } //或者简洁一点： //balance.getAndAdd(-1 * amount); } } } Copied! 其中的关键是 compareAndSet，它的简称就是 CAS （也有 Compare And Swap 的说法），它必须是原子操作。\n注意\n其实 CAS 的底层是 lock cmpxchg 指令（X86 架构），在单核 CPU 和多核 CPU 下都能够保证【比较-交 换】的原子性。\n在多核状态下，某个核执行到带 lock 的指令时，CPU 会让总线锁住，当这个核把此指令执行完毕，再 开启总线。这个过程中不会被线程的调度机制所打断，保证了多个线程对内存操作的准确性，是原子 的。\n慢动作分析 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 @Slf4j public class SlowMotion { public static void main(String[] args) { AtomicInteger balance = new AtomicInteger(10000); int mainPrev = balance.get(); log.debug(\u0026#34;try get {}\u0026#34;, mainPrev); new Thread(() -\u0026gt; { sleep(1000); int prev = balance.get(); balance.compareAndSet(prev, 9000); log.debug(balance.toString()); }, \u0026#34;t1\u0026#34;).start(); sleep(2000); log.debug(\u0026#34;try set 8000...\u0026#34;); boolean isSuccess = balance.compareAndSet(mainPrev, 8000); log.debug(\u0026#34;is success ? {}\u0026#34;, isSuccess); if(!isSuccess){ mainPrev = balance.get(); log.debug(\u0026#34;try set 8000...\u0026#34;); isSuccess = balance.compareAndSet(mainPrev, 8000); log.debug(\u0026#34;is success ? {}\u0026#34;, isSuccess); } } private static void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } } Copied! 输出结果\n1 2 3 4 5 6 2019-10-13 11:28:37.134 [main] try get 10000 2019-10-13 11:28:38.154 [t1] 9000 2019-10-13 11:28:39.154 [main] try set 8000... 2019-10-13 11:28:39.154 [main] is success ? false 2019-10-13 11:28:39.154 [main] try set 8000... 2019-10-13 11:28:39.154 [main] is success ? true Copied! volatile 获取共享变量时，为了保证该变量的可见性，需要使用 volatile 修饰。\n它可以用来修饰成员变量和静态成员变量，他可以避免线程从自己的工作缓存中查找变量的值，必须到主存中获取 它的值，线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改，对另一个线程可见。\n注意\nvolatile 仅仅保证了共享变量的可见性，让其它线程能够看到最新值，但不能解决指令交错问题（不能保证原 子性）\nCAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果。\n为什么无锁效率高\n无锁情况下，即使重试失败，线程始终在高速运行，没有停歇，类似于自旋。而 synchronized 会让线程在没有获得锁的时候，发生上下文切换，进入阻塞。线程的上下文切换是费时的，在重试次数不是太多时，无锁的效率高于有锁。 线程就好像高速跑道上的赛车，高速运行时，速度超快，一旦发生上下文切换，就好比赛车要减速、熄火， 等被唤醒又得重新打火、启动、加速\u0026hellip; 恢复到高速运行，代价比较大 但无锁情况下，因为线程要保持运行，需要额外 CPU 的支持，CPU 在这里就好比高速跑道，没有额外的跑 道，线程想高速运行也无从谈起，虽然不会进入阻塞，但由于没有分到时间片，仍然会进入可运行状态，还 是会导致上下文切换。所以总的来说，当线程数小于等于cpu核心数时，使用无锁方案是很合适的，因为有足够多的cpu让线程运行。当线程数远多于cpu核心数时，无锁效率相比于有锁就没有太大优势，因为依旧会发生上下文切换。 CAS 的特点 结合 CAS 和 volatile 可以实现无锁并发，适用于线程数少、多核 CPU 的场景下。\nCAS 是基于乐观锁的思想：最乐观的估计，不怕别的线程来修改共享变量，就算改了也没关系，我吃亏点再 重试呗。 synchronized 是基于悲观锁的思想：最悲观的估计，得防着其它线程来修改共享变量，我上了锁你们都别想 改，我改完了解开锁，你们才有机会。 CAS 体现的是无锁并发、无阻塞并发，请仔细体会这两句话的意思 因为没有使用 synchronized，所以线程不会陷入阻塞，这是效率提升的因素之一 但如果竞争激烈，可以想到重试必然频繁发生，反而效率会受影响 6.3 原子整数 J.U.C 并发包提供了：\nAtomicBoolean AtomicInteger AtomicLong 以 AtomicInteger 为例\n1 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 AtomicInteger i = new AtomicInteger(0); // 获取并自增（i = 0, 结果 i = 1, 返回 0），类似于 i++ System.out.println(i.getAndIncrement()); // 自增并获取（i = 1, 结果 i = 2, 返回 2），类似于 ++i System.out.println(i.incrementAndGet()); // 自减并获取（i = 2, 结果 i = 1, 返回 1），类似于 --i System.out.println(i.decrementAndGet()); // 获取并自减（i = 1, 结果 i = 0, 返回 1），类似于 i-- System.out.println(i.getAndDecrement()); // 获取并加值（i = 0, 结果 i = 5, 返回 0） System.out.println(i.getAndAdd(5)); // 加值并获取（i = 5, 结果 i = 0, 返回 0） System.out.println(i.addAndGet(-5)); // 获取并更新（i = 0, p 为 i 的当前值, 结果 i = -2, 返回 0） // 其中函数中的操作能保证原子，但函数需要无副作用 System.out.println(i.getAndUpdate(p -\u0026gt; p - 2)); // 更新并获取（i = -2, p 为 i 的当前值, 结果 i = 0, 返回 0） // 其中函数中的操作能保证原子，但函数需要无副作用 System.out.println(i.updateAndGet(p -\u0026gt; p + 2)); // 获取并计算（i = 0, p 为 i 的当前值, x 为参数1, 结果 i = 10, 返回 0） // 其中函数中的操作能保证原子，但函数需要无副作用 // getAndUpdate 如果在 lambda 中引用了外部的局部变量，要保证该局部变量是 final 的 // getAndAccumulate 可以通过 参数1 来引用外部的局部变量，但因为其不在 lambda 中因此不必是 final System.out.println(i.getAndAccumulate(10, (p, x) -\u0026gt; p + x)); // 计算并获取（i = 10, p 为 i 的当前值, x 为参数1, 结果 i = 0, 返回 0） // 其中函数中的操作能保证原子，但函数需要无副作用 System.out.println(i.accumulateAndGet(-10, (p, x) -\u0026gt; p + x)); Copied! 说明：\n以上方法都是以CAS为基础进行了封装，保证了方法的原子性和变量的可见性。\nupdateAndGet方法的手动实现：\n1 2 3 4 5 6 7 8 9 public static int updateAndGet(AtomicInteger i, IntUnaryOperator operator){ while (true){ int prev = i.get(); int next = operator.applyAsInt(prev); if(i.compareAndSet(prev,next)){ return next; } } } Copied! 6.4 原子引用 为什么需要原子引用类型？\nAtomicReference AtomicMarkableReference AtomicStampedReference 实际开发的过程中我们使用的不一定是int、long等基本数据类型，也有可能时BigDecimal这样的类型，这时就需要用到原子引用作为容器。原子引用设置值使用的是unsafe.compareAndSwapObject()方法。原子引用中表示数据的类型需要重写equals()方法。\n有如下方法\n1 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 public interface DecimalAccount { // 获取余额 BigDecimal getBalance(); // 取款 void withdraw(BigDecimal amount); /** * 方法内会启动 1000 个线程，每个线程做 -10 元 的操作 * 如果初始余额为 10000 那么正确的结果应当是 0 */ static void demo(DecimalAccount account) { List\u0026lt;Thread\u0026gt; ts = new ArrayList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; 1000; i++) { ts.add(new Thread(() -\u0026gt; { account.withdraw(BigDecimal.TEN); })); } ts.forEach(Thread::start); ts.forEach(t -\u0026gt; { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(account.getBalance()); } } Copied! 试着提供不同的 DecimalAccount 实现，实现安全的取款操作\n不安全实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class DecimalAccountUnsafe implements DecimalAccount { BigDecimal balance; public DecimalAccountUnsafe(BigDecimal balance) { this.balance = balance; } @Override public BigDecimal getBalance() { return balance; } @Override public void withdraw(BigDecimal amount) { BigDecimal balance = this.getBalance(); this.balance = balance.subtract(amount); } } Copied! 安全实现-使用锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class DecimalAccountSafeLock implements DecimalAccount { private final Object lock = new Object(); BigDecimal balance; public DecimalAccountSafeLock(BigDecimal balance) { this.balance = balance; } @Override public BigDecimal getBalance() { return balance; } @Override public void withdraw(BigDecimal amount) { synchronized (lock) { BigDecimal balance = this.getBalance(); this.balance = balance.subtract(amount); } } } Copied! 安全实现-使用 CAS 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class DecimalAccountSafeCas implements DecimalAccount { AtomicReference\u0026lt;BigDecimal\u0026gt; ref; public DecimalAccountSafeCas(BigDecimal balance) { ref = new AtomicReference\u0026lt;\u0026gt;(balance); } @Override public BigDecimal getBalance() { return ref.get(); } @Override public void withdraw(BigDecimal amount) { while (true) { BigDecimal prev = ref.get(); BigDecimal next = prev.subtract(amount); if (ref.compareAndSet(prev, next)) { break; } } } } Copied! 测试代码\n1 2 3 DecimalAccount.demo(new DecimalAccountUnsafe(new BigDecimal(\u0026#34;10000\u0026#34;))); DecimalAccount.demo(new DecimalAccountSafeLock(new BigDecimal(\u0026#34;10000\u0026#34;))); DecimalAccount.demo(new DecimalAccountSafeCas(new BigDecimal(\u0026#34;10000\u0026#34;))); Copied! 运行结果\n1 2 3 4310 cost: 425 ms 0 cost: 285 ms 0 cost: 274 ms Copied! ABA 问题及解决 ABA 问题\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static AtomicReference\u0026lt;String\u0026gt; ref = new AtomicReference\u0026lt;\u0026gt;(\u0026#34;A\u0026#34;); public static void main(String[] args) throws InterruptedException { log.debug(\u0026#34;main start...\u0026#34;); // 获取值 A // 这个共享变量被它线程修改过？ String prev = ref.get(); other(); sleep(1); // 尝试改为 C log.debug(\u0026#34;change A-\u0026gt;C {}\u0026#34;, ref.compareAndSet(prev, \u0026#34;C\u0026#34;)); } private static void other() { new Thread(() -\u0026gt; { log.debug(\u0026#34;change A-\u0026gt;B {}\u0026#34;, ref.compareAndSet(ref.get(), \u0026#34;B\u0026#34;)); }, \u0026#34;t1\u0026#34;).start(); sleep(0.5); new Thread(() -\u0026gt; { log.debug(\u0026#34;change B-\u0026gt;A {}\u0026#34;, ref.compareAndSet(ref.get(), \u0026#34;A\u0026#34;)); }, \u0026#34;t2\u0026#34;).start(); } Copied! 输出\n1 2 3 4 11:29:52.325 c.Test36 [main] - main start... 11:29:52.379 c.Test36 [t1] - change A-\u0026gt;B true 11:29:52.879 c.Test36 [t2] - change B-\u0026gt;A true 11:29:53.880 c.Test36 [main] - change A-\u0026gt;C true Copied! 主线程仅能判断出共享变量的值与最初值 A 是否相同，不能感知到这种从 A 改为 B 又 改回 A 的情况，如果主线程 希望：\n只要有其它线程【动过了】共享变量，那么自己的 cas 就算失败，这时，仅比较值是不够的，需要再加一个版本号\nAtomicStampedReference\n1 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 static AtomicStampedReference\u0026lt;String\u0026gt; ref = new AtomicStampedReference\u0026lt;\u0026gt;(\u0026#34;A\u0026#34;, 0); public static void main(String[] args) throws InterruptedException { log.debug(\u0026#34;main start...\u0026#34;); // 获取值 A String prev = ref.getReference(); // 获取版本号 int stamp = ref.getStamp(); log.debug(\u0026#34;版本 {}\u0026#34;, stamp); // 如果中间有其它线程干扰，发生了 ABA 现象 other(); sleep(1); // 尝试改为 C log.debug(\u0026#34;change A-\u0026gt;C {}\u0026#34;, ref.compareAndSet(prev, \u0026#34;C\u0026#34;, stamp, stamp + 1)); } private static void other() { new Thread(() -\u0026gt; { log.debug(\u0026#34;change A-\u0026gt;B {}\u0026#34;, ref.compareAndSet(ref.getReference(), \u0026#34;B\u0026#34;, ref.getStamp(), ref.getStamp() + 1)); log.debug(\u0026#34;更新版本为 {}\u0026#34;, ref.getStamp()); }, \u0026#34;t1\u0026#34;).start(); sleep(0.5); new Thread(() -\u0026gt; { log.debug(\u0026#34;change B-\u0026gt;A {}\u0026#34;, ref.compareAndSet(ref.getReference(), \u0026#34;A\u0026#34;, ref.getStamp(), ref.getStamp() + 1)); log.debug(\u0026#34;更新版本为 {}\u0026#34;, ref.getStamp()); }, \u0026#34;t2\u0026#34;).start(); } Copied! 输出为\n1 2 3 4 5 6 7 15:41:34.891 c.Test36 [main] - main start... 15:41:34.894 c.Test36 [main] - 版本 0 15:41:34.956 c.Test36 [t1] - change A-\u0026gt;B true 15:41:34.956 c.Test36 [t1] - 更新版本为 1 15:41:35.457 c.Test36 [t2] - change B-\u0026gt;A true 15:41:35.457 c.Test36 [t2] - 更新版本为 2 15:41:36.457 c.Test36 [main] - change A-\u0026gt;C false Copied! AtomicStampedReference 可以给原子引用加上版本号，追踪原子引用整个的变化过程，如： A -\u0026gt; B -\u0026gt; A -\u0026gt; C ，通过AtomicStampedReference，我们可以知道，引用变量中途被更改了几次。\n但是有时候，并不关心引用变量更改了几次，只是单纯的关心是否更改过，所以就有了 AtomicMarkableReference\n1 2 3 4 5 6 7 8 9 10 graph TD s(保洁阿姨) m(主人) g1(垃圾袋) g2(新垃圾袋) s -. 倒空 .-\u0026gt; g1 m -- 检查 --\u0026gt; g1 g1 -- 已满 --\u0026gt; g2 g1 -- 还空 --\u0026gt; g1 Copied! AtomicMarkableReference\n1 2 3 4 5 6 7 8 9 10 11 12 13 class GarbageBag { String desc; public GarbageBag(String desc) { this.desc = desc; } public void setDesc(String desc) { this.desc = desc; } @Override public String toString() { return super.toString() + \u0026#34; \u0026#34; + desc; } } Copied! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Slf4j public class TestABAAtomicMarkableReference { public static void main(String[] args) throws InterruptedException { GarbageBag bag = new GarbageBag(\u0026#34;装满了垃圾\u0026#34;); // 参数2 mark 可以看作一个标记，表示垃圾袋满了 AtomicMarkableReference\u0026lt;GarbageBag\u0026gt; ref = new AtomicMarkableReference\u0026lt;\u0026gt;(bag, true); log.debug(\u0026#34;主线程 start...\u0026#34;); GarbageBag prev = ref.getReference(); log.debug(prev.toString()); new Thread(() -\u0026gt; { log.debug(\u0026#34;打扫卫生的线程 start...\u0026#34;); bag.setDesc(\u0026#34;空垃圾袋\u0026#34;); while (!ref.compareAndSet(bag, bag, true, false)) {} log.debug(bag.toString()); }).start(); Thread.sleep(1000); log.debug(\u0026#34;主线程想换一只新垃圾袋？\u0026#34;); boolean success = ref.compareAndSet(prev, new GarbageBag(\u0026#34;空垃圾袋\u0026#34;), true, false); log.debug(\u0026#34;换了么？\u0026#34; + success); log.debug(ref.getReference().toString()); } } Copied! 输出\n1 2 3 4 5 6 7 2019-10-13 15:30:09.264 [main] 主线程 start... 2019-10-13 15:30:09.270 [main] cn.itcast.GarbageBag@5f0fd5a0 装满了垃圾 2019-10-13 15:30:09.293 [Thread-1] 打扫卫生的线程 start... 2019-10-13 15:30:09.294 [Thread-1] cn.itcast.GarbageBag@5f0fd5a0 空垃圾袋 2019-10-13 15:30:10.294 [main] 主线程想换一只新垃圾袋？ 2019-10-13 15:30:10.294 [main] 换了么？false 2019-10-13 15:30:10.294 [main] cn.itcast.GarbageBag@5f0fd5a0 空垃圾袋 Copied! 可以注释掉打扫卫生线程代码，再观察输出\n6.5 原子数组 AtomicIntegerArray AtomicLongArray AtomicReferenceArray 有如下方法\n1 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 /** 参数1，提供数组、可以是线程不安全数组或线程安全数组 参数2，获取数组长度的方法 参数3，自增方法，回传 array, index 参数4，打印数组的方法 */ // supplier 提供者 无中生有 ()-\u0026gt;结果 // function 函数 一个参数一个结果 (参数)-\u0026gt;结果 , BiFunction (参数1,参数2)-\u0026gt;结果 // consumer 消费者 一个参数没结果 (参数)-\u0026gt;void, BiConsumer (参数1,参数2)-\u0026gt; private static \u0026lt;T\u0026gt; void demo( Supplier\u0026lt;T\u0026gt; arraySupplier, Function\u0026lt;T, Integer\u0026gt; lengthFun, BiConsumer\u0026lt;T, Integer\u0026gt; putConsumer, Consumer\u0026lt;T\u0026gt; printConsumer ) { List\u0026lt;Thread\u0026gt; ts = new ArrayList\u0026lt;\u0026gt;(); T array = arraySupplier.get(); int length = lengthFun.apply(array); for (int i = 0; i \u0026lt; length; i++) { // 每个线程对数组作 10000 次操作 ts.add(new Thread(() -\u0026gt; { for (int j = 0; j \u0026lt; 10000; j++) { putConsumer.accept(array, j%length); } })); } ts.forEach(t -\u0026gt; t.start()); // 启动所有线程 ts.forEach(t -\u0026gt; { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); // 等所有线程结束 printConsumer.accept(array); } Copied! 不安全的数组\n1 2 3 4 5 6 demo( ()-\u0026gt;new int[10], (array)-\u0026gt;array.length, (array, index) -\u0026gt; array[index]++, array-\u0026gt; System.out.println(Arrays.toString(array)) ); Copied! 结果\n1 [9870, 9862, 9774, 9697, 9683, 9678, 9679, 9668, 9680, 9698] Copied! 安全的数组\n1 2 3 4 5 6 demo( ()-\u0026gt; new AtomicIntegerArray(10), (array) -\u0026gt; array.length(), (array, index) -\u0026gt; array.getAndIncrement(index), array -\u0026gt; System.out.println(array) ); Copied! 结果\n1 [10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000] Copied! 6.6 字段更新器 AtomicReferenceFieldUpdater // 域 字段 AtomicIntegerFieldUpdater AtomicLongFieldUpdater 利用字段更新器，可以针对对象的某个域（Field）进行原子操作，只能配合 volatile 修饰的字段使用，否则会出现 异常 1 Exception in thread \u0026#34;main\u0026#34; java.lang.IllegalArgumentException: Must be volatile type Copied! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Test5 { private volatile int field; public static void main(String[] args) { AtomicIntegerFieldUpdater fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Test5.class, \u0026#34;field\u0026#34;); Test5 test5 = new Test5(); fieldUpdater.compareAndSet(test5, 0, 10); // 修改成功 field = 10 System.out.println(test5.field); // 修改成功 field = 20 fieldUpdater.compareAndSet(test5, 10, 20); System.out.println(test5.field); // 修改失败 field = 20 fieldUpdater.compareAndSet(test5, 10, 30); System.out.println(test5.field); } } Copied! 输出\n1 2 3 10 20 20 Copied! 6.7 原子累加器 累加器性能比较 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private static \u0026lt;T\u0026gt; void demo(Supplier\u0026lt;T\u0026gt; adderSupplier, Consumer\u0026lt;T\u0026gt; action) { T adder = adderSupplier.get(); long start = System.nanoTime(); List\u0026lt;Thread\u0026gt; ts = new ArrayList\u0026lt;\u0026gt;(); // 4 个线程，每人累加 50 万 for (int i = 0; i \u0026lt; 40; i++) { ts.add(new Thread(() -\u0026gt; { for (int j = 0; j \u0026lt; 500000; j++) { action.accept(adder); } })); } ts.forEach(t -\u0026gt; t.start()); ts.forEach(t -\u0026gt; { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println(adder + \u0026#34; cost:\u0026#34; + (end - start)/1000_000); } Copied! 比较 AtomicLong 与 LongAdder\n1 2 3 4 5 6 for (int i = 0; i \u0026lt; 5; i++) { demo(() -\u0026gt; new LongAdder(), adder -\u0026gt; adder.increment()); } for (int i = 0; i \u0026lt; 5; i++) { demo(() -\u0026gt; new AtomicLong(), adder -\u0026gt; adder.getAndIncrement()); } Copied! 输出\n1 2 3 4 5 6 7 8 9 10 1000000 cost:43 1000000 cost:9 1000000 cost:7 1000000 cost:7 1000000 cost:7 1000000 cost:31 1000000 cost:27 1000000 cost:28 1000000 cost:24 1000000 cost:22 Copied! 性能提升的原因很简单，就是在有竞争时，设置多个累加单元，Therad-0 累加 Cell[0]，而 Thread-1 累加 Cell[1]\u0026hellip; 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量，因此减少了 CAS 重试失败，从而提高性 能。\n* 原理之伪共享(CPU 缓存结构) CPU 缓存结构 查看 cpu 缓存\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ⚡ root@yihang01 ~ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 1 On-line CPU(s) list: 0 Thread(s) per core: 1 Core(s) per socket: 1 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 142 Model name: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz Stepping: 11 CPU MHz: 1992.002 BogoMIPS: 3984.00 Hypervisor vendor: VMware Virtualization type: full L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 8192K NUMA node0 CPU(s): 0 Copied! 速度比较\n从cpu到 大约需要的时钟周期 寄存器 1 cycle L1 3~4 cycle L2 10~20 cycle L3 40~45 cycle 内存 120~240 cycle 查看 cpu 缓存行\n1 2 ⚡ root@yihang01 ~ cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size 64 Copied! cpu 拿到的内存地址格式是这样的\n1 [高位组标记][低位索引][偏移量] Copied! CPU 缓存读 读取数据流程如下\n根据低位，计算在缓存中的索引 判断是否有效 0 去内存读取新数据更新缓存行 1 再对比高位组标记是否一致 一致，根据偏移量返回缓存数据 不一致，去内存读取新数据更新缓存行 CPU 缓存一致性 MESI 协议\nE、S、M 状态的缓存行都可以满足 CPU 的读请求 E 状态的缓存行，有写请求，会将状态改为 M，这时并不触发向主存的写 E 状态的缓存行，必须监听该缓存行的读操作，如果有，要变为 S 状态 4. M 状态的缓存行，必须监听该缓存行的读操作，如果有，先将其它缓存（S 状态）中该缓存行变成 I 状态（即 6. 的流程），写入主存，自己变为 S 状态 5. S 状态的缓存行，有写请求，走 4. 的流程 6. S 状态的缓存行，必须监听该缓存行的失效操作，如果有，自己变为 I 状态 7. I 状态的缓存行，有读请求，必须从主存读取 内存屏障 Memory Barrier（Memory Fence）\n可见性\n写屏障（sfence）保证在该屏障之前的，对共享变量的改动，都同步到主存当中 而读屏障（lfence）保证在该屏障之后，对共享变量的读取，加载的是主存中最新数据 有序性\n写屏障会确保指令重排序时，不会将写屏障之前的代码排在写屏障之后 读屏障会确保指令重排序时，不会将读屏障之后的代码排在读屏障之前 * 源码之 LongAdder LongAdder 是并发大师 @author Doug Lea （大哥李）的作品，设计的非常精巧\nLongAdder 类有几个关键域\n1 2 3 4 5 6 // 累加单元数组, 懒惰初始化 transient volatile Cell[] cells; // 基础值, 如果没有竞争, 则用 cas 累加这个域 transient volatile long base; // 在 cells 创建或扩容时, 置为 1, 表示加锁 transient volatile int cellsBusy; Copied! 其中 Cell 即为累加单元\n1 2 3 4 5 6 7 8 9 10 11 12 // 防止缓存行伪共享 @sun.misc.Contended static final class Cell { volatile long value; Cell(long x) { value = x; } // 最重要的方法, 用来 cas 方式进行累加, prev 表示旧值, next 表示新值 final boolean cas(long prev, long next) { return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next); } // 省略不重要代码 } Copied! 得从缓存说起\n缓存与内存的速度比较\n因为 CPU 与 内存的速度差异很大，需要靠预读数据至缓存来提升效率。\n而缓存以缓存行为单位，每个缓存行对应着一块内存，一般是 64 byte（8 个 long）\n缓存的加入会造成数据副本的产生，即同一份数据会缓存在不同核心的缓存行中\nCPU 要保证数据的一致性，如果某个 CPU 核心更改了数据，其它 CPU 核心对应的整个缓存行必须失效\n因为 Cell 是数组形式，在内存中是连续存储的，一个 Cell 为 24 字节（16 字节的对象头和 8 字节的 value），因 此缓存行可以存下 2 个的 Cell 对象。这样问题来了：\nCore-0 要修改 Cell[0] Core-1 要修改 Cell[1] 无论谁修改成功，都会导致对方 Core 的缓存行失效，比如Core-0 中Cell[0]=6000, Cell[1]=8000要累加Cell[0]=6001, Cell[1]=8000 ，这时会让 Core-1 的缓存行失效\n@sun.misc.Contended 用来解决这个问题，它的原理是在使用此注解的对象或字段的前后各增加 128 字节大小的 padding，从而让 CPU 将对象预读至缓存时占用不同的缓存行，这样，不会造成对方缓存行的失效\n累加主要调用下面的方法\n1 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 public void add(long x) { // as 为累加单元数组 // b 为基础值 // x 为累加值 Cell[] as; long b, v; int m; Cell a; // 进入 if 的两个条件 // 1. as 有值, 表示已经发生过竞争, 进入 if // 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if if ((as = cells) != null || !casBase(b = base, b + x)) { // uncontended 表示 cell 没有竞争 boolean uncontended = true; if ( // as 还没有创建 as == null || (m = as.length - 1) \u0026lt; 0 || // 当前线程对应的 cell 还没有 // getProbe()方法返回的是线程中的threadLocalRandomProbe字段 // 它是通过随机数生成的一个值，对于一个确定的线程这个值是固定的 // 除非刻意修改它 (a = as[getProbe() \u0026amp; m]) == null || // cas 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell ) !(uncontended = a.cas(v = a.value, v + x)) ) { // 进入 cell 数组创建、cell 创建的流程 longAccumulate(x, null, uncontended); } } } Copied! 总结 ：\n如果已经有了累加数组或给base累加发生了竞争导致失败 如果累加数组没有创建或者累加数组长度为1或者当前线程还没有对应的cell或者累加cell失败 进入累加数组的创建流程 否者说明累加成功，退出。 否则累加成功 add 流程图\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) { int h; // 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell if ((h = getProbe()) == 0) { // 初始化 probe ThreadLocalRandom.current(); // h 对应新的 probe 值, 用来对应 cell h = getProbe(); wasUncontended = true; } // collide 为 true 表示最后一个槽非空，需要扩容 boolean collide = false; for (;;) { Cell[] as; Cell a; int n; long v; // 已经有了 cells if ((as = cells) != null \u0026amp;\u0026amp; (n = as.length) \u0026gt; 0) { // 还没有 cell if ((a = as[(n - 1) \u0026amp; h]) == null) { // 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x // 成功则 break, 否则继续 continue 循环 if (cellsBusy == 0) { // Try to attach new Cell Cell r = new Cell(x); // Optimistically create if (cellsBusy == 0 \u0026amp;\u0026amp; casCellsBusy()) { boolean created = false; try { // Recheck under lock Cell[] rs; int m, j; if ((rs = cells) != null \u0026amp;\u0026amp; (m = rs.length) \u0026gt; 0 \u0026amp;\u0026amp; rs[j = (m - 1) \u0026amp; h] == null) { rs[j] = r; created = true; } } finally { cellsBusy = 0; } if (created) break; continue; // Slot is now non-empty } } collide = false; } // 有竞争, 改变线程对应的 cell 来重试 cas else if (!wasUncontended) wasUncontended = true; // cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; // 如果 cells 长度已经超过了最大长度, 或者已经扩容, 改变线程对应的 cell 来重试 cas else if (n \u0026gt;= NCPU || cells != as) collide = false; // 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了 else if (!collide) collide = true; // 加锁 else if (cellsBusy == 0 \u0026amp;\u0026amp; casCellsBusy()) { // 加锁成功, 扩容 continue; } // 改变线程对应的 cell h = advanceProbe(h); } // 还没有 cells, 尝试给 cellsBusy 加锁 else if (cellsBusy == 0 \u0026amp;\u0026amp; cells == as \u0026amp;\u0026amp; casCellsBusy()) { // 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell // 成功则 break; boolean init = false; try { // Initialize table if (cells == as) { Cell[] rs = new Cell[2]; rs[h \u0026amp; 1] = new Cell(x); cells = rs; init = true; } } finally { cellsBusy = 0; } if (init) break; } // 上两种情况失败, 尝试给 base 累加 else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; } } Copied! 总结：\n先判断当前线程有没有对应的Cell\n如果没有，随机生成一个值，这个值与当前线程绑定，通过这个值的取模运算定位当前线程Cell的位置。 进入for循环\nif 有Cells累加数组且长度大于0\nif 如果当前线程没有cell\n准备扩容，如果前累加数组不繁忙（正在扩容之类） 将新建的cell放入对应的槽位中，新建Cell成功，进入下一次循环，尝试cas累加。 将collide置为false，表示无需扩容。 else if 有竞争\n将wasUncontended置为tue，进入分支底部，改变线程对应的cell来cas重试 else if cas重试累加成功\n退出循环。 else if cells 长度已经超过了最大长度, 或者已经扩容,\ncollide置为false，进入分支底部，改变线程对应的 cell 来重试 cas else if collide为false\n将collide置为true（确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了） else if 累加数组不繁忙且加锁成功\n退出本次循环，进入下一次循环（扩容） 改变线程对应的 cell 来重试 cas\nelse if 数组不繁忙且数组为null且加锁成功\n新建数组，在槽位处新建cell，释放锁，退出循环。 else if 尝试给base累加成功\n退出循环 longAccumulate 流程图\n每个线程刚进入 longAccumulate 时，会尝试对应一个 cell 对象（找到一个坑位）\n获取最终结果通过 sum 方法\n1 2 3 4 5 6 7 8 9 10 11 public long sum() { Cell[] as = cells; Cell a; long sum = base; if (as != null) { for (int i = 0; i \u0026lt; as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; } Copied! 与运算和取模的关系 参考链接：https://www.cnblogs.com/thrillerz/p/4530108.html\n6.8 Unsafe 概述 Unsafe 对象提供了非常底层的，操作内存、线程的方法，Unsafe 对象不能直接调用，只能通过反射获得。jdk8直接调用Unsafe.getUnsafe()获得的unsafe不能用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class UnsafeAccessor { static Unsafe unsafe; static { try { Field theUnsafe = Unsafe.class.getDeclaredField(\u0026#34;theUnsafe\u0026#34;); theUnsafe.setAccessible(true); unsafe = (Unsafe) theUnsafe.get(null); } catch (NoSuchFieldException | IllegalAccessException e) { throw new Error(e); } } static Unsafe getUnsafe() { return unsafe; } } Copied! 方法：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 //以下三个方法只执行一次，成功返回true，不成功返回false public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6); //以下方法都是在以上三个方法的基础上进行封装，会循环直到成功为止。 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } public final long getAndAddLong(Object var1, long var2, long var4) { long var6; do { var6 = this.getLongVolatile(var1, var2); } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4)); return var6; } public final int getAndSetInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var4)); return var5; } public final long getAndSetLong(Object var1, long var2, long var4) { long var6; do { var6 = this.getLongVolatile(var1, var2); } while(!this.compareAndSwapLong(var1, var2, var6, var4)); return var6; } public final Object getAndSetObject(Object var1, long var2, Object var4) { Object var5; do { var5 = this.getObjectVolatile(var1, var2); } while(!this.compareAndSwapObject(var1, var2, var5, var4)); Copied! Unsafe CAS 操作 unsafe实现字段更新 1 2 3 4 5 @Data class Student { volatile int id; volatile String name; } Copied! 1 2 3 4 5 6 7 8 9 10 11 Unsafe unsafe = UnsafeAccessor.getUnsafe(); Field id = Student.class.getDeclaredField(\u0026#34;id\u0026#34;); Field name = Student.class.getDeclaredField(\u0026#34;name\u0026#34;); // 获得成员变量的偏移量 long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id); long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name); Student student = new Student(); // 使用 cas 方法替换成员变量的值 UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, \u0026#34;张三\u0026#34;); // 返回 true System.out.println(student); Copied! 输出\n1 Student(id=20, name=张三) Copied! unsafe实现原子整数 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 class AtomicData { private volatile int data; static final Unsafe unsafe; static final long DATA_OFFSET; static { unsafe = UnsafeAccessor.getUnsafe(); try { // data 属性在 DataContainer 对象中的偏移量，用于 Unsafe 直接访问该属性 DATA_OFFSET = unsafe.objectFieldOffset(AtomicData.class.getDeclaredField(\u0026#34;data\u0026#34;)); } catch (NoSuchFieldException e) { throw new Error(e); } } public AtomicData(int data) { this.data = data; } public void decrease(int amount) { int oldValue; while(true) { // 获取共享变量旧值，可以在这一行加入断点，修改 data 调试来加深理解 oldValue = data; // cas 尝试修改 data 为 旧值 + amount，如果期间旧值被别的线程改了，返回 false if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - amount)) { return; } } } public int getData() { return data; } } Copied! Account 实现\n1 2 3 4 5 6 7 8 9 10 11 Account.demo(new Account() { AtomicData atomicData = new AtomicData(10000); @Override public Integer getBalance() { return atomicData.getData(); } @Override public void withdraw(Integer amount) { atomicData.decrease(amount); } }); Copied! 手动实现原子整数完整版+测试 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 public class UnsafeAtomicTest{ public static void main(String[] args) { //赋初始值10000，调用demo后正确的输出结果为0 AccountImpl account = new AccountImpl(10000); //结果正确地输出0 account.demo(); } } interface Account{ //获取balance的方法 int getBalance(); //取款的方法 void decrease(int amount); //演示多线程取款，检查安全性。 default void demo(){ ArrayList\u0026lt;Thread\u0026gt; ts = new ArrayList\u0026lt;\u0026gt;(1000); for (int i = 0; i \u0026lt; 1000; i++) { ts.add(new Thread(() -\u0026gt; { decrease(10); })); } for (Thread t:ts) { t.start(); } for (Thread t:ts) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(getBalance()); } } //实现账户类，使用手动实现的原子整数作为余额类型 class AccountImpl implements Account{ UnsafeAtomicInteger balance; public AccountImpl(int balance){ this.balance = new UnsafeAtomicInteger(balance); } @Override public int getBalance() { return balance.get(); } @Override public void decrease(int amount) { balance.getAndAccumulate(amount,(x,y) -\u0026gt; y - x); } } //手动实现原子整数类 class UnsafeAtomicInteger { //将value声明为volatile，因为乐观锁需要可见性。 private volatile int value; //需要Unsafe的cas本地方法实现操作。 private static final Unsafe unsafe; //偏移量，这两个变量很重要且通用、不可变，所以均声明为private static final private static final long offset; static{ //静态代码块初始化unsafe unsafe = UnsafeAccessor.getUnsafe(); try { //获取value在当前类中的偏移量 offset = unsafe.objectFieldOffset(UnsafeAtomicInteger.class.getDeclaredField(\u0026#34;value\u0026#34;)); } catch (NoSuchFieldException e) { e.printStackTrace(); //待研究 throw new Error(e); } } public UnsafeAtomicInteger(){ } public UnsafeAtomicInteger(int value){ this.value = value; } public final int get(){ return value; } public final boolean compareAndSet(int expext,int update){ return unsafe.compareAndSwapInt(this, offset, expext, update); } public final int getAndIncrement(){ //局部变量是必须的，因为多次从主存中读取value的值不可靠。 int oldValue; while (true){ oldValue = value; if(unsafe.compareAndSwapInt(this,offset,oldValue,oldValue + 1)){ return oldValue; } } } public final int incrementAndGet(){ int oldValue; while (true){ oldValue = value; if (unsafe.compareAndSwapInt(this, offset, oldValue, oldValue + 1)) { return oldValue + 1; } } } public final int getAndDecrement(){ int oldValue; while (true){ oldValue = value; if (unsafe.compareAndSwapInt(this, offset, oldValue, oldValue - 1)) { return oldValue; } } } public final int decrementAndGet(){ int oldValue; while (true){ oldValue = value; if (unsafe.compareAndSwapInt(this, offset, oldValue, oldValue - 1)) { return oldValue - 1; } } } public final int getAndUpdate(IntUnaryOperator operator){ int oldValue; int newValue; while (true){ oldValue = value; newValue = operator.applyAsInt(oldValue); if (unsafe.compareAndSwapInt(this, offset, oldValue, newValue)) { return oldValue; } } } public final int updateAndGet(IntUnaryOperator operator){ int oldValue; int newValue; while (true){ oldValue = value; newValue = operator.applyAsInt(oldValue); if (unsafe.compareAndSwapInt(this, offset, oldValue, newValue)) { return newValue; } } } public final int getAndAccumulate(int x, IntBinaryOperator operator){ int oldValue; int newValue; while (true){ oldValue = value; newValue = operator.applyAsInt(x,oldValue); if (unsafe.compareAndSwapInt(this, offset, oldValue, newValue)) { return newValue; } } } public final int accumulateAndGet(int x, IntBinaryOperator operator){ int oldValue; int newValue; while (true){ oldValue = value; newValue = operator.applyAsInt(x,oldValue); if (unsafe.compareAndSwapInt(this, offset, oldValue, newValue)) { return oldValue; } } } } class UnsafeAccessor{ public static Unsafe getUnsafe(){ Field field; Unsafe unsafe = null; try { field = Unsafe.class.getDeclaredField(\u0026#34;theUnsafe\u0026#34;); field.setAccessible(true); unsafe = (Unsafe)field.get(null); } catch (Exception e) { e.printStackTrace(); } return unsafe; } } Copied! 6.9 自定义cas锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 不要用于实践！！！ public class LockCas { private AtomicInteger state = new AtomicInteger(0); public void lock() { while (true) { if (state.compareAndSet(0, 1)) { break; } } } public void unlock() { log.debug(\u0026#34;unlock...\u0026#34;); state.set(0); } } Copied! 测试\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 LockCas lock = new LockCas(); new Thread(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); lock.lock(); try { log.debug(\u0026#34;lock...\u0026#34;); sleep(1); } finally { lock.unlock(); } }).start(); new Thread(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); lock.lock(); try { log.debug(\u0026#34;lock...\u0026#34;); } finally { lock.unlock(); } }).start(); Copied! 1 2 3 4 5 6 18:27:07.198 c.Test42 [Thread-0] - begin... 18:27:07.202 c.Test42 [Thread-0] - lock... 18:27:07.198 c.Test42 [Thread-1] - begin... 18:27:08.204 c.Test42 [Thread-0] - unlock... 18:27:08.204 c.Test42 [Thread-1] - lock... 18:27:08.204 c.Test42 [Thread-1] - unlock... Copied! 本章小结 CAS 与 volatile API 原子整数 原子引用 原子数组 字段更新器 原子累加器 Unsafe *原理方面 LongAdder 源码 伪共享 7.共享模型之不可变 7.1 日期转换的问题 问题提出 下面的代码在运行时，由于 SimpleDateFormat 不是线程安全的\n1 2 3 4 5 6 7 8 9 10 SimpleDateFormat sdf = new SimpleDateFormat(\u0026#34;yyyy-MM-dd\u0026#34;); for (int i = 0; i \u0026lt; 10; i++) { new Thread(() -\u0026gt; { try { log.debug(\u0026#34;{}\u0026#34;, sdf.parse(\u0026#34;1951-04-21\u0026#34;)); } catch (Exception e) { log.error(\u0026#34;{}\u0026#34;, e); } }).start(); } Copied! 有很大几率出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果，例如：\n1 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 19:10:40.859 [Thread-2] c.TestDateParse - {} java.lang.NumberFormatException: For input string: \u0026#34;\u0026#34; at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Long.parseLong(Long.java:601) at java.lang.Long.parseLong(Long.java:631) at java.text.DigitList.getLong(DigitList.java:195) at java.text.DecimalFormat.parse(DecimalFormat.java:2084) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) at cn.itcast.n7.TestDateParse.lambda$test1$0(TestDateParse.java:18) at java.lang.Thread.run(Thread.java:748) 19:10:40.859 [Thread-1] c.TestDateParse - {} java.lang.NumberFormatException: empty String at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842) at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110) at java.lang.Double.parseDouble(Double.java:538) at java.text.DigitList.getDouble(DigitList.java:169) at java.text.DecimalFormat.parse(DecimalFormat.java:2089) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) at cn.itcast.n7.TestDateParse.lambda$test1$0(TestDateParse.java:18) at java.lang.Thread.run(Thread.java:748) 19:10:40.857 [Thread-8] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 19:10:40.857 [Thread-9] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 19:10:40.857 [Thread-6] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 19:10:40.857 [Thread-4] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 19:10:40.857 [Thread-5] c.TestDateParse - Mon Apr 21 00:00:00 CST 178960645 19:10:40.857 [Thread-0] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 19:10:40.857 [Thread-7] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 19:10:40.857 [Thread-3] c.TestDateParse - Sat Apr 21 00:00:00 CST 1951 Copied! 思路 - 同步锁 这样虽能解决问题，但带来的是性能上的损失，并不算很好：\n1 2 3 4 5 6 7 8 9 10 11 12 SimpleDateFormat sdf = new SimpleDateFormat(\u0026#34;yyyy-MM-dd\u0026#34;); for (int i = 0; i \u0026lt; 50; i++) { new Thread(() -\u0026gt; { synchronized (sdf) { try { log.debug(\u0026#34;{}\u0026#34;, sdf.parse(\u0026#34;1951-04-21\u0026#34;)); } catch (Exception e) { log.error(\u0026#34;{}\u0026#34;, e); } } }).start(); } Copied! 思路 - 不可变 如果一个对象在不能够修改其内部状态（属性），那么它就是线程安全的，因为不存在并发修改啊！这样的对象在 Java 中有很多，例如在 Java 8 后，提供了一个新的日期格式化类：\n1 2 3 4 5 6 7 DateTimeFormatter dtf = DateTimeFormatter.ofPattern(\u0026#34;yyyy-MM-dd\u0026#34;); for (int i = 0; i \u0026lt; 10; i++) { new Thread(() -\u0026gt; { LocalDate date = dtf.parse(\u0026#34;2018-10-01\u0026#34;, LocalDate::from); log.debug(\u0026#34;{}\u0026#34;, date); }).start(); } Copied! 可以看 DateTimeFormatter 的文档：\n1 2 @implSpec //This class is immutable and thread-safe. Copied! 不可变对象，实际是另一种避免竞争的方式。\n7.2 不可变设计 String类的设计 另一个大家更为熟悉的 String 类也是不可变的，以它为例，说明一下不可变设计的要素\n1 2 3 4 5 6 7 8 9 10 public final class String implements java.io.Serializable, Comparable\u0026lt;String\u0026gt;, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 // ... } Copied! 说明：\n将类声明为final，避免被带外星方法的子类继承，从而破坏了不可变性。 将字符数组声明为final，避免被修改 hash虽然不是final的，但是其只有在调用hash()方法的时候才被赋值，除此之外再无别的方法修改。 final 的使用 发现该类、类中所有属性都是 final 的\n属性用 final 修饰保证了该属性是只读的，不能修改 类用 final 修饰保证了该类中的方法不能被覆盖，防止子类无意间破坏不可变性 保护性拷贝 但有同学会说，使用字符串时，也有一些跟修改相关的方法啊，比如 substring 等，那么下面就看一看这些方法是 如何实现的，就以 substring 为例：\n1 2 3 4 5 6 7 8 9 10 public String substring(int beginIndex) { if (beginIndex \u0026lt; 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen \u0026lt; 0) { throw new StringIndexOutOfBoundsException(subLen); } return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); } Copied! 发现其内部是调用 String 的构造方法创建了一个新字符串，再进入这个构造看看，是否对 final char[] value 做出 了修改：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public String(char value[], int offset, int count) { if (offset \u0026lt; 0) { throw new StringIndexOutOfBoundsException(offset); } if (count \u0026lt;= 0) { if (count \u0026lt; 0) { throw new StringIndexOutOfBoundsException(count); } if (offset \u0026lt;= value.length) { this.value = \u0026#34;\u0026#34;.value; return; } } if (offset \u0026gt; value.length - count) { throw new StringIndexOutOfBoundsException(offset + count); } this.value = Arrays.copyOfRange(value, offset, offset+count); } Copied! 结果发现也没有，构造新字符串对象时，会生成新的 char[] value，对内容进行复制 。这种通过创建副本对象来避 免共享的手段称之为【保护性拷贝（defensive copy）】\n*模式之享元 简介 定义 英文名称：Flyweight pattern. 当需要重用数量有限的同一类对象时\nwikipedia： A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects\n出自 \u0026ldquo;Gang of Four\u0026rdquo; design patterns\n归类 Structual patterns\n体现 包装类\n在JDK中 Boolean，Byte，Short，Integer，Long，Character 等包装类提供了 valueOf 方法，例如 Long 的 valueOf 会缓存 -128~127 之间的 Long 对象，在这个范围之间会重用对象，大于这个范围，才会新建 Long 对 象：\n1 2 3 4 5 6 7 public static Long valueOf(long l) { final int offset = 128; if (l \u0026gt;= -128 \u0026amp;\u0026amp; l \u0026lt;= 127) { // will cache return LongCache.cache[(int)l + offset]; } return new Long(l); } Copied! 注意：\nByte, Short, Long 缓存的范围都是 -128~127 Character 缓存的范围是 0~127 Integer的默认范围是 -128~127 最小值不能变 但最大值可以通过调整虚拟机参数 -Djava.lang.Integer.IntegerCache.high 来改变 Boolean 缓存了 TRUE 和 FALSE String 串池（不可变、线程安全）\n详见jvm\nBigDecimal BigInteger(不可变、线程安全)\n一部分数字使用了享元模式进行了缓存。\n手动实现一个连接池 例如：一个线上商城应用，QPS 达到数千，如果每次都重新创建和关闭数据库连接，性能会受到极大影响。 这时 预先创建好一批连接，放入连接池。一次请求到达后，从连接池获取连接，使用完毕后再还回连接池，这样既节约 了连接的创建和关闭时间，也实现了连接的重用，不至于让庞大的连接数压垮数据库。\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 class Pool { // 1. 连接池大小 private final int poolSize; // 2. 连接对象数组 private Connection[] connections; // 3. 连接状态数组 0 表示空闲， 1 表示繁忙 private AtomicIntegerArray states; // 4. 构造方法初始化 public Pool(int poolSize) { this.poolSize = poolSize; this.connections = new Connection[poolSize]; this.states = new AtomicIntegerArray(new int[poolSize]); for (int i = 0; i \u0026lt; poolSize; i++) { connections[i] = new MockConnection(\u0026#34;连接\u0026#34; + (i+1)); } } // 5. 借连接 public Connection borrow() { while(true) { for (int i = 0; i \u0026lt; poolSize; i++) { // 获取空闲连接 if(states.get(i) == 0) { if (states.compareAndSet(i, 0, 1)) { log.debug(\u0026#34;borrow {}\u0026#34;, connections[i]); return connections[i]; } } } // 如果没有空闲连接，当前线程进入等待 synchronized (this) { try { log.debug(\u0026#34;wait...\u0026#34;); this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } // 6. 归还连接 public void free(Connection conn) { for (int i = 0; i \u0026lt; poolSize; i++) { if (connections[i] == conn) { states.set(i, 0); synchronized (this) { log.debug(\u0026#34;free {}\u0026#34;, conn); this.notifyAll(); } break; } } } } class MockConnection implements Connection { // 实现略 } Copied! 使用连接池：\n1 2 3 4 5 6 7 8 9 10 11 12 Pool pool = new Pool(2); for (int i = 0; i \u0026lt; 5; i++) { new Thread(() -\u0026gt; { Connection conn = pool.borrow(); try { Thread.sleep(new Random().nextInt(1000)); } catch (InterruptedException e) { e.printStackTrace(); } pool.free(conn); }).start(); } Copied! 以上实现没有考虑：\n连接的动态增长与收缩 连接保活（可用性检测） 等待超时处理 分布式 hash 对于关系型数据库，有比较成熟的连接池实现，例如c3p0, druid等 对于更通用的对象池，可以考虑使用apache commons pool，例如redis连接池可以参考jedis中关于连接池的实现\n* 原理之 final 设置 final 变量的原理 理解了 volatile 原理，再对比 final 的实现就比较简单了\n1 2 3 public class TestFinal { final int a = 20; } Copied! 字节码\n1 2 3 4 5 6 7 0: aload_0 1: invokespecial #1 // Method java/lang/Object.\u0026#34;\u0026lt;init\u0026gt;\u0026#34;:()V 4: aload_0 5: bipush 20 7: putfield #2 // Field a:I \u0026lt;-- 写屏障 10: return Copied! 发现 final 变量的赋值也会通过 putfield 指令来完成，同样在这条指令之后也会加入写屏障，这样对final变量的写入不会重排序到构造方法之外，保证在其它线程读到 它的值时不会出现为 0 的情况。普通变量不能保证这一点了。\n读取final变量原理 有以下代码：\n1 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 37 38 39 40 41 public class TestFinal { final static int A = 10; final static int B = Short.MAX_VALUE+1; final int a = 20; final int b = Integer.MAX_VALUE; final void test1() { final int c = 30; new Thread(()-\u0026gt;{ System.out.println(c); }).start(); final int d = 30; class Task implements Runnable { @Override public void run() { System.out.println(d); } } new Thread(new Task()).start(); } } class UseFinal1 { public void test() { System.out.println(TestFinal.A); System.out.println(TestFinal.B); System.out.println(new TestFinal().a); System.out.println(new TestFinal().b); new TestFinal().test1(); } } class UseFinal2 { public void test() { System.out.println(TestFinal.A); } } Copied! 反编译UseFinal1中的test方法：\n1 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 37 38 39 40 41 42 43 44 45 public test()V L0 LINENUMBER 31 L0 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; BIPUSH 10 INVOKEVIRTUAL java/io/PrintStream.println (I)V L1 LINENUMBER 32 L1 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC 32768 INVOKEVIRTUAL java/io/PrintStream.println (I)V L2 LINENUMBER 33 L2 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; NEW cn/itcast/n5/TestFinal DUP INVOKESPECIAL cn/itcast/n5/TestFinal.\u0026lt;init\u0026gt; ()V INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class; POP BIPUSH 20 INVOKEVIRTUAL java/io/PrintStream.println (I)V L3 LINENUMBER 34 L3 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; NEW cn/itcast/n5/TestFinal DUP INVOKESPECIAL cn/itcast/n5/TestFinal.\u0026lt;init\u0026gt; ()V INVOKEVIRTUAL java/lang/Object.getClass ()Ljava/lang/Class; POP LDC 2147483647 INVOKEVIRTUAL java/io/PrintStream.println (I)V L4 LINENUMBER 35 L4 NEW cn/itcast/n5/TestFinal DUP INVOKESPECIAL cn/itcast/n5/TestFinal.\u0026lt;init\u0026gt; ()V INVOKEVIRTUAL cn/itcast/n5/TestFinal.test1 ()V L5 LINENUMBER 36 L5 RETURN L6 LOCALVARIABLE this Lcn/itcast/n5/UseFinal1; L0 L6 0 MAXSTACK = 3 MAXLOCALS = 1 } Copied! 可以看见，jvm对final变量的访问做出了优化：另一个类中的方法调用final变量是，不是从final变量所在类中获取（共享内存），而是直接复制一份到方法栈栈帧中的操作数栈中（工作内存），这样可以提升效率，是一种优化。\n总结：\n对于较小的static final变量：复制一份到操作数栈中 对于较大的static final变量：复制一份到当前类的常量池中 对于非静态final变量，优化同上。 final总结 final关键字的好处：\n（1）final关键字提高了性能。JVM和Java应用都会缓存final变量。\n（2）final变量可以安全的在多线程环境下进行共享，而不需要额外的同步开销。\n（3）使用final关键字，JVM会对方法、变量及类进行优化。\n关于final的重要知识点\n1、final关键字可以用于成员变量、本地变量、方法以及类。\n2、final成员变量必须在声明的时候初始化或者在构造器中初始化，否则就会报编译错误。\n3、你不能够对final变量再次赋值。\n4、本地变量必须在声明时赋值。\n5、在匿名类中所有变量都必须是final变量。\n6、final方法不能被重写。\n7、final类不能被继承。\n8、final关键字不同于finally关键字，后者用于异常处理。\n9、final关键字容易与finalize()方法搞混，后者是在Object类中定义的方法，是在垃圾回收之前被JVM调用的方法。\n10、接口中声明的所有变量本身是final的。\n11、final和abstract这两个关键字是反相关的，final类就不可能是abstract的。\n12、final方法在编译阶段绑定，称为静态绑定(static binding)。\n13、没有在声明时初始化final变量的称为空白final变量(blank final variable)，它们必须在构造器中初始化，或者调用this()初始化。不这么做的话，编译器会报错“final变量(变量名)需要进行初始化”。\n14、将类、方法、变量声明为final能够提高性能，这样JVM就有机会进行估计，然后优化。\n15、按照Java代码惯例，final变量就是常量，而且通常常量名要大写。\n16、对于集合对象声明为final指的是引用不能被更改，但是你可以向其中增加，删除或者改变内容。\n参考链接：Java中final实现原理的深入分析（附示例）-java教程-PHP中文网 7.3 无状态 在 web 阶段学习时，设计 Servlet 时为了保证其线程安全，都会有这样的建议，不要为 Servlet 设置成员变量，这 种没有任何成员变量的类是线程安全的 。\n因为成员变量保存的数据也可以称为状态信息，因此没有成员变量就称之为【无状态】\n8.共享模型之工具 线程池 自定义线程池 步骤1：自定义拒绝策略接口\n1 2 3 4 @FunctionalInterface //拒绝策略 interface RejectPolicy\u0026lt;T\u0026gt;{ void reject(BlockingQueue\u0026lt;T\u0026gt; queue,T task); } Copied! 步骤2：自定义任务队列\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 class BlockingQueue\u0026lt;T\u0026gt;{ //阻塞队列，存放任务 private Deque\u0026lt;T\u0026gt; queue = new ArrayDeque\u0026lt;\u0026gt;(); //队列的最大容量 private int capacity; //锁 private ReentrantLock lock = new ReentrantLock(); //生产者条件变量 private Condition fullWaitSet = lock.newCondition(); //消费者条件变量 private Condition emptyWaitSet = lock.newCondition(); //构造方法 public BlockingQueue(int capacity) { this.capacity = capacity; } //超时阻塞获取 public T poll(long timeout, TimeUnit unit){ lock.lock(); //将时间转换为纳秒 long nanoTime = unit.toNanos(timeout); try{ while(queue.size() == 0){ try { //等待超时依旧没有获取，返回null if(nanoTime \u0026lt;= 0){ return null; } //该方法返回的是剩余时间 nanoTime = emptyWaitSet.awaitNanos(nanoTime); } catch (InterruptedException e) { e.printStackTrace(); } } T t = queue.pollFirst(); fullWaitSet.signal(); return t; }finally { lock.unlock(); } } //阻塞获取 public T take(){ lock.lock(); try{ while(queue.size() == 0){ try { emptyWaitSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } T t = queue.pollFirst(); fullWaitSet.signal(); return t; }finally { lock.unlock(); } } //阻塞添加 public void put(T t){ lock.lock(); try{ while (queue.size() == capacity){ try { System.out.println(Thread.currentThread().toString() + \u0026#34;等待加入任务队列:\u0026#34; + t.toString()); fullWaitSet.await(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().toString() + \u0026#34;加入任务队列:\u0026#34; + t.toString()); queue.addLast(t); emptyWaitSet.signal(); }finally { lock.unlock(); } } //超时阻塞添加 public boolean offer(T t,long timeout,TimeUnit timeUnit){ lock.lock(); try{ long nanoTime = timeUnit.toNanos(timeout); while (queue.size() == capacity){ try { if(nanoTime \u0026lt;= 0){ System.out.println(\u0026#34;等待超时，加入失败：\u0026#34; + t); return false; } System.out.println(Thread.currentThread().toString() + \u0026#34;等待加入任务队列:\u0026#34; + t.toString()); nanoTime = fullWaitSet.awaitNanos(nanoTime); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().toString() + \u0026#34;加入任务队列:\u0026#34; + t.toString()); queue.addLast(t); emptyWaitSet.signal(); return true; }finally { lock.unlock(); } } public int size(){ lock.lock(); try{ return queue.size(); }finally{ lock.unlock(); } } //从形参接收拒绝策略的put方法 public void tryPut(RejectPolicy\u0026lt;T\u0026gt; rejectPolicy,T task){ lock.lock(); try{ if(queue.size() == capacity){ rejectPolicy.reject(this,task); }else{ System.out.println(\u0026#34;加入任务队列：\u0026#34; + task); queue.addLast(task); emptyWaitSet.signal(); } }finally { lock.unlock(); } } } Copied! 步骤3：自定义线程池\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 class ThreadPool{ //阻塞队列 BlockingQueue\u0026lt;Runnable\u0026gt; taskQue; //线程集合 HashSet\u0026lt;Worker\u0026gt; workers = new HashSet\u0026lt;\u0026gt;(); //拒绝策略 private RejectPolicy\u0026lt;Runnable\u0026gt; rejectPolicy; //构造方法 public ThreadPool(int coreSize,long timeout,TimeUnit timeUnit,int queueCapacity,RejectPolicy\u0026lt;Runnable\u0026gt; rejectPolicy){ this.coreSize = coreSize; this.timeout = timeout; this.timeUnit = timeUnit; this.rejectPolicy = rejectPolicy; taskQue = new BlockingQueue\u0026lt;Runnable\u0026gt;(queueCapacity); } //线程数 private int coreSize; //任务超时时间 private long timeout; //时间单元 private TimeUnit timeUnit; //线程池的执行方法 public void execute(Runnable task){ //当线程数大于等于coreSize的时候，将任务放入阻塞队列 //当线程数小于coreSize的时候，新建一个Worker放入workers //注意workers类不是线程安全的， 需要加锁 synchronized (workers){ if(workers.size() \u0026gt;= coreSize){ // taskQue.put(task); //死等 //带超时等待 //让调用者放弃执行任务 //让调用者抛出异常 //让调用者自己执行任务 taskQue.tryPut(rejectPolicy,task); }else { Worker worker = new Worker(task); System.out.println(Thread.currentThread().toString() + \u0026#34;新增worker:\u0026#34; + worker + \u0026#34;,task:\u0026#34; + task); workers.add(worker); worker.start(); } } } //工作类 class Worker extends Thread{ private Runnable task; public Worker(Runnable task){ this.task = task; } @Override public void run() { //巧妙的判断 while(task != null || (task = taskQue.poll(timeout,timeUnit)) != null){ try{ System.out.println(Thread.currentThread().toString() + \u0026#34;正在执行:\u0026#34; + task); task.run(); }catch (Exception e){ }finally { task = null; } } synchronized (workers){ System.out.println(Thread.currentThread().toString() + \u0026#34;worker被移除:\u0026#34; + this.toString()); workers.remove(this); } } } } Copied! 步骤4：编写测试类\n1 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 public class ThreadPoolTest { public static void main(String[] args) { ThreadPool threadPool = new ThreadPool(1, 1000, TimeUnit.MILLISECONDS, 1, (queue,task)-\u0026gt;{ //死等 // queue.put(task); //带超时等待 // queue.offer(task, 1500, TimeUnit.MILLISECONDS); //让调用者放弃任务执行 // System.out.println(\u0026#34;放弃：\u0026#34; + task); //让调用者抛出异常 // throw new RuntimeException(\u0026#34;任务执行失败\u0026#34; + task); //让调用者自己执行任务 task.run(); }); for (int i = 0; i \u0026lt;3; i++) { int j = i; threadPool.execute(()-\u0026gt;{ try { System.out.println(Thread.currentThread().toString() + \u0026#34;执行任务：\u0026#34; + j); Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } }); } } } Copied! ThreadPoolExecutor 说明：\nScheduledThreadPoolExecutor是带调度的线程池 ThreadPoolExecutor是不带调度的线程池 线程池状态 ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态，低 29 位表示线程数量\n状态名 高3位 接收新任务 处理阻塞队列任务 说明 RUNNING 111 Y Y SHUTDOWN 000 N Y 不会接收新任务，但会处理阻塞队列剩余 任务 STOP 001 N N 会中断正在执行的任务，并抛弃阻塞队列 任务 TIDYING 010 任务全执行完毕，活动线程为 0 即将进入 终结 TERMINATED 011 终结状态 从数字上比较，TERMINATED \u0026gt; TIDYING \u0026gt; STOP \u0026gt; SHUTDOWN \u0026gt; RUNNING\n这些信息存储在一个原子变量 ctl 中，目的是将线程池状态与线程个数合二为一，这样就可以用一次 cas 原子操作 进行赋值\n1 2 3 4 // c 为旧值， ctlOf 返回结果为新值 ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))); // rs 为高 3 位代表线程池状态， wc 为低 29 位代表线程个数，ctl 是合并它们 private static int ctlOf(int rs, int wc) { return rs | wc; } Copied! 构造方法 1 2 3 4 5 6 7 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue\u0026lt;Runnable\u0026gt; workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) Copied! corePoolSize 核心线程数目 (最多保留的线程数) maximumPoolSize 最大线程数目 keepAliveTime 生存时间 - 针对救急线程 unit 时间单位 - 针对救急线程 workQueue 阻塞队列 threadFactory 线程工厂 - 可以为线程创建时起个好名字 handler 拒绝策略 工作方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 graph LR subgraph 阻塞队列 size=2 t3(任务3) t4(任务4) end subgraph 线程池c-2,m=3 ct1(核心线程1) ct2(核心线程2) mt1(救急线程1) ct1 --\u0026gt; t1(任务1) ct2 --\u0026gt; t2(任务2) end t1(任务1) style ct1 fill:#ccf,stroke:#f66,stroke-width:2px style ct2 fill:#ccf,stroke:#f66,stroke-width:2px style mt1 fill:#ccf,stroke:#f66,stroke-width:2px,stroke-dasharray:5,5 Copied! 线程池中刚开始没有线程，当一个任务提交给线程池后，线程池会创建一个新线程来执行任务。\n当线程数达到 corePoolSize 并没有线程空闲，这时再加入任务，新加的任务会被加入workQueue 队列排 队，直到有空闲的线程。\n如果队列选择了有界队列，那么任务超过了队列大小时，会创建 maximumPoolSize - corePoolSize 数目的线程来救急。\n如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现，其它 著名框架也提供了实现\nAbortPolicy 让调用者抛出 RejectedExecutionException 异常，这是默认策略 CallerRunsPolicy 让调用者运行任务 DiscardPolicy 放弃本次任务 DiscardOldestPolicy 放弃队列中最早的任务，本任务取而代之 Dubbo 的实现，在抛出 RejectedExecutionException 异常之前会记录日志，并 dump 线程栈信息，方 便定位问题 Netty 的实现，是创建一个新线程来执行任务 ActiveMQ 的实现，带超时等待（60s）尝试放入队列，类似我们之前自定义的拒绝策略 PinPoint 的实现，它使用了一个拒绝策略链，会逐一尝试策略链中每种拒绝策略 当高峰过去后，超过corePoolSize 的救急线程如果一段时间没有任务做，需要结束节省资源，这个时间由 keepAliveTime 和 unit 来控制。\n根据这个构造方法，JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池。\nnewFixedThreadPool 1 2 3 4 5 public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue\u0026lt;Runnable\u0026gt;()); } Copied! 内部调用了：ThreadPoolExecutor的一个构造方法\n1 2 3 4 5 6 7 8 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue\u0026lt;Runnable\u0026gt; workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); } Copied! 默认工厂以及默认构造线程的方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = \u0026#34;pool-\u0026#34; + poolNumber.getAndIncrement() + \u0026#34;-thread-\u0026#34;; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } Copied! 默认拒绝策略：抛出异常\n1 private static final RejectedExecutionHandler defaultHandler = new AbortPolicy(); Copied! 特点\n核心线程数 == 最大线程数（没有救急线程被创建），因此也无需超时时间 阻塞队列是无界的，可以放任意数量的任务 评价 适用于任务量已知，相对耗时的任务\nnewCachedThreadPool 1 2 3 4 5 public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue\u0026lt;Runnable\u0026gt;()); } Copied! 特点\n核心线程数是 0， 最大线程数是 Integer.MAX_VALUE，救急线程的空闲生存时间是 60s， 意味着全部都是救急线程（60s 后可以回收） 救急线程可以无限创建 队列采用了 SynchronousQueue 实现特点是，它没有容量，没有线程来取是放不进去的（一手交钱、一手交货） 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 SynchronousQueue\u0026lt;Integer\u0026gt; integers = new SynchronousQueue\u0026lt;\u0026gt;(); new Thread(() -\u0026gt; { try { log.debug(\u0026#34;putting {} \u0026#34;, 1); integers.put(1); log.debug(\u0026#34;{} putted...\u0026#34;, 1); log.debug(\u0026#34;putting...{} \u0026#34;, 2); integers.put(2); log.debug(\u0026#34;{} putted...\u0026#34;, 2); } catch (InterruptedException e) { e.printStackTrace(); } },\u0026#34;t1\u0026#34;).start(); sleep(1); new Thread(() -\u0026gt; { try { log.debug(\u0026#34;taking {}\u0026#34;, 1); integers.take(); } catch (InterruptedException e) { e.printStackTrace(); } },\u0026#34;t2\u0026#34;).start(); sleep(1); new Thread(() -\u0026gt; { try { log.debug(\u0026#34;taking {}\u0026#34;, 2); integers.take(); } catch (InterruptedException e) { e.printStackTrace(); } },\u0026#34;t3\u0026#34;).start(); Copied! 输出\n1 2 3 4 5 6 11:48:15.500 c.TestSynchronousQueue [t1] - putting 1 11:48:16.500 c.TestSynchronousQueue [t2] - taking 1 11:48:16.500 c.TestSynchronousQueue [t1] - 1 putted... 11:48:16.500 c.TestSynchronousQueue [t1] - putting...2 11:48:17.502 c.TestSynchronousQueue [t3] - taking 2 11:48:17.503 c.TestSynchronousQueue [t1] - 2 putted... Copied! 评价 整个线程池表现为线程数会根据任务量不断增长，没有上限，当任务执行完毕，空闲 1分钟后释放线 程。 适合任务数比较密集，但每个任务执行时间较短的情况\nnewSingleThreadExecutor 1 2 3 4 5 6 public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue\u0026lt;Runnable\u0026gt;())); } Copied! 使用场景：\n希望多个任务排队执行。线程数固定为 1，任务数多于 1 时，会放入无界队列排队。任务执行完毕，这唯一的线程 也不会被释放。\n区别：\n自己创建一个单线程串行执行任务，如果任务执行失败而终止那么没有任何补救措施，而线程池还会新建一 个线程，保证池的正常工作 Executors.newSingleThreadExecutor() 线程个数始终为1，不能修改 FinalizableDelegatedExecutorService 应用的是装饰器模式，在调用构造方法时将ThreadPoolExecutor对象传给了内部的ExecutorService接口。只对外暴露了 ExecutorService 接口，因此不能调用 ThreadPoolExecutor 中特有的方法，也不能重新设置线程池的大小。 Executors.newFixedThreadPool(1) 初始时为1，以后还可以修改 对外暴露的是 ThreadPoolExecutor 对象，可以强转后调用 setCorePoolSize 等方法进行修改 提交任务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 执行任务 void execute(Runnable command); // 提交任务 task，用返回值 Future 获得任务执行结果 \u0026lt;T\u0026gt; Future\u0026lt;T\u0026gt; submit(Callable\u0026lt;T\u0026gt; task); // 提交 tasks 中所有任务 \u0026lt;T\u0026gt; List\u0026lt;Future\u0026lt;T\u0026gt;\u0026gt; invokeAll(Collection\u0026lt;? extends Callable\u0026lt;T\u0026gt;\u0026gt; tasks) throws InterruptedException; // 提交 tasks 中所有任务，带超时时间，时间超时后，会放弃执行后面的任务 \u0026lt;T\u0026gt; List\u0026lt;Future\u0026lt;T\u0026gt;\u0026gt; invokeAll(Collection\u0026lt;? extends Callable\u0026lt;T\u0026gt;\u0026gt; tasks, long timeout, TimeUnit unit) throws InterruptedException; // 提交 tasks 中所有任务，哪个任务先成功执行完毕，返回此任务执行结果，其它任务取消 \u0026lt;T\u0026gt; T invokeAny(Collection\u0026lt;? extends Callable\u0026lt;T\u0026gt;\u0026gt; tasks) throws InterruptedException, ExecutionException; // 提交 tasks 中所有任务，哪个任务先成功执行完毕，返回此任务执行结果，其它任务取消，带超时时间 \u0026lt;T\u0026gt; T invokeAny(Collection\u0026lt;? extends Callable\u0026lt;T\u0026gt;\u0026gt; tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; Copied! 测试submit\n1 2 3 4 5 6 7 8 9 10 11 12 13 private static void method1(ExecutorService pool) throws InterruptedException, ExecutionException { Future\u0026lt;String\u0026gt; future = pool.submit(() -\u0026gt; { log.debug(\u0026#34;running\u0026#34;); Thread.sleep(1000); return \u0026#34;ok\u0026#34;; }); log.debug(\u0026#34;{}\u0026#34;, future.get()); } public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService pool = Executors.newFixedThreadPool(1); method1(pool); } Copied! 测试结果\n1 2 18:36:58.033 c.TestSubmit [pool-1-thread-1] - running 18:36:59.034 c.TestSubmit [main] - ok Copied! 测试invokeAll\n1 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 private static void method2(ExecutorService pool) throws InterruptedException { List\u0026lt;Future\u0026lt;String\u0026gt;\u0026gt; futures = pool.invokeAll(Arrays.asList( () -\u0026gt; { log.debug(\u0026#34;begin\u0026#34;); Thread.sleep(1000); return \u0026#34;1\u0026#34;; }, () -\u0026gt; { log.debug(\u0026#34;begin\u0026#34;); Thread.sleep(500); return \u0026#34;2\u0026#34;; }, () -\u0026gt; { log.debug(\u0026#34;begin\u0026#34;); Thread.sleep(2000); return \u0026#34;3\u0026#34;; } )); futures.forEach( f -\u0026gt; { try { log.debug(\u0026#34;{}\u0026#34;, f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); } public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService pool = Executors.newFixedThreadPool(1); method2(pool); } Copied! 测试结果\n1 2 3 4 5 6 19:33:16.530 c.TestSubmit [pool-1-thread-1] - begin 19:33:17.530 c.TestSubmit [pool-1-thread-1] - begin 19:33:18.040 c.TestSubmit [pool-1-thread-1] - begin 19:33:20.051 c.TestSubmit [main] - 1 19:33:20.051 c.TestSubmit [main] - 2 19:33:20.051 c.TestSubmit [main] - 3 Copied! 测试invokeAny\n1 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 private static void method3(ExecutorService pool) throws InterruptedException, ExecutionException { String result = pool.invokeAny(Arrays.asList( () -\u0026gt; { log.debug(\u0026#34;begin 1\u0026#34;); Thread.sleep(1000); log.debug(\u0026#34;end 1\u0026#34;); return \u0026#34;1\u0026#34;; }, () -\u0026gt; { log.debug(\u0026#34;begin 2\u0026#34;); Thread.sleep(500); log.debug(\u0026#34;end 2\u0026#34;); return \u0026#34;2\u0026#34;; }, () -\u0026gt; { log.debug(\u0026#34;begin 3\u0026#34;); Thread.sleep(2000); log.debug(\u0026#34;end 3\u0026#34;); return \u0026#34;3\u0026#34;; } )); log.debug(\u0026#34;{}\u0026#34;, result); } public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService pool = Executors.newFixedThreadPool(3); //ExecutorService pool = Executors.newFixedThreadPool(1); method3(pool); } Copied! 测试结果\n1 2 3 4 5 6 7 8 9 10 19:44:46.314 c.TestSubmit [pool-1-thread-1] - begin 1 19:44:46.314 c.TestSubmit [pool-1-thread-3] - begin 3 19:44:46.314 c.TestSubmit [pool-1-thread-2] - begin 2 19:44:46.817 c.TestSubmit [pool-1-thread-2] - end 2 19:44:46.817 c.TestSubmit [main] - 2 19:47:16.063 c.TestSubmit [pool-1-thread-1] - begin 1 19:47:17.063 c.TestSubmit [pool-1-thread-1] - end 1 19:47:17.063 c.TestSubmit [pool-1-thread-1] - begin 2 19:47:17.063 c.TestSubmit [main] - 1 Copied! 关闭线程池 shutdown\n1 2 3 4 5 6 7 /* 线程池状态变为 SHUTDOWN - 不会接收新任务 - 但已提交任务会执行完 - 此方法不会阻塞调用线程的执行 */ void shutdown(); Copied! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); // 修改线程池状态 advanceRunState(SHUTDOWN); // 仅会打断空闲线程 interruptIdleWorkers(); onShutdown(); // 扩展点 ScheduledThreadPoolExecutor } finally { mainLock.unlock(); } // 尝试终结(没有运行的线程可以立刻终结，如果还有运行的线程也不会等) tryTerminate(); } Copied! shutdownNow\n1 2 3 4 5 6 7 /* 线程池状态变为 STOP - 不会接收新任务 - 会将队列中的任务返回 - 并用 interrupt 的方式中断正在执行的任务 */ List\u0026lt;Runnable\u0026gt; shutdownNow(); Copied! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public List\u0026lt;Runnable\u0026gt; shutdownNow() { List\u0026lt;Runnable\u0026gt; tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); // 修改线程池状态 advanceRunState(STOP); // 打断所有线程 interruptWorkers(); // 获取队列中剩余任务 tasks = drainQueue(); } finally { mainLock.unlock(); } // 尝试终结 tryTerminate(); return tasks; } Copied! 其他方法\n1 2 3 4 5 6 7 // 不在 RUNNING 状态的线程池，此方法就返回 true boolean isShutdown(); // 线程池状态是否是 TERMINATED boolean isTerminated(); // 调用 shutdown 后，由于调用线程并不会等待所有任务运行结束，因此如果它想在线程池 TERMINATED 后做些事情，可以利用此方法等待 // 一般task是Callable类型的时候不用此方法，因为futureTask.get方法自带等待功能。 boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; Copied! 测试shutdown、shutdownNow、awaitTermination\n1 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 @Slf4j(topic = \u0026#34;c.TestShutDown\u0026#34;) public class TestShutDown { public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService pool = Executors.newFixedThreadPool(2); Future\u0026lt;Integer\u0026gt; result1 = pool.submit(() -\u0026gt; { log.debug(\u0026#34;task 1 running...\u0026#34;); Thread.sleep(1000); log.debug(\u0026#34;task 1 finish...\u0026#34;); return 1; }); Future\u0026lt;Integer\u0026gt; result2 = pool.submit(() -\u0026gt; { log.debug(\u0026#34;task 2 running...\u0026#34;); Thread.sleep(1000); log.debug(\u0026#34;task 2 finish...\u0026#34;); return 2; }); Future\u0026lt;Integer\u0026gt; result3 = pool.submit(() -\u0026gt; { log.debug(\u0026#34;task 3 running...\u0026#34;); Thread.sleep(1000); log.debug(\u0026#34;task 3 finish...\u0026#34;); return 3; }); log.debug(\u0026#34;shutdown\u0026#34;); pool.shutdown(); // pool.awaitTermination(3, TimeUnit.SECONDS); // List\u0026lt;Runnable\u0026gt; runnables = pool.shutdownNow(); // log.debug(\u0026#34;other.... {}\u0026#34; , runnables); } } Copied! 测试结果\n1 2 3 4 5 6 7 8 9 10 11 12 13 #shutdown依旧会执行剩下的任务 20:09:13.285 c.TestShutDown [main] - shutdown 20:09:13.285 c.TestShutDown [pool-1-thread-1] - task 1 running... 20:09:13.285 c.TestShutDown [pool-1-thread-2] - task 2 running... 20:09:14.293 c.TestShutDown [pool-1-thread-2] - task 2 finish... 20:09:14.293 c.TestShutDown [pool-1-thread-1] - task 1 finish... 20:09:14.293 c.TestShutDown [pool-1-thread-2] - task 3 running... 20:09:15.303 c.TestShutDown [pool-1-thread-2] - task 3 finish... #shutdownNow立刻停止所有任务 20:11:11.750 c.TestShutDown [main] - shutdown 20:11:11.750 c.TestShutDown [pool-1-thread-1] - task 1 running... 20:11:11.750 c.TestShutDown [pool-1-thread-2] - task 2 running... 20:11:11.750 c.TestShutDown [main] - other.... [java.util.concurrent.FutureTask@66d33a] Copied! *模式之 Worker Thread 定义\n让有限的工作线程（Worker Thread）来轮流异步处理无限多的任务。也可以将其归类为分工模式，它的典型实现 就是线程池，也体现了经典设计模式中的享元模式。\n例如，海底捞的服务员（线程），轮流处理每位客人的点餐（任务），如果为每位客人都配一名专属的服务员，那 么成本就太高了（对比另一种多线程设计模式：Thread-Per-Message）\n注意，不同任务类型应该使用不同的线程池，这样能够避免饥饿，并能提升效率\n例如，如果一个餐馆的工人既要招呼客人（任务类型A），又要到后厨做菜（任务类型B）显然效率不咋地，分成 服务员（线程池A）与厨师（线程池B）更为合理，当然你能想到更细致的分工\n饥饿\n固定大小线程池会有饥饿现象\n两个工人是同一个线程池中的两个线程\n他们要做的事情是：为客人点餐和到后厨做菜，这是两个阶段的工作\n客人点餐：必须先点完餐，等菜做好，上菜，在此期间处理点餐的工人必须等待 后厨做菜：没啥说的，做就是了 比如工人A 处理了点餐任务，接下来它要等着 工人B 把菜做好，然后上菜，他俩也配合的蛮好\n但现在同时来了两个客人，这个时候工人A 和工人B 都去处理点餐了，这时没人做饭了，饥饿\n1 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 public class TestDeadLock { static final List\u0026lt;String\u0026gt; MENU = Arrays.asList(\u0026#34;地三鲜\u0026#34;, \u0026#34;宫保鸡丁\u0026#34;, \u0026#34;辣子鸡丁\u0026#34;, \u0026#34;烤鸡翅\u0026#34;); static Random RANDOM = new Random(); static String cooking() { return MENU.get(RANDOM.nextInt(MENU.size())); } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(2); executorService.execute(() -\u0026gt; { log.debug(\u0026#34;处理点餐...\u0026#34;); Future\u0026lt;String\u0026gt; f = executorService.submit(() -\u0026gt; { log.debug(\u0026#34;做菜\u0026#34;); return cooking(); }); try { log.debug(\u0026#34;上菜: {}\u0026#34;, f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); /* executorService.execute(() -\u0026gt; { log.debug(\u0026#34;处理点餐...\u0026#34;); Future\u0026lt;String\u0026gt; f = executorService.submit(() -\u0026gt; { log.debug(\u0026#34;做菜\u0026#34;); return cooking(); }); try { log.debug(\u0026#34;上菜: {}\u0026#34;, f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); */ } } Copied! 输出\n1 2 3 17:21:27.883 c.TestDeadLock [pool-1-thread-1] - 处理点餐... 17:21:27.891 c.TestDeadLock [pool-1-thread-2] - 做菜 17:21:27.891 c.TestDeadLock [pool-1-thread-1] - 上菜: 烤鸡翅 Copied! 当注释取消后，可能的输出\n1 2 17:08:41.339 c.TestDeadLock [pool-1-thread-2] - 处理点餐... 17:08:41.339 c.TestDeadLock [pool-1-thread-1] - 处理点餐... Copied! 解决方法可以增加线程池的大小，不过不是根本解决方案，还是前面提到的，不同的任务类型，采用不同的线程 池，例如：\n1 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 public class TestDeadLock { static final List\u0026lt;String\u0026gt; MENU = Arrays.asList(\u0026#34;地三鲜\u0026#34;, \u0026#34;宫保鸡丁\u0026#34;, \u0026#34;辣子鸡丁\u0026#34;, \u0026#34;烤鸡翅\u0026#34;); static Random RANDOM = new Random(); static String cooking() { return MENU.get(RANDOM.nextInt(MENU.size())); } public static void main(String[] args) { ExecutorService waiterPool = Executors.newFixedThreadPool(1); ExecutorService cookPool = Executors.newFixedThreadPool(1); waiterPool.execute(() -\u0026gt; { log.debug(\u0026#34;处理点餐...\u0026#34;); Future\u0026lt;String\u0026gt; f = cookPool.submit(() -\u0026gt; { log.debug(\u0026#34;做菜\u0026#34;); return cooking(); }); try { log.debug(\u0026#34;上菜: {}\u0026#34;, f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); waiterPool.execute(() -\u0026gt; { log.debug(\u0026#34;处理点餐...\u0026#34;); Future\u0026lt;String\u0026gt; f = cookPool.submit(() -\u0026gt; { log.debug(\u0026#34;做菜\u0026#34;); return cooking(); }); try { log.debug(\u0026#34;上菜: {}\u0026#34;, f.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } }); } } Copied! 输出\n1 2 3 4 5 6 17:25:14.626 c.TestDeadLock [pool-1-thread-1] - 处理点餐... 17:25:14.630 c.TestDeadLock [pool-2-thread-1] - 做菜 17:25:14.631 c.TestDeadLock [pool-1-thread-1] - 上菜: 地三鲜 17:25:14.632 c.TestDeadLock [pool-1-thread-1] - 处理点餐... 17:25:14.632 c.TestDeadLock [pool-2-thread-1] - 做菜 17:25:14.632 c.TestDeadLock [pool-1-thread-1] - 上菜: 辣子鸡丁 Copied! 创建多少线程池合适\n过小会导致程序不能充分地利用系统资源、容易导致饥饿 过大会导致更多的线程上下文切换，占用更多内存 CPU 密集型运算\n通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率，+1 是保证当线程由于页缺失故障（操作系统）或其它原因 导致暂停时，额外的这个线程就能顶上去，保证 CPU 时钟周期不被浪费\nI/O 密集型运算\nCPU 不总是处于繁忙状态，例如，当你执行业务计算时，这时候会使用 CPU 资源，但当你执行 I/O 操作时、远程 RPC 调用时，包括进行数据库操作时，这时候 CPU 就闲下来了，你可以利用多线程提高它的利用率。\n经验公式如下\n线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间\n例如 4 核 CPU 计算时间是 50% ，其它等待时间是 50%，期望 cpu 被 100% 利用，套用公式\n4 * 100% * 100% / 50% = 8\n例如 4 核 CPU 计算时间是 10% ，其它等待时间是 90%，期望 cpu 被 100% 利用，套用公式\n4 * 100% * 100% / 10% = 40\n任务调度线程池 在『任务调度线程池』功能加入之前(JDK1.3)，可以使用 java.util.Timer 来实现定时功能，Timer 的优点在于简单易用，但 由于所有任务都是由同一个线程来调度，因此所有任务都是串行执行的，同一时间只能有一个任务在执行，前一个 任务的延迟或异常都将会影响到之后的任务。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main(String[] args) { Timer timer = new Timer(); TimerTask task1 = new TimerTask() { @Override public void run() { log.debug(\u0026#34;task 1\u0026#34;); sleep(2); } }; TimerTask task2 = new TimerTask() { @Override public void run() { log.debug(\u0026#34;task 2\u0026#34;); } }; // 使用 timer 添加两个任务，希望它们都在 1s 后执行 // 但由于 timer 内只有一个线程来顺序执行队列中的任务，因此『任务1』的延时，影响了『任务2』的执行 timer.schedule(task1, 1000); timer.schedule(task2, 1000); } Copied! 输出\n1 2 3 20:46:09.444 c.TestTimer [main] - start... 20:46:10.447 c.TestTimer [Timer-0] - task 1 20:46:12.448 c.TestTimer [Timer-0] - task 2 Copied! 使用 ScheduledExecutorService 改写：\n1 2 3 4 5 6 7 8 9 ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); // 添加两个任务，希望它们都在 1s 后执行 executor.schedule(() -\u0026gt; { System.out.println(\u0026#34;任务1，执行时间：\u0026#34; + new Date()); try { Thread.sleep(2000); } catch (InterruptedException e) { } }, 1000, TimeUnit.MILLISECONDS); executor.schedule(() -\u0026gt; { System.out.println(\u0026#34;任务2，执行时间：\u0026#34; + new Date()); }, 1000, TimeUnit.MILLISECONDS); Copied! 输出\n1 2 任务1，执行时间：Thu Jan 03 12:45:17 CST 2019 任务2，执行时间：Thu Jan 03 12:45:17 CST 2019 Copied! scheduleAtFixedRate 例子：\n1 2 3 4 5 ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); log.debug(\u0026#34;start...\u0026#34;); pool.scheduleAtFixedRate(() -\u0026gt; { log.debug(\u0026#34;running...\u0026#34;); }, 1, 1, TimeUnit.SECONDS); Copied! 输出\n1 2 3 4 5 21:45:43.167 c.TestTimer [main] - start... 21:45:44.215 c.TestTimer [pool-1-thread-1] - running... 21:45:45.215 c.TestTimer [pool-1-thread-1] - running... 21:45:46.215 c.TestTimer [pool-1-thread-1] - running... 21:45:47.215 c.TestTimer [pool-1-thread-1] - running... Copied! scheduleAtFixedRate 例子（任务执行时间超过了间隔时间）：\n1 2 3 4 5 6 ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); log.debug(\u0026#34;start...\u0026#34;); pool.scheduleAtFixedRate(() -\u0026gt; { log.debug(\u0026#34;running...\u0026#34;); sleep(2); }, 1, 1, TimeUnit.SECONDS); Copied! 输出分析：一开始，延时 1s，接下来，由于任务执行时间 \u0026gt; 间隔时间，间隔被『撑』到了 2s\n1 2 3 4 5 21:44:30.311 c.TestTimer [main] - start... 21:44:31.360 c.TestTimer [pool-1-thread-1] - running... 21:44:33.361 c.TestTimer [pool-1-thread-1] - running... 21:44:35.362 c.TestTimer [pool-1-thread-1] - running... 21:44:37.362 c.TestTimer [pool-1-thread-1] - running... Copied! scheduleWithFixedDelay 例子：\n1 2 3 4 5 6 ScheduledExecutorService pool = Executors.newScheduledThreadPool(1); log.debug(\u0026#34;start...\u0026#34;); pool.scheduleWithFixedDelay(()-\u0026gt; { log.debug(\u0026#34;running...\u0026#34;); sleep(2); }, 1, 1, TimeUnit.SECONDS); Copied! 输出分析：一开始，延时 1s，scheduleWithFixedDelay 的间隔是 上一个任务结束 \u0026lt;-\u0026gt; 延时 \u0026lt;-\u0026gt; 下一个任务开始 所 以间隔都是 3s\n1 2 3 4 5 21:40:55.078 c.TestTimer [main] - start... 21:40:56.140 c.TestTimer [pool-1-thread-1] - running... 21:40:59.143 c.TestTimer [pool-1-thread-1] - running... 21:41:02.145 c.TestTimer [pool-1-thread-1] - running... 21:41:05.147 c.TestTimer [pool-1-thread-1] - running... Copied! 评价 整个线程池表现为：线程数固定，任务数多于线程数时，会放入无界队列排队。任务执行完毕，这些线 程也不会被释放。用来执行延迟或反复执行的任务\n正确处理执行任务异常 不论是哪个线程池，在线程执行的任务发生异常后既不会抛出，也不会捕获，这时就需要我们做一定的处理。\n方法1：主动捉异常\n1 2 3 4 5 6 7 8 9 ExecutorService pool = Executors.newFixedThreadPool(1); pool.submit(() -\u0026gt; { try { log.debug(\u0026#34;task1\u0026#34;); int i = 1 / 0; } catch (Exception e) { log.error(\u0026#34;error:\u0026#34;, e); } }); Copied! 输出\n1 2 3 4 5 6 7 8 9 21:59:04.558 c.TestTimer [pool-1-thread-1] - task1 21:59:04.562 c.TestTimer [pool-1-thread-1] - error: java.lang.ArithmeticException: / by zero at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:28) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Copied! 方法2：使用 Future\n说明：\nlambda表达式内要有返回值，编译器才能将其识别为Callable，否则将识别为Runnable，也就不能用FutureTask 方法中如果出异常，futuretask.get会返回这个异常，否者正常返回。 1 2 3 4 5 6 7 ExecutorService pool = Executors.newFixedThreadPool(1); Future\u0026lt;Boolean\u0026gt; f = pool.submit(() -\u0026gt; { log.debug(\u0026#34;task1\u0026#34;); int i = 1 / 0; return true; }); log.debug(\u0026#34;result:{}\u0026#34;, f.get()); Copied! 输出\n1 2 3 4 5 6 7 8 9 10 11 12 21:54:58.208 c.TestTimer [pool-1-thread-1] - task1 Exception in thread \u0026#34;main\u0026#34; java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero at java.util.concurrent.FutureTask.report(FutureTask.java:122) at java.util.concurrent.FutureTask.get(FutureTask.java:192) at cn.itcast.n8.TestTimer.main(TestTimer.java:31) Caused by: java.lang.ArithmeticException: / by zero at cn.itcast.n8.TestTimer.lambda$main$0(TestTimer.java:28) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Copied! * 应用之定时任务 如何让每周四 18:00:00 定时执行任务？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 获得当前时间 LocalDateTime now = LocalDateTime.now(); // 获取本周四 18:00:00.000 LocalDateTime thursday = now.with(DayOfWeek.THURSDAY).withHour(18).withMinute(0).withSecond(0).withNano(0); // 如果当前时间已经超过 本周四 18:00:00.000， 那么找下周四 18:00:00.000 if(now.compareTo(thursday) \u0026gt;= 0) { thursday = thursday.plusWeeks(1); } // 计算时间差，即延时执行时间 long initialDelay = Duration.between(now, thursday).toMillis(); // 计算间隔时间，即 1 周的毫秒值 long oneWeek = 7 * 24 * 3600 * 1000; ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); System.out.println(\u0026#34;开始时间：\u0026#34; + new Date()); executor.scheduleAtFixedRate(() -\u0026gt; { System.out.println(\u0026#34;执行时间：\u0026#34; + new Date()); }, initialDelay, oneWeek, TimeUnit.MILLISECONDS); Copied! Tomcat 线程池 Tomcat 在哪里用到了线程池呢\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 graph LR subgraph Connector-\u0026gt;NIO EndPoint t1(LimitLatch) t2(Acceptor) t3(SocketChannel 1) t4(SocketChannel 2) t5(Poller) subgraph Executor t7(worker1) t8(worker2) end t1 --\u0026gt; t2 t2 --\u0026gt; t3 t2 --\u0026gt; t4 t3 --有读--\u0026gt; t5 t4 --有读--\u0026gt; t5 t5 --socketProcessor--\u0026gt; t7 t5 --socketProcessor--\u0026gt; t8 end Copied! LimitLatch 用来限流，可以控制最大连接个数，类似 J.U.C 中的 Semaphore 后面再讲 Acceptor 只负责【接收新的 socket 连接】 Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】 一旦可读，封装一个任务对象（socketProcessor），提交给 Executor 线程池处理 Executor 线程池中的工作线程最终负责【处理请求】 Tomcat 线程池扩展了 ThreadPoolExecutor，行为稍有不同\n如果总线程数达到 maximumPoolSize 这时不会立刻抛 RejectedExecutionException 异常 而是再次尝试将任务放入队列，如果还失败，才抛出 RejectedExecutionException 异常 源码 tomcat-7.0.42\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void execute(Runnable command, long timeout, TimeUnit unit) { submittedCount.incrementAndGet(); try { super.execute(command); } catch (RejectedExecutionException rx) { if (super.getQueue() instanceof TaskQueue) { final TaskQueue queue = (TaskQueue)super.getQueue(); try { if (!queue.force(command, timeout, unit)) { submittedCount.decrementAndGet(); throw new RejectedExecutionException(\u0026#34;Queue capacity is full.\u0026#34;); } } catch (InterruptedException x) { submittedCount.decrementAndGet(); Thread.interrupted(); throw new RejectedExecutionException(x); } } else { submittedCount.decrementAndGet(); throw rx; } } } Copied! TaskQueue.java\n1 2 3 4 5 6 7 8 public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException { if ( parent.isShutdown() ) throw new RejectedExecutionException( \u0026#34;Executor not running, can\u0026#39;t force a command into the queue\u0026#34; ); return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task is rejected } Copied! Connector 配置\n配置项 默认值 说明 acceptorThreadCount 1 acceptor 线程数量 pollerThreadCount 1 poller 线程数量 minSpareThreads 10 核心线程数，即 corePoolSize maxThreads 200 最大线程数，即 maximumPoolSize executor - Executor 名称，用来引用下面的 Executor Executor 线程配置\n配置项 默认值 说明 threadPriority 5 线程优先级 deamon true 是否守护线程 minSpareThreads 25 核心线程数，即corePoolSize maxThreads 200 最大线程数，即 maximumPoolSize maxIdleTime 60000 线程生存时间，单位是毫秒，默认值即 1 分钟 maxQueueSize Integer.MAX_VALUE 队列长度 prestartminSpareThreads false 核心线程是否在服务器启动时启动 Fork/Join 概念 Fork/Join 是 JDK 1.7 加入的新的线程池实现，它体现的是一种分治思想，适用于能够进行任务拆分的 cpu 密集型 运算\n所谓的任务拆分，是将一个大任务拆分为算法上相同的小任务，直至不能拆分可以直接求解。跟递归相关的一些计 算，如归并排序、斐波那契数列、都可以用分治思想进行求解\nFork/Join 在分治的基础上加入了多线程，可以把每个任务的分解和合并交给不同的线程来完成，进一步提升了运 算效率\nFork/Join 默认会创建与 cpu 核心数大小相同的线程池\n应用之求和 提交给 Fork/Join 线程池的任务需要继承 RecursiveTask（有返回值）或 RecursiveAction（没有返回值），例如下 面定义了一个对 1~n 之间的整数求和的任务\n1 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 @Slf4j(topic = \u0026#34;c.AddTask\u0026#34;) class AddTask1 extends RecursiveTask\u0026lt;Integer\u0026gt; { int n; public AddTask1(int n) { this.n = n; } @Override public String toString() { return \u0026#34;{\u0026#34; + n + \u0026#39;}\u0026#39;; } @Override protected Integer compute() { // 如果 n 已经为 1，可以求得结果了 if (n == 1) { log.debug(\u0026#34;join() {}\u0026#34;, n); return n; } // 将任务进行拆分(fork) AddTask1 t1 = new AddTask1(n - 1); t1.fork(); log.debug(\u0026#34;fork() {} + {}\u0026#34;, n, t1); // 合并(join)结果 int result = n + t1.join(); log.debug(\u0026#34;join() {} + {} = {}\u0026#34;, n, t1, result); return result; } } Copied! 然后提交给 ForkJoinPool 来执行\n1 2 3 4 public static void main(String[] args) { ForkJoinPool pool = new ForkJoinPool(4); System.out.println(pool.invoke(new AddTask1(5))); } Copied! 结果\n1 2 3 4 5 6 7 8 9 10 [ForkJoinPool-1-worker-0] - fork() 2 + {1} [ForkJoinPool-1-worker-1] - fork() 5 + {4} [ForkJoinPool-1-worker-0] - join() 1 [ForkJoinPool-1-worker-0] - join() 2 + {1} = 3 [ForkJoinPool-1-worker-2] - fork() 4 + {3} [ForkJoinPool-1-worker-3] - fork() 3 + {2} [ForkJoinPool-1-worker-3] - join() 3 + {2} = 6 [ForkJoinPool-1-worker-2] - join() 4 + {3} = 10 [ForkJoinPool-1-worker-1] - join() 5 + {4} = 15 15 Copied! 用图来表示\n改进\n1 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 37 class AddTask3 extends RecursiveTask\u0026lt;Integer\u0026gt; { int begin; int end; public AddTask3(int begin, int end) { this.begin = begin; this.end = end; } @Override public String toString() { return \u0026#34;{\u0026#34; + begin + \u0026#34;,\u0026#34; + end + \u0026#39;}\u0026#39;; } @Override protected Integer compute() { // 5, 5 if (begin == end) { log.debug(\u0026#34;join() {}\u0026#34;, begin); return begin; } // 4, 5 if (end - begin == 1) { log.debug(\u0026#34;join() {} + {} = {}\u0026#34;, begin, end, end + begin); return end + begin; } // 1 5 int mid = (end + begin) / 2; // 3 AddTask3 t1 = new AddTask3(begin, mid); // 1,3 t1.fork(); AddTask3 t2 = new AddTask3(mid + 1, end); // 4,5 t2.fork(); log.debug(\u0026#34;fork() {} + {} = ?\u0026#34;, t1, t2); int result = t1.join() + t2.join(); log.debug(\u0026#34;join() {} + {} = {}\u0026#34;, t1, t2, result); return result; } } Copied! 然后提交给 ForkJoinPool 来执行\n1 2 3 4 public static void main(String[] args) { ForkJoinPool pool = new ForkJoinPool(4); System.out.println(pool.invoke(new AddTask3(1, 10))); } Copied! 结果\n1 2 3 4 5 6 7 8 [ForkJoinPool-1-worker-0] - join() 1 + 2 = 3 [ForkJoinPool-1-worker-3] - join() 4 + 5 = 9 [ForkJoinPool-1-worker-0] - join() 3 [ForkJoinPool-1-worker-1] - fork() {1,3} + {4,5} = ? [ForkJoinPool-1-worker-2] - fork() {1,2} + {3,3} = ? [ForkJoinPool-1-worker-2] - join() {1,2} + {3,3} = 6 [ForkJoinPool-1-worker-1] - join() {1,3} + {4,5} = 15 15 Copied! 用图来表示\n*AQS 原理 概述 全称是 AbstractQueuedSynchronizer，是阻塞式锁和相关的同步器工具的框架\n特点：\n用 state 属性来表示资源的状态（分独占模式和共享模式），子类需要定义如何维护这个状态，控制如何获取 锁和释放锁 getState - 获取 state 状态 setState - 设置 state 状态 compareAndSetState - cas 机制设置 state 状态 独占模式是只有一个线程能够访问资源，而共享模式可以允许多个线程访问资源 提供了基于 FIFO 的等待队列，类似于 Monitor 的 EntryList 条件变量来实现等待、唤醒机制，支持多个条件变量，类似于 Monitor 的 WaitSet 子类主要实现这样一些方法（默认抛出 UnsupportedOperationException）\ntryAcquire tryRelease tryAcquireShared tryReleaseShared isHeldExclusively 获取锁的姿势\n1 2 3 4 // 如果获取锁失败 if (!tryAcquire(arg)) { // 入队, 可以选择阻塞当前线程 park unpark } Copied! 释放锁的姿势\n1 2 3 4 // 如果释放锁成功 if (tryRelease(arg)) { // 让阻塞线程恢复运行 } Copied! 实现不可重入锁 自定义同步器 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 final class MySync extends AbstractQueuedSynchronizer { @Override protected boolean tryAcquire(int acquires) { if (acquires == 1){ if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } } return false; } @Override protected boolean tryRelease(int acquires) { if(acquires == 1) { if(getState() == 0) { throw new IllegalMonitorStateException(); } setExclusiveOwnerThread(null); setState(0); return true; } return false; } protected Condition newCondition() { return new ConditionObject(); } @Override protected boolean isHeldExclusively() { return getState() == 1; } } Copied! 自定义锁 有了自定义同步器，很容易复用 AQS ，实现一个功能完备的自定义锁\n1 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 class MyLock implements Lock { static MySync sync = new MySync(); @Override // 尝试，不成功，进入等待队列 public void lock() { sync.acquire(1); } @Override // 尝试，不成功，进入等待队列，可打断 public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } @Override // 尝试一次，不成功返回，不进入队列 public boolean tryLock() { return sync.tryAcquire(1); } @Override // 尝试，不成功，进入等待队列，有时限 public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(time)); } @Override // 释放锁 public void unlock() { sync.release(1); } @Override // 生成条件变量 public Condition newCondition() { return sync.newCondition(); } } Copied! 测试一下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 MyLock lock = new MyLock(); new Thread(() -\u0026gt; { lock.lock(); try { log.debug(\u0026#34;locking...\u0026#34;); sleep(1); } finally { log.debug(\u0026#34;unlocking...\u0026#34;); lock.unlock(); } },\u0026#34;t1\u0026#34;).start(); new Thread(() -\u0026gt; { lock.lock(); try { log.debug(\u0026#34;locking...\u0026#34;); } finally { log.debug(\u0026#34;unlocking...\u0026#34;); lock.unlock(); } },\u0026#34;t2\u0026#34;).start(); Copied! 输出\n1 2 3 4 22:29:28.727 c.TestAqs [t1] - locking... 22:29:29.732 c.TestAqs [t1] - unlocking... 22:29:29.732 c.TestAqs [t2] - locking... 22:29:29.732 c.TestAqs [t2] - unlocking... Copied! 不可重入测试\n如果改为下面代码，会发现自己也会被挡住（只会打印一次 locking）\n1 2 3 4 lock.lock(); log.debug(\u0026#34;locking...\u0026#34;); lock.lock(); log.debug(\u0026#34;locking...\u0026#34;); Copied! 心得 起源 早期程序员会自己通过一种同步器去实现另一种相近的同步器，例如用可重入锁去实现信号量，或反之。这显然不 够优雅，于是在 JSR166（java 规范提案）中创建了 AQS，提供了这种通用的同步器机制。\n目标 AQS 要实现的功能目标\n阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire 获取锁超时机制 通过打断取消机制 独占机制及共享机制 条件不满足时的等待机制 要实现的性能目标\nInstead, the primary performance goal here is scalability: to predictably maintain efficiency even, or especially, when synchronizers are contended.\n设计 AQS 的基本思想其实很简单\n获取锁的逻辑\n1 2 3 4 5 6 while(state 状态不允许获取) { if(队列中还没有此线程) { 入队并阻塞 } } 当前线程出队 Copied! 释放锁的逻辑\n1 2 3 if(state 状态允许了) { 恢复阻塞的线程(s) } Copied! 要点\n原子维护 state 状态 阻塞及恢复线程 维护队列 state 设计 state 使用 volatile 配合 cas 保证其修改时的原子性 state 使用了 32bit int 来维护同步状态，因为当时使用 long 在很多平台下测试的结果并不理想 阻塞恢复设计 早期的控制线程暂停和恢复的 api 有 suspend 和 resume，但它们是不可用的，因为如果先调用的 resume 那么 suspend 将感知不到 解决方法是使用 park \u0026amp; unpark 来实现线程的暂停和恢复，具体原理在之前讲过了，先 unpark 再 park 也没 问题 park \u0026amp; unpark 是针对线程的，而不是针对同步器的，因此控制粒度更为精细 park 线程还可以通过 interrupt 打断 队列设计 使用了 FIFO 先入先出队列，并不支持优先级队列 设计时借鉴了 CLH 队列，它是一种单向无锁队列 队列中有 head 和 tail 两个指针节点，都用 volatile 修饰配合 cas 使用，每个节点有 state 维护节点状态 入队伪代码，只需要考虑 tail 赋值的原子性\n1 2 3 4 5 do { // 原来的 tail Node prev = tail; // 用 cas 在原来 tail 的基础上改为 node } while(tail.compareAndSet(prev, node)) Copied! 出队伪代码\n1 2 3 4 5 // prev 是上一个节点 while((Node prev=node.prev).state != 唤醒状态) { } // 设置头节点 head = node; Copied! CLH 好处：\n无锁，使用自旋 快速，无阻塞 AQS 在一些方面改进了 CLH\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private Node enq(final Node node) { for (;;) { Node t = tail; // 队列中还没有元素 tail 为 null if (t == null) { // 将 head 从 null -\u0026gt; dummy if (compareAndSetHead(new Node())) tail = head; } else { // 将 node 的 prev 设置为原来的 tail node.prev = t; // 将 tail 从原来的 tail 设置为 node if (compareAndSetTail(t, node)) { // 原来 tail 的 next 设置为 node t.next = node; return t; } } } } Copied! 主要用到 AQS 的并发工具类 ReentrantLock 原理 非公平锁实现原理 加锁解锁流程 先从构造器开始看，默认为非公平锁实现\n1 2 3 public ReentrantLock() { sync = new NonfairSync(); } Copied! NonfairSync 继承自 AQS 没有竞争时\n第一个竞争出现时\nThread-1 执行了\nCAS 尝试将 state 由 0 改为 1，结果失败 进入 tryAcquire 逻辑，这时 state 已经是1，结果仍然失败 接下来进入 addWaiter 逻辑，构造 Node 队列 图中黄色三角表示该 Node 的 waitStatus 状态，其中 0 为默认正常状态 Node 的创建是懒惰的 其中第一个 Node 称为 Dummy（哑元）或哨兵，用来占位，并不关联线程 当前线程进入 acquireQueued 逻辑\nacquireQueued 会在一个死循环中不断尝试获得锁，失败后进入 park 阻塞 如果自己是紧邻着 head（排第二位），那么再次 tryAcquire 尝试获取锁，当然这时 state 仍为 1，失败 进入 shouldParkAfterFailedAcquire 逻辑，将前驱 node，即 head 的 waitStatus 改为 -1，这次返回 false shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ，再次 tryAcquire 尝试获取锁，当然这时 state 仍为 1，失败 当再次进入 shouldParkAfterFailedAcquire 时，这时因为其前驱 node 的 waitStatus 已经是 -1，这次返回 true 进入 parkAndCheckInterrupt， Thread-1 park（灰色表示） 再次有多个线程经历上述过程竞争失败，变成这个样子\nThread-0 释放锁，进入 tryRelease 流程，如果成功\n设置 exclusiveOwnerThread 为 null state = 0 当前队列不为 null，并且 head 的 waitStatus = -1，进入 unparkSuccessor 流程\n找到队列中离 head 最近的一个 Node（没取消的），unpark 恢复其运行，本例中即为 Thread-1\n回到 Thread-1 的 acquireQueued 流程\n如果加锁成功（没有竞争），会设置\nexclusiveOwnerThread 为 Thread-1，state = 1 head 指向刚刚 Thread-1 所在的 Node，该 Node 清空 Thread 原本的 head 因为从链表断开，而可被垃圾回收 如果这时候有其它线程来竞争（非公平的体现），例如这时有 Thread-4 来了\n如果不巧又被 Thread-4 占了先\nThread-4 被设置为 exclusiveOwnerThread，state = 1 Thread-1 再次进入 acquireQueued 流程，获取锁失败，重新进入 park 阻塞 加锁源码 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 // Sync 继承自 AQS static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; // 加锁实现 final void lock() { // 首先用 cas 尝试（仅尝试一次）将 state 从 0 改为 1, 如果成功表示获得了独占锁 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else // 如果尝试失败，进入 ㈠ acquire(1); } // ㈠ AQS 继承过来的方法, 方便阅读, 放在此处 public final void acquire(int arg) { // ㈡ tryAcquire if ( !tryAcquire(arg) \u0026amp;\u0026amp; // 当 tryAcquire 返回为 false 时, 先调用 addWaiter ㈣, 接着 acquireQueued ㈤ acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ) { selfInterrupt(); } } // ㈡ 进入 ㈢ protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } // ㈢ Sync 继承过来的方法, 方便阅读, 放在此处 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 如果还没有获得锁 if (c == 0) { // 尝试用 cas 获得, 这里体现了非公平性: 不去检查 AQS 队列 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入 else if (current == getExclusiveOwnerThread()) { // state++ int nextc = c + acquires; if (nextc \u0026lt; 0) // overflow throw new Error(\u0026#34;Maximum lock count exceeded\u0026#34;); setState(nextc); return true; } // 获取失败, 回到调用处 return false; } // ㈣ AQS 继承过来的方法, 方便阅读, 放在此处 //将当前node加入等待队列末尾等待，并返回当前node private Node addWaiter(Node mode) { // 将当前线程关联到一个 Node 对象上, 模式为独占模式 Node node = new Node(Thread.currentThread(), mode); //非公平同步器中有head和tail两个引用分别指向了等待队列的第一个和最后一个节点 //pred指的是node的前驱，从队尾插入，所以pred为tail Node pred = tail; // 如果 tail 不为 null, 说明已经有了等待队列了，cas 尝试将 Node 对象加入 AQS 队列尾部 if (pred != null) { //将node的前驱节点设置为pred node.prev = pred; //尝试将队列的tial从当前的pred修改为node if (compareAndSetTail(pred, node)) { // 双向链表 pred.next = node; return node; } } //如果pred为null，说明等待队列还未创建，调用enq方法创建队列 // 尝试将 Node 加入 AQS, 进入 ㈥ enq(node); return node; } // ㈥ AQS 继承过来的方法, 方便阅读, 放在此处 //该方法就是创建等待队列，并将node插入队列的尾部。 private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // 还没有, 设置 head 为哨兵节点（不对应线程，状态为 0） if (compareAndSetHead(new Node())) { //将head赋值给tail，head和tail同时指向哨兵节点 tail = head; } } else { // cas 尝试将 Node 对象加入 AQS 队列尾部 //设置node的前驱节点为队列的最后一个节点 node.prev = t; //尝试将队列的尾部从当前的tail设置为node if (compareAndSetTail(t, node)) { //将node设为上一个tail的后继节点 t.next = node; return t; } } } } // ㈤ AQS 继承过来的方法, 方便阅读, 放在此处 //在队列中循环等待，只有当排队排到第一名并且获得了锁才能出队并从方法中退出。 //返回打断状态 final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { //找到当前node的前驱节点 final Node p = node.predecessor(); // 上一个节点是 head, 表示轮到自己（当前线程对应的 node）了, 尝试获取 if (p == head \u0026amp;\u0026amp; tryAcquire(arg)) { // 获取成功, 设置自己（当前线程对应的 node）为 head setHead(node); // 上一个节点 help GC p.next = null; failed = false; // 返回中断标记 false return interrupted; } if ( // 判断是否应当 park, 进入 ㈦ shouldParkAfterFailedAcquire(p, node) \u0026amp;\u0026amp; // park 等待, 此时 Node 的状态被置为 Node.SIGNAL ㈧ parkAndCheckInterrupt() ) { interrupted = true; } } } finally { if (failed) cancelAcquire(node); } } // ㈦ AQS 继承过来的方法, 方便阅读, 放在此处 //判断acquire失败以后是否应该阻塞等待。从规则上来讲： //1.如果前驱节点都阻塞了，那么当前节点也应该阻塞 //2.如果前驱节点取消，那么应该将前驱节点前移，直到其状态不为取消为止。 //3.如果前两种情况都不是，尝试将前驱节点状态设为SIGNAL，返回false（不用阻塞，等到下次在阻塞） private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { // 获取上一个节点的状态 int ws = pred.waitStatus; if (ws == Node.SIGNAL) { // 上一个节点都在阻塞, 那么自己也阻塞好了 return true; } // \u0026gt; 0 表示取消状态 if (ws \u0026gt; 0) { // 上一个节点取消, 那么重构删除前面所有取消的节点, 返回到外层循环重试 do { node.prev = pred = pred.prev; } while (pred.waitStatus \u0026gt; 0); pred.next = node; } else { // 这次还没有阻塞 // 但下次如果重试不成功, 则需要阻塞，这时需要设置上一个节点状态为 Node.SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } // ㈧ 阻塞当前线程 private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } } Copied! 注意\n是否需要 unpark 是由当前节点的前驱节点的 waitStatus == Node.SIGNAL 来决定，而不是本节点的 waitStatus 决定\n总结：\n调用lock，尝试将state从0修改为1 成功：将owner设为当前线程 失败：调用acquire-\u0026gt;tryAcquire-\u0026gt;nonfairTryAcquire，判断state=0则获得锁，或者state不为0但当前线程持有锁则重入锁，以上两种情况tryAcquire返回true，剩余情况返回false。 true：获得锁 false：调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg),其中addwiter将关联线程的节点插入AQS队列尾部，进入acquireQueued中的for循环: 如果当前节点是头节点，并尝试获得锁成功，将当前节点设为头节点，清除此节点信息，返回打断标记。 调用shoudParkAfterFailure,第一次调用返回false，并将前驱节点改为-1，第二次循环如果再进入此方法，会进入阻塞并检查打断的方法。 解锁源码 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 // Sync 继承自 AQS static final class NonfairSync extends Sync { // 解锁实现 public void unlock() { sync.release(1); } // AQS 继承过来的方法, 方便阅读, 放在此处 public final boolean release(int arg) { // 尝试释放锁, 进入 ㈠ if (tryRelease(arg)) { // 队列头节点 unpark Node h = head; if ( // 队列不为 null h != null \u0026amp;\u0026amp; // waitStatus == Node.SIGNAL 才需要 unpark h.waitStatus != 0 ) { // unpark AQS 中等待的线程, 进入 ㈡ unparkSuccessor(h); } return true; } return false; } // ㈠ Sync 继承过来的方法, 方便阅读, 放在此处 protected final boolean tryRelease(int releases) { // state-- int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 支持锁重入, 只有 state 减为 0, 才释放成功 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } // ㈡ AQS 继承过来的方法, 方便阅读, 放在此处 private void unparkSuccessor(Node node) { // 如果状态为 Node.SIGNAL 尝试重置状态为 0 // 不成功也可以 int ws = node.waitStatus; if (ws \u0026lt; 0) { compareAndSetWaitStatus(node, ws, 0); } // 找到需要 unpark 的节点, 但本节点从 AQS 队列中脱离, 是由唤醒节点完成的 Node s = node.next; // 不考虑已取消的节点, 从 AQS 队列从后至前找到队列最前面需要 unpark 的节点 if (s == null || s.waitStatus \u0026gt; 0) { s = null; for (Node t = tail; t != null \u0026amp;\u0026amp; t != node; t = t.prev) if (t.waitStatus \u0026lt;= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); } } Copied! 总结：\nunlock-\u0026gt;syn.release(1)-\u0026gt;tryRelease(1),如果当前线程并不持有锁，抛异常。state减去1,如果之后state为0，解锁成功，返回true；如果仍大于0，表示解锁不完全，当前线程依旧持有锁，返回false。 返回true：检查AQS队列第一个节点状态图是否为SIGNAL(意味着有责任唤醒其后记节点)，如果有，调用unparkSuccessor。 再unparkSuccessor中，不考虑已取消的节点, 从 AQS 队列从后至前找到队列最前面需要 unpark 的节点，如果有，将其唤醒。 返回false： 可重入原理 当持有锁的线程再次尝试获取锁时，会将state的值加1，state表示锁的重入量。\n1 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 37 38 39 40 41 static final class NonfairSync extends Sync { // ... // Sync 继承过来的方法, 方便阅读, 放在此处 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入 else if (current == getExclusiveOwnerThread()) { // state++ int nextc = c + acquires; if (nextc \u0026lt; 0) // overflow throw new Error(\u0026#34;Maximum lock count exceeded\u0026#34;); setState(nextc); return true; } return false; } // Sync 继承过来的方法, 方便阅读, 放在此处 protected final boolean tryRelease(int releases) { // state-- int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 支持锁重入, 只有 state 减为 0, 才释放成功 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } } Copied! 可打断原理 不可打断模式\n在此模式下，即使它被打断，仍会驻留在 AQS 队列中，并将打断信号存储在一个interrupt变量中。一直要等到获得锁后方能得知自己被打断了,并且调用selfInterrupt方法打断自己。\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 // Sync 继承自 AQS static final class NonfairSync extends Sync { // ... private final boolean parkAndCheckInterrupt() { // 如果打断标记已经是 true, 则 park 会失效 LockSupport.park(this); // interrupted 会清除打断标记 return Thread.interrupted(); } final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head \u0026amp;\u0026amp; tryAcquire(arg)) { setHead(node); p.next = null; failed = false; // 还是需要获得锁后, 才能返回打断状态 return interrupted; } if ( shouldParkAfterFailedAcquire(p, node) \u0026amp;\u0026amp; parkAndCheckInterrupt() ) { // 如果是因为 interrupt 被唤醒, 返回打断状态为 true interrupted = true; } } } finally { if (failed) cancelAcquire(node); } } public final void acquire(int arg) { if ( !tryAcquire(arg) \u0026amp;\u0026amp; acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ) { // 如果打断状态为 true selfInterrupt(); } } //响应打断标记，打断自己 static void selfInterrupt() { // 重新产生一次中断 Thread.currentThread().interrupt(); } } Copied! 可打断模式\n此模式下即使线程在等待队列中等待，一旦被打断，就会立刻抛出打断异常。\n1 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 static final class NonfairSync extends Sync { public final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); // 如果没有获得到锁, 进入 ㈠ if (!tryAcquire(arg)) doAcquireInterruptibly(arg); } // ㈠ 可打断的获取锁流程 private void doAcquireInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head \u0026amp;\u0026amp; tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return; } if (shouldParkAfterFailedAcquire(p, node) \u0026amp;\u0026amp; parkAndCheckInterrupt()) { // 在 park 过程中如果被 interrupt 会进入此 // 这时候抛出异常, 而不会再次进入 for (;;) throw new InterruptedException(); } } } finally { if (failed) cancelAcquire(node); } } } Copied! 公平锁实现原理 简而言之，公平与非公平的区别在于，公平锁中的tryAcquire方法被重写了，新来的线程即便得知了锁的state为0，也要先判断等待队列中是否还有线程等待，只有当队列没有线程等待式，才获得锁。\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } // AQS 继承过来的方法, 方便阅读, 放在此处 public final void acquire(int arg) { if ( !tryAcquire(arg) \u0026amp;\u0026amp; acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ) { selfInterrupt(); } } // 与非公平锁主要区别在于 tryAcquire 方法的实现 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 先检查 AQS 队列中是否有前驱节点, 没有才去竞争 if (!hasQueuedPredecessors() \u0026amp;\u0026amp; compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc \u0026lt; 0) throw new Error(\u0026#34;Maximum lock count exceeded\u0026#34;); setState(nextc); return true; } return false; } // ㈠ AQS 继承过来的方法, 方便阅读, 放在此处 //存疑 public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; // h != t 时表示队列中有 Node return h != t \u0026amp;\u0026amp; ( // (s = h.next) == null 表示队列中还有没有老二 (s = h.next) == null || // 或者队列中老二线程不是此线程 s.thread != Thread.currentThread() ); } } Copied! 条件变量实现原理 每个条件变量其实就对应着一个等待队列，其实现类是 ConditionObject\nawait 流程 开始 Thread-0 持有锁，调用 await，进入 ConditionObject 的 addConditionWaiter 流程\n创建新的 Node 状态为 -2（Node.CONDITION），关联 Thread-0，加入等待队列尾部\n接下来进入 AQS 的 fullyRelease 流程，释放同步器上的锁\nunpark AQS 队列中的下一个节点，竞争锁，假设没有其他竞争线程，那么 Thread-1 竞争成功\npark 阻塞 Thread-0\n总结：\n创建一个节点，关联当前线程，并插入到当前Condition队列的尾部 调用fullRelease，完全释放同步器中的锁，并记录当前线程的锁重入数 唤醒(park)AQS队列中的第一个线程 调用park方法，阻塞当前线程。 signal 流程 假设 Thread-1 要来唤醒 Thread-0\n进入 ConditionObject 的 doSignal 流程，取得等待队列中第一个 Node，即 Thread-0 所在 Node\n执行 transferForSignal 流程，将该 Node 加入 AQS 队列尾部，将 Thread-0 的 waitStatus 改为 0，Thread-3 的 waitStatus 改为 -1\nThread-1 释放锁，进入 unlock 流程，略\n总结：\n当前持有锁的线程唤醒等待队列中的线程，调用doSignal或doSignalAll方法，将等待队列中的第一个（或全部）节点插入到AQS队列中的尾部。 将插入的节点的状态从Condition设置为0，将插入节点的前一个节点的状态设置为-1（意味着要承担唤醒后一个节点的责任） 当前线程释放锁，parkAQS队列中的第一个节点线程。 源码 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 public class ConditionObject implements Condition, java.io.Serializable { private static final long serialVersionUID = 1173984872572414699L; // 第一个等待节点 private transient Node firstWaiter; // 最后一个等待节点 private transient Node lastWaiter; public ConditionObject() { } // ㈠ 添加一个 Node 至等待队列 private Node addConditionWaiter() { Node t = lastWaiter; // 所有已取消的 Node 从队列链表删除, 见 ㈡ if (t != null \u0026amp;\u0026amp; t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } // 创建一个关联当前线程的新 Node, 添加至队列尾部 Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; } // 唤醒 - 将没取消的第一个节点转移至 AQS 队列 private void doSignal(Node first) { do { // 已经是尾节点了 if ( (firstWaiter = first.nextWaiter) == null) { lastWaiter = null; } first.nextWaiter = null; } while ( // 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 ㈢ !transferForSignal(first) \u0026amp;\u0026amp; // 队列还有节点 (first = firstWaiter) != null ); } // 外部类方法, 方便阅读, 放在此处 // ㈢ 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功 final boolean transferForSignal(Node node) { // 如果状态已经不是 Node.CONDITION, 说明被取消了 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; // 加入 AQS 队列尾部 Node p = enq(node); int ws = p.waitStatus; if ( // 上一个节点被取消 ws \u0026gt; 0 || // 上一个节点不能设置状态为 Node.SIGNAL !compareAndSetWaitStatus(p, ws, Node.SIGNAL) ) { // unpark 取消阻塞, 让线程重新同步状态 LockSupport.unpark(node.thread); } return true; } // 全部唤醒 - 等待队列的所有节点转移至 AQS 队列 private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; do { Node next = first.nextWaiter; first.nextWaiter = null; transferForSignal(first); first = next; } while (first != null); } // ㈡ private void unlinkCancelledWaiters() { // ... } // 唤醒 - 必须持有锁才能唤醒, 因此 doSignal 内无需考虑加锁 public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); } // 全部唤醒 - 必须持有锁才能唤醒, 因此 doSignalAll 内无需考虑加锁 public final void signalAll() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignalAll(first); } // 不可打断等待 - 直到被唤醒 public final void awaitUninterruptibly() { // 添加一个 Node 至等待队列, 见 ㈠ Node node = addConditionWaiter(); // 释放节点持有的锁, 见 ㈣ int savedState = fullyRelease(node); boolean interrupted = false; // 如果该节点还没有转移至 AQS 队列, 阻塞 while (!isOnSyncQueue(node)) { // park 阻塞 LockSupport.park(this); // 如果被打断, 仅设置打断状态 if (Thread.interrupted()) interrupted = true; } // 唤醒后, 尝试竞争锁, 如果失败进入 AQS 队列 if (acquireQueued(node, savedState) || interrupted) selfInterrupt(); } private void doSignalAll(Node first) { lastWaiter = firstWaiter = null; do { Node next = first.nextWaiter; first.nextWaiter = null; transferForSignal(first); first = next; } while (first != null); } // ㈡ private void unlinkCancelledWaiters() { // ... } // 唤醒 - 必须持有锁才能唤醒, 因此 doSignal 内无需考虑加锁 public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignal(first); } // 全部唤醒 - 必须持有锁才能唤醒, 因此 doSignalAll 内无需考虑加锁 public final void signalAll() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first = firstWaiter; if (first != null) doSignalAll(first); } // 不可打断等待 - 直到被唤醒 public final void awaitUninterruptibly() { // 添加一个 Node 至等待队列, 见 ㈠ Node node = addConditionWaiter(); // 释放节点持有的锁, 见 ㈣ int savedState = fullyRelease(node); boolean interrupted = false; // 如果该节点还没有转移至 AQS 队列, 阻塞 while (!isOnSyncQueue(node)) { // park 阻塞 LockSupport.park(this); // 如果被打断, 仅设置打断状态 if (Thread.interrupted()) interrupted = true; } // 唤醒后, 尝试竞争锁, 如果失败进入 AQS 队列 if (acquireQueued(node, savedState) || interrupted) selfInterrupt(); } // 外部类方法, 方便阅读, 放在此处 // ㈣ 因为某线程可能重入，需要将 state 全部释放 final int fullyRelease(Node node) { boolean failed = true; try { int savedState = getState(); if (release(savedState)) { failed = false; return savedState; } else { throw new IllegalMonitorStateException(); } } finally { if (failed) node.waitStatus = Node.CANCELLED; } } // 打断模式 - 在退出等待时重新设置打断状态 private static final int REINTERRUPT = 1; // 打断模式 - 在退出等待时抛出异常 private static final int THROW_IE = -1; // 判断打断模式 private int checkInterruptWhileWaiting(Node node) { return Thread.interrupted() ? (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) : 0; } // ㈤ 应用打断模式 private void reportInterruptAfterWait(int interruptMode) throws InterruptedException { if (interruptMode == THROW_IE) throw new InterruptedException(); else if (interruptMode == REINTERRUPT) selfInterrupt(); } // 等待 - 直到被唤醒或打断 public final void await() throws InterruptedException { if (Thread.interrupted()) { throw new InterruptedException(); } // 添加一个 Node 至等待队列, 见 ㈠ Node node = addConditionWaiter(); // 释放节点持有的锁 int savedState = fullyRelease(node); int interruptMode = 0; // 如果该节点还没有转移至 AQS 队列, 阻塞 while (!isOnSyncQueue(node)) { // park 阻塞 LockSupport.park(this); // 如果被打断, 退出等待队列 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; } // 退出等待队列后, 还需要获得 AQS 队列的锁 if (acquireQueued(node, savedState) \u0026amp;\u0026amp; interruptMode != THROW_IE) interruptMode = REINTERRUPT; // 所有已取消的 Node 从队列链表删除, 见 ㈡ if (node.nextWaiter != null) unlinkCancelledWaiters(); // 应用打断模式, 见 ㈤ if (interruptMode != 0) reportInterruptAfterWait(interruptMode); } //向Condition中的等待队列中新增节点，并将此节点返回 private Node addConditionWaiter() { Node t = lastWaiter; // If lastWaiter is cancelled, clean out. if (t != null \u0026amp;\u0026amp; t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); t = lastWaiter; } Node node = new Node(Thread.currentThread(), Node.CONDITION); if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; } //判断当前节点是否在同步器中的队列中等待锁 final boolean isOnSyncQueue(Node node) { if (node.waitStatus == Node.CONDITION || node.prev == null) return false; if (node.next != null) // If has successor, it must be on queue return true; /* * node.prev can be non-null, but not yet on queue because * the CAS to place it on queue can fail. So we have to * traverse from tail to make sure it actually made it. It * will always be near the tail in calls to this method, and * unless the CAS failed (which is unlikely), it will be * there, so we hardly ever traverse much. */ return findNodeFromTail(node); } // 等待 - 直到被唤醒或打断或超时 public final long awaitNanos(long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) { throw new InterruptedException(); } // 添加一个 Node 至等待队列, 见 ㈠ Node node = addConditionWaiter(); // 释放节点持有的锁 int savedState = fullyRelease(node); // 获得最后期限 final long deadline = System.nanoTime() + nanosTimeout; int interruptMode = 0; // 如果该节点还没有转移至 AQS 队列, 阻塞 while (!isOnSyncQueue(node)) { // 已超时, 退出等待队列 if (nanosTimeout \u0026lt;= 0L) { transferAfterCancelledWait(node); break; } // park 阻塞一定时间, spinForTimeoutThreshold 为 1000 ns if (nanosTimeout \u0026gt;= spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); // 如果被打断, 退出等待队列 if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break; nanosTimeout = deadline - System.nanoTime(); } // 退出等待队列后, 还需要获得 AQS 队列的锁 if (acquireQueued(node, savedState) \u0026amp;\u0026amp; interruptMode != THROW_IE) interruptMode = REINTERRUPT; // 所有已取消的 Node 从队列链表删除, 见 ㈡ if (node.nextWaiter != null) unlinkCancelledWaiters(); // 应用打断模式, 见 ㈤ if (interruptMode != 0) reportInterruptAfterWait(interruptMode); return deadline - System.nanoTime(); } // 等待 - 直到被唤醒或打断或超时, 逻辑类似于 awaitNanos public final boolean awaitUntil(Date deadline) throws InterruptedException { // ... } // 等待 - 直到被唤醒或打断或超时, 逻辑类似于 awaitNanos public final boolean await(long time, TimeUnit unit) throws InterruptedException { // ... } // 工具方法 省略 ... } Copied! 读写锁 ReentrantReadWriteLock 当读操作远远高于写操作时，这时候使用读写锁让读-读可以并发，提高性能。 类似于数据库中的select ... from ... lock in share mode\n提供一个数据容器类内部分别使用读锁保护数据的 read() 方法，写锁保护数据的 write() 方法\n测试\n1 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 class DataContainer { private Object data; private ReentrantReadWriteLock rw = new ReentrantReadWriteLock(); private ReentrantReadWriteLock.ReadLock r = rw.readLock(); private ReentrantReadWriteLock.WriteLock w = rw.writeLock(); public Object read() { log.debug(\u0026#34;获取读锁...\u0026#34;); r.lock(); try { log.debug(\u0026#34;读取\u0026#34;); sleep(1); return data; } finally { log.debug(\u0026#34;释放读锁...\u0026#34;); r.unlock(); } } public void write() { log.debug(\u0026#34;获取写锁...\u0026#34;); w.lock(); try { log.debug(\u0026#34;写入\u0026#34;); sleep(1); } finally { log.debug(\u0026#34;释放写锁...\u0026#34;); w.unlock(); } } } Copied! 测试读锁-读锁可以并发\n1 2 3 4 5 6 7 DataContainer dataContainer = new DataContainer(); new Thread(() -\u0026gt; { dataContainer.read(); }, \u0026#34;t1\u0026#34;).start(); new Thread(() -\u0026gt; { dataContainer.read(); }, \u0026#34;t2\u0026#34;).start(); Copied! 输出结果，从这里可以看到 Thread-0 锁定期间，Thread-1 的读操作不受影响\n1 2 3 4 5 6 14:05:14.341 c.DataContainer [t2] - 获取读锁... 14:05:14.341 c.DataContainer [t1] - 获取读锁... 14:05:14.345 c.DataContainer [t1] - 读取 14:05:14.345 c.DataContainer [t2] - 读取 14:05:15.365 c.DataContainer [t2] - 释放读锁... 14:05:15.386 c.DataContainer [t1] - 释放读锁... Copied! 测试读锁-写锁相互阻塞\n1 2 3 4 5 6 7 8 DataContainer dataContainer = new DataContainer(); new Thread(() -\u0026gt; { dataContainer.read(); }, \u0026#34;t1\u0026#34;).start(); Thread.sleep(100); new Thread(() -\u0026gt; { dataContainer.write(); }, \u0026#34;t2\u0026#34;).start(); Copied! 输出结果\n1 2 3 4 5 6 14:04:21.838 c.DataContainer [t1] - 获取读锁... 14:04:21.838 c.DataContainer [t2] - 获取写锁... 14:04:21.841 c.DataContainer [t2] - 写入 14:04:22.843 c.DataContainer [t2] - 释放写锁... 14:04:22.843 c.DataContainer [t1] - 读取 14:04:23.843 c.DataContainer [t1] - 释放读锁... Copied! 写锁-写锁也是相互阻塞的，这里就不测试了\n注意事项\n读锁不支持条件变量 重入时升级不支持：即持有读锁的情况下去获取写锁，会导致获取写锁永久等待 1 2 3 4 5 6 7 8 9 10 11 12 r.lock(); try { // ... w.lock(); try { // ... } finally{ w.unlock(); } } finally{ r.unlock(); } Copied! 重入时降级支持：即持有写锁的情况下去获取读锁 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 class CachedData { Object data; // 是否有效，如果失效，需要重新计算 data volatile boolean cacheValid; final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // 获取写锁前必须释放读锁 rwl.readLock().unlock(); rwl.writeLock().lock(); try { // 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新 if (!cacheValid) { data = ... cacheValid = true; } // 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存 rwl.readLock().lock(); } finally { rwl.writeLock().unlock(); } } // 自己用完数据, 释放读锁 try { use(data); } finally { rwl.readLock().unlock(); } } } Copied! * 应用之缓存 缓存更新策略 更新时，是先清缓存还是先更新数据库\n先清缓存\n先更新数据库\n补充一种情况，假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效，或是第一次查询\n这种情况的出现几率非常小，见 facebook 论文\n读写锁实现一致性缓存 使用读写锁实现一个简单的按需加载缓存\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 class GenericCachedDao\u0026lt;T\u0026gt; { // HashMap 作为缓存非线程安全, 需要保护 HashMap\u0026lt;SqlPair, T\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); GenericDao genericDao = new GenericDao(); public int update(String sql, Object... params) { SqlPair key = new SqlPair(sql, params); // 加写锁, 防止其它线程对缓存读取和更改 lock.writeLock().lock(); try { int rows = genericDao.update(sql, params); map.clear(); return rows; } finally { lock.writeLock().unlock(); } } public T queryOne(Class\u0026lt;T\u0026gt; beanClass, String sql, Object... params) { SqlPair key = new SqlPair(sql, params); // 加读锁, 防止其它线程对缓存更改 lock.readLock().lock(); try { T value = map.get(key); if (value != null) { return value; } } finally { lock.readLock().unlock(); } // 加写锁, 防止其它线程对缓存读取和更改 lock.writeLock().lock(); try { // get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据 // 为防止重复查询数据库, 再次验证 T value = map.get(key); if (value == null) { // 如果没有, 查询数据库 value = genericDao.queryOne(beanClass, sql, params); map.put(key, value); } return value; } finally { lock.writeLock().unlock(); } } // 作为 key 保证其是不可变的 class SqlPair { private String sql; private Object[] params; public SqlPair(String sql, Object[] params) { this.sql = sql; this.params = params; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } SqlPair sqlPair = (SqlPair) o; return sql.equals(sqlPair.sql) \u0026amp;\u0026amp; Arrays.equals(params, sqlPair.params); } @Override public int hashCode() { int result = Objects.hash(sql); result = 31 * result + Arrays.hashCode(params); return result; } } } Copied! 注意\n以上实现体现的是读写锁的应用，保证缓存和数据库的一致性，但有下面的问题没有考虑\n适合读多写少，如果写操作比较频繁，以上实现性能低\n没有考虑缓存容量\n没有考虑缓存过期\n只适合单机\n并发性还是低，目前只会用一把锁\n更新方法太过简单粗暴，清空了所有 key（考虑按类型分区或重新设计 key）\n乐观锁实现：用 CAS 去更新\n* 读写锁原理 图解流程 读写锁用的是同一个 Sycn 同步器，因此等待队列、state 等也是同一个\nt1 w.lock，t2 r.lock\n1） t1 成功上锁，流程与 ReentrantLock 加锁相比没有特殊之处，不同是写锁状态占了 state 的低 16 位，而读锁 使用的是 state 的高 16 位\n2）t2 执行 r.lock，这时进入读锁的 sync.acquireShared(1) 流程，首先会进入 tryAcquireShared 流程。如果有写 锁占据，那么 tryAcquireShared 返回 -1 表示失败\ntryAcquireShared 返回值表示\n-1 表示失败 0 表示成功，但后继节点不会继续唤醒 正数表示成功，而且数值是还有几个后继节点需要唤醒，读写锁返回 1 3）这时会进入 sync.doAcquireShared(1) 流程，首先也是调用 addWaiter 添加节点，不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式，注意此时 t2 仍处于活跃状态\n4）t2 会看看自己的节点是不是老二，如果是，还会再次调用 tryAcquireShared(1) 来尝试获取锁\n5）如果没有成功，在 doAcquireShared 内 for (;;) 循环一次，把前驱节点的 waitStatus 改为 -1，再 for (;;) 循环一 次尝试 tryAcquireShared(1) 如果还不成功，那么在 parkAndCheckInterrupt() 处 park\nt3 r.lock，t4 w.lock\n这种状态下，假设又有 t3 加读锁和 t4 加写锁，这期间 t1 仍然持有锁，就变成了下面的样子\nt1 w.unlock\n这时会走到写锁的 sync.release(1) 流程，调用 sync.tryRelease(1) 成功，变成下面的样子\n接下来执行唤醒流程 sync.unparkSuccessor，即让老二恢复运行，这时 t2 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行\n这回再来一次 for (;;) 执行 tryAcquireShared 成功则让读锁计数加一\n这时 t2 已经恢复运行，接下来 t2 调用 setHeadAndPropagate(node, 1)，它原本所在节点被置为头节点\n事情还没完，在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared，如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二，这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行\n这回再来一次 for (;;) 执行 tryAcquireShared 成功则让读锁计数加一\n这时 t3 已经恢复运行，接下来 t3 调用 setHeadAndPropagate(node, 1)，它原本所在节点被置为头节点\n下一个节点不是 shared 了，因此不会继续唤醒 t4 所在节点\nt2 r.unlock，t3 r.unlock\nt2 进入 sync.releaseShared(1) 中，调用 tryReleaseShared(1) 让计数减一，但由于计数还不为零\nt3 进入 sync.releaseShared(1) 中，调用 tryReleaseShared(1) 让计数减一，这回计数为零了，进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二，即\n之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行，再次 for (;;) 这次自己是老二，并且没有其他 竞争，tryAcquire(1) 成功，修改头结点，流程结束\n源码分析 写锁上锁流程 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 static final class NonfairSync extends Sync { // ... 省略无关代码 // 外部类 WriteLock 方法, 方便阅读, 放在此处 public void lock() { sync.acquire(1); } // AQS 继承过来的方法, 方便阅读, 放在此处 public final void acquire(int arg) { if ( // 尝试获得写锁失败 !tryAcquire(arg) \u0026amp;\u0026amp; // 将当前线程关联到一个 Node 对象上, 模式为独占模式 // 进入 AQS 队列阻塞 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) ) { selfInterrupt(); } } // Sync 继承过来的方法, 方便阅读, 放在此处 protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); // 获得低 16 位, 代表写锁的 state 计数 int w = exclusiveCount(c); //表示有写锁或者有读锁 if (c != 0) { if ( // c != 0 and w == 0 表示有读锁, 或者 w == 0 || // 如果 exclusiveOwnerThread 不是自己 current != getExclusiveOwnerThread() ) { // 获得锁失败 return false; } // 写锁计数超过低 16 位, 报异常 if (w + exclusiveCount(acquires) \u0026gt; MAX_COUNT) throw new Error(\u0026#34;Maximum lock count exceeded\u0026#34;); // 写锁重入, 获得锁成功 setState(c + acquires); return true; } if ( // 判断写锁是否该阻塞, 或者 //非公平锁下，总是返回false writerShouldBlock() || // 尝试更改计数失败 !compareAndSetState(c, c + acquires) ) { // 获得锁失败 return false; } // 获得锁成功 setExclusiveOwnerThread(current); return true; } // 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞 final boolean writerShouldBlock() { return false; } } Copied! 总结：\nlock -\u0026gt; syn.acquire -\u0026gt;tryAquire 如果有锁： 如果是写锁或者锁持有者不为自己，返回false 如果时写锁且为自己持有，则重入 如果无锁： 判断无序阻塞并设置state成功后，将owner设为自己，返回true 成功，则获得了锁 失败： 调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)进入阻塞队列，将节点状态设置为EXCLUSIVE，之后的逻辑与之前的aquireQueued类似。 写锁释放流程 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 static final class NonfairSync extends Sync { // ... 省略无关代码 // WriteLock 方法, 方便阅读, 放在此处 public void unlock() { sync.release(1); } // AQS 继承过来的方法, 方便阅读, 放在此处 public final boolean release(int arg) { // 尝试释放写锁成功 if (tryRelease(arg)) { // unpark AQS 中等待的线程 Node h = head; if (h != null \u0026amp;\u0026amp; h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } // Sync 继承过来的方法, 方便阅读, 放在此处 protected final boolean tryRelease(int releases) { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); int nextc = getState() - releases; // 因为可重入的原因, 写锁计数为 0, 才算释放成功 boolean free = exclusiveCount(nextc) == 0; if (free) { setExclusiveOwnerThread(null); } setState(nextc); return free; } } Copied! 总结：\nunlock-\u0026gt;syn.release-\u0026gt;tryRelease\nstate状态减少 如果减为零，表示解锁成功，返回true 没有减为0，当前线程依旧持有锁 成功：解锁成功\n如果ASQ队列不为空，则唤醒第一个节点。 失败：解锁失败。\n读锁上锁流程 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 static final class NonfairSync extends Sync { // ReadLock 方法, 方便阅读, 放在此处 public void lock() { sync.acquireShared(1); } // AQS 继承过来的方法, 方便阅读, 放在此处 public final void acquireShared(int arg) { // tryAcquireShared 返回负数, 表示获取读锁失败 //大于0的情况在读写锁这里无区别，后面信号量会做进一步处理。 if (tryAcquireShared(arg) \u0026lt; 0) { doAcquireShared(arg); } } // Sync 继承过来的方法, 方便阅读, 放在此处 protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); // 如果是其它线程持有写锁, 获取读锁失败 if ( exclusiveCount(c) != 0 \u0026amp;\u0026amp; getExclusiveOwnerThread() != current ) { return -1; } int r = sharedCount(c); if ( // 读锁不该阻塞(如果老二是写锁，读锁该阻塞), 并且 !readerShouldBlock() \u0026amp;\u0026amp; // 小于读锁计数, 并且 r \u0026lt; MAX_COUNT \u0026amp;\u0026amp; // 尝试增加计数成功 compareAndSetState(c, c + SHARED_UNIT) ) { // ... 省略不重要的代码 return 1; } return fullTryAcquireShared(current); } // 非公平锁 readerShouldBlock 看 AQS 队列中第一个节点是否是写锁 // true 则该阻塞, false 则不阻塞 final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); } // AQS 继承过来的方法, 方便阅读, 放在此处 // 与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞 final int fullTryAcquireShared(Thread current) { HoldCounter rh = null; for (;;) { int c = getState(); if (exclusiveCount(c) != 0) { if (getExclusiveOwnerThread() != current) return -1; } else if (readerShouldBlock()) { // ... 省略不重要的代码 } if (sharedCount(c) == MAX_COUNT) throw new Error(\u0026#34;Maximum lock count exceeded\u0026#34;); if (compareAndSetState(c, c + SHARED_UNIT)) { // ... 省略不重要的代码 return 1; } } } // AQS 继承过来的方法, 方便阅读, 放在此处 private void doAcquireShared(int arg) { // 将当前线程关联到一个 Node 对象上, 模式为共享模式 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { // 再一次尝试获取读锁 int r = tryAcquireShared(arg); // 成功 if (r \u0026gt;= 0) { // ㈠ // r 表示可用资源数, 在这里总是 1 允许传播 //（唤醒 AQS 中下一个 Share 节点） setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } if ( // 是否在获取读锁失败时阻塞（前一个阶段 waitStatus == Node.SIGNAL） shouldParkAfterFailedAcquire(p, node) \u0026amp;\u0026amp; // park 当前线程 parkAndCheckInterrupt() ) { interrupted = true; } } } finally { if (failed) cancelAcquire(node); } } // ㈠ AQS 继承过来的方法, 方便阅读, 放在此处 private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below // 设置自己为 head setHead(node); // propagate 表示有共享资源（例如共享读锁或信号量） // 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE // 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE if (propagate \u0026gt; 0 || h == null || h.waitStatus \u0026lt; 0 || (h = head) == null || h.waitStatus \u0026lt; 0) { Node s = node.next; // 如果是最后一个节点或者是等待共享读锁的节点 if (s == null || s.isShared()) { // 进入 ㈡ doReleaseShared(); } } } // ㈡ AQS 继承过来的方法, 方便阅读, 放在此处 private void doReleaseShared() { // 如果 head.waitStatus == Node.SIGNAL ==\u0026gt; 0 成功, 下一个节点 unpark // 如果 head.waitStatus == 0 ==\u0026gt; Node.PROPAGATE, 为了解决 bug, 见后面分析 for (;;) { Node h = head; // 队列还有节点 if (h != null \u0026amp;\u0026amp; h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases // 下一个节点 unpark 如果成功获取读锁 // 并且下下个节点还是 shared, 继续 doReleaseShared unparkSuccessor(h); } else if (ws == 0 \u0026amp;\u0026amp; !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } } } Copied! 总结：\nlock-\u0026gt;syn.acquireShare-\u0026gt;tryAcquireShare 如果其他线程持有写锁：则失败，返回-1 否则：判断无需等待后，将state加上一个写锁的单位，返回1 返回值大于等于0：成功 返回值小于0： 调用doAcquireShare，类似之前的aquireQueued,将当前线程关联节点，状态设置为SHARE，插入AQS队列尾部。在for循环中判断当前节点的前驱节点是否为头节点 是：调用tryAcquireShare 如果返回值大于等于0，则获取锁成功，并调用setHeadAndPropagate，出队，并不断唤醒AQS队列中的状态为SHARE的节点，直到下一个节点为EXCLUSIVE。记录打断标记，之后退出方法（不返回打断标记） 判断是否在失败后阻塞 是：阻塞住，并监测打断信号。 否则：将前驱节点状态设为-1。（下一次循环就又要阻塞了） 读锁释放流程 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 static final class NonfairSync extends Sync { // ReadLock 方法, 方便阅读, 放在此处 public void unlock() { sync.releaseShared(1); } // AQS 继承过来的方法, 方便阅读, 放在此处 public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } // Sync 继承过来的方法, 方便阅读, 放在此处 protected final boolean tryReleaseShared(int unused) { // ... 省略不重要的代码 for (;;) { int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) { // 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程 // 计数为 0 才是真正释放 return nextc == 0; } } } // AQS 继承过来的方法, 方便阅读, 放在此处 private void doReleaseShared() { // 如果 head.waitStatus == Node.SIGNAL ==\u0026gt; 0 成功, 下一个节点 unpark // 如果 head.waitStatus == 0 ==\u0026gt; Node.PROPAGATE for (;;) { Node h = head; if (h != null \u0026amp;\u0026amp; h != tail) { int ws = h.waitStatus; // 如果有其它线程也在释放读锁，那么需要将 waitStatus 先改为 0 // 防止 unparkSuccessor 被多次执行 if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } // 如果已经是 0 了，改为 -3，用来解决传播性，见后文信号量 bug 分析 else if (ws == 0 \u0026amp;\u0026amp; !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } } } Copied! 总结：\nunlock-\u0026gt;releaseShared-\u0026gt;tryReleaseShared,将state减去一个share单元，最后state为0则返回true，不然返回false。 返回tue：调用doReleaseShare,唤醒队列中的节点。 返回false：解锁不完全。 StampedLock 该类自 JDK 8 加入，是为了进一步优化读性能，它的特点是在使用读锁、写锁时都必须配合【戳】使用 加解读锁\n1 2 long stamp = lock.readLock(); lock.unlockRead(stamp); Copied! 加解写锁\n1 2 long stamp = lock.writeLock(); lock.unlockWrite(stamp); Copied! 乐观读，StampedLock 支持 tryOptimisticRead() 方法（乐观读），读取完毕后需要做一次 戳校验 如果校验通 过，表示这期间确实没有写操作，数据可以安全使用，如果校验没通过，需要重新获取读锁，保证数据安全。\n1 2 3 4 5 long stamp = lock.tryOptimisticRead(); // 验戳 if(!lock.validate(stamp)){ // 锁升级 } Copied! 提供一个数据容器类内部分别使用读锁保护数据的read()方法，写锁保护数据的write()方法\n1 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 37 38 39 40 41 42 43 class DataContainerStamped { private int data; private final StampedLock lock = new StampedLock(); public DataContainerStamped(int data) { this.data = data; } public int read(int readTime) { //获取戳 long stamp = lock.tryOptimisticRead(); log.debug(\u0026#34;optimistic read locking...{}\u0026#34;, stamp); //读取数据 sleep(readTime); //读取数据之后再验戳 if (lock.validate(stamp)) { log.debug(\u0026#34;read finish...{}, data:{}\u0026#34;, stamp, data); return data; } //如果验戳失败，说明已经数据已经被修改，需要升级锁重新读。 // 锁升级 - 读锁 log.debug(\u0026#34;updating to read lock... {}\u0026#34;, stamp); try { stamp = lock.readLock(); log.debug(\u0026#34;read lock {}\u0026#34;, stamp); sleep(readTime); log.debug(\u0026#34;read finish...{}, data:{}\u0026#34;, stamp, data); return data; } finally { log.debug(\u0026#34;read unlock {}\u0026#34;, stamp); lock.unlockRead(stamp); } } public void write(int newData) { long stamp = lock.writeLock(); log.debug(\u0026#34;write lock {}\u0026#34;, stamp); try { sleep(2); this.data = newData; } finally { log.debug(\u0026#34;write unlock {}\u0026#34;, stamp); lock.unlockWrite(stamp); } } } Copied! 测试读-读可以优化\n1 2 3 4 5 6 7 8 9 10 public static void main(String[] args) { DataContainerStamped dataContainer = new DataContainerStamped(1); new Thread(() -\u0026gt; { dataContainer.read(1); }, \u0026#34;t1\u0026#34;).start(); sleep(0.5); new Thread(() -\u0026gt; { dataContainer.read(0); }, \u0026#34;t2\u0026#34;).start(); } Copied! 输出结果，可以看到实际没有加读锁\n1 2 3 4 15:58:50.217 c.DataContainerStamped [t1] - optimistic read locking...256 15:58:50.717 c.DataContainerStamped [t2] - optimistic read locking...256 15:58:50.717 c.DataContainerStamped [t2] - read finish...256, data:1 15:58:51.220 c.DataContainerStamped [t1] - read finish...256, data:1 Copied! 测试读-写时优化读补加读锁\n1 2 3 4 5 6 7 8 9 10 public static void main(String[] args) { DataContainerStamped dataContainer = new DataContainerStamped(1); new Thread(() -\u0026gt; { dataContainer.read(1); }, \u0026#34;t1\u0026#34;).start(); sleep(0.5); new Thread(() -\u0026gt; { dataContainer.write(100); }, \u0026#34;t2\u0026#34;).start(); } Copied! 输出结果\n1 2 3 4 5 6 7 15:57:00.219 c.DataContainerStamped [t1] - optimistic read locking...256 15:57:00.717 c.DataContainerStamped [t2] - write lock 384 15:57:01.225 c.DataContainerStamped [t1] - updating to read lock... 256 15:57:02.719 c.DataContainerStamped [t2] - write unlock 384 15:57:02.719 c.DataContainerStamped [t1] - read lock 513 15:57:03.719 c.DataContainerStamped [t1] - read finish...513, data:1000 15:57:03.719 c.DataContainerStamped [t1] - read unlock 513 Copied! 注意\nStampedLock 不支持条件变量 StampedLock 不支持可重入 Semaphore 基本使用 [ˈsɛməˌfɔr] 信号量，用来限制能同时访问共享资源的线程上限。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static void main(String[] args) { // 1. 创建 semaphore 对象 Semaphore semaphore = new Semaphore(3); // 2. 10个线程同时运行 for (int i = 0; i \u0026lt; 10; i++) { new Thread(() -\u0026gt; { // 3. 获取许可 try { semaphore.acquire(); //对于非打断式获取，如果此过程中被打断，线程依旧会等到获取了信号量之后才进入catch块。 //catch块中的线程依旧持有信号量，捕获该异常后catch块可以不做任何处理。 } catch (InterruptedException e) { e.printStackTrace(); } try { log.debug(\u0026#34;running...\u0026#34;); sleep(1); log.debug(\u0026#34;end...\u0026#34;); } finally { // 4. 释放许可 semaphore.release(); } }).start(); } } Copied! 输出\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 07:35:15.485 c.TestSemaphore [Thread-2] - running... 07:35:15.485 c.TestSemaphore [Thread-1] - running... 07:35:15.485 c.TestSemaphore [Thread-0] - running... 07:35:16.490 c.TestSemaphore [Thread-2] - end... 07:35:16.490 c.TestSemaphore [Thread-0] - end... 07:35:16.490 c.TestSemaphore [Thread-1] - end... 07:35:16.490 c.TestSemaphore [Thread-3] - running... 07:35:16.490 c.TestSemaphore [Thread-5] - running... 07:35:16.490 c.TestSemaphore [Thread-4] - running... 07:35:17.490 c.TestSemaphore [Thread-5] - end... 07:35:17.490 c.TestSemaphore [Thread-4] - end... 07:35:17.490 c.TestSemaphore [Thread-3] - end... 07:35:17.490 c.TestSemaphore [Thread-6] - running... 07:35:17.490 c.TestSemaphore [Thread-7] - running... 07:35:17.490 c.TestSemaphore [Thread-9] - running... 07:35:18.491 c.TestSemaphore [Thread-6] - end... 07:35:18.491 c.TestSemaphore [Thread-7] - end... 07:35:18.491 c.TestSemaphore [Thread-9] - end... 07:35:18.491 c.TestSemaphore [Thread-8] - running... 07:35:19.492 c.TestSemaphore [Thread-8] - end... Copied! 说明：\nSemaphore有两个构造器：Semaphore(int permits)和Semaphore(int permits,boolean fair) permits表示允许同时访问共享资源的线程数。 fair表示公平与否，与之前的ReentrantLock一样。 * Semaphore 应用 semaphore 限制对共享资源的使用\n使用 Semaphore 限流，在访问高峰期时，让请求线程阻塞，高峰期过去再释放许可，当然它只适合限制单机 线程数量，并且仅是限制线程数，而不是限制资源数（例如连接数，请对比 Tomcat LimitLatch 的实现） 用 Semaphore 实现简单连接池，对比『享元模式』下的实现（用wait notify），性能和可读性显然更好， 注意下面的实现中线程数和数据库连接数是相等的 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @Slf4j(topic = \u0026#34;c.Pool\u0026#34;) class Pool { // 1. 连接池大小 private final int poolSize; // 2. 连接对象数组 private Connection[] connections; // 3. 连接状态数组 0 表示空闲， 1 表示繁忙 private AtomicIntegerArray states; private Semaphore semaphore; // 4. 构造方法初始化 public Pool(int poolSize) { this.poolSize = poolSize; // 让许可数与资源数一致 this.semaphore = new Semaphore(poolSize); this.connections = new Connection[poolSize]; this.states = new AtomicIntegerArray(new int[poolSize]); for (int i = 0; i \u0026lt; poolSize; i++) { connections[i] = new MockConnection(\u0026#34;连接\u0026#34; + (i+1)); } } // 5. 借连接 public Connection borrow() {// t1, t2, t3 // 获取许可 try { semaphore.acquire(); // 没有许可的线程，在此等待 } catch (InterruptedException e) { e.printStackTrace(); } for (int i = 0; i \u0026lt; poolSize; i++) { // 获取空闲连接 if(states.get(i) == 0) { if (states.compareAndSet(i, 0, 1)) { log.debug(\u0026#34;borrow {}\u0026#34;, connections[i]); return connections[i]; } } } // 不会执行到这里 return null; } // 6. 归还连接 public void free(Connection conn) { for (int i = 0; i \u0026lt; poolSize; i++) { if (connections[i] == conn) { states.set(i, 0); log.debug(\u0026#34;free {}\u0026#34;, conn); semaphore.release(); break; } } } } Copied! * Semaphore 原理 加锁解锁流程 Semaphore有点像一个停车场，permits就好像停车位数量，当线程获得了permits就像是获得了停车位，然后停车场显示空余车位减一。\n刚开始，permits（state）为 3，这时 5 个线程来获取资源\n假设其中 Thread-1，Thread-2，Thread-4 cas 竞争成功，而 Thread-0 和 Thread-3 竞争失败，进入 AQS 队列 park 阻塞\n这时 Thread-4 释放了 permits，状态如下\n接下来 Thread-0 竞争成功，permits 再次设置为 0，设置自己为 head 节点，断开原来的 head 节点，unpark 接 下来的 Thread-3 节点，但由于 permits 是 0，因此 Thread-3 在尝试不成功后再次进入 park 状态\n源码分析 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 static final class NonfairSync extends Sync { private static final long serialVersionUID = -2694183684443567898L; NonfairSync(int permits) { // permits 即 state super(permits); } // Semaphore 方法, 方便阅读, 放在此处 public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); } // AQS 继承过来的方法, 方便阅读, 放在此处 public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) \u0026lt; 0) doAcquireSharedInterruptibly(arg); } // 尝试获得共享锁 protected int tryAcquireShared(int acquires) { return nonfairTryAcquireShared(acquires); } // Sync 继承过来的方法, 方便阅读, 放在此处 final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; if ( // 如果许可已经用完, 返回负数, 表示获取失败, 进入 doAcquireSharedInterruptibly remaining \u0026lt; 0 || // 如果 cas 重试成功, 返回正数, 表示获取成功 compareAndSetState(available, remaining) ) { return remaining; } } } // AQS 继承过来的方法, 方便阅读, 放在此处 private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { // 再次尝试获取许可 int r = tryAcquireShared(arg); if (r \u0026gt;= 0) { // 成功后本线程出队（AQS）, 所在 Node设置为 head // 如果 head.waitStatus == Node.SIGNAL ==\u0026gt; 0 成功, 下一个节点 unpark // 如果 head.waitStatus == 0 ==\u0026gt; Node.PROPAGATE // r 表示可用资源数, 为 0 则不会继续传播 setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } // 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞 if (shouldParkAfterFailedAcquire(p, node) \u0026amp;\u0026amp; parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } // Semaphore 方法, 方便阅读, 放在此处 public void release() { sync.releaseShared(1); } // AQS 继承过来的方法, 方便阅读, 放在此处 public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } // Sync 继承过来的方法, 方便阅读, 放在此处 protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); int next = current + releases; if (next \u0026lt; current) // overflow throw new Error(\u0026#34;Maximum permit count exceeded\u0026#34;); if (compareAndSetState(current, next)) return true; } } } private void setHeadAndPropagate(Node node, int propagate) { Node h = head; // Record old head for check below // 设置自己为 head setHead(node); // propagate 表示有共享资源（例如共享读锁或信号量） // 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE // 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE if (propagate \u0026gt; 0 || h == null || h.waitStatus \u0026lt; 0 || (h = head) == null || h.waitStatus \u0026lt; 0) { Node s = node.next; // 如果是最后一个节点或者是等待共享读锁的节点 if (s == null || s.isShared()) { doReleaseShared(); } } } private void doReleaseShared() { for (;;) { Node h = head; if (h != null \u0026amp;\u0026amp; h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } else if (ws == 0 \u0026amp;\u0026amp; !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } } Copied! 加锁流程总结： acquire-\u0026gt;acquireSharedInterruptibly(1)-\u0026gt;tryAcquireShared(1)-\u0026gt;nonfairTryAcquireShared(1),如果资源用完了，返回负数，tryAcquireShared返回负数，表示失败。否则返回正数，tryAcquireShared返回正数,表示成功。 如果成功，获取信号量成功。 如果失败，调用doAcquireSharedInterruptibly,进入for循环： 如果当前驱节点为头节点，调用tryAcquireShared尝试获取锁 如果结果大于等于0，表明获取锁成功，调用setHeadAndPropagate，将当前节点设为头节点，之后又调用doReleaseShared，唤醒后继节点。 调用shoudParkAfterFailure,第一次调用返回false，并将前驱节点改为-1，第二次循环如果再进入此方法，会进入阻塞并检查打断的方法。 解锁流程总结： release-\u0026gt;sync.releaseShared(1)-\u0026gt;tryReleaseShared(1),只要不发生整数溢出，就返回true 如果返回true，调用doReleaseShared，唤醒后继节点。 如果返回false，解锁失败。 为什么要有 PROPAGATE CountdownLatch 用来进行线程同步协作，等待所有线程完成倒计时。\n其中构造参数用来初始化等待计数值，await() 用来等待计数归零，countDown() 用来让计数减一\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); new Thread(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); sleep(1); latch.countDown(); log.debug(\u0026#34;end...{}\u0026#34;, latch.getCount()); }).start(); new Thread(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); sleep(2); latch.countDown(); log.debug(\u0026#34;end...{}\u0026#34;, latch.getCount()); }).start(); new Thread(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); sleep(1.5); latch.countDown(); log.debug(\u0026#34;end...{}\u0026#34;, latch.getCount()); }).start(); log.debug(\u0026#34;waiting...\u0026#34;); latch.await(); log.debug(\u0026#34;wait end...\u0026#34;); } Copied! 输出\n1 2 3 4 5 6 7 8 18:44:00.778 c.TestCountDownLatch [main] - waiting... 18:44:00.778 c.TestCountDownLatch [Thread-2] - begin... 18:44:00.778 c.TestCountDownLatch [Thread-0] - begin... 18:44:00.778 c.TestCountDownLatch [Thread-1] - begin... 18:44:01.782 c.TestCountDownLatch [Thread-0] - end...2 18:44:02.283 c.TestCountDownLatch [Thread-2] - end...1 18:44:02.782 c.TestCountDownLatch [Thread-1] - end...0 18:44:02.782 c.TestCountDownLatch [main] - wait end... Copied! 相比于join，CountDownLatch能配合线程池使用。\n1 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 public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(3); ExecutorService service = Executors.newFixedThreadPool(4); service.submit(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); sleep(1); latch.countDown(); log.debug(\u0026#34;end...{}\u0026#34;, latch.getCount()); }); service.submit(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); sleep(1.5); latch.countDown(); log.debug(\u0026#34;end...{}\u0026#34;, latch.getCount()); }); service.submit(() -\u0026gt; { log.debug(\u0026#34;begin...\u0026#34;); sleep(2); latch.countDown(); log.debug(\u0026#34;end...{}\u0026#34;, latch.getCount()); }); service.submit(()-\u0026gt;{ try { log.debug(\u0026#34;waiting...\u0026#34;); latch.await(); log.debug(\u0026#34;wait end...\u0026#34;); } catch (InterruptedException e) { e.printStackTrace(); } }); } Copied! * 应用之同步等待多线程准备完毕 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 AtomicInteger num = new AtomicInteger(0); ExecutorService service = Executors.newFixedThreadPool(10, (r) -\u0026gt; { return new Thread(r, \u0026#34;t\u0026#34; + num.getAndIncrement()); }); CountDownLatch latch = new CountDownLatch(10); String[] all = new String[10]; Random r = new Random(); for (int j = 0; j \u0026lt; 10; j++) { int x = j; service.submit(() -\u0026gt; { for (int i = 0; i \u0026lt;= 100; i++) { try { //随机休眠，模拟网络延迟 Thread.sleep(r.nextInt(100)); } catch (InterruptedException e) { } all[x] = Thread.currentThread().getName() + \u0026#34;(\u0026#34; + (i + \u0026#34;%\u0026#34;) + \u0026#34;)\u0026#34;; //\\r可以让当前输出覆盖上一次的输出。 System.out.print(\u0026#34;\\r\u0026#34; + Arrays.toString(all)); } latch.countDown(); }); } latch.await(); System.out.println(\u0026#34;\\n游戏开始...\u0026#34;); service.shutdown(); Copied! 中间输出\n1 [t0(52%), t1(47%), t2(51%), t3(40%), t4(49%), t5(44%), t6(49%), t7(52%), t8(46%), t9(46%)] Copied! 最后输出\n1 2 3 [t0(100%), t1(100%), t2(100%), t3(100%), t4(100%), t5(100%), t6(100%), t7(100%), t8(100%), t9(100%)] 游戏开始... Copied! * 应用之同步等待多个远程调用结束 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 37 38 39 40 @RestController public class TestCountDownlatchController { @GetMapping(\u0026#34;/order/{id}\u0026#34;) public Map\u0026lt;String, Object\u0026gt; order(@PathVariable int id) { HashMap\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;id\u0026#34;, id); map.put(\u0026#34;total\u0026#34;, \u0026#34;2300.00\u0026#34;); sleep(2000); return map; } @GetMapping(\u0026#34;/product/{id}\u0026#34;) public Map\u0026lt;String, Object\u0026gt; product(@PathVariable int id) { HashMap\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); if (id == 1) { map.put(\u0026#34;name\u0026#34;, \u0026#34;小爱音箱\u0026#34;); map.put(\u0026#34;price\u0026#34;, 300); } else if (id == 2) { map.put(\u0026#34;name\u0026#34;, \u0026#34;小米手机\u0026#34;); map.put(\u0026#34;price\u0026#34;, 2000); } map.put(\u0026#34;id\u0026#34;, id); sleep(1000); return map; } @GetMapping(\u0026#34;/logistics/{id}\u0026#34;) public Map\u0026lt;String, Object\u0026gt; logistics(@PathVariable int id) { HashMap\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;id\u0026#34;, id); map.put(\u0026#34;name\u0026#34;, \u0026#34;中通快递\u0026#34;); sleep(2500); return map; } private void sleep(int millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { e.printStackTrace(); } } } Copied! rest远程调用\n1 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 RestTemplate restTemplate = new RestTemplate(); log.debug(\u0026#34;begin\u0026#34;); ExecutorService service = Executors.newCachedThreadPool(); CountDownLatch latch = new CountDownLatch(4); Future\u0026lt;Map\u0026lt;String,Object\u0026gt;\u0026gt; f1 = service.submit(() -\u0026gt; { Map\u0026lt;String, Object\u0026gt; r = restTemplate.getForObject(\u0026#34;http://localhost:8080/order/{1}\u0026#34;, Map.class, 1); return r; }); Future\u0026lt;Map\u0026lt;String, Object\u0026gt;\u0026gt; f2 = service.submit(() -\u0026gt; { Map\u0026lt;String, Object\u0026gt; r = restTemplate.getForObject(\u0026#34;http://localhost:8080/product/{1}\u0026#34;, Map.class, 1); return r; }); Future\u0026lt;Map\u0026lt;String, Object\u0026gt;\u0026gt; f3 = service.submit(() -\u0026gt; { Map\u0026lt;String, Object\u0026gt; r = restTemplate.getForObject(\u0026#34;http://localhost:8080/product/{1}\u0026#34;, Map.class, 2); return r; }); Future\u0026lt;Map\u0026lt;String, Object\u0026gt;\u0026gt; f4 = service.submit(() -\u0026gt; { Map\u0026lt;String, Object\u0026gt; r = restTemplate.getForObject(\u0026#34;http://localhost:8080/logistics/{1}\u0026#34;, Map.class, 1); return r; }); System.out.println(f1.get()); System.out.println(f2.get()); System.out.println(f3.get()); System.out.println(f4.get()); log.debug(\u0026#34;执行完毕\u0026#34;); service.shutdown(); Copied! 执行结果\n1 2 3 4 5 6 19:51:39.711 c.TestCountDownLatch [main] - begin {total=2300.00, id=1} {price=300, name=小爱音箱, id=1} {price=2000, name=小米手机, id=2} {name=中通快递, id=1} 19:51:42.407 c.TestCountDownLatch [main] - 执行完毕 Copied! 说明：\n这种等待多个带有返回值的任务的场景，还是用future比较合适，CountdownLatch适合任务没有返回值的场景。 CyclicBarrier CountdownLatch的缺点在于不能重用，见下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private static void test1() { ExecutorService service = Executors.newFixedThreadPool(5); for (int i = 0; i \u0026lt; 3; i++) { CountDownLatch latch = new CountDownLatch(2); service.submit(() -\u0026gt; { log.debug(\u0026#34;task1 start...\u0026#34;); sleep(1); latch.countDown(); }); service.submit(() -\u0026gt; { log.debug(\u0026#34;task2 start...\u0026#34;); sleep(2); latch.countDown(); }); try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(\u0026#34;task1 task2 finish...\u0026#34;); } service.shutdown(); } Copied! 想要重复使用CountdownLatch进行同步，必须创建多个CountDownLatch对象。\n[ˈsaɪklɪk ˈbæriɚ] 循环栅栏，用来进行线程协作，等待线程满足某个计数。构造时设置『计数个数』，每个线程执 行到某个需要“同步”的时刻调用 await() 方法进行等待，当等待的线程数满足『计数个数』时，继续执行\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 CyclicBarrier cb = new CyclicBarrier(2); // 个数为2时才会继续执行 new Thread(()-\u0026gt;{ System.out.println(\u0026#34;线程1开始..\u0026#34;+new Date()); try { cb.await(); // 当个数不足时，等待 } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } System.out.println(\u0026#34;线程1继续向下运行...\u0026#34;+new Date()); }).start(); new Thread(()-\u0026gt;{ System.out.println(\u0026#34;线程2开始..\u0026#34;+new Date()); try { Thread.sleep(2000); } catch (InterruptedException e) { } try { cb.await(); // 2 秒后，线程个数够2，继续运行 } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } System.out.println(\u0026#34;线程2继续向下运行...\u0026#34;+new Date()); }).start(); Copied! 注意\nCyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比 喻为『人满发车』 CountDownLatch的计数和阻塞方法是分开的两个方法，而CyclicBarrier是一个方法。 CyclicBarrier的构造器还有一个Runnable类型的参数，在计数为0时会执行其中的run方法。 线程安全集合类概述 线程安全集合类可以分为三大类：\n遗留的线程安全集合如Hashtable，Vector 使用Collections装饰的线程安全集合，如： Collections.synchronizedCollection Collections.synchronizedList Collections.synchronizedMap Collections.synchronizedSet Collections.synchronizedNavigableMap Collections.synchronizedNavigableSet Collections.synchronizedSortedMap Collections.synchronizedSortedSet 说明：以上集合均采用修饰模式设计，将非线程安全的集合包装后，在调用方法时包裹了一层synchronized代码块。其并发性并不比遗留的安全集合好。 java.util.concurrent.* 重点介绍java.util.concurrent.* 下的线程安全集合类，可以发现它们有规律，里面包含三类关键词： Blocking、CopyOnWrite、Concurrent\nBlocking 大部分实现基于锁，并提供用来阻塞的方法 CopyOnWrite 之类容器修改开销相对较重 Concurrent 类型的容器 内部很多操作使用 cas 优化，一般可以提供较高吞吐量 弱一致性 遍历时弱一致性，例如，当利用迭代器遍历时，如果容器发生修改，迭代器仍然可以继续进行遍 历，这时内容是旧的 求大小弱一致性，size 操作未必是 100% 准确 读取弱一致性 遍历时如果发生了修改，对于非安全容器来讲，使用 fail-fast 机制也就是让遍历立刻失败，抛出 ConcurrentModificationException，不再继续遍历\nConcurrentHashMap 应用之单词计数 搭建练习环境：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 public class Test { public static void main(String[] args){ //在main方法中实现两个接口 } //开启26个线程，每个线程调用get方法获取map，从对应的文件读取单词并存储到list中，最后调用accept方法进行统计。 public static \u0026lt;V\u0026gt; void calculate(Supplier\u0026lt;Map\u0026lt;String,V\u0026gt;\u0026gt; supplier, BiConsumer\u0026lt;Map\u0026lt;String,V\u0026gt;, List\u0026lt;String\u0026gt;\u0026gt; consumer) { Map\u0026lt;String, V\u0026gt; map = supplier.get(); CountDownLatch count = new CountDownLatch(26); for (int i = 1; i \u0026lt; 27; i++) { int k = i; new Thread(()-\u0026gt;{ ArrayList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); read(list,k); consumer.accept(map,list); count.countDown(); }).start(); } try { count.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(map.toString()); } //读单词方法的实现 public static void read(List\u0026lt;String\u0026gt; list,int i){ try{ String element; BufferedReader reader = new BufferedReader(new FileReader(i + \u0026#34;.txt\u0026#34;)); while((element = reader.readLine()) != null){ list.add(element); } }catch (IOException e){ } } //生成测试数据 public void construct(){ String str = \u0026#34;abcdefghijklmnopqrstuvwxyz\u0026#34;; ArrayList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; str.length(); i++) { for (int j = 0; j \u0026lt; 200; j++) { list.add(String.valueOf(str.charAt(i))); } } Collections.shuffle(list); for (int i = 0; i \u0026lt; 26; i++) { try (PrintWriter out = new PrintWriter(new FileWriter(i + 1 + \u0026#34;.txt\u0026#34;))) { String collect = list.subList(i * 200, (i + 1) * 200).stream().collect(Collectors.joining(\u0026#34;\\n\u0026#34;)); out.println(collect); } catch (IOException e) { e.printStackTrace(); } } } } Copied! 实现一： 1 2 3 4 5 6 7 8 9 10 11 12 13 demo( // 创建 map 集合 // 创建 ConcurrentHashMap 对不对？ () -\u0026gt; new ConcurrentHashMap\u0026lt;String, Integer\u0026gt;(), // 进行计数 (map, words) -\u0026gt; { for (String word : words) { Integer counter = map.get(word); int newValue = counter == null ? 1 : counter + 1; map.put(word, newValue); } } ); Copied! 输出：\n1 2 {a=186, b=192, c=187, d=184, e=185, f=185, g=176, h=185, i=193, j=189, k=187, l=157, m=189, n=181, o=180, p=178, q=185, r=188, s=181, t=183, u=177, v=186, w=188, x=178, y=189, z=186} 47 Copied! 错误原因：\nConcurrentHashMap虽然每个方法都是线程安全的，但是多个方法的组合并不是线程安全的。 正确答案一： 1 2 3 4 5 6 7 8 9 demo( () -\u0026gt; new ConcurrentHashMap\u0026lt;String, LongAdder\u0026gt;(), (map, words) -\u0026gt; { for (String word : words) { // 注意不能使用 putIfAbsent，此方法返回的是上一次的 value，首次调用返回 null map.computeIfAbsent(word, (key) -\u0026gt; new LongAdder()).increment(); } } ); Copied! 说明：\ncomputIfAbsent方法的作用是：当map中不存在以参数1为key对应的value时，会将参数2函数式接口的返回值作为value，put进map中，然后返回该value。如果存在key，则直接返回value 以上两部均是线程安全的。 正确答案二： 1 2 3 4 5 6 7 8 9 demo( () -\u0026gt; new ConcurrentHashMap\u0026lt;String, Integer\u0026gt;(), (map, words) -\u0026gt; { for (String word : words) { // 函数式编程，无需原子变量 map.merge(word, 1, Integer::sum); } } ); Copied! * ConcurrentHashMap 原理 JDK 7 HashMap 并发死链 测试代码 注意\n要在 JDK 7 下运行，否则扩容机制和 hash 的计算方法都变了 以下测试代码是精心准备的，不要随便改动 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public static void main(String[] args) { // 测试 java 7 中哪些数字的 hash 结果相等 System.out.println(\u0026#34;长度为16时，桶下标为1的key\u0026#34;); for (int i = 0; i \u0026lt; 64; i++) { if (hash(i) % 16 == 1) { System.out.println(i); } } System.out.println(\u0026#34;长度为32时，桶下标为1的key\u0026#34;); for (int i = 0; i \u0026lt; 64; i++) { if (hash(i) % 32 == 1) { System.out.println(i); } } // 1, 35, 16, 50 当大小为16时，它们在一个桶内 final HashMap\u0026lt;Integer, Integer\u0026gt; map = new HashMap\u0026lt;Integer, Integer\u0026gt;(); // 放 12 个元素 map.put(2, null); map.put(3, null); map.put(4, null); map.put(5, null); map.put(6, null); map.put(7, null); map.put(8, null); map.put(9, null); map.put(10, null); map.put(16, null); map.put(35, null); map.put(1, null); System.out.println(\u0026#34;扩容前大小[main]:\u0026#34;+map.size()); new Thread() { @Override public void run() { // 放第 13 个元素, 发生扩容 map.put(50, null); System.out.println(\u0026#34;扩容后大小[Thread-0]:\u0026#34;+map.size()); } }.start(); new Thread() { @Override public void run() { // 放第 13 个元素, 发生扩容 map.put(50, null); System.out.println(\u0026#34;扩容后大小[Thread-1]:\u0026#34;+map.size()); } }.start(); } final static int hash(Object k) { int h = 0; if (0 != h \u0026amp;\u0026amp; k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h \u0026gt;\u0026gt;\u0026gt; 20) ^ (h \u0026gt;\u0026gt;\u0026gt; 12); return h ^ (h \u0026gt;\u0026gt;\u0026gt; 7) ^ (h \u0026gt;\u0026gt;\u0026gt; 4); } Copied! 死链复现 调试工具使用 idea\n在 HashMap 源码 590 行加断点\n1 int newCapacity = newTable.length; Copied! 断点的条件如下，目的是让 HashMap 在扩容为 32 时，并且线程为 Thread-0 或 Thread-1 时停下来\n1 2 3 4 5 newTable.length==32 \u0026amp;\u0026amp; ( Thread.currentThread().getName().equals(\u0026#34;Thread-0\u0026#34;)|| Thread.currentThread().getName().equals(\u0026#34;Thread-1\u0026#34;) ) Copied! 断点暂停方式选择 Thread，否则在调试 Thread-0 时，Thread-1 无法恢复运行\n运行代码，程序在预料的断点位置停了下来，输出\n1 2 3 4 5 6 7 8 9 长度为16时，桶下标为1的key 1 16 35 50 长度为32时，桶下标为1的key 1 35 扩容前大小[main]:12 Copied! 接下来进入扩容流程调试\n在 HashMap 源码 594 行加断点\n1 2 3 Entry\u0026lt;K,V\u0026gt; next = e.next; // 593 if (rehash) // 594 // ... Copied! 这是为了观察 e 节点和 next 节点的状态，Thread-0 单步执行到 594 行，再 594 处再添加一个断点（条件 Thread.currentThread().getName().equals(\u0026ldquo;Thread-0\u0026rdquo;)）\n这时可以在 Variables 面板观察到 e 和 next 变量，使用view as -\u0026gt; Object查看节点状态\n1 2 e (1)-\u0026gt;(35)-\u0026gt;(16)-\u0026gt;null next (35)-\u0026gt;(16)-\u0026gt;null Copied! 在 Threads 面板选中 Thread-1 恢复运行，可以看到控制台输出新的内容如下，Thread-1 扩容已完成\n1 newTable[1] (35)-\u0026gt;(1)-\u0026gt;null Copied! 1 扩容后大小:13 Copied! 这时 Thread-0 还停在 594 处， Variables 面板变量的状态已经变化为\n1 2 e (1)-\u0026gt;null next (35)-\u0026gt;(1)-\u0026gt;null Copied! 为什么呢，因为 Thread-1 扩容时链表也是后加入的元素放入链表头，因此链表就倒过来了，但 Thread-1 虽然结 果正确，但它结束后 Thread-0 还要继续运行\n接下来就可以单步调试（F8）观察死链的产生了\n下一轮循环到 594，将 e 搬迁到 newTable 链表头\n1 2 3 newTable[1] (1)-\u0026gt;null e (35)-\u0026gt;(1)-\u0026gt;null next (1)-\u0026gt;null Copied! 下一轮循环到 594，将 e 搬迁到 newTable 链表头\n1 2 3 newTable[1] (35)-\u0026gt;(1)-\u0026gt;null e (1)-\u0026gt;null next null Copied! 再看看源码\n1 2 3 4 5 6 7 e.next = newTable[1]; // 这时 e (1,35) // 而 newTable[1] (35,1)-\u0026gt;(1,35) 因为是同一个对象 newTable[1] = e; // 再尝试将 e 作为链表头, 死链已成 e = next; // 虽然 next 是 null, 会进入下一个链表的复制, 但死链已经形成了 Copied! 源码分析 HashMap 的并发死链发生在扩容时\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 将 table 迁移至 newTable void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry\u0026lt;K,V\u0026gt; e : table) { while(null != e) { Entry\u0026lt;K,V\u0026gt; next = e.next; // 1 处 if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); // 2 处 // 将新元素加入 newTable[i], 原 newTable[i] 作为新元素的 next e.next = newTable[i]; newTable[i] = e; e = next; } } } Copied! 假设 map 中初始元素是\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 原始链表，格式：[下标] (key,next) [1] (1,35)-\u0026gt;(35,16)-\u0026gt;(16,null) 线程 a 执行到 1 处 ，此时局部变量 e 为 (1,35)，而局部变量 next 为 (35,16) 线程 a 挂起 线程 b 开始执行 第一次循环 [1] (1,null) 第二次循环 [1] (35,1)-\u0026gt;(1,null) 第三次循环 [1] (35,1)-\u0026gt;(1,null) [17] (16,null) 切换回线程 a，此时局部变量 e 和 next 被恢复，引用没变但内容变了：e 的内容被改为 (1,null)，而 next 的内 容被改为 (35,1) 并链向 (1,null) 第一次循环 [1] (1,null) 第二次循环，注意这时 e 是 (35,1) 并链向 (1,null) 所以 next 又是 (1,null) [1] (35,1)-\u0026gt;(1,null) 第三次循环，e 是 (1,null)，而 next 是 null，但 e 被放入链表头，这样 e.next 变成了 35 （2 处） [1] (1,35)-\u0026gt;(35,1)-\u0026gt;(1,35) 已经是死链了 Copied! 小结\n究其原因，是因为在多线程环境下使用了非线程安全的 map 集合 JDK 8 虽然将扩容算法做了调整，不再将元素加入链表头（而是保持与扩容前一样的顺序），但仍不意味着能 够在多线程环境下能够安全扩容，还会出现其它问题（如扩容丢数据） JDK 8 ConcurrentHashMap 重要属性和内部类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 默认为 0 // 当初始化时, 为 -1 // 当扩容时, 为 -(1 + 扩容线程数) // 当初始化或扩容完成后，为 下一次的扩容的阈值大小 private transient volatile int sizeCtl; // 整个 ConcurrentHashMap 就是一个 Node[] static class Node\u0026lt;K,V\u0026gt; implements Map.Entry\u0026lt;K,V\u0026gt; {} // hash 表 transient volatile Node\u0026lt;K,V\u0026gt;[] table; // 扩容时的 新 hash 表 private transient volatile Node\u0026lt;K,V\u0026gt;[] nextTable; // 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点 static final class ForwardingNode\u0026lt;K,V\u0026gt; extends Node\u0026lt;K,V\u0026gt; {} // 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node static final class ReservationNode\u0026lt;K,V\u0026gt; extends Node\u0026lt;K,V\u0026gt; {} // 作为 treebin 的头节点, 存储 root 和 first static final class TreeBin\u0026lt;K,V\u0026gt; extends Node\u0026lt;K,V\u0026gt; {} // 作为 treebin 的节点, 存储 parent, left, right static final class TreeNode\u0026lt;K,V\u0026gt; extends Node\u0026lt;K,V\u0026gt; {} Copied! 重要方法 1 2 3 4 5 6 7 8 // 获取 Node[] 中第 i 个 Node static final \u0026lt;K,V\u0026gt; Node\u0026lt;K,V\u0026gt; tabAt(Node\u0026lt;K,V\u0026gt;[] tab, int i) // cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值 static final \u0026lt;K,V\u0026gt; boolean casTabAt(Node\u0026lt;K,V\u0026gt;[] tab, int i, Node\u0026lt;K,V\u0026gt; c, Node\u0026lt;K,V\u0026gt; v) // 直接修改 Node[] 中第 i 个 Node 的值, v 为新值 static final \u0026lt;K,V\u0026gt; void setTabAt(Node\u0026lt;K,V\u0026gt;[] tab, int i, Node\u0026lt;K,V\u0026gt; v) Copied! 构造器分析 可以看到实现了懒惰初始化，在构造方法中仅仅计算了 table 的大小，以后在第一次使用时才会真正创建\n1 2 3 4 5 6 7 8 9 10 11 public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor \u0026gt; 0.0f) || initialCapacity \u0026lt; 0 || concurrencyLevel \u0026lt;= 0) throw new IllegalArgumentException(); if (initialCapacity \u0026lt; concurrencyLevel) // Use at least as many bins initialCapacity = concurrencyLevel; // as estimated threads long size = (long)(1.0 + (long)initialCapacity / loadFactor); // tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ... int cap = (size \u0026gt;= (long)MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)size); this.sizeCtl = cap; } Copied! get流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public V get(Object key) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; e, p; int n, eh; K ek; // spread 方法能确保返回结果是正数 int h = spread(key.hashCode()); if ((tab = table) != null \u0026amp;\u0026amp; (n = tab.length) \u0026gt; 0 \u0026amp;\u0026amp; (e = tabAt(tab, (n - 1) \u0026amp; h)) != null) { // 如果头结点已经是要查找的 key if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null \u0026amp;\u0026amp; key.equals(ek))) return e.val; } // hash 为负数表示该 bin 在扩容中或是 treebin, 这时调用 find 方法来查找 else if (eh \u0026lt; 0) return (p = e.find(h, key)) != null ? p.val : null; // 正常遍历链表, 用 equals 比较 while ((e = e.next) != null) { if (e.hash == h \u0026amp;\u0026amp; ((ek = e.key) == key || (ek != null \u0026amp;\u0026amp; key.equals(ek)))) return e.val; } } return null; } Copied! 总结：\n如果table不为空且长度大于0且索引位置有元素 if 头节点key的hash值相等 头节点的key指向同一个地址或者equals 返回value else if 头节点的hash为负数（bin在扩容或者是treebin） 调用find方法查找 进入循环（e不为空）： 节点key的hash值相等，且key指向同一个地址或equals 返回value 返回null put 流程 以下数组简称（table），链表简称（bin）\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 public V put(K key, V value) { return putVal(key, value, false); } final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); // 其中 spread 方法会综合高位低位, 具有更好的 hash 性 int hash = spread(key.hashCode()); int binCount = 0; for (Node\u0026lt;K,V\u0026gt;[] tab = table;;) { // f 是链表头节点 // fh 是链表头结点的 hash // i 是链表在 table 中的下标 Node\u0026lt;K,V\u0026gt; f; int n, i, fh; // 要创建 table if (tab == null || (n = tab.length) == 0) // 初始化 table 使用了 cas, 无需 synchronized 创建成功, 进入下一轮循环 tab = initTable(); // 要创建链表头节点 else if ((f = tabAt(tab, i = (n - 1) \u0026amp; hash)) == null) { // 添加链表头使用了 cas, 无需 synchronized if (casTabAt(tab, i, null, new Node\u0026lt;K,V\u0026gt;(hash, key, value, null))) break; } // 帮忙扩容 else if ((fh = f.hash) == MOVED) // 帮忙之后, 进入下一轮循环 tab = helpTransfer(tab, f); else { V oldVal = null; // 锁住链表头节点 synchronized (f) { // 再次确认链表头节点没有被移动 if (tabAt(tab, i) == f) { // 链表 if (fh \u0026gt;= 0) { binCount = 1; // 遍历链表 for (Node\u0026lt;K,V\u0026gt; e = f;; ++binCount) { K ek; // 找到相同的 key if (e.hash == hash \u0026amp;\u0026amp; ((ek = e.key) == key || (ek != null \u0026amp;\u0026amp; key.equals(ek)))) { oldVal = e.val; // 更新 if (!onlyIfAbsent) e.val = value; break; } Node\u0026lt;K,V\u0026gt; pred = e; // 已经是最后的节点了, 新增 Node, 追加至链表尾 if ((e = e.next) == null) { pred.next = new Node\u0026lt;K,V\u0026gt;(hash, key, value, null); break; } } } // 红黑树 else if (f instanceof TreeBin) { Node\u0026lt;K,V\u0026gt; p; binCount = 2; // putTreeVal 会看 key 是否已经在树中, 是, 则返回对应的 TreeNode if ((p = ((TreeBin\u0026lt;K,V\u0026gt;)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } // 释放链表头节点的锁 } if (binCount != 0) { if (binCount \u0026gt;= TREEIFY_THRESHOLD) // 如果链表长度 \u0026gt;= 树化阈值(8), 进行链表转为红黑树 treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // 增加 size 计数 addCount(1L, binCount); return null; } private final Node\u0026lt;K,V\u0026gt;[] initTable() { Node\u0026lt;K,V\u0026gt;[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) \u0026lt; 0) Thread.yield(); // 尝试将 sizeCtl 设置为 -1（表示初始化 table） else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 获得锁, 创建 table, 这时其它线程会在 while() 循环中 yield 直至 table 创建 try { if ((tab = table) == null || tab.length == 0) { int n = (sc \u0026gt; 0) ? sc : DEFAULT_CAPACITY; Node\u0026lt;K,V\u0026gt;[] nt = (Node\u0026lt;K,V\u0026gt;[])new Node\u0026lt;?,?\u0026gt;[n]; table = tab = nt; sc = n - (n \u0026gt;\u0026gt;\u0026gt; 2); } } finally { sizeCtl = sc; } break; } } return tab; } // check 是之前 binCount 的个数 private final void addCount(long x, int check) { CounterCell[] as; long b, s; if ( // 已经有了 counterCells, 向 cell 累加 (as = counterCells) != null || // 还没有, 向 baseCount 累加 !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x) ) { CounterCell a; long v; int m; boolean uncontended = true; if ( // 还没有 counterCells as == null || (m = as.length - 1) \u0026lt; 0 || // 还没有 cell (a = as[ThreadLocalRandom.getProbe() \u0026amp; m]) == null || // cell cas 增加计数失败 !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)) ) { // 创建累加单元数组和cell, 累加重试 fullAddCount(x, uncontended); return; } if (check \u0026lt;= 1) return; // 获取元素个数 s = sumCount(); } if (check \u0026gt;= 0) { Node\u0026lt;K,V\u0026gt;[] tab, nt; int n, sc; while (s \u0026gt;= (long)(sc = sizeCtl) \u0026amp;\u0026amp; (tab = table) != null \u0026amp;\u0026amp; (n = tab.length) \u0026lt; MAXIMUM_CAPACITY) { int rs = resizeStamp(n); if (sc \u0026lt; 0) { if ((sc \u0026gt;\u0026gt;\u0026gt; RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex \u0026lt;= 0) break; // newtable 已经创建了，帮忙扩容 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) transfer(tab, nt); } // 需要扩容，这时 newtable 未创建 else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs \u0026lt;\u0026lt; RESIZE_STAMP_SHIFT) + 2)) transfer(tab, null); s = sumCount(); } } } Copied! 总结：\n进入for循环： if table为null或者长度 为0 初始化表 else if 索引处无节点 创建节点，填入key和value，放入table，退出循环 else if 索引处节点的hash值为MOVE（ForwardingNode），表示正在扩容和迁移 帮忙 else 锁住头节点 if 再次确认头节点没有被移动 if 头节点hash值大于0（表示这是一个链表） 遍历链表找到对应key，如果没有，创建。 else if 节点为红黑树节点 调用putTreeVal查看是否有对应key的数节点 如果有且为覆盖模式，将值覆盖，返回旧值 如果没有，创建并插入，返回null 解锁 if binCount不为0 如果binCount大于树化阈值8 树化 如果旧值不为null 返回旧值 break 增加size计数 return null size 计算流程 size 计算实际发生在 put，remove 改变集合元素的操作之中\n没有竞争发生，向 baseCount 累加计数 有竞争发生，新建 counterCells，向其中的一个 cell 累加计 counterCells 初始有两个 cell 如果计数竞争比较激烈，会创建新的 cell 来累加计数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public int size() { long n = sumCount(); return ((n \u0026lt; 0L) ? 0 : (n \u0026gt; (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n); } final long sumCount() { CounterCell[] as = counterCells; CounterCell a; // 将 baseCount 计数与所有 cell 计数累加 long sum = baseCount; if (as != null) { for (int i = 0; i \u0026lt; as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; } Copied! 总结 Java 8 数组（Node） +（ 链表 Node | 红黑树 TreeNode ） 以下数组简称（table），链表简称（bin）\n初始化，使用 cas 来保证并发安全，懒惰初始化 table 树化，当 table.length \u0026lt; 64 时，先尝试扩容，超过 64 时，并且 bin.length \u0026gt; 8 时，会将链表树化，树化过程 会用 synchronized 锁住链表头 put，如果该 bin 尚未创建，只需要使用 cas 创建 bin；如果已经有了，锁住链表头进行后续 put 操作，元素 添加至 bin 的尾部 get，无锁操作仅需要保证可见性，扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新 table 进行搜索 扩容，扩容时以 bin 为单位进行，需要对 bin 进行 synchronized，但这时妙的是其它竞争线程也不是无事可 做，它们会帮助把其它 bin 进行扩容，扩容时平均只有 1/6 的节点会把复制到新 table 中 size，元素个数保存在 baseCount 中，并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加 即可 源码分析 http://www.importnew.com/28263.html 其它实现 Cliff Click\u0026rsquo;s high scale lib JDK 7 ConcurrentHashMap 它维护了一个 segment 数组，每个 segment 对应一把锁\n优点：如果多个线程访问不同的 segment，实际是没有冲突的，这与 jdk8 中是类似的 缺点：Segments 数组默认大小为16，这个容量初始化指定后就不能改变了，并且不是懒惰初始化 构造器分析 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 public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { if (!(loadFactor \u0026gt; 0) || initialCapacity \u0026lt; 0 || concurrencyLevel \u0026lt;= 0) throw new IllegalArgumentException(); if (concurrencyLevel \u0026gt; MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; // ssize 必须是 2^n, 即 2, 4, 8, 16 ... 表示了 segments 数组的大小 int sshift = 0; int ssize = 1; while (ssize \u0026lt; concurrencyLevel) { ++sshift; ssize \u0026lt;\u0026lt;= 1; } // segmentShift 默认是 32 - 4 = 28 this.segmentShift = 32 - sshift; // segmentMask 默认是 15 即 0000 0000 0000 1111 this.segmentMask = ssize - 1; if (initialCapacity \u0026gt; MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; int c = initialCapacity / ssize; if (c * ssize \u0026lt; initialCapacity) ++c; int cap = MIN_SEGMENT_TABLE_CAPACITY; while (cap \u0026lt; c) cap \u0026lt;\u0026lt;= 1; // 创建 segments and segments[0] Segment\u0026lt;K,V\u0026gt; s0 = new Segment\u0026lt;K,V\u0026gt;(loadFactor, (int)(cap * loadFactor), (HashEntry\u0026lt;K,V\u0026gt;[])new HashEntry[cap]); Segment\u0026lt;K,V\u0026gt;[] ss = (Segment\u0026lt;K,V\u0026gt;[])new Segment[ssize]; UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0] this.segments = ss; } Copied! 构造完成，如下图所示\n可以看到 ConcurrentHashMap 没有实现懒惰初始化，空间占用不友好\n其中 this.segmentShift 和 this.segmentMask 的作用是决定将 key 的 hash 结果匹配到哪个 segment\n例如，根据某一 hash 值求 segment 位置，先将高位向低位移动 this.segmentShift 位\n结果再与 this.segmentMask 做位于运算，最终得到 1010 即下标为 10 的 segment\nput 流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public V put(K key, V value) { Segment\u0026lt;K,V\u0026gt; s; if (value == null) throw new NullPointerException(); int hash = hash(key); // 计算出 segment 下标 int j = (hash \u0026gt;\u0026gt;\u0026gt; segmentShift) \u0026amp; segmentMask; // 获得 segment 对象, 判断是否为 null, 是则创建该 segment if ((s = (Segment\u0026lt;K,V\u0026gt;)UNSAFE.getObject (segments, (j \u0026lt;\u0026lt; SSHIFT) + SBASE)) == null) { // 这时不能确定是否真的为 null, 因为其它线程也发现该 segment 为 null, // 因此在 ensureSegment 里用 cas 方式保证该 segment 安全性 s = ensureSegment(j); } // 进入 segment 的put 流程 return s.put(key, hash, value, false); } Copied! segment 继承了可重入锁（ReentrantLock），它的 put 方法为\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 final V put(K key, int hash, V value, boolean onlyIfAbsent) { // 尝试加锁 HashEntry\u0026lt;K,V\u0026gt; node = tryLock() ? null : // 如果不成功, 进入 scanAndLockForPut 流程 // 如果是多核 cpu 最多 tryLock 64 次, 进入 lock 流程 // 在尝试期间, 还可以顺便看该节点在链表中有没有, 如果没有顺便创建出来 scanAndLockForPut(key, hash, value); // 执行到这里 segment 已经被成功加锁, 可以安全执行 V oldValue; try { HashEntry\u0026lt;K,V\u0026gt;[] tab = table; int index = (tab.length - 1) \u0026amp; hash; HashEntry\u0026lt;K,V\u0026gt; first = entryAt(tab, index); for (HashEntry\u0026lt;K,V\u0026gt; e = first;;) { if (e != null) { // 更新 K k; if ((k = e.key) == key || (e.hash == hash \u0026amp;\u0026amp; key.equals(k))) { oldValue = e.value; if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } e = e.next; } else { // 新增 // 1) 之前等待锁时, node 已经被创建, next 指向链表头 if (node != null) node.setNext(first); else // 2) 创建新 node node = new HashEntry\u0026lt;K,V\u0026gt;(hash, key, value, first); int c = count + 1; // 3) 扩容 if (c \u0026gt; threshold \u0026amp;\u0026amp; tab.length \u0026lt; MAXIMUM_CAPACITY) rehash(node); else // 将 node 作为链表头 setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; } Copied! rehash 流程 发生在 put 中，因为此时已经获得了锁，因此 rehash 时不需要考虑线程安全\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 private void rehash(HashEntry\u0026lt;K,V\u0026gt; node) { HashEntry\u0026lt;K,V\u0026gt;[] oldTable = table; int oldCapacity = oldTable.length; int newCapacity = oldCapacity \u0026lt;\u0026lt; 1; threshold = (int)(newCapacity * loadFactor); HashEntry\u0026lt;K,V\u0026gt;[] newTable = (HashEntry\u0026lt;K,V\u0026gt;[]) new HashEntry[newCapacity]; int sizeMask = newCapacity - 1; for (int i = 0; i \u0026lt; oldCapacity ; i++) { HashEntry\u0026lt;K,V\u0026gt; e = oldTable[i]; if (e != null) { HashEntry\u0026lt;K,V\u0026gt; next = e.next; int idx = e.hash \u0026amp; sizeMask; if (next == null) // Single node on list newTable[idx] = e; else { // Reuse consecutive sequence at same slot HashEntry\u0026lt;K,V\u0026gt; lastRun = e; int lastIdx = idx; // 过一遍链表, 尽可能把 rehash 后 idx 不变的节点重用 for (HashEntry\u0026lt;K,V\u0026gt; last = next; last != null; last = last.next) { int k = last.hash \u0026amp; sizeMask; if (k != lastIdx) { lastIdx = k; lastRun = last; } } newTable[lastIdx] = lastRun; // 剩余节点需要新建 for (HashEntry\u0026lt;K,V\u0026gt; p = e; p != lastRun; p = p.next) { V v = p.value; int h = p.hash; int k = h \u0026amp; sizeMask; HashEntry\u0026lt;K,V\u0026gt; n = newTable[k]; newTable[k] = new HashEntry\u0026lt;K,V\u0026gt;(h, p.key, v, n); } } } } // 扩容完成, 才加入新的节点 int nodeIndex = node.hash \u0026amp; sizeMask; // add the new node node.setNext(newTable[nodeIndex]); newTable[nodeIndex] = node; // 替换为新的 HashEntry table table = newTable; } Copied! 附，调试代码\n1 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 37 public static void main(String[] args) { ConcurrentHashMap\u0026lt;Integer, String\u0026gt; map = new ConcurrentHashMap\u0026lt;\u0026gt;(); for (int i = 0; i \u0026lt; 1000; i++) { int hash = hash(i); int segmentIndex = (hash \u0026gt;\u0026gt;\u0026gt; 28) \u0026amp; 15; if (segmentIndex == 4 \u0026amp;\u0026amp; hash % 8 == 2) { System.out.println(i + \u0026#34;\\t\u0026#34; + segmentIndex + \u0026#34;\\t\u0026#34; + hash % 2 + \u0026#34;\\t\u0026#34; + hash % 4 + \u0026#34;\\t\u0026#34; + hash % 8); } } map.put(1, \u0026#34;value\u0026#34;); map.put(15, \u0026#34;value\u0026#34;); // 2 扩容为 4 15 的 hash%8 与其他不同 map.put(169, \u0026#34;value\u0026#34;); map.put(197, \u0026#34;value\u0026#34;); // 4 扩容为 8 map.put(341, \u0026#34;value\u0026#34;); map.put(484, \u0026#34;value\u0026#34;); map.put(545, \u0026#34;value\u0026#34;); // 8 扩容为 16 map.put(912, \u0026#34;value\u0026#34;); map.put(941, \u0026#34;value\u0026#34;); System.out.println(\u0026#34;ok\u0026#34;); } private static int hash(Object k) { int h = 0; if ((0 != h) \u0026amp;\u0026amp; (k instanceof String)) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // Spread bits to regularize both segment and index locations, // using variant of single-word Wang/Jenkins hash. h += (h \u0026lt;\u0026lt; 15) ^ 0xffffcd7d; h ^= (h \u0026gt;\u0026gt;\u0026gt; 10); h += (h \u0026lt;\u0026lt; 3); h ^= (h \u0026gt;\u0026gt;\u0026gt; 6); h += (h \u0026lt;\u0026lt; 2) + (h \u0026lt;\u0026lt; 14); int v = h ^ (h \u0026gt;\u0026gt;\u0026gt; 16); return v; } Copied! get 流程 get 时并未加锁，用了 UNSAFE 方法保证了可见性，扩容过程中，get 先发生就从旧表取内容，get 后发生就从新 表取内容\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public V get(Object key) { Segment\u0026lt;K,V\u0026gt; s; // manually integrate access methods to reduce overhead HashEntry\u0026lt;K,V\u0026gt;[] tab; int h = hash(key); // u 为 segment 对象在数组中的偏移量 long u = (((h \u0026gt;\u0026gt;\u0026gt; segmentShift) \u0026amp; segmentMask) \u0026lt;\u0026lt; SSHIFT) + SBASE; // s 即为 segment if ((s = (Segment\u0026lt;K,V\u0026gt;)UNSAFE.getObjectVolatile(segments, u)) != null \u0026amp;\u0026amp; (tab = s.table) != null) { for (HashEntry\u0026lt;K,V\u0026gt; e = (HashEntry\u0026lt;K,V\u0026gt;) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) \u0026amp; h)) \u0026lt;\u0026lt; TSHIFT) + TBASE); e != null; e = e.next) { K k; if ((k = e.key) == key || (e.hash == h \u0026amp;\u0026amp; key.equals(k))) return e.value; } } return null; } Copied! size 计算流程 计算元素个数前，先不加锁计算两次，如果前后两次结果如一样，认为个数正确返回 如果不一样，进行重试，重试次数超过 3，将所有 segment 锁住，重新计算个数返回 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 37 38 39 40 public int size() { // Try a few times to get accurate count. On failure due to // continuous async changes in table, resort to locking. final Segment\u0026lt;K,V\u0026gt;[] segments = this.segments; int size; boolean overflow; // true if size overflows 32 bits long sum; // sum of modCounts long last = 0L; // previous sum int retries = -1; // first iteration isn\u0026#39;t retry try { for (;;) { if (retries++ == RETRIES_BEFORE_LOCK) { // 超过重试次数, 需要创建所有 segment 并加锁 for (int j = 0; j \u0026lt; segments.length; ++j) ensureSegment(j).lock(); // force creation } sum = 0L; size = 0; overflow = false; for (int j = 0; j \u0026lt; segments.length; ++j) { Segment\u0026lt;K,V\u0026gt; seg = segmentAt(segments, j); if (seg != null) { sum += seg.modCount; int c = seg.count; if (c \u0026lt; 0 || (size += c) \u0026lt; 0) overflow = true; } } if (sum == last) break; last = sum; } } finally { if (retries \u0026gt; RETRIES_BEFORE_LOCK) { for (int j = 0; j \u0026lt; segments.length; ++j) segmentAt(segments, j).unlock(); } } return overflow ? Integer.MAX_VALUE : size; } Copied! BlockingQueue * BlockingQueue 原理 基本的入队出队 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class LinkedBlockingQueue\u0026lt;E\u0026gt; extends AbstractQueue\u0026lt;E\u0026gt; implements BlockingQueue\u0026lt;E\u0026gt;, java.io.Serializable { static class Node\u0026lt;E\u0026gt; { E item; /** * 下列三种情况之一 * - 真正的后继节点 * - 自己, 发生在出队时 * - null, 表示是没有后继节点, 是最后了 */ Node\u0026lt;E\u0026gt; next; Node(E x) { item = x; } } } Copied! 初始化链表 last = head = new Node(null);Dummy 节点用来占位，item 为 null\n当一个节点入队 last = last.next = node;\n再来一个节点入队last = last.next = node;\n出队 1 2 3 4 5 6 7 8 9 10 11 //临时变量h用来指向哨兵 Node\u0026lt;E\u0026gt; h = head; //first用来指向第一个元素 Node\u0026lt;E\u0026gt; first = h.next; h.next = h; // help GC //head赋值为first，表示first节点就是下一个哨兵。 head = first; E x = first.item; //删除first节点中的数据，表示真正成为了哨兵，第一个元素出队。 first.item = null; return x; Copied! h = head\nfirst = h.next\nh.next = h\nhead = first\n1 2 3 E x = first.item; first.item = null; return x; Copied! 加锁分析 高明之处在于用了两把锁和 dummy 节点\n用一把锁，同一时刻，最多只允许有一个线程（生产者或消费者，二选一）执行 用两把锁，同一时刻，可以允许两个线程同时（一个生产者与一个消费者）执行 消费者与消费者线程仍然串行 生产者与生产者线程仍然串行 线程安全分析\n当节点总数大于 2 时（包括 dummy 节点），putLock 保证的是 last 节点的线程安全，takeLock 保证的是 head 节点的线程安全。两把锁保证了入队和出队没有竞争 当节点总数等于 2 时（即一个 dummy 节点，一个正常节点）这时候，仍然是两把锁锁两个对象，不会竞争 当节点总数等于 1 时（就一个 dummy 节点）这时 take 线程会被 notEmpty 条件阻塞，有竞争，会阻塞 1 2 3 4 // 用于 put(阻塞) offer(非阻塞) private final ReentrantLock putLock = new ReentrantLock(); // 用户 take(阻塞) poll(非阻塞) private final ReentrantLock takeLock = new ReentrantLock(); Copied! put 操作\n1 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 public void put(E e) throws InterruptedException { //LinkedBlockingQueue不支持空元素 if (e == null) throw new NullPointerException(); int c = -1; Node\u0026lt;E\u0026gt; node = new Node\u0026lt;E\u0026gt;(e); final ReentrantLock putLock = this.putLock; // count 用来维护元素计数 final AtomicInteger count = this.count; putLock.lockInterruptibly(); try { // 满了等待 while (count.get() == capacity) { // 倒过来读就好: 等待 notFull notFull.await(); } // 有空位, 入队且计数加一 enqueue(node); c = count.getAndIncrement(); // 除了自己 put 以外, 队列还有空位, 由自己叫醒其他 put 线程 if (c + 1 \u0026lt; capacity) notFull.signal(); } finally { putLock.unlock(); } // 如果队列中有一个元素, 叫醒 take 线程 if (c == 0) // 这里调用的是 notEmpty.signal() 而不是 notEmpty.signalAll() 是为了减少竞争 signalNotEmpty(); } Copied! take 操作\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); try { while (count.get() == 0) { notEmpty.await(); } x = dequeue(); c = count.getAndDecrement(); if (c \u0026gt; 1) notEmpty.signal(); } finally { takeLock.unlock(); } // 如果队列中只有一个空位时, 叫醒 put 线程 // 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c \u0026lt; capacity if (c == capacity) // 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争 signalNotFull() return x; } Copied! 由 put 唤醒 put 是为了避免信号不足\n性能比较 主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较\nLinked 支持有界，Array 强制有界 Linked 实现是链表，Array 实现是数组 Linked 是懒惰的，而 Array 需要提前初始化 Node 数组 Linked 每次入队会生成新 Node，而 Array 的 Node 是提前创建好的 Linked 两把锁，Array 一把锁 ConcurrentLinkedQueue ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像，也是\n两把【锁】，同一时刻，可以允许两个线程同时（一个生产者与一个消费者）执行 dummy 节点的引入让两把【锁】将来锁住的是不同对象，避免竞争 只是这【锁】使用了 cas 来实现 事实上，ConcurrentLinkedQueue 应用还是非常广泛的\n例如之前讲的 Tomcat 的 Connector 结构时，Acceptor 作为生产者向 Poller 消费者传递事件信息时，正是采用了 ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 graph LR subgraph Connector-\u0026gt;NIO EndPoint t1(LimitLatch) t2(Acceptor) t3(SocketChannel 1) t4(SocketChannel 2) t5(Poller) subgraph Executor t7(worker1) t8(worker2) end t1 --\u0026gt; t2 t2 --\u0026gt; t3 t2 --\u0026gt; t4 t3 --有读--\u0026gt; t5 t4 --有读--\u0026gt; t5 t5 --socketProcessor--\u0026gt; t7 t5 --socketProcessor--\u0026gt; t8 end Copied! *ConcurrentLinkedQueue 原理 模仿 ConcurrentLinkedQueue 初始代码\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 package cn.itcast.concurrent.thirdpart.test; import java.util.Collection; import java.util.Iterator; import java.util.Queue; import java.util.concurrent.atomic.AtomicReference; public class Test3 { public static void main(String[] args) { MyQueue\u0026lt;String\u0026gt; queue = new MyQueue\u0026lt;\u0026gt;(); queue.offer(\u0026#34;1\u0026#34;); queue.offer(\u0026#34;2\u0026#34;); queue.offer(\u0026#34;3\u0026#34;); System.out.println(queue); } } class MyQueue\u0026lt;E\u0026gt; implements Queue\u0026lt;E\u0026gt; { @Override public String toString() { StringBuilder sb = new StringBuilder(); for (Node\u0026lt;E\u0026gt; p = head; p != null; p = p.next.get()) { E item = p.item; if (item != null) { sb.append(item).append(\u0026#34;-\u0026gt;\u0026#34;); } } sb.append(\u0026#34;null\u0026#34;); return sb.toString(); } @Override public int size() { return 0; } @Override public boolean isEmpty() { return false; } @Override public boolean contains(Object o) { return false; } @Override public Iterator\u0026lt;E\u0026gt; iterator() { return null; } @Override public Object[] toArray() { return new Object[0]; } @Override public \u0026lt;T\u0026gt; T[] toArray(T[] a) { return null; } @Override public boolean add(E e) { return false; } @Override public boolean remove(Object o) { return false; } @Override public boolean containsAll(Collection\u0026lt;?\u0026gt; c) { return false; } @Override public boolean addAll(Collection\u0026lt;? extends E\u0026gt; c) { return false; } @Override public boolean removeAll(Collection\u0026lt;?\u0026gt; c) { return false; } @Override public boolean retainAll(Collection\u0026lt;?\u0026gt; c) { return false; } @Override public void clear() { } @Override public E remove() { return null; } @Override public E element() { return null; } @Override public E peek() { return null; } public MyQueue() { head = last = new Node\u0026lt;\u0026gt;(null, null); } private volatile Node\u0026lt;E\u0026gt; last; private volatile Node\u0026lt;E\u0026gt; head; private E dequeue() { /*Node\u0026lt;E\u0026gt; h = head; Node\u0026lt;E\u0026gt; first = h.next; h.next = h; head = first; E x = first.item; first.item = null; return x;*/ return null; } @Override public E poll() { return null; } @Override public boolean offer(E e) { return true; } static class Node\u0026lt;E\u0026gt; { volatile E item; public Node(E item, Node\u0026lt;E\u0026gt; next) { this.item = item; this.next = new AtomicReference\u0026lt;\u0026gt;(next); } AtomicReference\u0026lt;Node\u0026lt;E\u0026gt;\u0026gt; next; } } Copied! offer\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public boolean offer(E e) { Node\u0026lt;E\u0026gt; n = new Node\u0026lt;\u0026gt;(e, null); while(true) { // 获取尾节点 AtomicReference\u0026lt;Node\u0026lt;E\u0026gt;\u0026gt; next = last.next; // S1: 真正尾节点的 next 是 null, cas 从 null 到新节点 if(next.compareAndSet(null, n)) { // 这时的 last 已经是倒数第二, next 不为空了, 其它线程的 cas 肯定失败 // S2: 更新 last 为倒数第一的节点 last = n; return true; } } } Copied! CopyOnWriteArrayList CopyOnWriteArraySet是它的马甲 底层实现采用了 写入时拷贝 的思想，增删改操作会将底层数组拷贝一份，更 改操作在新数组上执行，这时不影响其它线程的并发读，读写分离。 以新增为例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public boolean add(E e) { synchronized (lock) { // 获取旧的数组 Object[] es = getArray(); int len = es.length; // 拷贝新的数组（这里是比较耗时的操作，但不影响其它读线程） es = Arrays.copyOf(es, len + 1); // 添加新元素 es[len] = e; // 替换旧的数组 setArray(es); return true; } } Copied! 这里的源码版本是 Java 11，在 Java 1.8 中使用的是可重入锁而不是 synchronized\n其它读操作并未加锁，例如：\n1 2 3 4 5 6 7 public void forEach(Consumer\u0026lt;? super E\u0026gt; action) { Objects.requireNonNull(action); for (Object x : getArray()) { @SuppressWarnings(\u0026#34;unchecked\u0026#34;) E e = (E) x; action.accept(e); } } Copied! 适合『读多写少』的应用场景\nget 弱一致性 时间点 操作 1 Thread-0 getArray() 2 Thread-1 getArray() 3 Thread-1 setArray(arrayCopy) 4 Thread-0 array[index] 不容易测试，但问题确实存在\n迭代器弱一致性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 CopyOnWriteArrayList\u0026lt;Integer\u0026gt; list = new CopyOnWriteArrayList\u0026lt;\u0026gt;(); list.add(1); list.add(2); list.add(3); Iterator\u0026lt;Integer\u0026gt; iter = list.iterator(); new Thread(() -\u0026gt; { list.remove(0); System.out.println(list); }).start(); sleep1s(); //此时主线程的iterator依旧指向旧的数组。 while (iter.hasNext()) { System.out.println(iter.next()); } Copied! 不要觉得弱一致性就不好\n数据库的 MVCC 都是弱一致性的表现 并发高和一致性是矛盾的，需要权衡 ","date":"2024-03-03T12:49:51+08:00","permalink":"https://qh.1357810.xyz/articles/java/juc/juc/","title":"JUC"},{"content":" 查看brew的帮助 1 brew –help Copied! 安装软件 1 brew install git Copied! 卸载软件 1 brew uninstall git Copied! 搜索软件 1 brew search git Copied! 显示已经安装软件列表 1 brew list Copied! 更新homebrew自己，把所有的Formula目录更新，并且会对本机已经安装并有更新的软件用*标明。 1 brew update Copied! 更新某具体软件 1 brew upgrade git Copied! 显示软件内容信息 1 brew info git Copied! 用浏览器打开官网 1 brew home Copied! 显示已安装或者未安装的包的依赖 1 brew deps git Copied! 显示包的依赖树 1 brew deps --installed --tree Copied! 显示包的依赖 –简化 1 brew leaves | xargs brew deps --installed --for-each | sed \u0026#34;s/^.*:/$( tput setaf 4)\u0026amp;$(tput sgr0)/\u0026#34; Copied! 启动web服务器，可以通过浏览器访问http://localhost:4567/ 来同网页来管理包 1 brew server #要安装serve Copied! 删除程序旧版本，单个软件删除和所有程序老版删除。 1 2 3 4 brew cleanup git brew cleanup brew cleanup -n #不真的删除，打印出将会删除的旧包 brew cleanup --prune=all #删除旧包，以及下载的包缓存 Copied! 查看那些已安装的程序需要更新 1 brew outdated Copied! 管理后台软件 诸如 Nginx、MySQL 等软件，都是有一些服务端软件在后台运行，如果你希望对这些软件进行管理，可以使用 brew services 命令来进行管理\n1 2 3 4 5 brew services list #查看所有服务 brew services run [服务名] #单次运行某个服务 brew services start [服务名] #运行某个服务，并设置开机自动运行。 brew services stop [服务名] #停止某个服务 brew services restart #重启某个服务。 Copied! 检查 Hombrew 环境 1 brew doctor Copied! 添加一个新的 tap\nhomebrew 官方在安装的时候会有一些 tap 但是在使用时，依然会需要安装一些特殊的 tap ，这个时候，我们就要用到 tap 的命令来添加新的 tap.\n1 brew tap [user/repo] Copied! 使用 Brewfile 完成环境迁移 设备用久了，我们的电脑中会有大量的软件，如果你需要迁移环境，重新安装会是一个大麻烦，好在 Homebrew 本身为我们提供了一个非常好用的环境迁移的工具 —— Homebrew Bundle\n你首先需要在之前的电脑中执行 brew bundle dump 来完成当前环境的导出,导出完成后，你会得到一个 Brewfile。\n然后将 Brewfile 复制到新的电脑中，并执行 brew bundle 来开始安装的过程。\nCakebrew Cakebrew 是 Homebrew 的 GUI 管理器，在 Cakebrew 中，你可以看到当前所有已经安装的软件，并可以在 Caskbrew 中对其他软件执行升级等操作。\n你只需要执行 brew cask install cakebrew 就可以完成 Cakebrew 的安装。\n安装完成后，在 LaunchPad 中打开即可。\nlaunchrocket launchrocket 可以用于管理 Homebrew 安装的服务，在使用时，你需要先添加对应的tap，然后再安装软件。\n1 2 brew tap jimbojsb/launchrocket brew cask install launchrocket Copied! 安装完成后，在 LaunchPad 中打开即可。\n","date":"2024-03-03T12:49:51+08:00","permalink":"https://qh.1357810.xyz/articles/mac/mac-brew/","title":"mac使用homebrew"},{"content":" 1 隐藏/取消隐藏文件 1 2 chflags hidden a.txt chflags nohidden a.txt Copied! 2 文件传输 上传命令，下载就颠倒参数位置\n1 2 3 4 5 6 7 8 9 10 # 在mac本地终端将mac本地的文件传输至远程服务器 scp path/to/local_file remote_host:path/to/remote_file scp path/to/local_file root@remote_host:path/to/remote_file # 在mac本地终端将mac本地的文件夹里的内容传输至远程服务器一个文件夹内 scp -r path/to/local_directory remote_host:path/to/remote_directory # 在mac本地终端，把两个远程服务器文件互传 scp -3 host1:path/to/remote_file host2:path/to/remote_directory Copied! 以上是在mac本地执行命令，所以是mac端连接服务端，需要服务端开放22端口\n但是如果在服务端执行命令，连接mac端，需要mac开放端口，即 设置-共享-远程登录\n","date":"2024-03-03T12:49:51+08:00","permalink":"https://qh.1357810.xyz/articles/mac/mac-scp/","title":"mac使用scp"},{"content":"Hugo ships with several Built-in Shortcodes for rich content, along with a Privacy Config and a set of Simple Shortcodes that enable static and no-JS versions of various social media embeds.\nYouTube Privacy Enhanced Shortcode Twitter Simple Shortcode {{\u0026lt; twitter_simple user=\u0026ldquo;DesignReviewed\u0026rdquo; id=\u0026ldquo;1085870671291310081\u0026rdquo; \u0026gt;}}\nVimeo Simple Shortcode {{\u0026lt; vimeo_simple 48912912 \u0026gt;}}\nbilibilibi Shortcode Gist Shortcode {{\u0026lt; gist spf13 7896402 \u0026gt;}}\nGitlab Snippets Shortcode {{\u0026lt; gitlab 2349278 \u0026gt;}}\nQuote Shortcode Stack adds a quote shortcode. For example:\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n― A famous person, The book they wrote Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n― Anonymous book Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n― Some book Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n― Somebody","date":"2024-02-09T00:00:00+08:00","image":"https://logan.1357810.xyz/cover/pic_078.jpg","permalink":"https://qh.1357810.xyz/example/rich-content/","title":"Rich Content"},{"content":" 正文测试 而这些并不是完全重要，更加重要的问题是， 带着这些问题，我们来审视一下学生会退会。 既然如何， 对我个人而言，学生会退会不仅仅是一个重大的事件，还可能会改变我的人生。 我们不得不面对一个非常尴尬的事实，那就是， 可是，即使是这样，学生会退会的出现仍然代表了一定的意义。 学生会退会，发生了会如何，不发生又会如何。 经过上述讨论， 生活中，若学生会退会出现了，我们就不得不考虑它出现了的事实。 学生会退会，到底应该如何实现。 这样看来， 在这种困难的抉择下，本人思来想去，寝食难安。 对我个人而言，学生会退会不仅仅是一个重大的事件，还可能会改变我的人生。 就我个人来说，学生会退会对我的意义，不能不说非常重大。 莎士比亚曾经提到过，人的一生是短的，但如果卑劣地过这一生，就太长了。这似乎解答了我的疑惑。 莫扎特说过一句富有哲理的话，谁和我一样用功，谁就会和我一样成功。这启发了我， 对我个人而言，学生会退会不仅仅是一个重大的事件，还可能会改变我的人生。 学生会退会，到底应该如何实现。 一般来说， 从这个角度来看， 这种事实对本人来说意义重大，相信对这个世界也是有一定意义的。 在这种困难的抉择下，本人思来想去，寝食难安。 了解清楚学生会退会到底是一种怎么样的存在，是解决一切问题的关键。 一般来说， 生活中，若学生会退会出现了，我们就不得不考虑它出现了的事实。 问题的关键究竟为何？ 而这些并不是完全重要，更加重要的问题是。\n奥斯特洛夫斯基曾经说过，共同的事业，共同的斗争，可以使人们产生忍受一切的力量。　带着这句话，我们还要更加慎重的审视这个问题： 一般来讲，我们都必须务必慎重的考虑考虑。 既然如此， 这种事实对本人来说意义重大，相信对这个世界也是有一定意义的。 带着这些问题，我们来审视一下学生会退会。 我认为， 我认为， 在这种困难的抉择下，本人思来想去，寝食难安。 问题的关键究竟为何？ 每个人都不得不面对这些问题。 在面对这种问题时， 要想清楚，学生会退会，到底是一种怎么样的存在。 我认为， 既然如此， 每个人都不得不面对这些问题。 在面对这种问题时， 那么， 我认为， 学生会退会因何而发生。\n引用 思念是最暖的忧伤像一双翅膀\n让我停不了飞不远在过往游荡\n不告而别的你 就算为了我着想\n这么沉痛的呵护 我怎么能翱翔\n最暖的憂傷 - 田馥甄 ","date":"2024-02-09T00:00:00+08:00","image":"https://logan.1357810.xyz/cover/pic_071.jpg","permalink":"https://qh.1357810.xyz/example/test-chinese/","title":"测试中文"},{"content":" 一、特点 键值（key-value）型，value支持多种不同数据结构，功能丰富 单线程，每个命令具备原子性 低延迟，速度快（基于内存、IO多路复用、良好的编码）。 支持数据持久化 支持主从集群、分片集群 支持多语言客户端 1、redis是单线程的，保证原子性；6.0后网络请求是多线程，但是最终的命令执行依然是单线程\n2、为什么快？低延迟，速度快（基于内存、10多路复用、良好的编码、c语言）\n二、安装 1 2 3 4 5 6 7 8 9 # 安装依赖 yum install -y gcc tcl tar -xzf redis-6.2.6.tar.gz cd redis-6.2.6 # 测试 make test # 安装 make \u0026amp;\u0026amp; make install Copied! 错误的本质是我们在开始执行make 时遇到了错误（大部分是由于gcc未安装），然后我们安装好了gcc 后，我们再执行make ,这时就出现了jemalloc/jemalloc.h: No such file or directory。这是因为上次的\n编译失败，有残留的文件，我们需要清理下，然后重新编译就可以了。\n1 make distclean Copied! 默认的安装路径是在 /usr/local/bin目录下：\nredis-cli：是redis提供的命令行客户端\nredis-server：是redis的服务端启动脚本\nredis-sentinel：是redis的哨兵启动脚本\n前台启动\n1 redis-server Copied! 指定配置启动\n1 cp redis.conf redis.conf.bck #配置文件备份 Copied! 修改配置文件\n1 2 3 4 5 6 # 允许访问的地址，默认是127.0.0.1，会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问，生产环境不要设置为0.0.0.0 bind 0.0.0.0 # 守护进程，修改为yes后即可后台运行 daemonize yes # 密码，设置后访问Redis必须输入密码 requirepass 123321 Copied! 其他配置\n1 2 3 4 5 6 7 8 9 10 # 监听的端口 port 6379 # 工作目录，默认是当前目录，也就是运行redis-server时的命令，日志、持久化等文件会保存在这个目录 dir data # 数据库数量，设置为1，代表只使用1个库，默认有16个库，编号0~15 databases 1 # 设置redis能够使用的最大内存 maxmemory 512mb # 日志文件，默认为空，不记录日志，可以指定日志文件名 logfile \u0026#34;redis.log\u0026#34; Copied! 启动\n1 2 3 4 # 进入redis安装目录 cd /usr/local/src/redis-6.2.6 # 启动 redis-server redis.conf Copied! 停止服务\n1 2 3 # 利用redis-cli来执行 shutdown 命令，即可停止 Redis 服务， # 因为之前配置了密码，因此需要通过 -a 来指定密码 redis-cli -a 123321 shutdown Copied! 开机自启\n1 vi /etc/systemd/system/redis.service #新建一个系统服务文件 Copied! 1 2 3 4 5 6 7 8 9 10 11 [Unit] Description=redis-server After=network.target [Service] Type=forking ExecStart=/usr/local/bin/redis-server /usr/local/src/redis-6.2.6/redis.conf PrivateTmp=true [Install] WantedBy=multi-user.target Copied! 1 systemctl daemon-reload #重载系统服务 Copied! 现在，我们可以用下面这组命令来操作redis了：\n1 2 3 4 5 6 7 8 # 启动 systemctl start redis # 停止 systemctl stop redis # 重启 systemctl restart redis # 查看状态 systemctl status redis Copied! 执行下面的命令，可以让redis开机自启：\n1 systemctl enable redis Copied! 三、应用 1、全局ID生成器 一位标志位(0)+42位时间戳+21位redis自增 100年\n2、缓存 缓存预热 缓存穿透 1、缓存null数据 2、布隆过滤\n缓存雪崩 1、设置不同过期时间 2、redis高可用 3、多层缓存 4、缓存预热 5、降级限流\n缓存击穿 1、互斥锁(性能差，有死锁风险) 2、逻辑过期\n3、秒杀 秒杀 一人一单、超卖问题 三种方式：\n1、乐观锁减库存，成功率低，其他判断逻辑不能保证原子性\n2、分布式锁执行，性能差\n3、使用lua脚本判断资格，有序异步执行数据库逻辑\n3.1、放入JDK阻塞队列\n内存溢出问题、重启队列内数据丢失问题、取出数据后执行代码逻辑出现异常 3.2、或者redis的消息队列\n3.2.1、list-双向链表 模拟 队列(LPUSH+BRPOP)(RPUSH+BLPOP),B:阻塞 支持数据持久化 只支持单消费者 问题：取出数据后执行代码逻辑出现异常\n3.2.2、发布订阅 pub sub(天生阻塞) ，\n​\tPUBLISH channel msg ，SUBSCRIBE channel ，PSUBSCRIBE channe* ​\t支持多消费者,多生产者 ​\t问题：不支持数据持久化 ​\t取出数据后执行代码逻辑出现异常 ​\t消息堆积有上限，超出时数据丢失\n3.2.3、stream redis5.0后引入的一种新的数据类型，可以实现一个功能完善的消息队列 XADD s1 * k1 v1 k2 v2 发送消息，*表示使用redis自增id XLEN s1 查看消息数量 XREAD 读取消息，读完不会删除 消息可回溯 支持数据持久化 支持多消费者 可以阻塞读取 有消息漏读问题\n3.2.4、stream-消费者组 ：将多个消费者划分到一个组，监听同一个队列 消息分流：队列中消息会分流给组内的不同消费者，而不是重复消费，加快消息处理速度 消息标志：消费者组会维护一个标志，记录最后一个被处理的消息，消费者重启后，会从标志之后读取消息，避免消息漏读 消息确认：消费者获取消息后，消息处于pending状态，并存入一个pending-list(在redis中) 当处理完成后需要通过XACK来确认消息，标记消息为已处理，才会从pending-list移除。 XGROUP CREATE key groupname ID XREADGROUP GROUP XACK KEY group ID XPENDING\n4、点赞和关注 大V 普通 僵尸粉-活跃粉丝 ， 拉取关注的用户发布的内容，sortedSet 滚动分页查询，按照时间戳倒序排序。 因为查完第一页后，可能会新增数据，所以不能用索引来查，所以用score存时间戳，用时间戳筛选数据 ZREVRANGEBYSCORE key max min WITHSCORES LIMIT offset count max：第一次查询时，max=当前时间戳；当前查询的score的最大值，在本业务中即上一次排序列表中的最后一个元素的score-时间戳； min=0 不变 offset：第一次查询时，为0；后面查询时，offset为上一次查询结果中，与最后一个元素的score相等的元素个数，即跳过这些元素，避免重复显示 count=3 不变，每页查多少数据\n5、附近商户-地理坐标 ​\tGEO数据结构，底层就时一个sortedset，经纬度转换为score ​\tGEOADD：添加一个地理空间信息，包含：经度（longitude）、纬度（latitude）、值（member） ​\tGEOADD g1 116.42803 39.903738 bjz 116.322287 39.893729 bjx ​\tGEODIST：计算指定的两个点之间的距离并返回 ​\tGEODIST g1 bjx bjz km ​\tGEOHASH：将指定member的坐标转为hash字符串形式并返回 ​\tGEOPOS：返回指定member的坐标 ​\tGEOPOS g1 bjz ​\tGEORADIUS：指定圆心、半径，找到该圆内包含的所有member，并按照与圆心之间的距离排序后返回。6.2以后已废弃 ​\tGEOSEARCH：在指定范围内搜索member，并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能 ​\tGEOSEARCH g1 FROMMEMBER bjx BYRADIUS 300 km ASC WITHDIST 默认升序 ​\tGEOSEARCH g1 FROMLONLAT 116 39 BYRADIUS 300 km ASC WITHDIST ​\tGEOSEARCHSTORE：与GEOSEARCH功能一致，不过可以把结果存储到一个指定的key。 6.2.新功能\n6、用户签到 BitMap - 位图 - 假设一次签到数据库中生成一行，每行占22字节，那么1000万个用户签到一个月=非常大的内存和行数 BitMap把每天签到与否抽象为bit的0或1，一个月31天，4个字节就能存下 BitMap利用redis的String类型数据结构(key-value),最大上限为512M，转为bit为2^32bit key为用户id:月份，value为bit转换成的string SETBIT key offset value \u0026ndash;value为0或1 GETBIT key offset BITCOUNT key [start end] 从0开始，统计一个月内签到次数 BITFIELD查询、BITOP位运算、BITPOS查找第一个出现的位置 连续签到次数：从最后一次签到开始向前统计 BIFIELD key GET u5 0 ,获取0号到5号的所有签到数据，u表示返回无符号数 // num为取出签到数据bit转换成的十进制数 int count = 0; while (true) { // 6.1.让这个数字与1做与运算，得到数字的最后一个bit位 // 判断这个bit位是否为0 if ((num \u0026amp; 1) == 0) { // 如果为0，说明未签到，结束 break; } else { // 如果不为0，说明已签到，计数器+1 count++; } // 把数字右移一位，抛弃最后一个bit位，继续下一个bit位 num \u0026raquo;\u0026gt;= 1; }\n7、UV PV统计 HyperLogLog UV：Unique Visitor ，浏览网页的自然人，一天内同一个用户访问多次，只记录一次 PV：Page View，页面访问量，用户多次打开页面，记录多次 HyperLogLog：基于LogLog算法，用于确定非常大的集合的基数，而不需要存储所有值；伯努利实验，抛硬币 基于redis中的string结构实现，单个HLL内存永远小于16KB，测量结果有小于0.81%的误差，对于UV统计来说可以忽略 PFADD key element [element \u0026hellip;] 添加 PFCOUNT key [key \u0026hellip;] 统计数量 PFMERGE destkey sourcekey [sourcekey \u0026hellip;] 合并 HyperLogLog存储数据天生是唯一的，适合做UV\n8、分布式锁 redlock和mutilock 某台节点时钟漂移\n四、分布式 单点问题：\n数据丢失问题 – redis持久化 并发能力 - 主从，读写分离 存储空间 - 分片集群，利用插槽机制实现动态扩容 单点故障 -redis哨兵，健康监测和自动恢复 ","date":"2023-09-04T02:02:09+08:00","permalink":"https://qh.1357810.xyz/articles/redis/redis/","title":"redis概述"},{"content":" 分布式缓存 \u0026ndash; 基于Redis集群解决单机Redis存在的问题\n单机的Redis存在四大问题：\n0.学习目标 1.Redis持久化 Redis有两种持久化方案：\nRDB持久化 AOF持久化 1.1.RDB持久化 RDB全称Redis Database Backup file（Redis数据备份文件），也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后，从磁盘读取快照文件，恢复数据。快照文件称为RDB文件，默认是保存在当前运行目录。\n1.1.1.执行时机 RDB持久化在四种情况下会执行：\n执行save命令 执行bgsave命令 Redis停机时 触发RDB条件时 1）save命令\n执行下面的命令，可以立即执行一次RDB：\nsave命令会导致主进程执行RDB，这个过程中其它所有命令都会被阻塞。只有在数据迁移时可能用到。\n2）bgsave命令\n下面的命令可以异步执行RDB：\n这个命令执行后会开启独立进程完成RDB，主进程可以持续处理用户请求，不受影响。\n3）停机时\nRedis停机时会执行一次save命令，实现RDB持久化。\n4）触发RDB条件\nRedis内部有触发RDB的机制，可以在redis.conf文件中找到，格式如下：\n1 2 3 4 # 900秒内，如果至少有1个key被修改，则执行bgsave ， 如果是save \u0026#34;\u0026#34; 则表示禁用RDB save 900 1 save 300 10 save 60 10000 Copied! RDB的其它配置也可以在redis.conf文件中设置：\n1 2 3 4 5 6 7 8 # 是否压缩 ,建议不开启，压缩也会消耗cpu，磁盘的话不值钱 rdbcompression yes # RDB文件名称 dbfilename dump.rdb # 文件保存的路径目录 dir ./ Copied! RDB是从头开始创建，更健壮和稳定\n1.1.2.RDB原理 bgsave开始时会fork主进程得到子进程，子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。\nfork采用的是copy-on-write技术：\n当主进程执行读操作时，访问共享内存； 当主进程执行写操作时，则会拷贝一份数据，执行写操作。 1.1.3.小结 RDB方式bgsave的基本流程？\nfork主进程得到一个子进程，共享内存空间 子进程读取内存数据并写入新的RDB文件 用新RDB文件替换旧的RDB文件 RDB会在什么时候执行？save 60 1000代表什么含义？\n默认是服务停止时 代表60秒内至少执行1000次修改则触发RDB RDB的缺点？\nRDB执行间隔时间长，两次RDB之间写入数据有丢失的风险 fork子进程、压缩、写出RDB文件都比较耗时 1.2.AOF持久化 1.2.1.AOF原理 AOF全称为Append Only File（追加文件）。Redis处理的每一个写命令都会记录在AOF文件，可以看做是命令日志文件。\n1.2.2.AOF配置 AOF默认是关闭的，需要修改redis.conf配置文件来开启AOF：\n1 2 3 4 # 是否开启AOF功能，默认是no appendonly yes # AOF文件的名称 appendfilename \u0026#34;appendonly.aof\u0026#34; Copied! AOF的命令记录的频率也可以通过redis.conf文件来配：\n1 2 3 4 5 6 # 表示每执行一次写命令，立即记录到AOF文件 appendfsync always # 写命令执行完先放入AOF缓冲区，然后表示每隔1秒将缓冲区数据写到AOF文件，是默认方案 appendfsync everysec # 写命令执行完先放入AOF缓冲区，由操作系统决定何时将缓冲区内容写回磁盘 appendfsync no Copied! 三种策略对比：\n1.2.3.AOF文件重写 因为是记录命令，AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作，但只有最后一次写操作才有意义。通过执行bgrewriteaof命令，可以让AOF文件执行重写功能，用最少的命令达到相同效果。\n如图，AOF原本有三个命令，但是set num 123 和 set num 666都是对num的操作，第二次会覆盖第一次的值，因此第一个命令记录下来没有意义。\n所以重写命令后，AOF文件内容就是：mset name jack num 666\nRedis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置：\n1 2 3 4 # AOF文件比上次文件 增长超过多少百分比则触发重写 auto-aof-rewrite-percentage 100 # AOF文件体积最小多大以上才触发重写 auto-aof-rewrite-min-size 64mb Copied! 1.3.RDB与AOF对比 RDB和AOF各有自己的优缺点，如果对数据安全性要求较高，在实际开发中往往会结合两者来使用。\nRDB是从头开始创建，更健壮和稳定\n当RDB和AOF在配置文件中同时开启时，当redis重新启动，则优先从AOF中恢复数据\n如果redis先只用RDB运行一段时间，并且有数据；然后关闭，修改配置AOF开启，然后重启redis，这时数据会丢失；从AOF中恢复数据，并且把RDB的存储文件也覆盖。\n1.4 RDB转AOF 对数据进行cp 备份 使用 bgrewriteaof 命令，把RDB重写到AOF文件里 动态配置 AOF 启动 1 2 3 4 5 6 7 8 9 10 11 # 备份 cp appendonly.aof appendonly.aof20181010bak cp dump.rdb dump.rdb20181010bak config get * # 查看配置 config set appendonly \u0026#34;yes\u0026#34; #修改配置 # BGREWRITEAOF必须要在 config set appendonly后执行， 写完之后就去看看有生成aof文件没 BGREWRITEAOF #写入AOF shutdown #修改 redis.conf 文件，把 appendonly 设置为 yes，启动AOF,以防redis重启 Copied! 2.Redis主从 2.1.搭建主从架构 单节点Redis的并发能力是有上限的，要进一步提高Redis的并发能力，就需要搭建主从集群，实现读写分离。\n不用哨兵，就不支持主从自动切换 具体搭建流程参考课前资料《Redis集群.md》：\n2.2.主从数据同步原理 2.2.1.全量同步 主从第一次建立连接时，会执行全量同步，将master节点的所有数据都拷贝给slave节点，流程：\n这里有一个问题，master如何得知salve是第一次来连接呢？？\n有几个概念，可以作为判断依据：\nReplication Id：简称replid，是数据集的标记，id一致则说明是同一数据集。每一个master都有唯一的replid，slave则会继承master节点的replid offset：偏移量，随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset，说明slave数据落后于master，需要更新。 因此slave做数据同步，必须向master声明自己的replication id 和offset，master才可以判断到底需要同步哪些数据。\n因为slave原本也是一个master，有自己的replid和offset，当第一次变成slave，与master建立连接时，发送的replid和offset是自己的replid和offset。\nmaster判断发现slave发送来的replid与自己的不一致，说明这是一个全新的slave，就知道要做全量同步了。\nmaster会将自己的replid和offset都发送给这个slave，slave保存这些信息。以后slave的replid就与master一致了。\n因此，master判断一个节点是否是第一次同步的依据，就是看replid是否一致。\n如图：\n完整流程描述：\nslave节点请求增量同步 master节点判断replid，发现不一致，拒绝增量同步 master将完整内存数据生成RDB，发送RDB到slave slave清空本地数据，加载master的RDB master将RDB期间的命令记录在repl_baklog，并持续将log中的命令发送给slave slave执行接收到的命令，保持与master之间的同步 2.2.2.增量同步 全量同步需要先做RDB，然后将RDB文件通过网络传输个slave，成本太高了。因此除了第一次做全量同步，其它大多数时候slave与master都是做增量同步。\n什么是增量同步？就是只更新slave与master存在差异的部分数据。如图：\n那么master怎么知道slave与自己的数据差异在哪里呢?\n2.2.3.repl_backlog原理 master怎么知道slave与自己的数据差异在哪里呢?\n这就要说到全量同步时的repl_baklog文件了。\n这个文件是一个固定大小的数组，只不过数组是环形，也就是说角标到达数组末尾后，会再次从0开始读写，这样数组头部的数据就会被覆盖。\nrepl_baklog中会记录Redis处理过的命令日志及offset，包括master当前的offset，和slave已经拷贝到的offset：\nslave与master的offset之间的差异，就是salve需要增量拷贝的数据了。\n随着不断有数据写入，master的offset逐渐变大，slave也不断的拷贝，追赶master的offset：\n直到数组被填满：\n此时，如果有新的数据写入，就会覆盖数组中的旧数据。不过，旧的数据只要是绿色的，说明是已经被同步到slave的数据，即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。\n但是，如果slave出现网络阻塞，导致master的offset远远超过了slave的offset：\n如果master继续写入新数据，其offset就会覆盖旧的数据，直到将slave现在的offset也覆盖：\n棕色框中的红色部分，就是尚未同步，但是却已经被覆盖的数据。此时如果slave恢复，需要同步，却发现自己的offset都没有了，无法完成增量同步了。只能做全量同步。\n2.3.主从同步优化 主从同步可以保证主从数据的一致性，非常重要。\n可以从以下几个方面来优化Redis主从就集群：\n在master中配置repl-diskless-sync yes启用无磁盘复制，避免全量同步时的磁盘IO。 Redis单节点上的内存占用不要太大，减少RDB导致的过多磁盘IO 适当提高repl_baklog的大小，发现slave宕机时尽快实现故障恢复，尽可能避免全量同步 限制一个master上的slave节点数量，如果实在是太多slave，则可以采用主-从-从链式结构，减少master压力 主从从架构图：\n2.4.小结 简述全量同步和增量同步区别？\n全量同步：master将完整内存数据生成RDB，发送RDB到slave。后续命令则记录在repl_baklog，逐个发送给slave。 增量同步：slave提交自己的offset到master，master获取repl_baklog中从offset之后的命令给slave 什么时候执行全量同步？\nslave节点第一次连接master节点时 slave节点断开时间太久，repl_baklog中的offset已经被覆盖时 什么时候执行增量同步？\nslave节点断开又恢复，并且在repl_baklog中能找到offset时 3.Redis哨兵 Redis提供了哨兵（Sentinel）机制来实现主从集群的自动故障恢复。\n3.1.哨兵原理 3.1.1.集群结构和作用 哨兵的结构如图：\n哨兵的作用如下：\n监控：Sentinel 会不断检查您的master和slave是否按预期工作 自动故障恢复：如果master故障，Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主 通知：Sentinel充当Redis客户端的服务发现来源，当集群发生故障转移时，会将最新信息推送给Redis的客户端 3.1.2.集群监控原理 Sentinel基于心跳机制监测服务状态，每隔1秒向集群的每个实例发送ping命令：\n•主观下线：如果某sentinel节点发现某实例未在规定时间响应，则认为该实例主观下线。\n•客观下线：若超过指定数量（quorum）的sentinel都认为该实例主观下线，则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。\n3.1.3.集群故障恢复原理 一旦发现master故障，sentinel需要在salve中选择一个作为新的master，选择依据是这样的：\n首先会判断slave节点与master节点断开时间长短，如果超过指定值（down-after-milliseconds * 10）则会排除该slave节点 然后判断slave节点的slave-priority值，越小优先级越高，如果是0则永不参与选举 如果slave-prority一样，则判断slave节点的offset值，越大说明数据越新，优先级越高 最后是判断slave节点的运行id大小，越小优先级越高。 当选出一个新的master后，该如何实现切换呢？\n流程如下：\nsentinel给备选的slave1节点发送slaveof no one命令，让该节点成为master sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令，让这些slave成为新master的从节点，开始从新的master上同步数据。 最后，sentinel将故障节点标记为slave，当故障节点恢复后会自动成为新的master的slave节点 3.1.4.小结 Sentinel的三个作用是什么？\n监控 故障转移 通知 Sentinel如何判断一个redis实例是否健康？\n每隔1秒发送一次ping命令，如果超过一定时间没有相向则认为是主观下线 如果大多数sentinel都认为实例主观下线，则判定服务下线 故障转移步骤有哪些？\n首先选定一个slave作为新的master，执行slaveof no one 然后让所有节点都执行slaveof 新master 修改故障节点配置，添加slaveof 新master 3.2.搭建哨兵集群 具体搭建流程参考课前资料《Redis集群.md》：\n3.3.RedisTemplate 在Sentinel集群监管下的Redis主从集群，其节点会因为自动故障转移而发生变化，Redis的客户端必须感知这种变化，及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。\n下面，我们通过一个测试来实现RedisTemplate集成哨兵机制。\n3.3.1.导入Demo工程 首先，我们引入课前资料提供的Demo工程：\n3.3.2.引入依赖 在项目的pom文件中引入依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-data-redis\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 3.3.3.配置Redis地址 然后在配置文件application.yml中指定redis的sentinel相关信息：\n1 2 3 4 5 6 7 8 9 10 spring: redis: sentinel: master: mymaster # sentinel配置文件中定义的集群名字 nodes: - 127.0.0.1:27001 - 127.0.0.1:27002 - 127.0.0.1:27003 #password: 123456 #访问sentinel的密码 password: 123456 #访问redis集群的密码 Copied! 3.3.4.配置读写分离 在项目的启动类中，添加一个新的bean：\n1 2 3 4 @Bean public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){ return clientConfigurationBuilder -\u0026gt; clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED); } Copied! 这个bean中配置的就是读写策略，包括四种：\nMASTER：从主节点读取 MASTER_PREFERRED：优先从master节点读取，master不可用才读取replica REPLICA：从slave（replica）节点读取 REPLICA _PREFERRED：优先从slave（replica）节点读取，所有的slave都不可用才读取master 4.Redis分片集群 4.1.搭建分片集群 主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决：\n海量数据存储问题\n高并发写的问题\n使用分片集群可以解决上述问题，如图:\n分片集群特征：\n集群中有多个master，每个master保存不同数据\n每个master都可以有多个slave节点\nmaster之间通过ping监测彼此健康状态\n客户端请求可以访问集群任意节点，最终都会被转发到正确节点\n具体搭建流程参考课前资料《Redis集群.md》：\n4.2.散列插槽 4.2.1.插槽原理 Redis会把每一个master节点映射到0~16383共16384个插槽（hash slot）上，查看集群信息时就能看到：\n数据key不是与节点绑定，而是与插槽绑定。redis会根据key的有效部分计算插槽值，分两种情况：\nkey中包含\u0026quot;{}\u0026quot;，且“{}”中至少包含1个字符，“{}”中的部分是有效部分 key中不包含“{}”，整个key都是有效部分 例如：key是num，那么就根据num计算，如果是{itcast}num，则根据itcast计算。计算方式是利用CRC16算法得到一个hash值，然后对16384取余，得到的结果就是slot值。\n如图，在7001这个节点执行set a 1时，对a做hash运算，对16384取余，得到的结果是15495，因此要存储到103节点。\n到了7003后，执行get num时，对num做hash运算，对16384取余，得到的结果是2765，因此需要切换到7001节点\n4.2.1.小结 Redis如何判断某个key应该在哪个实例？\n将16384个插槽分配到不同的实例 根据key的有效部分计算哈希值，对16384取余 余数作为插槽，寻找插槽所在实例即可 如何将同一类数据固定的保存在同一个Redis实例？\n这一类数据使用相同的有效部分，例如key都以{typeId}为前缀 4.3.集群伸缩 redis-cli \u0026ndash;cluster提供了很多操作集群的命令，可以通过下面方式查看：\n比如，添加节点的命令：\n4.3.1.需求分析 需求：向集群中添加一个新的master节点，并向其中存储 num = 10\n启动一个新的redis实例，端口为7004 添加7004到之前的集群，并作为一个master节点 给7004节点分配插槽，使得num这个key可以存储到7004实例 这里需要两个新的功能：\n添加一个节点到集群中 将部分插槽分配到新插槽 4.3.2.创建新的redis实例 创建一个文件夹：\n1 mkdir 7004 Copied! 拷贝配置文件：\n1 cp redis.conf /7004 Copied! 修改配置文件：\n1 sed /s/6379/7004/g 7004/redis.conf Copied! 启动\n1 redis-server 7004/redis.conf Copied! 4.3.3.添加新节点到redis 添加节点的语法如下：\n执行命令：\n1 redis-cli --cluster add-node 192.168.150.101:7004 192.168.150.101:7001 Copied! 通过命令查看集群状态：\n1 redis-cli -p 7001 cluster nodes Copied! 如图，7004加入了集群，并且默认是一个master节点：\n但是，可以看到7004节点的插槽数量为0，因此没有任何数据可以存储到7004上\n4.3.4.转移插槽 我们要将num存储到7004节点，因此需要先看看num的插槽是多少：\n如上图所示，num的插槽为2765.\n我们可以将0~3000的插槽从7001转移到7004，命令格式如下：\n具体命令如下：\n建立连接：\n得到下面的反馈：\n询问要移动多少个插槽，我们计划是3000个：\n新的问题来了：\n那个node来接收这些插槽？？\n显然是7004，那么7004节点的id是多少呢？\n复制这个id，然后拷贝到刚才的控制台后：\n这里询问，你的插槽是从哪里移动过来的？\nall：代表全部，也就是三个节点各转移一部分 具体的id：目标节点的id done：没有了 这里我们要从7001获取，因此填写7001的id：\n填完后，点击done，这样插槽转移就准备好了：\n确认要转移吗？输入yes：\n然后，通过命令查看结果：\n可以看到：\n目的达成。\n4.4.故障转移 集群初识状态是这样的：\n其中7001、7002、7003都是master，我们计划让7002宕机。\n4.4.1.自动故障转移 当集群中有一个master宕机会发生什么呢？\n直接停止一个redis实例，例如7002：\n1 redis-cli -p 7002 shutdown Copied! 1）首先是该实例与其它实例失去连接\n2）然后是疑似宕机：\n3）最后是确定下线，自动提升一个slave为新的master：\n4）当7002再次启动，就会变为一个slave节点了：\n4.4.2.手动故障转移 利用cluster failover命令可以手动让集群中的某个master宕机，切换到执行cluster failover命令的这个slave节点，实现无感知的数据迁移。其流程如下：\n这种failover命令可以指定三种模式：\n缺省：默认的流程，如图1~6歩 force：省略了对offset的一致性校验 takeover：直接执行第5歩，忽略数据一致性、忽略master状态和其它master的意见 案例需求：在7002这个slave节点执行手动故障转移，重新夺回master地位\n步骤如下：\n1）利用redis-cli连接7002这个节点\n2）执行cluster failover命令\n如图：\n效果：\n4.5.RedisTemplate访问分片集群 RedisTemplate底层同样基于lettuce实现了分片集群的支持，而使用的步骤与哨兵模式基本一致：\n1）引入redis的starter依赖\n2）配置分片集群地址\n3）配置读写分离\n与哨兵模式相比，其中只有分片集群的配置方式略有差异，如下：\n1 2 3 4 5 6 7 8 9 10 11 spring: redis: cluster: nodes: - 172.16.251.128:7001 - 172.16.251.128:7002 - 172.16.251.128:7003 - 172.16.251.128:8001 - 172.16.251.128:8002 - 172.16.251.128:8003 password: 123456 #访问redis集群的密码 Copied! ","date":"2023-09-04T02:02:09+08:00","permalink":"https://qh.1357810.xyz/articles/redis/redis-cache/","title":"Redis高级-分布式缓存"},{"content":" 配置 单实例 保持默认配置，然后修改以下\n1 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 37 38 39 40 # 绑定地址，默认是127.0.0.1；为网卡的ip，如果为0.0.0.0则会绑定所有网卡的ip # bind 0.0.0.0 # 关闭保护模式，让其他机器也能访问 protected-mode no # 数据库数量，设置为1 databases 16 # 守护进程，修改为yes后即可后台运行 daemonize yes # 密码，设置后访问Redis必须输入密码 requirepass 123456 # 监听的端口 port 6379 # 数据库数量，设置为1，代表只使用1个库，默认有16个库，编号0~15 databases 16 # 设置redis能够使用的最大内存 maxmemory 512mb # 日志文件，默认为空，不记录日志，可以指定日志文件名 logfile \u0026#34;redis.log\u0026#34; # 300秒内，如果至少有100个key被修改，则执行bgsave ， 如果是save \u0026#34;\u0026#34; 则表示禁用RDB save 300 100 # 是否压缩 ,建议不开启，压缩也会消耗cpu，磁盘的话不值钱 rdbcompression no # RDB文件名称 dbfilename dump.rdb # 工作目录，默认是当前目录，也就是运行redis-server时的命令，日志、持久化等文件会保存在这个目录 dir data # 是否开启AOF功能，默认是no appendonly yes # AOF文件的名称 appendfilename \u0026#34;appendonly.aof\u0026#34; # 表示每执行一次写命令，立即记录到AOF文件 appendfsync always # 写命令执行完先放入AOF缓冲区，然后表示每隔1秒将缓冲区数据写到AOF文件，是默认方案 appendfsync everysec # 写命令执行完先放入AOF缓冲区，由操作系统决定何时将缓冲区内容写回磁盘 appendfsync no # AOF文件比上次文件 增长超过多少百分比则触发重写 auto-aof-rewrite-percentage 100 # AOF文件体积最小多大以上才触发重写 auto-aof-rewrite-min-size 64mb Copied! 主从 修改以下，其他配置与单机相同\n主 1 2 3 4 5 6 7 8 9 10 # 绑定节点IP，避免机器有多个ip的情况 replica-announce-ip 127.0.0.1 port 7001 # 配置PID，以免冲突 pidfile /var/run/redis_7001.pid # 开启RDB，主从用的就是RDB # save \u0026#34;\u0026#34; save 300 100 # 关闭AOF appendonly no Copied! 从 1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 绑定节点IP，避免机器有多个ip的情况 replica-announce-ip 127.0.0.1 port 7002 # 配置PID，以免冲突 pidfile /var/run/redis_7002.pid # 开启RDB，主从用的就是RDB # save \u0026#34;\u0026#34; save 300 100 # 关闭AOF appendonly no # 访问主节点时要带上密码 masterauth 123456 # 配置主节点ip和端口 replicaof 127.0.0.1 7001 Copied! 哨兵 保持主从节点配置；以下是哨兵配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 port 27001 #绑定ip sentinel announce-ip \u0026#34;127.0.0.1\u0026#34; #监控的主节点ip和端口，2为多数派 sentinel monitor mymaster 127.0.0.1 7001 2 #时间配置 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 60000 #工作目录 dir \u0026#34;s1/data\u0026#34; logfile \u0026#34;sentinel.log\u0026#34; #后台启动 daemonize yes #访问主节点要带上密码 sentinel auth-pass mymaster 123456 protected-mode no Copied! 分片集群 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 # 开启集群功能 cluster-enabled yes # 集群的配置文件名称，不需要我们创建，由redis自己维护 cluster-config-file ./nodes.conf # 节点心跳失败的超时时间 cluster-node-timeout 5000 # 注册的实例ip replica-announce-ip 127.0.0.1 # 绑定地址，默认是127.0.0.1，会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问 # bind 0.0.0.0 # 关闭保护模式，让其他机器也能访问 protected-mode no # 数据库数量，设置为1 databases 16 # 守护进程，修改为yes后即可后台运行 daemonize yes # 密码，设置后访问Redis必须输入密码 requirepass 123456 # 监听的端口 port 8001 # 配置PID，以免冲突 pidfile /var/run/redis_8001.pid # 数据库数量，设置为1，代表只使用1个库，默认有16个库，编号0~15 databases 16 # 设置redis能够使用的最大内存 maxmemory 512mb # 日志文件，默认为空，不记录日志，可以指定日志文件名 logfile \u0026#34;redis.log\u0026#34; # 300秒内，如果至少有100个key被修改，则执行bgsave ， 如果是save \u0026#34;\u0026#34; 则表示禁用RDB save 300 100 # 是否压缩 ,建议不开启，压缩也会消耗cpu，磁盘的话不值钱 rdbcompression no # RDB文件名称 dbfilename dump.rdb # 工作目录，默认是当前目录，也就是运行redis-server时的命令，日志、持久化等文件会保存在这个目录 dir 8001/data # 是否开启AOF功能，默认是no appendonly no # AOF文件的名称 appendfilename \u0026#34;appendonly.aof\u0026#34; # 表示每执行一次写命令，立即记录到AOF文件 appendfsync always # 写命令执行完先放入AOF缓冲区，然后表示每隔1秒将缓冲区数据写到AOF文件，是默认方案 appendfsync everysec # 写命令执行完先放入AOF缓冲区，由操作系统决定何时将缓冲区内容写回磁盘 appendfsync no # AOF文件比上次文件 增长超过多少百分比则触发重写 auto-aof-rewrite-percentage 100 # AOF文件体积最小多大以上才触发重写 auto-aof-rewrite-min-size 64mb Copied! Redis集群 本章是基于CentOS7下的Redis集群教程，包括：\n单机安装Redis Redis主从 Redis分片集群 1.单机安装Redis 首先需要安装Redis所需要的依赖：\n1 yum install -y gcc tcl Copied! 然后将课前资料提供的Redis安装包上传到虚拟机的任意目录：\n例如，我放到了/tmp目录：\n解压缩：\n1 tar -xzf redis-6.2.4.tar.gz Copied! 解压后：\n进入redis目录：\n1 cd redis-6.2.4 Copied! 运行编译命令：\n1 make \u0026amp;\u0026amp; make install Copied! 如果没有出错，应该就安装成功了。\n然后修改redis.conf文件中的一些配置：\n1 2 3 4 5 6 # 绑定地址，默认是127.0.0.1，会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问 bind 0.0.0.0 # 保护模式，关闭保护模式 protected-mode no # 数据库数量，设置为1 databases 1 Copied! 启动Redis：\n1 redis-server redis.conf Copied! 停止redis服务：\n1 redis-cli shutdown Copied! 2.Redis主从集群 2.1.集群结构 我们搭建的主从集群结构如图：\n共包含三个节点，一个主节点，两个从节点。\n这里我们会在同一台虚拟机中开启3个redis实例，模拟主从集群，信息如下：\nIP PORT 角色 192.168.150.101 7001 master 192.168.150.101 7002 slave 192.168.150.101 7003 slave 2.2.准备实例和配置 要在同一台虚拟机开启3个实例，必须准备三份不同的配置文件和目录，配置文件所在目录也就是工作目录。\n1）创建目录\n我们创建三个文件夹，名字分别叫7001、7002、7003：\n1 2 3 4 # 进入/tmp目录 cd /tmp # 创建目录 mkdir 7001 7002 7003 Copied! 如图：\n2）恢复原始配置\n修改redis-6.2.4/redis.conf文件，将其中的持久化模式改为默认的RDB模式，AOF保持关闭状态。\n1 2 3 4 5 6 7 8 # 开启RDB # save \u0026#34;\u0026#34; save 3600 1 save 300 100 save 60 10000 # 关闭AOF appendonly no Copied! 3）拷贝配置文件到每个实例目录\n然后将redis-6.2.4/redis.conf文件拷贝到三个目录中（在/tmp目录执行下列命令）：\n1 2 3 4 5 6 7 # 方式一：逐个拷贝 cp redis-6.2.4/redis.conf 7001/conf cp redis-6.2.4/redis.conf 7002/conf cp redis-6.2.4/redis.conf 7003/conf # 方式二：管道组合命令，一键拷贝 echo 7001/conf 7002/conf 7003/conf | xargs -t -n 1 cp redis-6.2.4/redis.conf Copied! 4）修改每个实例的端口、工作目录\n修改每个文件夹内的配置文件，将端口分别修改为7001、7002、7003，将rdb文件保存位置都修改为自己所在目录（在/tmp目录执行下列命令）：\n1 2 3 sed -i -e \u0026#39;s/6379/7001/g\u0026#39; -e \u0026#39;s/dir .\\//dir \\/tmp\\/7001\\//g\u0026#39; 7001/conf/redis.conf sed -i -e \u0026#39;s/6379/7002/g\u0026#39; -e \u0026#39;s/dir .\\//dir \\/tmp\\/7002\\//g\u0026#39; 7002/conf/redis.conf sed -i -e \u0026#39;s/6379/7003/g\u0026#39; -e \u0026#39;s/dir .\\//dir \\/tmp\\/7003\\//g\u0026#39; 7003/conf/redis.conf Copied! 5）修改每个实例的声明IP\n虚拟机本身有多个IP，为了避免将来混乱，我们需要在redis.conf文件中指定每一个实例的绑定ip信息，格式如下：\n1 2 # redis实例的声明 IP replica-announce-ip 127.0.0.1 Copied! 每个目录都要改，我们一键完成修改（在/tmp目录执行下列命令）：\n1 2 3 4 5 6 7 # 逐一执行 sed -i \u0026#39;1a replica-announce-ip 127.0.0.1\u0026#39; 7001/conf/redis.conf sed -i \u0026#39;1a replica-announce-ip 127.0.0.1\u0026#39; 7002/conf/redis.conf sed -i \u0026#39;1a replica-announce-ip 127.0.0.1\u0026#39; 7003/conf/redis.conf # 或者一键修改 printf \u0026#39;%s\\n\u0026#39; 7001 7002 7003 | xargs -I{} -t sed -i \u0026#39;1a replica-announce-ip 127.0.0.1\u0026#39; {}/conf/redis.conf Copied! 6）修改pidfile\n2.3.启动 为了方便查看日志，我们打开3个ssh窗口，分别启动3个redis实例，启动命令：\n1 2 3 4 5 6 # 第1个 ./redis-server 7001/conf/redis.conf # 第2个 ./redis-server 7002/conf/redis.conf # 第3个 ./redis-server 7003/conf/redis.conf Copied! 启动后：\n如果要一键停止，可以运行下面命令：\n1 printf \u0026#39;%s\\n\u0026#39; 7001 7002 7003 | xargs -I{} -t redis-cli -p {} shutdown Copied! 2.4.开启主从关系 不用哨兵，就不支持主从自动切换\n从节点配置文件需要设置主节点的密码\n1 masterauth 123456 Copied! 现在三个实例还没有任何关系，要配置主从可以使用replicaof 或者slaveof（5.0以前）命令。\n有临时和永久两种模式：\n修改配置文件（永久生效）\n在redis.conf中添加一行配置：replicaof \u0026lt;masterip\u0026gt; \u0026lt;masterport\u0026gt; 使用redis-cli客户端连接到redis服务，执行slaveof命令（重启后失效）：\n1 replicaof \u0026lt;masterip\u0026gt; \u0026lt;masterport\u0026gt; Copied! 注意：在5.0以后新增命令replicaof，与salveof效果一致。\n这里我们为了演示方便，使用方式二。\n通过redis-cli命令连接7002，执行下面命令：\n1 2 3 4 # 连接 7002 ./redis-cli -a 123456 -p 7002 # 执行replicaof replicaof 127.0.0.1 7001 Copied! 通过redis-cli命令连接7003，执行下面命令：\n1 2 3 4 # 连接 7003 ./redis-cli -a 123456 -p 7003 # 执行replicaof replicaof 127.0.0.1 7001 Copied! 然后连接 7001节点，查看集群状态：\n1 2 3 4 # 连接 7001 ./redis-cli -a 123456 -p 7001 # 查看状态 info replication Copied! 结果：\n2.5.测试 执行下列操作以测试：\n利用redis-cli连接7001，执行set num 123\n利用redis-cli连接7002，执行get num，再执行set num 666\n利用redis-cli连接7003，执行get num，再执行set num 888\n可以发现，只有在7001这个master节点上可以执行写操作，7002和7003这两个slave节点只能执行读操作。\n3.搭建哨兵集群 3.1.集群结构 这里我们搭建一个三节点形成的Sentinel集群，来监管之前的Redis主从集群。如图：\n三个sentinel实例信息如下：\n节点 IP PORT s1 192.168.150.101 27001 s2 192.168.150.101 27002 s3 192.168.150.101 27003 3.2.准备实例和配置 vim /etc/sysctl.conf\n1 2 3 4 5 6 net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 30 net.core.somaxconn = 1024 vm.overcommit_memory = 1 Copied! sudo sysctl -p\n要在同一台虚拟机开启3个实例，必须准备三份不同的配置文件和目录，配置文件所在目录也就是工作目录。\n我们创建三个文件夹，名字分别叫s1、s2、s3：\n1 2 3 4 # 进入/tmp目录 cd /tmp # 创建目录 mkdir s1 s2 s3 Copied! 如图：\n然后我们在s1目录创建一个sentinel.conf文件，添加下面的内容：\n1 2 3 4 5 6 7 8 9 10 port 27001 sentinel announce-ip \u0026#34;127.0.0.1\u0026#34; sentinel monitor mymaster 127.0.0.1 7001 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 60000 dir \u0026#34;s1/data\u0026#34; logfile \u0026#34;sentinel.log\u0026#34; daemonize yes sentinel auth-pass mymaster 123456 protected-mode no Copied! 解读：\nport 27001：是当前sentinel实例的端口 sentinel monitor mymaster 127.0.0.1 7001 2：指定主节点信息 mymaster：主节点名称，自定义，任意写 127.0.0.1：主节点的ip和端口 2：选举master时的quorum值 然后将s1/sentinel.conf文件拷贝到s2、s3两个目录中（在/tmp目录执行下列命令）：\n1 2 3 4 5 # 方式一：逐个拷贝 cp s1/conf/sentinel.conf s2/conf cp s1/conf/sentinel.conf s3/conf # 方式二：管道组合命令，一键拷贝 echo s2 s3 | xargs -t -n 1 cp s1/conf/sentinel.conf Copied! 修改s2、s3两个文件夹内的配置文件，将端口分别修改为27002、27003：\n1 2 sed -i -e \u0026#39;s/27001/27002/g\u0026#39; -e \u0026#39;s/s1/s2/g\u0026#39; s2/conf/sentinel.conf sed -i -e \u0026#39;s/27001/27003/g\u0026#39; -e \u0026#39;s/s1/s3/g\u0026#39; s3/conf/sentinel.conf Copied! 3.3.启动 为了方便查看日志，我们打开3个ssh窗口，分别启动3个redis实例，启动命令：\n1 2 3 4 5 6 # 第1个 ./redis-sentinel s1/conf/sentinel.conf # 第2个 ./redis-sentinel s2/conf/sentinel.conf # 第3个 ./redis-sentinel s3/conf/sentinel.conf Copied! 启动后：\n3.4.测试 尝试让master节点7001宕机，查看sentinel日志：\n查看7003的日志：\n查看7002的日志：\n4.搭建分片集群 4.1.集群结构 分片集群需要的节点数量较多，这里我们搭建一个最小的分片集群，包含3个master节点，每个master包含一个slave节点，结构如下：\n这里我们会在同一台虚拟机中开启6个redis实例，模拟分片集群，信息如下：\nIP PORT 角色 192.168.150.101 7001 master 192.168.150.101 7002 master 192.168.150.101 7003 master 192.168.150.101 8001 slave 192.168.150.101 8002 slave 192.168.150.101 8003 slave 4.2.准备实例和配置 删除之前的7001、7002、7003这几个目录，重新创建出7001、7002、7003、8001、8002、8003目录：\n1 2 3 4 5 6 # 进入/tmp目录 cd /tmp # 删除旧的，避免配置干扰 rm -rf 7001 7002 7003 # 创建目录 mkdir 7001 7002 7003 8001 8002 8003 Copied! 在/tmp下准备一个新的redis.conf文件，内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 port 6379 # 开启集群功能 cluster-enabled yes # 集群的配置文件名称，不需要我们创建，由redis自己维护 cluster-config-file /tmp/6379/nodes.conf # 节点心跳失败的超时时间 cluster-node-timeout 5000 # 持久化文件存放目录 dir /tmp/6379 # 绑定地址 bind 0.0.0.0 # 让redis后台运行 daemonize yes # 注册的实例ip replica-announce-ip 192.168.150.101 # 保护模式 protected-mode no # 数据库数量 databases 1 # 日志 logfile /tmp/6379/run.log Copied! 将这个文件拷贝到每个目录下：\n1 2 3 4 # 进入/tmp目录 cd /tmp # 执行拷贝 echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf Copied! 修改每个目录下的redis.conf，将其中的6379修改为与所在目录一致：\n1 2 3 4 # 进入/tmp目录 cd /tmp # 修改配置文件 printf \u0026#39;%s\\n\u0026#39; 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i \u0026#39;s/6379/{}/g\u0026#39; {}/redis.conf Copied! 4.3.启动 因为已经配置了后台启动模式，所以可以直接启动服务：\n1 2 3 4 # 进入/tmp目录 cd /tmp # 一键启动所有服务 printf \u0026#39;%s\\n\u0026#39; 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf Copied! 通过ps查看状态：\n1 ps -ef | grep redis Copied! 发现服务都已经正常启动：\n如果要关闭所有进程，可以执行命令：\n1 ps -ef | grep redis | awk \u0026#39;{print $2}\u0026#39; | xargs kill Copied! 或者（推荐这种方式）：\n1 printf \u0026#39;%s\\n\u0026#39; 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-cli -p {} shutdown Copied! 4.4.创建集群 虽然服务启动了，但是目前每个服务之间都是独立的，没有任何关联。\n我们需要执行命令来创建集群，在Redis5.0之前创建集群比较麻烦，5.0之后集群管理命令都集成到了redis-cli中。\n1）Redis5.0之前\nRedis5.0之前集群命令都是用redis安装包下的src/redis-trib.rb来实现的。因为redis-trib.rb是有ruby语言编写的所以需要安装ruby环境。\n1 2 3 # 安装依赖 yum -y install zlib ruby rubygems gem install redis Copied! 然后通过命令来管理集群：\n1 2 3 4 # 进入redis的src目录 cd /tmp/redis-6.2.4/src # 创建集群 ./redis-trib.rb create --replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003 Copied! 2）Redis5.0以后\n我们使用的是Redis6.2.4版本，集群管理以及集成到了redis-cli中，格式如下：\n1 redis-cli --cluster create --cluster-replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003 Copied! 命令说明：\nredis-cli --cluster或者./redis-trib.rb：代表集群操作命令 create：代表是创建集群 --replicas 1或者--cluster-replicas 1 ：指定集群中每个master的副本个数为1，此时节点总数 ÷ (replicas + 1) 得到的就是master的数量。因此节点列表中的前n个就是master，其它节点都是slave节点，随机分配到不同master 运行后的样子：\n这里输入yes，则集群开始创建：\n通过命令可以查看集群状态：\n1 redis-cli -p 7001 cluster nodes Copied! 4.5.测试 尝试连接7001节点，存储一个数据：\n1 2 3 4 5 6 7 8 # 连接 redis-cli -p 7001 # 存储数据 set num 123 # 读取数据 get num # 再次存储 set a 1 Copied! 结果悲剧了：\n集群操作时，需要给redis-cli加上-c参数才可以：\n1 redis-cli -c -p 7001 Copied! 这次可以了：\n","date":"2023-09-04T02:02:09+08:00","permalink":"https://qh.1357810.xyz/articles/redis/redis-cluster/","title":"Redis集群"},{"content":" 多级缓存 0.学习目标 1.什么是多级缓存 传统的缓存策略一般是请求到达Tomcat后，先查询Redis，如果未命中则查询数据库，如图：\n存在下面的问题：\n•请求要经过Tomcat处理，Tomcat的性能成为整个系统的瓶颈\n•Redis缓存失效时，会对数据库产生冲击\n多级缓存就是充分利用请求处理的每个环节，分别添加缓存，减轻Tomcat压力，提升服务性能：\n浏览器访问静态资源时，优先读取浏览器本地缓存 访问非静态资源（ajax查询数据）时，访问服务端 请求到达Nginx后，优先读取Nginx本地缓存 如果Nginx本地缓存未命中，则去直接查询Redis（不经过Tomcat） 如果Redis查询未命中，则查询Tomcat 请求进入Tomcat后，优先查询JVM进程缓存 如果JVM进程缓存未命中，则查询数据库 在多级缓存架构中，Nginx内部需要编写本地缓存查询、Redis查询、Tomcat查询的业务逻辑，因此这样的nginx服务不再是一个反向代理服务器，而是一个编写业务的Web服务器了。\n因此这样的业务Nginx服务也需要搭建集群来提高并发，再有专门的nginx服务来做反向代理，如图：\n另外，我们的Tomcat服务将来也会部署为集群模式：\n可见，多级缓存的关键有两个：\n一个是在nginx中编写业务，实现nginx本地缓存、Redis、Tomcat的查询\n另一个就是在Tomcat中实现JVM进程缓存\n其中Nginx编程则会用到OpenResty框架结合Lua这样的语言。\n这也是今天课程的难点和重点。\n2.JVM进程缓存 为了演示多级缓存的案例，我们先准备一个商品查询的业务。\n2.1.导入案例 参考课前资料的：《案例导入说明.md》\n2.2.初识Caffeine 缓存在日常开发中启动至关重要的作用，由于是存储在内存中，数据的读取速度是非常快的，能大量减少对数据库的访问，减少数据库的压力。我们把缓存分为两类：\n分布式缓存，例如Redis： 优点：存储容量更大、可靠性更好、可以在集群间共享 缺点：访问缓存有网络开销 场景：缓存数据量较大、可靠性要求较高、需要在集群间共享 进程本地缓存，例如HashMap、GuavaCache： 优点：读取本地内存，没有网络开销，速度更快 缺点：存储容量有限、可靠性较低、无法共享 场景：性能要求较高，缓存数据量较小 我们今天会利用Caffeine框架来实现JVM进程缓存。\nCaffeine是一个基于Java8开发的，提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址：https://github.com/ben-manes/caffeine\nCaffeine的性能非常好，下图是官方给出的性能对比：\n可以看到Caffeine的性能遥遥领先！\n缓存使用的基本API：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test void testBasicOps() { // 构建cache对象 Cache\u0026lt;String, String\u0026gt; cache = Caffeine.newBuilder().build(); // 存数据 cache.put(\u0026#34;gf\u0026#34;, \u0026#34;迪丽热巴\u0026#34;); // 取数据 String gf = cache.getIfPresent(\u0026#34;gf\u0026#34;); System.out.println(\u0026#34;gf = \u0026#34; + gf); // 取数据，包含两个参数： // 参数一：缓存的key // 参数二：Lambda表达式，表达式参数就是缓存的key，方法体是查询数据库的逻辑 // 优先根据key查询JVM缓存，如果未命中，则执行参数二的Lambda表达式 String defaultGF = cache.get(\u0026#34;defaultGF\u0026#34;, key -\u0026gt; { // 根据key去数据库查询数据 return \u0026#34;柳岩\u0026#34;; }); System.out.println(\u0026#34;defaultGF = \u0026#34; + defaultGF); } Copied! Caffeine既然是缓存的一种，肯定需要有缓存的清除策略，不然的话内存总会有耗尽的时候。\nCaffeine提供了三种缓存驱逐策略：\n基于容量：设置缓存的数量上限\n1 2 3 4 // 创建缓存对象 Cache\u0026lt;String, String\u0026gt; cache = Caffeine.newBuilder() .maximumSize(1) // 设置缓存大小上限为 1 .build(); Copied! 基于时间：设置缓存的有效时间\n1 2 3 4 5 // 创建缓存对象 Cache\u0026lt;String, String\u0026gt; cache = Caffeine.newBuilder() // 设置缓存有效期为 10 秒，从最后一次写入开始计时 .expireAfterWrite(Duration.ofSeconds(10)) .build(); Copied! 基于引用：设置缓存为软引用或弱引用，利用GC来回收缓存数据。性能较差，不建议使用。\n注意：在默认情况下，当一个缓存元素过期的时候，Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后，或者在空闲时间完成对失效数据的驱逐。\n2.3.实现JVM进程缓存 2.3.1.需求 利用Caffeine实现下列需求：\n给根据id查询商品的业务添加缓存，缓存未命中时查询数据库 给根据id查询商品库存的业务添加缓存，缓存未命中时查询数据库 缓存初始大小为100 缓存上限为10000 2.3.2.实现 首先，我们需要定义两个Caffeine的缓存对象，分别保存商品、库存的缓存数据。\n在item-service的com.heima.item.config包下定义CaffeineConfig类：\n1 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 package com.heima.item.config; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.heima.item.pojo.Item; import com.heima.item.pojo.ItemStock; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class CaffeineConfig { @Bean public Cache\u0026lt;Long, Item\u0026gt; itemCache(){ return Caffeine.newBuilder() .initialCapacity(100) .maximumSize(10_000) .build(); } @Bean public Cache\u0026lt;Long, ItemStock\u0026gt; stockCache(){ return Caffeine.newBuilder() .initialCapacity(100) .maximumSize(10_000) .build(); } } Copied! 然后，修改item-service中的com.heima.item.web包下的ItemController类，添加缓存逻辑：\n1 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 @RestController @RequestMapping(\u0026#34;item\u0026#34;) public class ItemController { @Autowired private IItemService itemService; @Autowired private IItemStockService stockService; @Autowired private Cache\u0026lt;Long, Item\u0026gt; itemCache; @Autowired private Cache\u0026lt;Long, ItemStock\u0026gt; stockCache; // ...其它略 @GetMapping(\u0026#34;/{id}\u0026#34;) public Item findById(@PathVariable(\u0026#34;id\u0026#34;) Long id) { return itemCache.get(id, key -\u0026gt; itemService.query() .ne(\u0026#34;status\u0026#34;, 3).eq(\u0026#34;id\u0026#34;, key) .one() ); } @GetMapping(\u0026#34;/stock/{id}\u0026#34;) public ItemStock findStockById(@PathVariable(\u0026#34;id\u0026#34;) Long id) { return stockCache.get(id, key -\u0026gt; stockService.getById(key)); } } Copied! 3.Lua语法入门 Nginx编程需要用到Lua语言，因此我们必须先入门Lua的基本语法。\n3.1.初识Lua Lua 是一种轻量小巧的脚本语言，用标准C语言编写并以源代码形式开放， 其设计目的是为了嵌入应用程序中，从而为应用程序提供灵活的扩展和定制功能。官网：https://www.lua.org/\nLua经常嵌入到C语言开发的程序中，例如游戏开发、游戏插件等。\nNginx本身也是C语言开发，因此也允许基于Lua做拓展。\n3.1.HelloWorld CentOS7默认已经安装了Lua语言环境，所以可以直接运行Lua代码。\n1）在Linux虚拟机的任意目录下，新建一个hello.lua文件\n2）添加下面的内容\n1 print(\u0026#34;Hello World!\u0026#34;) Copied! 3）运行\n3.2.变量和循环 学习任何语言必然离不开变量，而变量的声明必须先知道数据的类型。\n3.2.1.Lua的数据类型 Lua中支持的常见数据类型包括：\n另外，Lua提供了type()函数来判断一个变量的数据类型：\n3.2.2.声明变量 Lua声明变量的时候无需指定数据类型，而是用local来声明变量为局部变量：\n1 2 3 4 5 6 7 8 -- 声明字符串，可以用单引号或双引号， local str = \u0026#39;hello\u0026#39; -- 字符串拼接可以使用 .. local str2 = \u0026#39;hello\u0026#39; .. \u0026#39;world\u0026#39; -- 声明数字 local num = 21 -- 声明布尔类型 local flag = true Copied! Lua中的table类型既可以作为数组，又可以作为Java中的map来使用。数组就是特殊的table，key是数组角标而已：\n1 2 3 4 -- 声明数组 ，key为角标的 table local arr = {\u0026#39;java\u0026#39;, \u0026#39;python\u0026#39;, \u0026#39;lua\u0026#39;} -- 声明table，类似java的map local map = {name=\u0026#39;Jack\u0026#39;, age=21} Copied! Lua中的数组角标是从1开始，访问的时候与Java中类似：\n1 2 -- 访问数组，lua数组的角标从1开始 print(arr[1]) Copied! Lua中的table可以用key来访问：\n1 2 3 -- 访问table print(map[\u0026#39;name\u0026#39;]) print(map.name) Copied! 3.2.3.循环 对于table，我们可以利用for循环来遍历。不过数组和普通table遍历略有差异。\n遍历数组：\n1 2 3 4 5 6 -- 声明数组 key为索引的 table local arr = {\u0026#39;java\u0026#39;, \u0026#39;python\u0026#39;, \u0026#39;lua\u0026#39;} -- 遍历数组 for index,value in ipairs(arr) do print(index, value) end Copied! 遍历普通table\n1 2 3 4 5 6 -- 声明map，也就是table local map = {name=\u0026#39;Jack\u0026#39;, age=21} -- 遍历table for key,value in pairs(map) do print(key, value) end Copied! 3.3.条件控制、函数 Lua中的条件控制和函数声明与Java类似。\n3.3.1.函数 定义函数的语法：\n1 2 3 4 function 函数名( argument1, argument2..., argumentn) -- 函数体 return 返回值 end Copied! 例如，定义一个函数，用来打印数组：\n1 2 3 4 5 function printArr(arr) for index, value in ipairs(arr) do print(value) end end Copied! 3.3.2.条件控制 类似Java的条件控制，例如if、else语法：\n1 2 3 4 5 6 if(布尔表达式) then --[ 布尔表达式为 true 时执行该语句块 --] else --[ 布尔表达式为 false 时执行该语句块 --] end Copied! 与java不同，布尔表达式中的逻辑运算是基于英文单词：\n3.3.3.案例 需求：自定义一个函数，可以打印table，当参数为nil时，打印错误信息\n1 2 3 4 5 6 7 8 9 function printArr(arr) if not arr then print(\u0026#39;数组不能为空！\u0026#39;) return nil end for index, value in ipairs(arr) do print(value) end end Copied! 4.实现多级缓存 多级缓存的实现离不开Nginx编程，而Nginx编程又离不开OpenResty。\n4.1.安装OpenResty OpenResty® 是一个基于 Nginx的高性能 Web 平台，用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点：\n具备Nginx的完整功能 基于Lua语言进行扩展，集成了大量精良的 Lua 库、第三方模块 允许使用Lua自定义业务逻辑、自定义库 官方网站： https://openresty.org/cn/ 安装Lua可以参考课前资料提供的《安装OpenResty.md》：\n4.2.OpenResty快速入门 我们希望达到的多级缓存架构如图：\n其中：\nwindows上的nginx用来做反向代理服务，将前端的查询商品的ajax请求代理到OpenResty集群\nOpenResty集群用来编写多级缓存业务\n4.2.1.反向代理流程 现在，商品详情页使用的是假的商品数据。不过在浏览器中，可以看到页面有发起ajax请求查询真实商品数据。\n这个请求如下：\n请求地址是localhost，端口是80，就被windows上安装的Nginx服务给接收到了。然后代理给了OpenResty集群：\n我们需要在OpenResty中编写业务，查询商品数据并返回到浏览器。\n但是这次，我们先在OpenResty接收请求，返回假的商品数据。\n4.2.2.OpenResty监听请求 OpenResty的很多功能都依赖于其目录下的Lua库，需要在nginx.conf中指定依赖库的目录，并导入依赖：\n1）添加对OpenResty的Lua模块的加载\n修改/usr/local/openresty/nginx/conf/nginx.conf文件，在其中的http下面，添加下面代码：\n1 2 3 4 #lua 模块 lua_package_path \u0026#34;/usr/local/openresty/lualib/?.lua;;\u0026#34;; #c模块 lua_package_cpath \u0026#34;/usr/local/openresty/lualib/?.so;;\u0026#34;; Copied! 2）监听/api/item路径\n修改/usr/local/openresty/nginx/conf/nginx.conf文件，在nginx.conf的server下面，添加对/api/item这个路径的监听：\n1 2 3 4 5 6 location /api/item { # 默认的响应类型 default_type application/json; # 响应结果由lua/item.lua文件来决定 content_by_lua_file lua/item.lua; } Copied! 这个监听，就类似于SpringMVC中的@GetMapping(\u0026quot;/api/item\u0026quot;)做路径映射。\n而content_by_lua_file lua/item.lua则相当于调用item.lua这个文件，执行其中的业务，把结果返回给用户。相当于java中调用service。\n4.2.3.编写item.lua 1）在/usr/loca/openresty/nginx目录创建文件夹：lua\n2）在/usr/loca/openresty/nginx/lua文件夹下，新建文件：item.lua\n3）编写item.lua，返回假数据\nitem.lua中，利用ngx.say()函数返回数据到Response中\n1 ngx.say(\u0026#39;{\u0026#34;id\u0026#34;:10001,\u0026#34;name\u0026#34;:\u0026#34;SALSA AIR\u0026#34;,\u0026#34;title\u0026#34;:\u0026#34;RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4\u0026#34;,\u0026#34;price\u0026#34;:17900,\u0026#34;image\u0026#34;:\u0026#34;https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp\u0026#34;,\u0026#34;category\u0026#34;:\u0026#34;拉杆箱\u0026#34;,\u0026#34;brand\u0026#34;:\u0026#34;RIMOWA\u0026#34;,\u0026#34;spec\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;status\u0026#34;:1,\u0026#34;createTime\u0026#34;:\u0026#34;2019-04-30T16:00:00.000+00:00\u0026#34;,\u0026#34;updateTime\u0026#34;:\u0026#34;2019-04-30T16:00:00.000+00:00\u0026#34;,\u0026#34;stock\u0026#34;:2999,\u0026#34;sold\u0026#34;:31290}\u0026#39;) Copied! 4）重新加载配置\n1 nginx -s reload Copied! 刷新商品页面：http://localhost/item.html?id=1001，即可看到效果：\n4.3.请求参数处理 上一节中，我们在OpenResty接收前端请求，但是返回的是假数据。\n要返回真实数据，必须根据前端传递来的商品id，查询商品信息才可以。\n那么如何获取前端传递的商品参数呢？\n4.3.1.获取参数的API OpenResty中提供了一些API用来获取不同类型的前端请求参数：\n4.3.2.获取参数并返回 在前端发起的ajax请求如图：\n可以看到商品id是以路径占位符方式传递的，因此可以利用正则表达式匹配的方式来获取ID\n1）获取商品id\n修改/usr/loca/openresty/nginx/nginx.conf文件中监听/api/item的代码，利用正则表达式获取ID：\n1 2 3 4 5 6 location ~ /api/item/(\\d+) { # 默认的响应类型 default_type application/json; # 响应结果由lua/item.lua文件来决定 content_by_lua_file lua/item.lua; } Copied! 2）拼接ID并返回\n修改/usr/loca/openresty/nginx/lua/item.lua文件，获取id并拼接到结果中返回：\n1 2 3 4 -- 获取商品id local id = ngx.var[1] -- 拼接并返回 ngx.say(\u0026#39;{\u0026#34;id\u0026#34;:\u0026#39; .. id .. \u0026#39;,\u0026#34;name\u0026#34;:\u0026#34;SALSA AIR\u0026#34;,\u0026#34;title\u0026#34;:\u0026#34;RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4\u0026#34;,\u0026#34;price\u0026#34;:17900,\u0026#34;image\u0026#34;:\u0026#34;https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp\u0026#34;,\u0026#34;category\u0026#34;:\u0026#34;拉杆箱\u0026#34;,\u0026#34;brand\u0026#34;:\u0026#34;RIMOWA\u0026#34;,\u0026#34;spec\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;status\u0026#34;:1,\u0026#34;createTime\u0026#34;:\u0026#34;2019-04-30T16:00:00.000+00:00\u0026#34;,\u0026#34;updateTime\u0026#34;:\u0026#34;2019-04-30T16:00:00.000+00:00\u0026#34;,\u0026#34;stock\u0026#34;:2999,\u0026#34;sold\u0026#34;:31290}\u0026#39;) Copied! 3）重新加载并测试\n运行命令以重新加载OpenResty配置：\n1 nginx -s reload Copied! 刷新页面可以看到结果中已经带上了ID：\n4.4.查询Tomcat 拿到商品ID后，本应去缓存中查询商品信息，不过目前我们还未建立nginx、redis缓存。因此，这里我们先根据商品id去tomcat查询商品信息。我们实现如图部分：\n需要注意的是，我们的OpenResty是在虚拟机，Tomcat是在Windows电脑上。两者IP一定不要搞错了。\n4.4.1.发送http请求的API nginx提供了内部API用以发送http请求：\n1 2 3 4 local resp = ngx.location.capture(\u0026#34;/path\u0026#34;,{ method = ngx.HTTP_GET, -- 请求方式 args = {a=1,b=2}, -- get方式传参数 }) Copied! 返回的响应内容包括：\nresp.status：响应状态码 resp.header：响应头，是一个table resp.body：响应体，就是响应数据 注意：这里的path是路径，并不包含IP和端口。这个请求会被nginx内部的server监听并处理。\n但是我们希望这个请求发送到Tomcat服务器，所以还需要编写一个server来对这个路径做反向代理：\n1 2 3 4 location /path { # 这里是windows电脑的ip和Java服务端口，需要确保windows防火墙处于关闭状态 proxy_pass http://192.168.150.1:8081; } Copied! 原理如图：\n4.4.2.封装http工具 下面，我们封装一个发送Http请求的工具，基于ngx.location.capture来实现查询tomcat。\n1）添加反向代理，到windows的Java服务\n因为item-service中的接口都是/item开头，所以我们监听/item路径，代理到windows上的tomcat服务。\n修改 /usr/local/openresty/nginx/conf/nginx.conf文件，添加一个location：\n1 2 3 location /item { proxy_pass http://192.168.150.1:8081; } Copied! 以后，只要我们调用ngx.location.capture(\u0026quot;/item\u0026quot;)，就一定能发送请求到windows的tomcat服务。\n2）封装工具类\n之前我们说过，OpenResty启动时会加载以下两个目录中的工具文件：\n所以，自定义的http工具也需要放到这个目录下。\n在/usr/local/openresty/lualib目录下，新建一个common.lua文件：\n1 vi /usr/local/openresty/lualib/common.lua Copied! 内容如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- 封装函数，发送http请求，并解析响应 local function read_http(path, params) local resp = ngx.location.capture(path,{ method = ngx.HTTP_GET, args = params, }) if not resp then -- 记录错误信息，返回404 ngx.log(ngx.ERR, \u0026#34;http请求查询失败, path: \u0026#34;, path , \u0026#34;, args: \u0026#34;, args) ngx.exit(404) end return resp.body end -- 将方法导出 local _M = { read_http = read_http } return _M Copied! 这个工具将read_http函数封装到_M这个table类型的变量中，并且返回，这类似于导出。\n使用的时候，可以利用require('common')来导入该函数库，这里的common是函数库的文件名。\n3）实现商品查询\n最后，我们修改/usr/local/openresty/lua/item.lua文件，利用刚刚封装的函数库实现对tomcat的查询：\n1 2 3 4 5 6 7 8 9 10 -- 引入自定义common工具模块，返回值是common中返回的 _M local common = require(\u0026#34;common\u0026#34;) -- 从 common中获取read_http这个函数 local read_http = common.read_http -- 获取路径参数 local id = ngx.var[1] -- 根据id查询商品 local itemJSON = read_http(\u0026#34;/item/\u0026#34;.. id, nil) -- 根据id查询商品库存 local itemStockJSON = read_http(\u0026#34;/item/stock/\u0026#34;.. id, nil) Copied! 这里查询到的结果是json字符串，并且包含商品、库存两个json字符串，页面最终需要的是把两个json拼接为一个json：\n这就需要我们先把JSON变为lua的table，完成数据整合后，再转为JSON。\n4.4.3.CJSON工具类 OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。\n官方地址： https://github.com/openresty/lua-cjson/ 1）引入cjson模块：\n1 local cjson = require \u0026#34;cjson\u0026#34; Copied! 2）序列化：\n1 2 3 4 5 6 local obj = { name = \u0026#39;jack\u0026#39;, age = 21 } -- 把 table 序列化为 json local json = cjson.encode(obj) Copied! 3）反序列化：\n1 2 3 4 local json = \u0026#39;{\u0026#34;name\u0026#34;: \u0026#34;jack\u0026#34;, \u0026#34;age\u0026#34;: 21}\u0026#39; -- 反序列化 json为 table local obj = cjson.decode(json); print(obj.name) Copied! 4.4.4.实现Tomcat查询 下面，我们修改之前的item.lua中的业务，添加json处理功能：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 -- 导入common函数库 local common = require(\u0026#39;common\u0026#39;) local read_http = common.read_http -- 导入cjson库 local cjson = require(\u0026#39;cjson\u0026#39;) -- 获取路径参数 local id = ngx.var[1] -- 根据id查询商品 local itemJSON = read_http(\u0026#34;/item/\u0026#34;.. id, nil) -- 根据id查询商品库存 local itemStockJSON = read_http(\u0026#34;/item/stock/\u0026#34;.. id, nil) -- JSON转化为lua的table local item = cjson.decode(itemJSON) local stock = cjson.decode(stockJSON) -- 组合数据 item.stock = stock.stock item.sold = stock.sold -- 把item序列化为json 返回结果 ngx.say(cjson.encode(item)) Copied! 4.4.5.基于ID负载均衡 刚才的代码中，我们的tomcat是单机部署。而实际开发中，tomcat一定是集群模式：\n因此，OpenResty需要对tomcat集群做负载均衡。\n而默认的负载均衡规则是轮询模式，当我们查询/item/10001时：\n第一次会访问8081端口的tomcat服务，在该服务内部就形成了JVM进程缓存 第二次会访问8082端口的tomcat服务，该服务内部没有JVM缓存（因为JVM缓存无法共享），会查询数据库 \u0026hellip; 你看，因为轮询的原因，第一次查询8081形成的JVM缓存并未生效，直到下一次再次访问到8081时才可以生效，缓存命中率太低了。\n怎么办？\n如果能让同一个商品，每次查询时都访问同一个tomcat服务，那么JVM缓存就一定能生效了。\n也就是说，我们需要根据商品id做负载均衡，而不是轮询。\n1）原理 nginx提供了基于请求路径做负载均衡的算法：\nnginx根据请求路径做hash运算，把得到的数值对tomcat服务的数量取余，余数是几，就访问第几个服务，实现负载均衡。\n例如：\n我们的请求路径是 /item/10001 tomcat总数为2台（8081、8082） 对请求路径/item/1001做hash运算求余的结果为1 则访问第一个tomcat服务，也就是8081 只要id不变，每次hash运算结果也不会变，那就可以保证同一个商品，一直访问同一个tomcat服务，确保JVM缓存生效。\n2）实现 修改/usr/local/openresty/nginx/conf/nginx.conf文件，实现基于ID做负载均衡。\n首先，定义tomcat集群，并设置基于路径做负载均衡：\n1 2 3 4 5 upstream tomcat-cluster { hash $request_uri; server 192.168.150.1:8081; server 192.168.150.1:8082; } Copied! 然后，修改对tomcat服务的反向代理，目标指向tomcat集群：\n1 2 3 location /item { proxy_pass http://tomcat-cluster; } Copied! 重新加载OpenResty\n1 nginx -s reload Copied! 3）测试 启动两台tomcat服务：\n同时启动：\n清空日志后，再次访问页面，可以看到不同id的商品，访问到了不同的tomcat服务：\n4.5.Redis缓存预热 Redis缓存会面临冷启动问题：\n冷启动：服务刚刚启动时，Redis中并没有缓存，如果所有商品数据都在第一次查询时添加缓存，可能会给数据库带来较大压力。\n缓存预热：在实际开发中，我们可以利用大数据统计用户访问的热点数据，在项目启动时将这些热点数据提前查询并保存到Redis中。\n我们数据量较少，并且没有数据统计相关功能，目前可以在启动时将所有数据都放入缓存中。\n1）利用Docker安装Redis\n1 docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes Copied! 2）在item-service服务中引入Redis依赖\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-data-redis\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 3）配置Redis地址\n1 2 3 spring: redis: host: 192.168.150.101 Copied! 4）编写初始化类\n缓存预热需要在项目启动时完成，并且必须是拿到RedisTemplate之后。\n这里我们利用InitializingBean接口来实现，因为InitializingBean可以在对象被Spring创建并且成员变量全部注入后执行。\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package com.heima.item.config; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.heima.item.pojo.Item; import com.heima.item.pojo.ItemStock; import com.heima.item.service.IItemService; import com.heima.item.service.IItemStockService; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.List; @Component public class RedisHandler implements InitializingBean { @Autowired private StringRedisTemplate redisTemplate; @Autowired private IItemService itemService; @Autowired private IItemStockService stockService; private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public void afterPropertiesSet() throws Exception { // 初始化缓存 // 1.查询商品信息 List\u0026lt;Item\u0026gt; itemList = itemService.list(); // 2.放入缓存 for (Item item : itemList) { // 2.1.item序列化为JSON String json = MAPPER.writeValueAsString(item); // 2.2.存入redis redisTemplate.opsForValue().set(\u0026#34;item:id:\u0026#34; + item.getId(), json); } // 3.查询商品库存信息 List\u0026lt;ItemStock\u0026gt; stockList = stockService.list(); // 4.放入缓存 for (ItemStock stock : stockList) { // 2.1.item序列化为JSON String json = MAPPER.writeValueAsString(stock); // 2.2.存入redis redisTemplate.opsForValue().set(\u0026#34;item:stock:id:\u0026#34; + stock.getId(), json); } } } Copied! 4.6.查询Redis缓存 现在，Redis缓存已经准备就绪，我们可以再OpenResty中实现查询Redis的逻辑了。如下图红框所示：\n当请求进入OpenResty之后：\n优先查询Redis缓存 如果Redis缓存未命中，再查询Tomcat 4.6.1.封装Redis工具 OpenResty提供了操作Redis的模块，我们只要引入该模块就能直接使用。但是为了方便，我们将Redis操作封装到之前的common.lua工具库中。\n修改/usr/local/openresty/lualib/common.lua文件：\n1）引入Redis模块，并初始化Redis对象\n1 2 3 4 5 -- 导入redis local redis = require(\u0026#39;resty.redis\u0026#39;) -- 初始化redis local red = redis:new() red:set_timeouts(1000, 1000, 1000) Copied! 2）封装函数，用来释放Redis连接，其实是放入连接池\n1 2 3 4 5 6 7 8 9 -- 关闭redis连接的工具方法，其实是放入连接池 local function close_redis(red) local pool_max_idle_time = 10000 -- 连接的空闲时间，单位是毫秒 local pool_size = 100 --连接池大小 local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.log(ngx.ERR, \u0026#34;放入redis连接池失败: \u0026#34;, err) end end Copied! 3）封装函数，根据key查询Redis数据\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 -- 查询redis的方法 ip和port是redis地址，key是查询的key local function read_redis(ip, port, key) -- 获取一个连接 local ok, err = red:connect(ip, port) if not ok then ngx.log(ngx.ERR, \u0026#34;连接redis失败 : \u0026#34;, err) return nil end -- 查询redis local resp, err = red:get(key) -- 查询失败处理 if not resp then ngx.log(ngx.ERR, \u0026#34;查询Redis失败: \u0026#34;, err, \u0026#34;, key = \u0026#34; , key) end --得到的数据为空处理 if resp == ngx.null then resp = nil ngx.log(ngx.ERR, \u0026#34;查询Redis数据为空, key = \u0026#34;, key) end close_redis(red) return resp end Copied! 4）导出\n1 2 3 4 5 6 -- 将方法导出 local _M = { read_http = read_http, read_redis = read_redis } return _M Copied! 完整的common.lua：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 -- 导入redis local redis = require(\u0026#39;resty.redis\u0026#39;) -- 初始化redis local red = redis:new() red:set_timeouts(1000, 1000, 1000) -- 关闭redis连接的工具方法，其实是放入连接池 local function close_redis(red) local pool_max_idle_time = 10000 -- 连接的空闲时间，单位是毫秒 local pool_size = 100 --连接池大小 local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.log(ngx.ERR, \u0026#34;放入redis连接池失败: \u0026#34;, err) end end -- 查询redis的方法 ip和port是redis地址，key是查询的key local function read_redis(ip, port, key) -- 获取一个连接 local ok, err = red:connect(ip, port) if not ok then ngx.log(ngx.ERR, \u0026#34;连接redis失败 : \u0026#34;, err) return nil end -- 查询redis local resp, err = red:get(key) -- 查询失败处理 if not resp then ngx.log(ngx.ERR, \u0026#34;查询Redis失败: \u0026#34;, err, \u0026#34;, key = \u0026#34; , key) end --得到的数据为空处理 if resp == ngx.null then resp = nil ngx.log(ngx.ERR, \u0026#34;查询Redis数据为空, key = \u0026#34;, key) end close_redis(red) return resp end -- 封装函数，发送http请求，并解析响应 local function read_http(path, params) local resp = ngx.location.capture(path,{ method = ngx.HTTP_GET, args = params, }) if not resp then -- 记录错误信息，返回404 ngx.log(ngx.ERR, \u0026#34;http查询失败, path: \u0026#34;, path , \u0026#34;, args: \u0026#34;, args) ngx.exit(404) end return resp.body end -- 将方法导出 local _M = { read_http = read_http, read_redis = read_redis } return _M Copied! 4.6.2.实现Redis查询 接下来，我们就可以去修改item.lua文件，实现对Redis的查询了。\n查询逻辑是：\n根据id查询Redis 如果查询失败则继续查询Tomcat 将查询结果返回 1）修改/usr/local/openresty/lua/item.lua文件，添加一个查询函数：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -- 导入common函数库 local common = require(\u0026#39;common\u0026#39;) local read_http = common.read_http local read_redis = common.read_redis -- 封装查询函数 function read_data(key, path, params) -- 查询本地缓存 local val = read_redis(\u0026#34;127.0.0.1\u0026#34;, 6379, key) -- 判断查询结果 if not val then ngx.log(ngx.ERR, \u0026#34;redis查询失败，尝试查询http， key: \u0026#34;, key) -- redis查询失败，去查询http val = read_http(path, params) end -- 返回数据 return val end Copied! 2）而后修改商品查询、库存查询的业务：\n3）完整的item.lua代码：\n1 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 37 38 -- 导入common函数库 local common = require(\u0026#39;common\u0026#39;) local read_http = common.read_http local read_redis = common.read_redis -- 导入cjson库 local cjson = require(\u0026#39;cjson\u0026#39;) -- 封装查询函数 function read_data(key, path, params) -- 查询本地缓存 local val = read_redis(\u0026#34;127.0.0.1\u0026#34;, 6379, key) -- 判断查询结果 if not val then ngx.log(ngx.ERR, \u0026#34;redis查询失败，尝试查询http， key: \u0026#34;, key) -- redis查询失败，去查询http val = read_http(path, params) end -- 返回数据 return val end -- 获取路径参数 local id = ngx.var[1] -- 查询商品信息 local itemJSON = read_data(\u0026#34;item:id:\u0026#34; .. id, \u0026#34;/item/\u0026#34; .. id, nil) -- 查询库存信息 local stockJSON = read_data(\u0026#34;item:stock:id:\u0026#34; .. id, \u0026#34;/item/stock/\u0026#34; .. id, nil) -- JSON转化为lua的table local item = cjson.decode(itemJSON) local stock = cjson.decode(stockJSON) -- 组合数据 item.stock = stock.stock item.sold = stock.sold -- 把item序列化为json 返回结果 ngx.say(cjson.encode(item)) Copied! 4.7.Nginx本地缓存 现在，整个多级缓存中只差最后一环，也就是nginx的本地缓存了。如图：\n4.7.1.本地缓存API OpenResty为Nginx提供了shard dict的功能，可以在nginx的多个worker之间共享数据，实现缓存功能。\n1）开启共享字典，在nginx.conf的http下添加配置：\n1 2 # 共享字典，也就是本地缓存，名称叫做：item_cache，大小150m lua_shared_dict item_cache 150m; Copied! 2）操作共享字典：\n1 2 3 4 5 6 -- 获取本地缓存对象 local item_cache = ngx.shared.item_cache -- 存储, 指定key、value、过期时间，单位s，默认为0代表永不过期 item_cache:set(\u0026#39;key\u0026#39;, \u0026#39;value\u0026#39;, 1000) -- 读取 local val = item_cache:get(\u0026#39;key\u0026#39;) Copied! 4.7.2.实现本地缓存查询 1）修改/usr/local/openresty/lua/item.lua文件，修改read_data查询函数，添加本地缓存逻辑：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 -- 导入共享词典，本地缓存 local item_cache = ngx.shared.item_cache -- 封装查询函数 function read_data(key, expire, path, params) -- 查询本地缓存 local val = item_cache:get(key) if not val then ngx.log(ngx.ERR, \u0026#34;本地缓存查询失败，尝试查询Redis， key: \u0026#34;, key) -- 查询redis val = read_redis(\u0026#34;127.0.0.1\u0026#34;, 6379, key) -- 判断查询结果 if not val then ngx.log(ngx.ERR, \u0026#34;redis查询失败，尝试查询http， key: \u0026#34;, key) -- redis查询失败，去查询http val = read_http(path, params) end end -- 查询成功，把数据写入本地缓存 item_cache:set(key, val, expire) -- 返回数据 return val end Copied! 2）修改item.lua中查询商品和库存的业务，实现最新的read_data函数：\n其实就是多了缓存时间参数，过期后nginx缓存会自动删除，下次访问即可更新缓存。\n这里给商品基本信息设置超时时间为30分钟，库存为1分钟。\n因为库存更新频率较高，如果缓存时间过长，可能与数据库差异较大。\n3）完整的item.lua文件：\n1 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 37 38 39 40 41 42 43 44 45 46 47 -- 导入common函数库 local common = require(\u0026#39;common\u0026#39;) local read_http = common.read_http local read_redis = common.read_redis -- 导入cjson库 local cjson = require(\u0026#39;cjson\u0026#39;) -- 导入共享词典，本地缓存 local item_cache = ngx.shared.item_cache -- 封装查询函数 function read_data(key, expire, path, params) -- 查询本地缓存 local val = item_cache:get(key) if not val then ngx.log(ngx.ERR, \u0026#34;本地缓存查询失败，尝试查询Redis， key: \u0026#34;, key) -- 查询redis val = read_redis(\u0026#34;127.0.0.1\u0026#34;, 6379, key) -- 判断查询结果 if not val then ngx.log(ngx.ERR, \u0026#34;redis查询失败，尝试查询http， key: \u0026#34;, key) -- redis查询失败，去查询http val = read_http(path, params) end end -- 查询成功，把数据写入本地缓存 item_cache:set(key, val, expire) -- 返回数据 return val end -- 获取路径参数 local id = ngx.var[1] -- 查询商品信息 local itemJSON = read_data(\u0026#34;item:id:\u0026#34; .. id, 1800, \u0026#34;/item/\u0026#34; .. id, nil) -- 查询库存信息 local stockJSON = read_data(\u0026#34;item:stock:id:\u0026#34; .. id, 60, \u0026#34;/item/stock/\u0026#34; .. id, nil) -- JSON转化为lua的table local item = cjson.decode(itemJSON) local stock = cjson.decode(stockJSON) -- 组合数据 item.stock = stock.stock item.sold = stock.sold -- 把item序列化为json 返回结果 ngx.say(cjson.encode(item)) Copied! 5.缓存同步 大多数情况下，浏览器查询到的都是缓存数据，如果缓存数据与数据库数据存在较大差异，可能会产生比较严重的后果。\n所以我们必须保证数据库数据、缓存数据的一致性，这就是缓存与数据库的同步。\n5.1.数据同步策略 缓存数据同步的常见方式有三种：\n设置有效期：给缓存设置有效期，到期后自动删除。再次查询时更新\n优势：简单、方便 缺点：时效性差，缓存过期之前可能不一致 场景：更新频率较低，时效性要求低的业务 同步双写：在修改数据库的同时，直接修改缓存\n优势：时效性强，缓存与数据库强一致 缺点：有代码侵入，耦合度高； 场景：对一致性、时效性要求较高的缓存数据 **异步通知：**修改数据库时发送事件通知，相关服务监听到通知后修改缓存数据\n优势：低耦合，可以同时通知多个缓存服务 缺点：时效性一般，可能存在中间不一致状态 场景：时效性要求一般，有多个服务需要同步 而异步实现又可以基于MQ或者Canal来实现：\n1）基于MQ的异步通知：\n解读：\n商品服务完成对数据的修改后，只需要发送一条消息到MQ中。 缓存服务监听MQ消息，然后完成对缓存的更新 依然有少量的代码侵入。\n2）基于Canal的通知\n解读：\n商品服务完成商品修改后，业务直接结束，没有任何代码侵入 Canal监听MySQL变化，当发现变化后，立即通知缓存服务 缓存服务接收到canal通知，更新缓存 代码零侵入\n5.2.安装Canal 5.2.1.认识Canal Canal [kə\u0026rsquo;næl]，译意为水道/管道/沟渠，canal是阿里巴巴旗下的一款开源项目，基于Java开发。基于数据库增量日志解析，提供增量数据订阅\u0026amp;消费。GitHub的地址：https://github.com/alibaba/canal\nCanal是基于mysql的主从同步来实现的，MySQL主从同步的原理如下：\n1）MySQL master 将数据变更写入二进制日志( binary log），其中记录的数据叫做binary log events 2）MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log) 3）MySQL slave 重放 relay log 中事件，将数据变更反映它自己的数据 而Canal就是把自己伪装成MySQL的一个slave节点，从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端，进而完成对其它数据库的同步。\n5.2.2.安装Canal 安装和配置Canal参考课前资料文档：\n5.3.监听Canal Canal提供了各种语言的客户端，当Canal监听到binlog变化时，会通知Canal的客户端。\n我们可以利用Canal提供的Java客户端，监听Canal通知消息。当收到变化的消息时，完成对缓存的更新。\n不过这里我们会使用GitHub上的第三方开源的canal-starter客户端。地址：https://github.com/NormanGyllenhaal/canal-client\n与SpringBoot完美整合，自动装配，比官方客户端要简单好用很多。\n5.3.1.引入依赖： 1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;top.javatool\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;canal-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.1-RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 5.3.2.编写配置： 1 2 3 canal: destination: heima # canal的集群名字，要与安装canal时设置的名称一致 server: 192.168.150.101:11111 # canal服务地址 Copied! 5.3.3.修改Item实体类 通过@Id、@Column、等注解完成Item与数据库表字段的映射：\n1 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 37 package com.heima.item.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Transient; import javax.persistence.Column; import java.util.Date; @Data @TableName(\u0026#34;tb_item\u0026#34;) public class Item { @TableId(type = IdType.AUTO) @Id private Long id;//商品id @Column(name = \u0026#34;name\u0026#34;) private String name;//商品名称 private String title;//商品标题 private Long price;//价格（分） private String image;//商品图片 private String category;//分类名称 private String brand;//品牌名称 private String spec;//规格 private Integer status;//商品状态 1-正常，2-下架 private Date createTime;//创建时间 private Date updateTime;//更新时间 @TableField(exist = false) @Transient private Integer stock; @TableField(exist = false) @Transient private Integer sold; } Copied! 5.3.4.编写监听器 通过实现EntryHandler\u0026lt;T\u0026gt;接口编写监听器，监听Canal消息。注意两点：\n实现类通过@CanalTable(\u0026quot;tb_item\u0026quot;)指定监听的表信息 EntryHandler的泛型是与表对应的实体类 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 37 38 39 40 41 42 43 package com.heima.item.canal; import com.github.benmanes.caffeine.cache.Cache; import com.heima.item.config.RedisHandler; import com.heima.item.pojo.Item; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import top.javatool.canal.client.annotation.CanalTable; import top.javatool.canal.client.handler.EntryHandler; @CanalTable(\u0026#34;tb_item\u0026#34;) @Component public class ItemHandler implements EntryHandler\u0026lt;Item\u0026gt; { @Autowired private RedisHandler redisHandler; @Autowired private Cache\u0026lt;Long, Item\u0026gt; itemCache; @Override public void insert(Item item) { // 写数据到JVM进程缓存 itemCache.put(item.getId(), item); // 写数据到redis redisHandler.saveItem(item); } @Override public void update(Item before, Item after) { // 写数据到JVM进程缓存 itemCache.put(after.getId(), after); // 写数据到redis redisHandler.saveItem(after); } @Override public void delete(Item item) { // 删除数据到JVM进程缓存 itemCache.invalidate(item.getId()); // 删除数据到redis redisHandler.deleteItemById(item.getId()); } } Copied! 在这里对Redis的操作都封装到了RedisHandler这个对象中，是我们之前做缓存预热时编写的一个类，内容如下：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 package com.heima.item.config; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.heima.item.pojo.Item; import com.heima.item.pojo.ItemStock; import com.heima.item.service.IItemService; import com.heima.item.service.IItemStockService; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.List; @Component public class RedisHandler implements InitializingBean { @Autowired private StringRedisTemplate redisTemplate; @Autowired private IItemService itemService; @Autowired private IItemStockService stockService; private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public void afterPropertiesSet() throws Exception { // 初始化缓存 // 1.查询商品信息 List\u0026lt;Item\u0026gt; itemList = itemService.list(); // 2.放入缓存 for (Item item : itemList) { // 2.1.item序列化为JSON String json = MAPPER.writeValueAsString(item); // 2.2.存入redis redisTemplate.opsForValue().set(\u0026#34;item:id:\u0026#34; + item.getId(), json); } // 3.查询商品库存信息 List\u0026lt;ItemStock\u0026gt; stockList = stockService.list(); // 4.放入缓存 for (ItemStock stock : stockList) { // 2.1.item序列化为JSON String json = MAPPER.writeValueAsString(stock); // 2.2.存入redis redisTemplate.opsForValue().set(\u0026#34;item:stock:id:\u0026#34; + stock.getId(), json); } } public void saveItem(Item item) { try { String json = MAPPER.writeValueAsString(item); redisTemplate.opsForValue().set(\u0026#34;item:id:\u0026#34; + item.getId(), json); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } public void deleteItemById(Long id) { redisTemplate.delete(\u0026#34;item:id:\u0026#34; + id); } } Copied! ","date":"2023-09-04T02:02:09+08:00","permalink":"https://qh.1357810.xyz/articles/redis/multistage-cache/","title":"多级缓存"},{"content":"Hugo theme Stack supports the creation of interactive image galleries using Markdown. It\u0026rsquo;s powered by PhotoSwipe and its syntax was inspired by Typlog .\nTo use this feature, the image must be in the same directory as the Markdown file, as it uses Hugo\u0026rsquo;s page bundle feature to read the dimensions of the image. External images are not supported.\nSyntax 1 ![Image 1](1.jpg) ![Image 2](2.jpg) Copied! Result Photo by mymind and Luke Chesser on Unsplash 页绑定图片 全局图片 ","date":"2023-08-26T00:00:00Z","image":"https://logan.1357810.xyz/cover/pic_073.jpg","permalink":"https://qh.1357810.xyz/example/image-gallery/","title":"Image gallery"},{"content":" ctrl+u 删除一行 ctrl+w 删除上一个字符 ctrl+a 光标移入当前行首 ctrl+e 光标移入当前行尾 ","date":"2023-05-03T03:49:51+08:00","permalink":"https://qh.1357810.xyz/articles/linux/linux-command/","title":"linux常用命令"},{"content":" 1、问题 我们把一个 SpringBoot 工程导出为 jar 包，jar 包上传到阿里云 ECS 服务器上，使用 java -jar xxx-xxx.jar 命令启动这个 SpringBoot 程序。此时我们本地的 xshell 客户端必须一直开着，一旦 xshell 客户端关闭，java -jar xxx-xxx.jar 进程就会被结束，SpringBoot 程序就访问不了了。\n所以我们希望启动 SpringBoot 的 jar 包之后，对应的进程可以一直运行，不会因为 xshell 客户端关闭而被结束。\n2、解决 前台、后台运行 ​\t默认情况下 Linux 命令都是前台运行的，前台运行的特点是前面命令不执行完，命令行就一直被前面的命令占用，不能再输入、执行新的命令。\n1 2 3 4 #!/bin/bash echo \u0026#34;hello before sleep\u0026#34; sleep 20 echo \u0026#34;hello after sleep\u0026#34; Copied! 前台（默认情况）运行上面脚本的效果是：\n后台运行上面脚本的效果是：\n但是以后台方式运行并不能解决前面提出的问题：我们的 shell 客户端（例如：xshell）和服务器断开连接后，SpringBoot 进程会随之结束，这显然不满足我们部署运行项目的初衷。\n不挂断运行 所谓“不挂断”就是指客户端断开连接后，命令启动的进程仍然运行。nohup 命令就是 ”no hang up“ 的缩写。使用nohup 命令启动 SpringBoot 微服务工程的完整写法是：\n1 nohup java -jar spring-boot-demo.jar\u0026gt;springboot.log 2\u0026gt;\u0026amp;1 \u0026amp; Copied! ","date":"2023-05-03T03:49:51+08:00","permalink":"https://qh.1357810.xyz/articles/linux/linux-nohup/","title":"nohup命令"},{"content":" mac安装和配置ohmyzsh 1、ohmyzsh项目默认目录为 ～/.ohmyzsh ，github项目地址为：https://github.com/ohmyzsh/ohmyzsh\n2、配置文件默认目录为 ～/.zshrc my zshrc save in github\n3、安装ohmyzsh\n1 sh -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\u0026#34; Copied! 4、安装插件\nhttps://github.com/zsh-users/zsh-syntax-highlighting ~/.oh-my-zsh/custom/plugins\n1 git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting Copied! https://github.com/zsh-users/zsh-autosuggestions ~/.oh-my-zsh/custom/plugins\n1 git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions Copied! https://github.com/wting/autojump 1 2 3 4 brew install autojump # 直接配置在zshrc中的plugins里 # zsh直接通过.oh-my-zsh/plugins/autojump/autojump.plugin.zsh寻找autojump安装路径 # 从而实现功能 Copied! 4、安装字体 font-hack-nerd-font\n1 brew install font-hack-nerd-font --cask Copied! 5、安装主题 dracula https://draculatheme.com/zsh 软连接方式\n1 2 git clone https://github.com/dracula/zsh.git ln -s $DRACULA_THEME/dracula.zsh-theme $OH_MY_ZSH/themes/dracula.zsh-theme Copied! 文件方式\n1 2 3 1、Download using the GitHub .zip download option and unzip them. 2、Move dracula.zsh-theme file to oh-my-zsh\u0026#39;s theme folder: oh-my-zsh/themes/dracula.zsh-theme. 3、Move /lib to oh-my-zsh\u0026#39;s theme folder: oh-my-zsh/themes/lib. Copied! 我自己修改的 放在 ~/.oh-my-zsh/custom/themes/mydracula.zsh-theme\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 # -*- mode: sh; -*- # vim: set ft=sh : # Dracula Theme v1.2.5 # # https://github.com/dracula/dracula-theme # # Copyright 2019, All rights reserved # # Code licensed under the MIT license # http://zenorocha.mit-license.org # # @author Zeno Rocha \u0026lt;hi@zenorocha.com\u0026gt; # @maintainer Avalon Williams \u0026lt;avalonwilliams@protonmail.com\u0026gt; # Initialization {{{ source ${0:A:h}/lib/async.zsh autoload -Uz add-zsh-hook setopt PROMPT_SUBST async_init PROMPT=\u0026#39;\u0026#39; # }}} # Options {{{ # Set to 0 to disable the git status DRACULA_DISPLAY_GIT=${DRACULA_DISPLAY_GIT:-1} # Set to 1 to show the date DRACULA_DISPLAY_TIME=${DRACULA_DISPLAY_TIME:-1} # Set to 1 to show the \u0026#39;context\u0026#39; segment DRACULA_DISPLAY_CONTEXT=${DRACULA_DISPLAY_CONTEXT:-0} # Changes the arrow icon DRACULA_ARROW_ICON=${DRACULA_ARROW_ICON:-➜} # Set to 1 to use an new line for commands DRACULA_DISPLAY_NEW_LINE=${DRACULA_DISPLAY_NEW_LINE:-0} # function to detect if git has support for --no-optional-locks dracula_test_git_optional_lock() { local git_version=${DEBUG_OVERRIDE_V:-\u0026#34;$(git version | cut -d\u0026#39; \u0026#39; -f3)\u0026#34;} local git_version=\u0026#34;$(git version | cut -d\u0026#39; \u0026#39; -f3)\u0026#34; # test for git versions \u0026lt; 2.14.0 case \u0026#34;$git_version\u0026#34; in [0-1].*) echo 0 return 1 ;; 2.[0-9].*) echo 0 return 1 ;; 2.1[0-3].*) echo 0 return 1 ;; esac # if version \u0026gt; 2.14.0 return true echo 1 } # use --no-optional-locks flag on git DRACULA_GIT_NOLOCK=${DRACULA_GIT_NOLOCK:-$(dracula_test_git_optional_lock)} # }}} # Status segment {{{ # arrow is green if last command was successful, red if not, # turns yellow in vi command mode if (( ! DRACULA_DISPLAY_NEW_LINE )); then PROMPT+=\u0026#39;%(1V:%F{yellow}:%(?:%F{green}:%F{red}))${DRACULA_ARROW_ICON} \u0026#39; fi # }}} # Time segment {{{ dracula_time_segment() { if (( DRACULA_DISPLAY_TIME )); then if [[ -z \u0026#34;$TIME_FORMAT\u0026#34; ]]; then TIME_FORMAT=\u0026#34;%-H:%M:%S\u0026#34; # check if locale uses AM and PM # if ! locale -ck LC_TIME | grep \u0026#39;am_pm=\u0026#34;;\u0026#34;\u0026#39; \u0026gt; /dev/null; then # TIME_FORMAT=\u0026#34;%-I:%M%p\u0026#34; # fi fi print -P \u0026#34;%D{$TIME_FORMAT} \u0026#34; fi } PROMPT+=\u0026#39;%F{green}%B$(dracula_time_segment)\u0026#39; # }}} # User context segment {{{ dracula_context() { if (( DRACULA_DISPLAY_CONTEXT )); then if [[ -n \u0026#34;${SSH_CONNECTION-}${SSH_CLIENT-}${SSH_TTY-}\u0026#34; ]] || (( EUID == 0 )); then echo \u0026#39;%n@%m \u0026#39; else echo \u0026#39;%n \u0026#39; fi fi } PROMPT+=\u0026#39;%F{magenta}%B$(dracula_context)\u0026#39; # }}} # Directory segment {{{ PROMPT+=\u0026#39;%F{blue}%B%c \u0026#39; # }}} # Custom variable {{{ function custom_variable_prompt() { [[ -z $DRACULA_CUSTOM_VARIABLE ]] \u0026amp;\u0026amp; return echo \u0026#34;$FG[008]$DRACULA_CUSTOM_VARIABLE \u0026#34; } PROMPT+=\u0026#39;$(custom_variable_prompt)\u0026#39; # }}} # Async git segment {{{ dracula_git_status() { (( ! DRACULA_DISPLAY_GIT )) \u0026amp;\u0026amp; return cd \u0026#34;$1\u0026#34; local ref branch lockflag (( DRACULA_GIT_NOLOCK )) \u0026amp;\u0026amp; lockflag=\u0026#34;--no-optional-locks\u0026#34; ref=$(=git $lockflag symbolic-ref --quiet HEAD 2\u0026gt;/tmp/git-errors) case $? in 0) ;; 128) return ;; *) ref=$(=git $lockflag rev-parse --short HEAD 2\u0026gt;/tmp/git-errors) || return ;; esac branch=${ref#refs/heads/} if [[ -n $branch ]]; then echo -n \u0026#34;${ZSH_THEME_GIT_PROMPT_PREFIX}${branch}\u0026#34; local git_status icon git_status=\u0026#34;$(LC_ALL=C =git $lockflag status 2\u0026gt;\u0026amp;1)\u0026#34; if [[ \u0026#34;$git_status\u0026#34; =~ \u0026#39;new file:|deleted:|modified:|renamed:|Untracked files:\u0026#39; ]]; then echo -n \u0026#34;$ZSH_THEME_GIT_PROMPT_DIRTY\u0026#34; else echo -n \u0026#34;$ZSH_THEME_GIT_PROMPT_CLEAN\u0026#34; fi echo -n \u0026#34;$ZSH_THEME_GIT_PROMPT_SUFFIX\u0026#34; fi } dracula_git_callback() { DRACULA_GIT_STATUS=\u0026#34;$3\u0026#34; zle \u0026amp;\u0026amp; zle reset-prompt async_stop_worker dracula_git_worker dracula_git_status \u0026#34;$(pwd)\u0026#34; } dracula_git_async() { async_start_worker dracula_git_worker -n async_register_callback dracula_git_worker dracula_git_callback async_job dracula_git_worker dracula_git_status \u0026#34;$(pwd)\u0026#34; } add-zsh-hook precmd dracula_git_async PROMPT+=\u0026#39;$DRACULA_GIT_STATUS\u0026#39; ZSH_THEME_GIT_PROMPT_CLEAN=\u0026#34;) %F{green}%B✔ \u0026#34; ZSH_THEME_GIT_PROMPT_DIRTY=\u0026#34;) %F{yellow}%B✗ \u0026#34; ZSH_THEME_GIT_PROMPT_PREFIX=\u0026#34;%F{cyan}%B(\u0026#34; ZSH_THEME_GIT_PROMPT_SUFFIX=\u0026#34;%f%b\u0026#34; # }}} # Linebreak {{{ if (( DRACULA_DISPLAY_NEW_LINE )); then PROMPT+=$\u0026#39;\\n\u0026#39; PROMPT+=\u0026#39;%(1V:%F{yellow}:%(?:%F{green}:%F{red}))${DRACULA_ARROW_ICON} \u0026#39; fi # }}} # define widget without clobbering old definitions dracula_defwidget() { local fname=dracula-wrap-$1 local prev=($(zle -l -L \u0026#34;$1\u0026#34;)) local oldfn=${prev[4]:-$1} # if no existing zle functions, just define it normally if [[ -z \u0026#34;$prev\u0026#34; ]]; then zle -N $1 $2 return fi # if already defined, return [[ \u0026#34;${prev[4]}\u0026#34; = $fname ]] \u0026amp;\u0026amp; return oldfn=${prev[4]:-$1} zle -N dracula-old-$oldfn $oldfn eval \u0026#34;$fname() { $2 \\\u0026#34;\\$@\\\u0026#34;; zle dracula-old-$oldfn -- \\\u0026#34;\\$@\\\u0026#34;; }\u0026#34; zle -N $1 $fname } # ensure vi mode is handled by prompt dracula_zle_update() { if [[ $KEYMAP = vicmd ]]; then psvar[1]=vicmd else psvar[1]=\u0026#39;\u0026#39; fi zle reset-prompt zle -R } dracula_defwidget zle-line-init dracula_zle_update dracula_defwidget zle-keymap-select dracula_zle_update # Ensure effects are reset PROMPT+=\u0026#39;%f%b\u0026#39; Copied! 6、配置文件 ～/.zshrc\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 # If you come from bash you might have to change your $PATH. export PATH=$HOME/bin:/usr/local/bin:$PATH # Path to your oh-my-zsh installation. export ZSH=\u0026#34;$HOME/.oh-my-zsh\u0026#34; # Set name of the theme to load --- if set to \u0026#34;random\u0026#34;, it will # load a random theme each time oh-my-zsh is loaded, in which case, # to know which specific one was loaded, run: echo $RANDOM_THEME # See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes #ZSH_THEME=\u0026#34;agnoster\u0026#34; # ZSH_THEME=\u0026#34;dracula\u0026#34; ZSH_THEME=\u0026#34;mydracula\u0026#34; #ZSH_THEME=\u0026#34;random\u0026#34; # Set list of themes to pick from when loading at random # Setting this variable when ZSH_THEME=random will cause zsh to load # a theme from this variable instead of looking in $ZSH/themes/ # If set to an empty array, this variable will have no effect. # ZSH_THEME_RANDOM_CANDIDATES=( \u0026#34;robbyrussell\u0026#34; \u0026#34;agnoster\u0026#34; ) # Uncomment the following line to use case-sensitive completion. # CASE_SENSITIVE=\u0026#34;true\u0026#34; # Uncomment the following line to use hyphen-insensitive completion. # Case-sensitive completion must be off. _ and - will be interchangeable. # HYPHEN_INSENSITIVE=\u0026#34;true\u0026#34; # Uncomment one of the following lines to change the auto-update behavior # zstyle \u0026#39;:omz:update\u0026#39; mode disabled # disable automatic updates # zstyle \u0026#39;:omz:update\u0026#39; mode auto # update automatically without asking # zstyle \u0026#39;:omz:update\u0026#39; mode reminder # just remind me to update when it\u0026#39;s time # Uncomment the following line to change how often to auto-update (in days). # zstyle \u0026#39;:omz:update\u0026#39; frequency 13 # Uncomment the following line if pasting URLs and other text is messed up. # DISABLE_MAGIC_FUNCTIONS=\u0026#34;true\u0026#34; # Uncomment the following line to disable colors in ls. # DISABLE_LS_COLORS=\u0026#34;true\u0026#34; # Uncomment the following line to disable auto-setting terminal title. # DISABLE_AUTO_TITLE=\u0026#34;true\u0026#34; # Uncomment the following line to enable command auto-correction. # ENABLE_CORRECTION=\u0026#34;true\u0026#34; # Uncomment the following line to display red dots whilst waiting for completion. # You can also set it to another string to have that shown instead of the default red dots. # e.g. COMPLETION_WAITING_DOTS=\u0026#34;%F{yellow}waiting...%f\u0026#34; # Caution: this setting can cause issues with multiline prompts in zsh \u0026lt; 5.7.1 (see #5765) # COMPLETION_WAITING_DOTS=\u0026#34;true\u0026#34; # Uncomment the following line if you want to disable marking untracked files # under VCS as dirty. This makes repository status check for large repositories # much, much faster. # DISABLE_UNTRACKED_FILES_DIRTY=\u0026#34;true\u0026#34; # Uncomment the following line if you want to change the command execution time # stamp shown in the history command output. # You can set one of the optional three formats: # \u0026#34;mm/dd/yyyy\u0026#34;|\u0026#34;dd.mm.yyyy\u0026#34;|\u0026#34;yyyy-mm-dd\u0026#34; # or set a custom format using the strftime function format specifications, # see \u0026#39;man strftime\u0026#39; for details. HIST_STAMPS=\u0026#34;yyyy-mm-dd\u0026#34; # Would you like to use another custom folder than $ZSH/custom? # ZSH_CUSTOM=/path/to/new-custom-folder # Which plugins would you like to load? # Standard plugins can be found in $ZSH/plugins/ # Custom plugins may be added to $ZSH_CUSTOM/plugins/ # Example format: plugins=(rails git textmate ruby lighthouse) # Add wisely, as too many plugins slow down shell startup. # zsh-syntax-highlighting:~/.oh-my-zsh/custom/plugins,https://github.com/zsh-users/zsh-syntax-highlighting # zsh-autosuggestions:~/.oh-my-zsh/custom/plugins,https://github.com/zsh-users/zsh-autosuggestions plugins=( git colored-man-pages # macos # 以下需要自己安装 #~/.oh-my-zsh/custom/plugins,https://github.com/zsh-users/zsh-syntax-highlighting zsh-syntax-highlighting #~/.oh-my-zsh/custom/plugins,https://github.com/zsh-users/zsh-autosuggestions zsh-autosuggestions #brew install autojump autojump ) source $ZSH/oh-my-zsh.sh # User configuration # export MANPATH=\u0026#34;/usr/local/man:$MANPATH\u0026#34; # You may need to manually set your language environment # export LANG=en_US.UTF-8 # Preferred editor for local and remote sessions # if [[ -n $SSH_CONNECTION ]]; then # export EDITOR=\u0026#39;vim\u0026#39; # else # export EDITOR=\u0026#39;mvim\u0026#39; # fi # Compilation flags # export ARCHFLAGS=\u0026#34;-arch x86_64\u0026#34; # Set personal aliases, overriding those provided by oh-my-zsh libs, # plugins, and themes. Aliases can be placed here, though oh-my-zsh # users are encouraged to define aliases within the ZSH_CUSTOM folder. # For a full list of active aliases, run `alias`. # # Example aliases # alias zshconfig=\u0026#34;mate ~/.zshrc\u0026#34; # alias ohmyzsh=\u0026#34;mate ~/.oh-my-zsh\u0026#34; # 去重 zsh_history #setopt HIST_IGNORE_ALL_DUPS #setopt HIST_IGNORE_SPACE # proxy #alias setproxy=\u0026#34;export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7890\u0026#34; #alias unproxy=\u0026#34;unset http_proxy;unset https_proxy;unset ALL_PROXY\u0026#34; # git-proxy #alias gitProxy=\u0026#34;git config --global http.proxy socks5://127.0.0.1:7890;git config --global https.proxy socks5://127.0.0.1:7890\u0026#34; #alias gitUnproxy=\u0026#34;git config --global --unset http.proxy;git config --global --unset https.proxy\u0026#34; # 上面alias舍弃，代理用自定义函数方式去做 # maven export MAVEN_HOME=~/Data/Software/apache-maven-3.6.3 export PATH=$MAVEN_HOME/bin:$PATH # java export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_321.jdk/Contents/Home export PATH=$JAVA_HOME/bin:$PATH # brew 清华镜像源 export HOMEBREW_BREW_GIT_REMOTE=\u0026#34;https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/brew.git\u0026#34; export HOMEBREW_CORE_GIT_REMOTE=\u0026#34;https://mirrors.tuna.tsinghua.edu.cn/git/homebrew/homebrew-core.git\u0026#34; export HOMEBREW_BOTTLE_DOMAIN=\u0026#34;https://mirrors.tuna.tsinghua.edu.cn/homebrew-bottles\u0026#34; # brew命令 alias brewtree=\u0026#34;brew deps --tree --installed\u0026#34; alias brewlist=\u0026#34;brew leaves | xargs brew deps --installed --for-each | sed \u0026#39;s/^.*:/$(tput setaf 4)\u0026amp;$(tput sgr0)/\u0026#39;\u0026#34; # java多版本 export PATH=\u0026#34;$HOME/.jenv/bin:$PATH\u0026#34; eval \u0026#34;$(jenv init -)\u0026#34; # ls颜色和图标 alias ls=\u0026#34;lsd\u0026#34; # alias ll=\u0026#34;lsd -lh\u0026#34; # alias l=\u0026#34;lsd -alh\u0026#34; # bat主题 export BAT_THEME=\u0026#34;Dracula\u0026#34; # python alias python2=\u0026#39;/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python2.7\u0026#39; alias python3=\u0026#39;/usr/local/opt/python@3.10/bin/python3\u0026#39; alias python=python3 alias pips=\u0026#39;/usr/bin/pip3\u0026#39; alias pip=\u0026#39;/usr/local/opt/python@3.10/bin/pip3\u0026#39; # top替代 alias btop=\u0026#34;bpytop\u0026#34; # tree颜色 alias trees=\u0026#34;\\tree -C\u0026#34; # 加载自定义函数 alias au=\u0026#34;autoload -U\u0026#34; fpath+=~/Data/Config/my-functions autoload -U setproxy unproxy getproxy # 重载配置文件 alias rr=\u0026#34;echo \u0026#39;reloading...\u0026#39;\u0026amp;\u0026amp;source ~/.zshrc\u0026#34; Copied! 7、其他安装\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 # https://nodejs.org/en/ brew install node # https://npmmirror.com 阿里镜像源 npm install -g cnpm --registry=https://registry.npmmirror.com # https://github.com/bchatard/alfred-jetbrains alfred流 npm install -g @bchatard/alfred-jetbrains # https://picgo.github.io/PicGo-Core-Doc/zh/guide/ 图片上传 npm install picgo -g # https://tldr.sh 替代man npm install -g tldr # https://github.com/wting/autojump j跳转 brew install autojump # https://github.com/sharkdp/bat 替代cat brew install bat # https://github.com/aristocratos/bpytop 替代top brew install bpytop # https://github.com/bcicen/ctop 容器资源利用情况 brew install ctop # https://github.com/muesli/duf 磁盘空间 brew install duf # https://github.com/nicolargo/glances 服务 代替top brew install glances # https://github.com/hishamhm/htop 代替top brew install htop # https://github.com/jenv/jenv java版本管理 brew install jenv jenv doctor jenv versions jenv local 11.0.2 # https://github.com/Peltoche/lsd 代替ls brew install lsd # lsof brew install lsof # nginx brew install nginx # https://github.com/tmux/tmux brew install tmux # tree brew install tree # 文件搜索 https://github.com/ggreer/the_silver_searcher # ag -g filename ./ brew install the_silver_searcher # https://github.com/junegunn/fzf 文件查询 # 可以用来查找任何 列表 内容，文件、Git 分支、进程等 预览 # fzf、ctrl+T：显示下拉列表； ctrl+R：显示历史命令； ctrl+g：回到首行；cd \\ \u0026lt;TAB\u0026gt;：模糊匹配 brew install fzf # 文件名搜索 https://github.com/sharkdp/fd brew install fd # markdown https://github.com/charmbracelet/glow brew install glow # https://github.com/skywind3000/z.lua # clone z.lua 到 ~/Data/Software/z.lua brew install trash #rm 废纸篓 Copied! 8、自定义函数 ～/Data/Config/my-functions目录下\ngetproxy\n1 2 3 echo \u0026#34;http=$http_proxy;https=$https_proxy;all=$all_proxy\u0026#34; echo \u0026#34;HTTP=$HTTP_PROXY;HTTPS=$HTTPS_PROXY;ALL=$ALL_PROXY\u0026#34; echo \u0026#34;gh=$(git config --get http.proxy);ghs=$(git config --get https.proxy);\u0026#34; Copied! setproxy\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if [ \u0026#34;$1\u0026#34; != \u0026#34;git\u0026#34; ] \u0026amp;\u0026amp; [ \u0026#34;$1\u0026#34; != \u0026#34;ssh\u0026#34; ] \u0026amp;\u0026amp; [ \u0026#34;$1\u0026#34; != \u0026#34;all\u0026#34; ] ; then echo 输入不符合要求 fi if [ \u0026#34;$1\u0026#34; = \u0026#34;git\u0026#34; ] || [ \u0026#34;$1\u0026#34; = \u0026#34;all\u0026#34; ]; then echo \u0026#34;set git proxy ......\u0026#34; git config --global http.proxy socks5://127.0.0.1:7890 git config --global https.proxy socks5://127.0.0.1:7890 fi if [ \u0026#34;$1\u0026#34; = \u0026#34;ssh\u0026#34; ] || [ \u0026#34;$1\u0026#34; = \u0026#34;all\u0026#34; ]; then echo \u0026#34;set ssh proxy ......\u0026#34; export https_proxy=http://127.0.0.1:7890 export http_proxy=http://127.0.0.1:7890 export all_proxy=socks5://127.0.0.1:7890 export HTTPS_PROXY=http://127.0.0.1:7890 export HTTP_PROXY=http://127.0.0.1:7890 export ALL_PROXY=socks5://127.0.0.1:7890 fi Copied! unproxy\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if [ \u0026#34;$1\u0026#34; != \u0026#34;git\u0026#34; ] \u0026amp;\u0026amp; [ \u0026#34;$1\u0026#34; != \u0026#34;ssh\u0026#34; ] \u0026amp;\u0026amp; [ \u0026#34;$1\u0026#34; != \u0026#34;all\u0026#34; ] ; then echo 输入不符合要求 fi if [ \u0026#34;$1\u0026#34; = \u0026#34;git\u0026#34; ] || [ \u0026#34;$1\u0026#34; = \u0026#34;all\u0026#34; ]; then echo \u0026#34;unset git proxy ......\u0026#34; git config --global --unset http.proxy socks5://127.0.0.1:7890 git config --global --unset https.proxy socks5://127.0.0.1:7890 fi if [ \u0026#34;$1\u0026#34; = \u0026#34;ssh\u0026#34; ] || [ \u0026#34;$1\u0026#34; = \u0026#34;all\u0026#34; ]; then echo \u0026#34;unset ssh proxy ......\u0026#34; unset https_proxy unset http_proxy unset all_proxy unset HTTPS_PROXY unset HTTP_PROXY unset ALL_PROXY fi Copied! ","date":"2023-05-03T03:49:51+08:00","permalink":"https://qh.1357810.xyz/articles/linux/ohmyzsh/","title":"ohmyzsh使用教程"},{"content":" 1 2 ranger --copy-config=all export RANGER_LOAD_DEFAULT_RC FALSE Copied! rc.conf is used for setting various options and binding keys to functions. 设置和快捷键绑定 rifle.conf decides which program to use for opening a file. 文件打开方式 scope.sh is a shell script used to generate previews for various file types. 文件预览方式 commands.py contains various functions\u0026rsquo; implementation, written in Python, used to modify ranger's behavior, and implement your own Custom Commands . 自定义命令 默认前置键 g for navigation and tabs 导航和标签 r for :open_with command 打开文件 y for yank(copy) 复制 d for cut/delete 剪切删除 p for paste\t粘贴 o for sort\t排序 . for filter_stack z for changing settings\t变化 u for \u0026ldquo;undo\u0026rdquo;\t撤销 M for linemode\t+, -, = for setting access rights to files\n常用命令 command desc Ctrl+n 或者 g n 新建标签 Tab 切换标签 g c 或者 ctrl+w 关闭标签 zi zp 打开预览 alt+f 打开fzf alt+/ 打开fd查询文件 alt+p alt+n fd查询上一个、下一个 alt+r 在finder中打开 alt+h 打开历史 :z filename 打开z.lua dm 删除文件到废纸篓 ss console shell 注释了 危险S do shell sh cd ~ sc cd ~/Data/Config sd cd ~/Data 文件和文件夹操作 （复制粘贴） command desc yy 复制 pp 粘贴文件 #再按一下就会在复制，不过在文件名下加一个下划线 po 覆盖性粘贴文件 dd 剪切文件 yc 复制文本文件内容 cw 单个文件或者多文件重命名 dU 查看当前文件目录里面每个文件夹有多大 w 进入任务管理器，可以看到粘贴的进度条，还可以调整任务的优先级 dd 在任务管理器下按下dd取消任务 zh 显示隐藏文件 :mkcd 或者 sM 创建文件夹并进入 sT 创建文件 dD 删除文件 修改为移入废纸篓 uD 还原最后删除的文件 dh 查看文件删除历史 f 修改为边输入边过滤 i 大窗口预览 tmux command desc ev 在tmux中打开文件，水平分屏 es 在tmux中打开文件，垂直分屏 ew 新窗口打开 efv 在tmux中打开文件夹，水平分屏 efs 在tmux中打开文件夹，垂直分屏 efw 新窗口打开 ","date":"2023-05-03T03:49:51+08:00","permalink":"https://qh.1357810.xyz/articles/linux/ranger/","title":"ranger使用教程"},{"content":" 一. tmux 简介 指令全 https://gist.github.com/ryerh/14b7c24dfd623ef8edc7 tmux是一款优秀的终端复用软件，它比Screen更加强大，至于如何强大，网上有大量的文章讨论了这点，本文不再重复。tmux之所以受人们喜爱，主要得益于以下三处功能：\n丝滑分屏（split），虽然iTem2也提供了横向和竖向分屏功能，但这种分屏功能非常拙劣，完全等同于屏幕新开一个窗口，新开的pane不会自动进入到当前目录，也没有记住当前登录状态。这意味着如果我ssh进入到远程服务器时，iTem2新开的pane中，我依然要重新走一遍ssh登录的老路（omg）。tmux就不会这样，tmux窗口中，新开的pane，默认进入到之前的路径，如果是ssh连接，登录状态也依旧保持着，如此一来，我就可以随意的增删pane，这种灵活性，好处不言而喻。 保护现场（attach），即使命令行的工作只进行到一半，关闭终端后还可以重新进入到操作现场，继续工作。对于ssh远程连接而言，即使网络不稳定也没有关系，掉线后重新连接，可以直奔现场，之前运行中的任务，依旧在跑，就好像从来没有离开过一样；特别是在远程服务器上运行耗时的任务，tmux可以帮你一直保持住会话。如此一来，你就可以随时随地放心地进行移动办公，只要你附近的计算机装有tmux（没有你也可以花几分钟装一个），你就能继续刚才的工作。 会话共享（适用于结对编程或远程教学），将 tmux 会话的地址分享给他人，这样他们就可以通过 SSH 接入该会话。如果你要给同事演示远程服务器的操作，他不必直勾勾地盯着你的屏幕，借助tmux，他完全可以进入到你的会话，然后静静地看着他桌面上你风骚的键盘走位，只要他愿意，甚至还可以录个屏。 开始之前，我们先了解下基本概念：\ntmux采用C/S模型构建，输入tmux命令就相当于开启了一个服务器，此时默认将新建一个会话，然后会话中默认新建一个窗口，窗口中默认新建一个面板。会话、窗口、面板之间的联系如下：\n一个tmux session（会话）可以包含多个window（窗口），窗口默认充满会话界面，因此这些窗口中可以运行相关性不大的任务。\n一个window又可以包含多个pane（面板），窗口下的面板，都处于同一界面下，这些面板适合运行相关性高的任务，以便同时观察到它们的运行情况。\nserver 服务器。输入tmux命令时就开启了一个服务器。 session 会话。一个服务器可以包含多个会话。 window 窗口。一个会话可以包含多个窗口。 pane 面板。一个窗口可以包含多个面板。 二. tmux 安装和配置 1 新建会话 新建一个tmux session非常简单，语法为tmux new -s session-name，也可以简写为tmux，为了方便管理，建议指定会话名称，如下。\n1 2 tmux # 新建一个无名称的会话 tmux new -s demo # 新建一个名称为demo的会话 Copied! 2 断开当前会话 会话中操作了一段时间，我希望断开会话同时下次还能接着用，怎么做？此时可以使用detach命令\n1 tmux detach # 断开当前会话，会话在后台运行 Copied! 也许你觉得这个太麻烦了，是的，tmux的会话中，我们已经可以使用tmux快捷键了。使用快捷键组合Ctrl+b + d，三次按键就可以断开当前会话。\n3 进入之前的会话 断开会话后，想要接着上次留下的现场继续工作，就要使用到tmux的attach命令了，语法为tmux attach-session -t session-name，可简写为tmux a -t session-name 或 tmux a。通常我们使用如下两种方式之一即可：\n1 2 tmux a # 默认进入第一个会话 tmux a -t demo # 进入到名称为demo的会话 Copied! 4 关闭会话 会话的使命完成后，一定是要关闭的。我们可以使用tmux的kill命令，kill命令有kill-pane、kill-server、kill-session 和 kill-window共四种，其中kill-session的语法为tmux kill-session -t session-name。如下：\n1 2 tmux kill-session -t demo # 关闭demo会话 tmux kill-server # 关闭服务器，所有的会话都将关闭 Copied! 在会话内退出并删除session\n1 2 Ctrl+b :kill-session Copied! 5 查看会话 管理会话的第一步就是要查看所有的会话，我们可以使用如下命令：\n1 2 tmux list-session # 查看所有会话 tmux ls # 查看所有会话，提倡使用简写形式 Copied! 如果刚好处于会话中怎么办？别担心，我们可以使用对应的tmux快捷键Ctrl+b + s，此时tmux将打开一个会话列表，按上下键(⬆︎⬇︎)或者鼠标滚轮，可选中目标会话，按左右键（⬅︎➜）可收起或展开会话的窗口，选中目标会话或窗口后，按回车键即可完成切换。\n6 tmux快捷命令 关于快捷指令，首先要认识到的是：tmux的所有指令，都包含同一个前缀，默认为Ctrl+b，输入完前缀过后，控制台激活，命令按键才能生效。前面tmux会话相关的操作中，我们共用到了两个快捷键Ctrl+b + d、Ctrl+b + s，但这仅仅是冰山一角，欲窥tmux庞大的快捷键体系，请看下表。\nCtrl+b + s 进入信息列表，然后选中某个session或者window或者pane，然后按 x ，都可以删除\n表一：系统指令。 前缀 指令 描述 Ctrl+b ? 显示快捷键帮助文档 Ctrl+b d 断开当前会话 Ctrl+b D 选择要断开的会话 Ctrl+b Ctrl+z 挂起当前会话 Ctrl+b r 强制重载当前会话 Ctrl+b s 显示会话列表用于选择并切换 Ctrl+b : 进入命令行模式，此时可直接输入ls等命令 Ctrl+b [ 进入复制模式，按q退出 Ctrl+b ] 粘贴复制模式中复制的文本 Ctrl+b ~ 列出提示信息缓存 表二：窗口（window）指令。 前缀 指令 描述 Ctrl+b c 新建窗口 Ctrl+b \u0026amp; 关闭当前窗口（关闭前需输入y or n确认） Ctrl+b 0~9 切换到指定窗口 Ctrl+b p 切换到上一窗口 Ctrl+b n 切换到下一窗口 Ctrl+b w 打开窗口列表，用于且切换窗口 Ctrl+b , 重命名当前窗口 Ctrl+b . 修改当前窗口编号（适用于窗口重新排序） Ctrl+b f 快速定位到窗口（输入关键字匹配窗口名称） Ctrl+b z 最大化窗口 表三：面板（pane）指令。 前缀 指令 描述 Ctrl+b \u0026quot; 当前面板上下一分为二，下侧新建面板 Ctrl+b % 当前面板左右一分为二，右侧新建面板 Ctrl+b x 关闭当前面板（关闭前需输入y or n确认） Ctrl+b z 最大化当前面板，再重复一次按键后恢复正常（v1.8版本新增） Ctrl+b ! 将当前面板移动到新的窗口打开（原窗口中存在两个及以上面板有效） Ctrl+b ; 切换到最后一次使用的面板 Ctrl+b q 显示面板编号，在编号消失前输入对应的数字可切换到相应的面板 Ctrl+b { 向前置换当前面板 Ctrl+b } 向后置换当前面板 Ctrl+b Ctrl+o 顺时针旋转当前窗口中的所有面板 Ctrl+b 方向键 移动光标切换面板 Ctrl+b o 选择下一面板 Ctrl+b 空格键 在自带的面板布局中循环切换 Ctrl+b Alt+方向键 以5个单元格为单位调整当前面板边缘 Ctrl+b Ctrl+方向键 以1个单元格为单位调整当前面板边缘（Mac下被系统快捷键覆盖） Ctrl+b t 显示时钟 Ctrl+b x 关闭当前面板 Ctrl+b E 等分面板 tmux的丝滑分屏功能正是得益于以上系统、窗口、面板的快捷指令，只要你愿意，你就可以解除任意的快捷指令，然后绑上你喜欢的指令，当然这就涉及到它的可配置性了\n7 tmux滚屏 使用快捷键进入 copy-mode 模式，就可以利用方向键上下移动光标查看了，但是如果历史信息太多，一行一行未免太不爽了，该如何操作呢？\n1.先打开配置文件：\n1 vim ~/.tmux.conf Copied! 2.将下面设置输入：\n1 setw -g mode-keys vi Copied! 3.重新载入配置文件：\n1 tmux source-file ~/.tmux.conf Copied! 4.使用快捷键进入 copy-mode 模式，然后就可以像 vi 中一样操作了。\n比如：Ctrl-u 向上滚半屏， Ctrl-d 向下滚半屏。\n根据某关键字搜索：\n使用快捷键进入 copy-mode 模式，然后按 / ，就可以输入关键字了，回车查找。\n四. 恢复用户空间 1 复制模式 tmux中操作文本，自然离不开复制模式，通常使用复制模式的步骤如下：\n输入 ``+[` 进入复制模式 按下 空格键 开始复制，移动光标选择复制区域 按下 回车键 复制选中文本并退出复制模式 按下 ``+]` 粘贴文本 查看复制模式默认的快捷键风格：\n1 tmux show-window-options -g mode-keys # mode-keys emacs Copied! 默认情况下，快捷键为emacs风格。\n为了让复制模式更加方便，我们可以将快捷键设置为熟悉的vi风格，如下：\n1 setw -g mode-keys vi # 开启vi风格后，支持vi的C-d、C-u、hjkl等快捷键 Copied! 除了快捷键外，复制模式的启用、选择、复制、粘贴等按键也可以向vi风格靠拢。\n1 2 3 4 bind Escape copy-mode # 绑定esc键为进入复制模式 bind -t vi-copy v begin-selection # 绑定v键为开始选择文本 bind -t vi-copy y copy-selection # 绑定y键为复制选中文本 bind p pasteb # 绑定p键为粘贴文本（p键默认用于进入上一个窗口，不建议覆盖） Copied! 以上，绑定 v、y两键的设置只在tmux v2.4版本以下才有效，对于v2.4及以上的版本，绑定快捷键需要使用 -T 选项，发送指令需要使用 -X 选项，请参考如下设置：\n1 2 bind -T copy-mode-vi v send-keys -X begin-selection bind -T copy-mode-vi y send-keys -X copy-selection-and-cancel Copied! 2 buffer缓存 tmux复制操作的内容默认会存进buffer里，buffer是一个粘贴缓存区，新的缓存总是位于栈顶，它的操作命令如下：\n1 2 3 4 5 6 7 8 tmux list-buffers # 展示所有的 buffers tmux show-buffer [-b buffer-name] # 显示指定的 buffer 内容 tmux choose-buffer # 进入 buffer 选择页面(支持jk上下移动选择，回车选中并粘贴 buffer 内容到面板上) tmux set-buffer # 设置buffer内容 tmux load-buffer [-b buffer-name] file-path # 从文件中加载文本到buffer缓存 tmux save-buffer [-a] [-b buffer-name] path # 保存tmux的buffer缓存到本地 tmux paste-buffer # 粘贴buffer内容到会话中 tmux delete-buffer [-b buffer-name] # 删除指定名称的buffer Copied! 以上buffer操作在不指定buffer-name时，默认处理是栈顶的buffer缓存。\n在tmux会话的命令行输入时，可以省略上述tmux前缀，其中list-buffers的操作如下所示：\n·\nchoose-buffer的操作如下所示：\n默认情况下，buffers内容是独立于系统粘贴板的，它存在于tmux进程中，且可以在会话间共享。\n3 使用系统粘贴板 存在于tmux进程中的buffer缓存，虽然可以在会话间共享，但不能直接与系统粘贴板共享，不免有些遗憾。幸运的是，现在我们有成熟的方案来实现这个功能。\n在Linux上使用粘贴板\n通常，Linux中可以使用xclip工具来接入系统粘贴板。\n首先，需要安装xclip。\n1 sudo apt-get install xclip Copied! 然后，.tmux.conf的配置如下。\n1 2 3 4 # buffer缓存复制到Linux系统粘贴板 bind C-c run \u0026#34; tmux save-buffer - | xclip -i -sel clipboard\u0026#34; # Linux系统粘贴板内容复制到会话 bind C-v run \u0026#34; tmux set-buffer \\\u0026#34;$(xclip -o -sel clipboard)\\\u0026#34;; tmux paste-buffer\u0026#34; Copied! 按下prefix + Ctrl + c 键，buffer缓存的内容将通过xlip程序复制到粘贴板，按下prefix + Ctrl + v键，tmux将通过xclip访问粘贴板，然后由set-buffer命令设置给buffer缓存，最后由paste-buffer粘贴到tmux会话中。\n在Mac上使用粘贴板\n我们都知道，Mac自带 pbcopy 和 pbpaste命令，分别用于复制和粘贴，但在tmux命令中它们却不能正常运行。这里我将详细介绍下原因：\nMac的粘贴板服务是在引导命名空间注册的。命名空间存在层次之分，更高级别的命名空间拥有访问低级别命名空间（如root引导命名空间）的权限，反之却不行。流程创建的属于Mac登录会话的一部分，它会被自动包含在用户级的引导命名空间中，因此只有用户级的命名空间才能访问粘贴板服务。tmux使用守护进程(3)库函数创建其服务器进程，在Mac OS X 10.5中，苹果改变了守护进程(3)的策略，将生成的过程从最初的引导命名空间移到了根引导命名空间。而根引导命名空间访问权限较低，这意味着tmux服务器，和它的子进程，一同失去了原引导命名空间的访问权限（即无权限访问粘贴板服务）。\n如此，我们可以使用一个小小的包装程序来重新连接到合适的命名空间，然后执行访问用户级命名空间的粘贴板服务，这个包装程序就是reattach-to-user-namespace。\n那么，Mac下.tmux.conf的配置如下：\n1 2 3 4 # buffer缓存复制到Mac系统粘贴板 bind C-c run \u0026#34;tmux save-buffer - | reattach-to-user-namespace pbcopy\u0026#34; # Mac系统粘贴板内容复制到会话 bind C-v run \u0026#34;reattach-to-user-namespace pbpaste | tmux load-buffer - \\; paste-buffer -d\u0026#34; Copied! reattach-to-user-namespace 作为包装程序来访问Mac粘贴板，按下prefix + Ctrl + c 键，buffer缓存的内容将复制到粘贴板，按下prefix + Ctrl + v键，粘贴板的内容将通过 load-buffer 加载，然后由 paste-buffer 粘贴到tmux会话中。\n为了在复制模式中使用Mac系统的粘贴板，可做如下配置：\n1 2 3 4 # 绑定y键为复制选中文本到Mac系统粘贴板 bind-key -T copy-mode-vi \u0026#39;y\u0026#39; send-keys -X copy-pipe-and-cancel \u0026#39;reattach-to-user-namespace pbcopy\u0026#39; # 鼠标拖动选中文本，并复制到Mac系统粘贴板 bind-key -T copy-mode-vi MouseDragEnd1Pane send -X copy-pipe-and-cancel \u0026#34;pbcopy\u0026#34; Copied! 完成以上配置后记得重启tmux服务器。至此，复制模式中，按y键将保存选中的文本到Mac系统粘贴板，随后按Command + v键便可粘贴。\n六.保存Tmux会话 1 结对编程 tmux多会话连接实时同步的功能，使得结对编程成为了可能，这也是开发者最喜欢的功能之一。现在就差一步了，就是借助tmate把tmux会话分享出去。\ntmate是tmux的管理工具，它可以轻松的创建tmux会话，并且自动生成ssh链接。\n安装tmate\n1 brew install tmate Copied! 使用tmate新建一个tmux会话\n1 tmate Copied! 此时屏幕下方会显示ssh url，如下所示：\n查看tmate生成的ssh链接\n1 tmate show-messages Copied! 生成的ssh url如下所示，其中一个为只读，另一个可编辑。\n2 共享账号\u0026amp;组会话 使用tmate远程共享tmux会话，受制于多方的网络质量，必然会存在些许延迟。如果共享会话的多方拥有同一个远程服务器的账号，那么我们可以使用组会话解决这个问题。\n先在远程服务器上新建一个公共会话，命名为groupSession。\n1 tmux new -s groupSession Copied! 其他用户不去直接连接这个会话，而是通过创建一个新的会话来加入上面的公共会话groupSession。\n1 tmux new -t groupSession -s otherSession Copied! 此时两个用户都可以在同一个会话里操作，就会好像第二个用户连接到了groupSession的会话一样。此时两个用户都可以创建新建的窗口，新窗口的内容依然会实时同步，但是其中一个用户切换到其它窗口，对另外一个用户没有任何影响，因此在这个共享的组会话中，用户各自的操作可以通过新建窗口来执行。即使第二个用户关闭otherSession会话，共享会话groupSession依然存在。\n组会话在共享的同时，又保留了相对的独立，非常适合结对编程场景，它是结对编程最简单的方式，如果账号不能共享，我们就要使用下面的方案了。\n3 独立账号\u0026amp;Socket共享会话 开始之前我们需要确保用户对远程服务器上同一个目录拥有相同的读写权限，假设这个目录为/var/tmux/。\n使用new-session（简写new）创建会话时，使用的是默认的socket位置，默认socket无法操作，所以我们需要创建一个指定socket文件的会话。\n1 tmux -S /var/tmux/sharefile Copied! 另一个用户进入时，需要指定socket文件加入会话。\n1 tmux -S /var/tmux/sharefile attach Copied! 这样，两个不同的用户就可以共享同一个会话了。\n通常情况下，不同的用户使用不同的配置文件来创建会话，但是，使用指定socket文件创建的tmux会话，会话加载的是第一个创建会话的用户的~/.tmux.conf配置文件，随后加入会话的其他用户，依然使用同一份配置文件。\n八.Tmux优化 tmux作为终端复用软件，支持纯命令行操作也是其一大亮点。你既可以启用可视化界面创建会话，也可以运行脚本生成会话，对于tmux依赖者而言，编写几个tmux脚本批量维护会话列表，快速重启、切换、甚至分享部分会话都是非常方便的。可能会有人说为什么不用Tmux Resurrect呢？是的，Tmux Resurrect很好，一键恢复也很诱人，但是对于一个维护大量tmux会话的用户而言，一键恢复可能不见得好，分批次恢复可能是他（她）更想要的，脚本化的tmux就很好地满足了这点。\n脚本中创建tmux会话时，由于不需要开启可视化界面，需要输入-d参数指定会话后台运行，如下。\n1 tmux new -s init -d # 后台创建一个名称为init的会话 Copied! 新建的会话，建议重命令会话的窗口名称，以便后续维护。\n1 2 # 重命名init会话的第一个窗口名称为service tmux rename-window -t \u0026#34;init:1\u0026#34; service Copied! 现在，可以在刚才的窗口中输入指令了。\n1 2 # 切换到指定目录并运行python服务 tmux send -t \u0026#34;init:service\u0026#34; \u0026#34;cd ~/workspace/language/python/;python2.7 server.py\u0026#34; Enter Copied! 一个面板占用一个窗口可能太浪费了，我们来分个屏吧。\n1 2 3 4 # 默认上下分屏 tmux split-window -t \u0026#34;init:service\u0026#34; # 切换到指定目录并运行node服务 tmux send -t \u0026#34;init:service\u0026#34; \u0026#39;cd ~/data/louiszhai/node-webserver/;npm start\u0026#39; Enter Copied! 现在一个窗口拥有上下两个面板，是时候创建一个新的窗口来运行更多的程序了。\n1 2 3 4 # 新建一个名称为tool的窗口 tmux neww -a -n tool -t init # neww等同于new window # 运行weinre调试工具 tmux send -t \u0026#34;init:tool\u0026#34; \u0026#34;weinre --httpPort 8881 --boundHost -all-\u0026#34; Enter Copied! 另外新建窗口运行程序，有更方便的方式，比如使用 processes 选项。\n1 2 tmux neww-n processes ls # 新建窗口并执行命令，命令执行结束后窗口将关闭 tmux neww-n processes top # 由于top命令持续在前台运行，因此窗口将保留，直到top命令退出 Copied! 新的窗口，我们尝试下水平分屏。\n1 2 3 4 # 水平分屏 tmux split-window -h -t \u0026#34;init:tool\u0026#34; # 切换到指定目录并启用aria2 web管理后台 tmux send -t \u0026#34;init:tool\u0026#34; \u0026#34;cd ~/data/tools/AriaNg/dist/;python -m SimpleHTTPServer 10108\u0026#34; Enter Copied! 类似的脚本，我们可以编写一打，这样快速重启、切换、甚至分享会话都将更加便捷。\n十. 开机自动启用Web服务器 ","date":"2023-05-03T03:49:51+08:00","permalink":"https://qh.1357810.xyz/articles/linux/tmux/","title":"Tmux使用教程"},{"content":" 1. 关于Vim vim是我最喜欢的编辑器，也是Linux 下第二强大的编辑器。 虽然emacs是公认的世界第一，我认为使用emacs并没有使用vi进行编辑来得高效。 如果是初学vi，运行一下vimtutor是个聪明的决定。 （如果你的系统环境不是中文，而你想使用中文的vimtutor，就运行vimtutor zh）\n1.1 Vim的几种模式 正常模式：可以使用快捷键命令，或按:输入命令行。 插入模式：可以输入文本，在正常模式下，按i、a、o等都可以进入插入模式。 可视模式：正常模式下按v可以进入可视模式， 在可视模式下，移动光标可以选择文本。按V进入可视行模式， 总是整行整行的选中。ctrl+v进入可视块模式。 替换模式：正常模式下，按R进入。 2. 启动Vim vim -c cmd file: 在打开文件前，先执行指定的命令； vim -r file: 恢复上次异常退出的文件； vim -R file: 以只读的方式打开文件，但可以强制保存； vim -M file: 以只读的方式打开文件，不可以强制保存； vim -y num file: 将编辑窗口的大小设为num行； vim + file: 从文件的末尾开始； vim +num file: 从第num行开始； vim +/string file: 打开file，并将光标停留在第一个找到的string上。 vim –remote file: 用已有的vim进程打开指定的文件。 如果你不想启用多个vim会话，这个很有用。但要注意， 如果你用vim，会寻找名叫VIM的服务器；如果你已经有一个gvim在运行了， 你可以用gvim –remote file在已有的gvim中打开文件。 3. 文档操作 :e file –关闭当前编辑的文件，并开启新的文件。 如果对当前文件的修改未保存，vi会警告。 :e! file –放弃对当前文件的修改，编辑新的文件。 :e+file – 开始新的文件，并从文件尾开始编辑。 :e+n file – 开始新的文件，并从第n行开始编辑。 :enew –编译一个未命名的新文档。(CTRL-W n) :e – 重新加载当前文档。 :e! – 重新加载当前文档，并丢弃已做的改动。 :e#或ctrl+^ – 回到刚才编辑的文件，很实用。 :f或ctrl+g – 显示文档名，是否修改，和光标位置。 :f filename – 改变编辑的文件名，这时再保存相当于另存为。 gf – 打开以光标所在字符串为文件名的文件。 :w – 保存修改。 :n1,n2w filename – 选择性保存从某n1行到另n2行的内容。 :wq – 保存并退出。 ZZ – 保存并退出。 :x – 保存并退出。 :q[uit] ——退出当前窗口。(CTRL-W q或CTRL-W CTRL-Q) :saveas newfilename – 另存为 :browse e – 会打开一个文件浏览器让你选择要编辑的文件。 如果是终端中，则会打开netrw的文件浏览窗口； 如果是gvim，则会打开一个图形界面的浏览窗口。 实际上:browse后可以跟任何编辑文档的命令，如sp等。 用browse打开的起始目录可以由browsedir来设置： :set browsedir=last – 用上次访问过的目录（默认）； :set browsedir=buffer – 用当前文件所在目录； :set browsedir=current – 用当前工作目录； :Sex – 水平分割一个窗口，浏览文件系统； :Vex – 垂直分割一个窗口，浏览文件系统； 4. 光标的移动 4.1 基本移动 以下移动都是在normal模式下。\nh或退格: 左移一个字符； l或空格: 右移一个字符； j: 下移一行； k: 上移一行； gj: 移动到一段内的下一行； gk: 移动到一段内的上一行； +或Enter: 把光标移至下一行第一个非空白字符。 -: 把光标移至上一行第一个非空白字符。 w: 前移一个单词，光标停在下一个单词开头； W: 移动下一个单词开头，但忽略一些标点； e: 前移一个单词，光标停在下一个单词末尾； E: 移动到下一个单词末尾，如果词尾有标点，则移动到标点； b: 后移一个单词，光标停在上一个单词开头； B: 移动到上一个单词开头，忽略一些标点； ge: 后移一个单词，光标停在上一个单词末尾； gE: 同 ge ，不过‘单词’包含单词相邻的标点。 (: 前移1句。 ): 后移1句。 {: 前移1段。 }: 后移1段。 fc: 把光标移到同一行的下一个c字符处 Fc: 把光标移到同一行的上一个c字符处 tc: 把光标移到同一行的下一个c字符前 Tc: 把光标移到同一行的上一个c字符后 ;: 配合f \u0026amp; t使用，重复一次 ,: 配合f \u0026amp; t使用，反向重复一次 上面的操作都可以配合n使用，比如在正常模式(下面会讲到)下输入3h， 则光标向左移动3个字符。\n0: 移动到行首。 g0: 移到光标所在屏幕行行首。 ^: 移动到本行第一个非空白字符。 g^: 同 ^ ，但是移动到当前屏幕行第一个非空字符处。 : 移动光标所在屏幕行行尾。 n|: 把光标移到递n列上。 nG: 到文件第n行。 :n 移动到第n行。 :$ 移动到最后一行。 H: 把光标移到屏幕最顶端一行。 M: 把光标移到屏幕中间一行。 L: 把光标移到屏幕最底端一行。 gg: 到文件头部。 G: 到文件尾部。 4.2 翻屏 ctrl+f: 下翻一屏。 ctrl+b: 上翻一屏。 ctrl+d: 下翻半屏。 ctrl+u: 上翻半屏。 ctrl+e: 向下滚动一行。 ctrl+y: 向上滚动一行。 n%: 到文件n%的位置。 zz: 将当前行移动到屏幕中央。 zt: 将当前行移动到屏幕顶端。 zb: 将当前行移动到屏幕底端。 4.3 标记 使用标记可以快速移动。到达标记后，可以用Ctrl+o返回原来的位置。 Ctrl+o和Ctrl+i 很像浏览器上的 后退 和 前进 。\nm{a-z}: 标记光标所在位置，局部标记，只用于当前文件。 m{A-Z}: 标记光标所在位置，全局标记。标记之后，退出Vim， 重新启动，标记仍然有效。 `{a-z}: 移动到标记位置。 ‘{a-z}: 移动到标记行的行首。 `{0-9}：回到上[2-10]次关闭vim时最后离开的位置。 “: 移动到上次编辑的位置。”也可以，不过“精确到列，而”精确到行 。如果想跳转到更老的位置，可以按C-o，跳转到更新的位置用C-i。 `”: 移动到上次离开的地方。 `.: 移动到最后改动的地方。 :marks 显示所有标记。 :delmarks a b – 删除标记a和b。 :delmarks a-c – 删除标记a、b和c。 :delmarks a c-f – 删除标记a、c、d、e、f。 :delmarks! – 删除当前缓冲区的所有标记。 :help mark-motions 查看更多关于mark的知识。 5. 插入文本 5.1 基本插入 i: 在光标前插入；一个小技巧：按8，再按i，进入插入模式，输入=， 按esc进入命令模式，就会出现8个=。 这在插入分割线时非常有用，如30i+就插入了36个+组成的分割线。 I: 在当前行第一个非空字符前插入； gI: 在当前行第一列插入； a: 在光标后插入； A: 在当前行最后插入； o: 在下面新建一行插入； O: 在上面新建一行插入； :r filename在当前位置插入另一个文件的内容。 :[n]r filename在第n行插入另一个文件的内容。 :r !date 在光标处插入当前日期与时间。同理，:r !command可以将其它shell命令的输出插入当前文档。 5.2 改写插入 c[n]w: 改写光标后1(n)个词。 c[n]l: 改写光标后n个字母。 c[n]h: 改写光标前n个字母。 [n]cc: 修改当前[n]行。 [n]s: 以输入的文本替代光标之后1(n)个字符，相当于c[n]l。 [n]S: 删除指定数目的行，并以所输入文本代替之。 注意，类似cnw,dnw,ynw的形式同样可以写为ncw,ndw,nyw。\n6. 剪切复制和寄存器 6.1 剪切和复制、粘贴 [n]x: 剪切光标右边n个字符，相当于d[n]l。 [n]X: 剪切光标左边n个字符，相当于d[n]h。 y: 复制在可视模式下选中的文本。 yy or Y: 复制整行文本。 y[n]w: 复制一(n)个词。 y[n]l: 复制光标右边1(n)个字符。 y[n]h: 复制光标左边1(n)个字符。 yor D: 删除（剪切）当前位置到行尾的内容。 d[n]w: 删除（剪切）1(n)个单词 d[n]l: 删除（剪切）光标右边1(n)个字符。 d[n]h: 删除（剪切）光标左边1(n)个字符。 d0: 删除（剪切）当前位置到行首的内容 [n] dd: 删除（剪切）1(n)行。 :m,nd 剪切m行到n行的内容。 d1G或dgg: 剪切光标以上的所有行。 dG: 剪切光标以下的所有行。 daw和das：剪切一个词和剪切一个句子，即使光标不在词首和句首也没关系。 d/f：这是一个比较高级的组合命令，它将删除当前位置 到下一个f之间的内容。 p: 在光标之后粘贴。 P: 在光标之前粘贴。 6.2 文本对象 aw：一个词 as：一句。 ap：一段。 ab：一块（包含在圆括号中的）。 y, d, c, v都可以跟文本对象。\n6.3 寄存器 a-z：都可以用作寄存器名。”ayy把当前行的内容放入a寄存器。 A-Z：用大写字母索引寄存器，可以在寄存器中追加内容。 如”Ayy把当前行的内容追加到a寄存器中。 :reg 显示所有寄存器的内容。 “”：不加寄存器索引时，默认使用的寄存器。 “*：当前选择缓冲区，”*yy把当前行的内容放入当前选择缓冲区。 “+：系统剪贴板。”+yy把当前行的内容放入系统剪贴板。 7. 查找与替换 7.1 查找 /something: 在后面的文本中查找something。 ?something: 在前面的文本中查找something。 /pattern/+number: 将光标停在包含pattern的行后面第number行上。 /pattern/-number: 将光标停在包含pattern的行前面第number行上。 n: 向后查找下一个。 N: 向前查找下一个。 可以用grep或vimgrep查找一个模式都在哪些地方出现过，\n其中:grep是调用外部的grep程序，而:vimgrep是vim自己的查找算法。\n用法为： :vim[grep]/pattern/[g] [j] files\ng的含义是如果一个模式在一行中多次出现，则这一行也在结果中多次出现。\nj的含义是grep结束后，结果停在第j项，默认是停在第一项。\nvimgrep前面可以加数字限定搜索结果的上限，如\n:1vim/pattern/ % 只查找那个模式在本文件中的第一个出现。\n其实vimgrep在读纯文本电子书时特别有用，可以生成导航的目录。\n比如电子书中每一节的标题形式为：n. xxxx。你就可以这样：\n:vim/^d{1,}./ %\n然后用:cw或:copen查看结果，可以用C-w H把quickfix窗口移到左侧，\n就更像个目录了。\n7.2 替换 :s/old/new - 用new替换当前行第一个old。 :s/old/new/g - 用new替换当前行所有的old。 :n1,n2s/old/new/g - 用new替换文件n1行到n2行所有的old。 :%s/old/new/g - 用new替换文件中所有的old。 :%s/^/xxx/g - 在每一行的行首插入xxx，^表示行首。 :%s/表示行尾。 所有替换命令末尾加上c，每个替换都将需要用户确认。 如：%s/old/new/gc，加上i则忽略大小写(ignore)。 还有一种比替换更灵活的方式，它是匹配到某个模式后执行某种命令，\n语法为 :[range]g/pattern/command\n例如 :%g/^ xyz/normal dd。\n表示对于以一个空格和xyz开头的行执行normal模式下的dd命令。\n关于range的规定为：\n如果不指定range，则表示当前行。 m,n: 从m行到n行。 0: 最开始一行（可能是这样）。 $: 最后一行 .: 当前行 %: 所有行 7.3 正则表达式 高级的查找替换就要用到正则表达式。\n\\d: 表示十进制数（我猜的） \\s: 表示空格 \\S: 非空字符 \\a: 英文字母 |: 表示 或 .: 表示. {m,n}: 表示m到n个字符。这要和 \\s与\\a等连用，如 \\a{m,n} 表示m 到n个英文字母。 {m,}: 表示m到无限多个字符。 **: 当前目录下的所有子目录。 :help pattern得到更多帮助。\n9. 编辑多个文件 9.1 一次编辑多个文件 我们可以一次打开多个文件，如\n1 vi a.txt b.txt c.txt Copied! 使用:next(:n)编辑下一个文件。 :2n 编辑下2个文件。 使用:previous或:N编辑上一个文件。 使用:wnext，保存当前文件，并编辑下一个文件。 使用:wprevious，保存当前文件，并编辑上一个文件。 使用:args 显示文件列表。 :n filenames或:args filenames 指定新的文件列表。 vi -o filenames 在水平分割的多个窗口中编辑多个文件。 vi -O filenames 在垂直分割的多个窗口中编辑多个文件。 9.2 多标签编辑 vim -p files: 打开多个文件，每个文件占用一个标签页。 :tabe, tabnew – 如果加文件名，就在新的标签中打开这个文件， 否则打开一个空缓冲区。 ^w gf – 在新的标签页里打开光标下路径指定的文件。 :tabn – 切换到下一个标签。Control + PageDown，也可以。 :tabp – 切换到上一个标签。Control + PageUp，也可以。 [n] gt – 切换到下一个标签。如果前面加了 n ， 就切换到第n个标签。第一个标签的序号就是1。 :tab split – 将当前缓冲区的内容在新页签中打开。 :tabc[lose] – 关闭当前的标签页。 :tabo[nly] – 关闭其它的标签页。 :tabs – 列出所有的标签页和它们包含的窗口。 :tabm[ove] [N] – 移动标签页，移动到第N个标签页之后。 如 tabm 0 当前标签页，就会变成第一个标签页。 9.3 缓冲区 :buffers或:ls或:files 显示缓冲区列表。 ctrl+^：在最近两个缓冲区间切换。 :bn – 下一个缓冲区。 :bp – 上一个缓冲区。 :bl – 最后一个缓冲区。 :b[n]或:[n]b – 切换到第n个缓冲区。 :nbw(ipeout) – 彻底删除第n个缓冲区。 :nbd(elete) – 删除第n个缓冲区，并未真正删除，还在unlisted列表中。 :ba[ll] – 把所有的缓冲区在当前页中打开，每个缓冲区占一个窗口。 10. 分屏编辑 vim -o file1 file2:水平分割窗口，同时打开file1和file2 vim -O file1 file2:垂直分割窗口，同时打开file1和file2 10.1 水平分割 :split(:sp) – 把当前窗水平分割成两个窗口。(CTRL-W s 或 CTRL-W CTRL-S) 注意如果在终端下，CTRL-S可能会冻结终端，请按CTRL-Q继续。 :split filename – 水平分割窗口，并在新窗口中显示另一个文件。 :nsplit(:nsp) – 水平分割出一个n行高的窗口。 :[N]new – 水平分割出一个N行高的窗口，并编辑一个新文件。 (CTRL-W n或 CTRL-W CTRL-N) ctrl+w f –水平分割出一个窗口，并在新窗口打开名称为光标所在词的文件 。 C-w C-^ – 水平分割一个窗口，打开刚才编辑的文件。 10.2 垂直分割 :vsplit(:vsp) – 把当前窗口分割成水平分布的两个窗口。 (CTRL-W v或CTRL CTRL-V) :[N]vne[w] – 垂直分割出一个新窗口。 :vertical 水平分割的命令： 相应的垂直分割。 10.3 关闭子窗口 :qall – 关闭所有窗口，退出vim。 :wall – 保存所有修改过的窗口。 :only – 只保留当前窗口，关闭其它窗口。(CTRL-W o) :close – 关闭当前窗口，CTRL-W c能实现同样的功能。 (象 :q :x同样工作 ) 10.4 调整窗口大小 ctrl+w + –当前窗口增高一行。也可以用n增高n行。 ctrl+w - –当前窗口减小一行。也可以用n减小n行。 ctrl+w _ –当前窗口扩展到尽可能的大。也可以用n设定行数。 :resize n – 当前窗口n行高。 ctrl+w = – 所有窗口同样高度。 n ctrl+w _ – 当前窗口的高度设定为n行。 ctrl+w \u0026lt; –当前窗口减少一列。也可以用n减少n列。 ctrl+w \u0026gt; –当前窗口增宽一列。也可以用n增宽n列。 ctrl+w | –当前窗口尽可能的宽。也可以用n设定列数。 10.5 切换和移动窗口 如果支持鼠标，切换和调整子窗口的大小就简单了。\nctrl+w ctrl+w: 切换到下一个窗口。或者是ctrl+w w。 ctrl+w p: 切换到前一个窗口。 ctrl+w h(l,j,k):切换到左（右，下，上）的窗口。 ctrl+w t(b):切换到最上（下）面的窗口。 ctrl+w H(L,K,J): 将当前窗口移动到最左（右、上、下）面。 ctrl+w r：旋转窗口的位置。 ctrl+w T: 将当前的窗口移动到新的标签页上。 11. 快速编辑 11.1 改变大小写 ~: 反转光标所在字符的大小写。 可视模式下的U或u：把选中的文本变为大写或小写。 gu(U)接范围（如$，或G），可以把从光标当前位置到指定位置之间字母全部 转换成小写或大写。如ggguG，就是把开头到最后一行之间的字母全部变为小 写。再如gu5j，把当前行和下面四行全部变成小写。 11.2 替换（normal模式） r: 替换光标处的字符，同样支持汉字。 R: 进入替换模式，按esc回到正常模式。 11.3 撤消与重做（normal模式） [n] u: 取消一(n)个改动。 :undo 5 – 撤销5个改变。 :undolist – 你的撤销历史。 ctrl + r: 重做最后的改动。 U: 取消当前行中所有的改动。 :earlier 4m – 回到4分钟前 :later 55s – 前进55秒 11.4 宏 . –重复上一个编辑动作 qa：开始录制宏a（键盘操作记录） q：停止录制 @a：播放宏a 12. 编辑特殊文件 12.1 文件加解密 vim -x file: 开始编辑一个加密的文件。 :X – 为当前文件设置密码。 :set key= – 去除文件的密码。 这里是 滇狐总结的比较高级的vi技巧。\n12.2 文件的编码 :e ++enc=utf8 filename, 让vim用utf-8的编码打开这个文件。 :w ++enc=gbk，不管当前文件什么编码，把它转存成gbk编码。 :set fenc或:set fileencoding，查看当前文件的编码。 在vimrc中添加set fileencoding=ucs-bom,utf-8,cp936，vim会根据要打开的文件选择合适的编码。 注意：编码之间不要留空格。 cp936对应于gbk编码。 ucs-bom对应于windows下的文件格式。 让vim 正确处理文件格式和文件编码，有赖于 ~/.vimrc的正确配置 12.3 文件格式 大致有三种文件格式：unix, dos, mac. 三种格式的区别主要在于回车键的编码：dos 下是回车加换行，unix 下只有 换行符，mac 下只有回车符。\n:e ++ff=dos filename, 让vim用dos格式打开这个文件。 :w ++ff=mac filename, 以mac格式存储这个文件。 :set ff，显示当前文件的格式。 在vimrc中添加set fileformats=unix,dos,mac，让vim自动识别文件格式。 13. 编程辅助 13.1 一些按键 gd: 跳转到局部变量的定义处； gD: 跳转到全局变量的定义处，从当前文件开头开始搜索； g;: 上一个修改过的地方； g,: 下一个修改过的地方； [[: 跳转到上一个函数块开始，需要有单独一行的{。 ]]: 跳转到下一个函数块开始，需要有单独一行的{。 []: 跳转到上一个函数块结束，需要有单独一行的}。 ][: 跳转到下一个函数块结束，需要有单独一行的}。 [{: 跳转到当前块开始处； ]}: 跳转到当前块结束处； [/: 跳转到当前注释块开始处； ]/: 跳转到当前注释块结束处； %: 不仅能移动到匹配的(),{}或[]上，而且能在#if，#else， #endif之间跳跃。 下面的括号匹配对编程很实用的。\nci’, di’, yi’：修改、剪切或复制’之间的内容。 ca’, da’, ya’：修改、剪切或复制’之间的内容，包含’。 ci”, di”, yi”：修改、剪切或复制”之间的内容。 ca”, da”, ya”：修改、剪切或复制”之间的内容，包含”。 ci(, di(, yi(：修改、剪切或复制()之间的内容。 ca(, da(, ya(：修改、剪切或复制()之间的内容，包含()。 ci[, di[, yi[：修改、剪切或复制[]之间的内容。 ca[, da[, ya[：修改、剪切或复制[]之间的内容，包含[]。 ci{, di{, yi{：修改、剪切或复制{}之间的内容。 ca{, da{, ya{：修改、剪切或复制{}之间的内容，包含{}。 ci\u0026lt;, di\u0026lt;, yi\u0026lt;：修改、剪切或复制\u0026lt;\u0026gt;之间的内容。 ca\u0026lt;, da\u0026lt;, ya\u0026lt;：修改、剪切或复制\u0026lt;\u0026gt;之间的内容，包含\u0026lt;\u0026gt;。 13.2 ctags ctags -R: 生成tag文件，-R表示也为子目录中的文件生成tags :set tags=path/tags – 告诉ctags使用哪个tag文件 :tag xyz – 跳到xyz的定义处，或者将光标放在xyz上按C-]，返回用C-t :stag xyz – 用分割的窗口显示xyz的定义，或者C-w ]， 如果用C-w n ]，就会打开一个n行高的窗口 :ptag xyz – 在预览窗口中打开xyz的定义，热键是C-w }。 :pclose – 关闭预览窗口。热键是C-w z。 :pedit abc.h – 在预览窗口中编辑abc.h :psearch abc – 搜索当前文件和当前文件include的文件，显示包含abc的行。 有时一个tag可能有多个匹配，如函数重载，一个函数名就会有多个匹配。 这种情况会先跳转到第一个匹配处。\n:[n]tnext – 下一[n]个匹配。 :[n]tprev – 上一[n]个匹配。 :tfirst – 第一个匹配 :tlast – 最后一个匹配 :tselect tagname – 打开选择列表 tab键补齐\n:tag xyz – 补齐以xyz开头的tag名，继续按tab键，会显示其他的。 :tag /xyz – 会用名字中含有xyz的tag名补全。 13.3 cscope cscope -Rbq: 生成cscope.out文件 :cs add /path/to/cscope.out /your/work/dir :cs find c func – 查找func在哪些地方被调用 :cw – 打开quickfix窗口查看结果 13.4 gtags Gtags综合了ctags和cscope的功能。 使用Gtags之前，你需要安装GNU Gtags。 然后在工程目录运行 gtags 。\n:Gtags funcname 定位到 funcname 的定义处。 :Gtags -r funcname 查询 funcname被引用的地方。 :Gtags -s symbol 定位 symbol 出现的地方。 :Gtags -g string Goto string 出现的地方。 :Gtags -gi string 忽略大小写。 :Gtags -f filename 显示 filename 中的函数列表。 你可以用 :Gtags -f % 显示当前文件。 :Gtags -P pattern 显示路径中包含特定模式的文件。 如 :Gtags -P .h$ 显示所有头文件， :Gtags -P /vm/ 显示vm目录下的文件。 13.5 编译 vim提供了:make来编译程序，默认调用的是make， 如果你当前目录下有makefile，简单地:make即可。\n如果你没有make程序，你可以通过配置makeprg选项来更改make调用的程序。 如果你只有一个abc.Java 文件，你可以这样设置：\n1 set makeprg=javac\\ abc.java Copied! 然后:make即可。如果程序有错，可以通过quickfix窗口查看错误。 不过如果要正确定位错误，需要设置好errorformat，让vim识别错误信息。 如：\n1 :setl efm=%A%f:%l:\\ %m,%-Z%p^,%-C%.%# Copied! %f表示文件名，%l表示行号， %m表示错误信息，其它的还不能理解。 请参考 :help errorformat。\n13.6 快速修改窗口 其实是quickfix插件提供的功能， 对编译调试程序非常有用 :)\n:copen – 打开快速修改窗口。 :cclose – 关闭快速修改窗口。 快速修改窗口在make程序时非常有用，当make之后：\n:cl – 在快速修改窗口中列出错误。 :cn – 定位到下一个错误。 :cp – 定位到上一个错误。 :cr – 定位到第一个错误。 13.7 自动补全 C-x C-s – 拼写建议。 C-x C-v – 补全vim选项和命令。 C-x C-l – 整行补全。 C-x C-f – 自动补全文件路径。弹出菜单后，按C-f循环选择，当然也可以按 C-n和C-p。 C-x C-p 和C-x C-n – 用文档中出现过的单词补全当前的词。 直接按C-p和C-n也可以。 C-x C-o – 编程时可以补全关键字和函数名啊。 C-x C-i – 根据头文件内关键字补全。 C-x C-d – 补全宏定义。 C-x C-n – 按缓冲区中出现过的关键字补全。 直接按C-n或C-p即可。 当弹出补全菜单后：\nC-p 向前切换成员； C-n 向后切换成员； C-e 退出下拉菜单，并退回到原来录入的文字； C-y 退出下拉菜单，并接受当前选项。 13.8 多行缩进缩出 正常模式下，按两下\u0026gt;;光标所在行会缩进。 如果先按了n，再按两下\u0026gt;;，光标以下的n行会缩进。 对应的，按两下\u0026lt;;，光标所在行会缩出。 如果在编辑代码文件，可以用=进行调整。 在可视模式下，选择要调整的代码块，按=，代码会按书写规则缩排好。 或者n =，调整n行代码的缩排。 13.9 折叠 zf – 创建折叠的命令，可以在一个可视区域上使用该命令； zd – 删除当前行的折叠； zD – 删除当前行的折叠； zfap – 折叠光标所在的段； zo – 打开折叠的文本； zc – 收起折叠； za – 打开/关闭当前折叠； zr – 打开嵌套的折行； zm – 收起嵌套的折行； zR (zO) – 打开所有折行； zM (zC) – 收起所有折行； zj – 跳到下一个折叠处； zk – 跳到上一个折叠处； zi – enable/disable fold; 14. 命令行 normal模式下按:进入命令行模式\n14.1 命令行模式下的快捷键： 上下方向键：上一条或者下一条命令。如果已经输入了部分命令，则找上一 条或者下一条匹配的命令。 左右方向键：左/右移一个字符。 C-w： 向前删除一个单词。 C-h： 向前删除一个字符，等同于Backspace。 C-u： 从当前位置移动到命令行开头。 C-b： 移动到命令行开头。 C-e： 移动到命令行末尾。 Shift-Left： 左移一个单词。 Shift-Right： 右移一个单词。 @： 重复上一次的冒号命令。 q： 正常模式下，q然后按’:’，打开命令行历史缓冲区， 可以像编辑文件一样编辑命令。 q/和q? 可以打开查找历史记录。 14.2 执行外部命令 :! cmd 执行外部命令。 :!! 执行上一次的外部命令。 :sh 调用shell，用exit返回vim。 :r !cmd 将命令的返回结果插入文件当前位置。 :m,nw !cmd 将文件的m行到n行之间的内容做为命令输入执行命令。 15. 其它 15.1 工作目录 :pwd 显示vim的工作目录。 :cd path 改变vim的工作目录。 :set autochdir 可以让vim 根据编辑的文件自动切换工作目录。 15.2 一些快捷键（收集中） K: 打开光标所在词的manpage。 *: 向下搜索光标所在词。 g*: 同上，但部分符合即可。 #: 向上搜索光标所在词。 g#: 同上，但部分符合即可。 g C-g: 统计全文或统计部分的字数。 15.3 在线帮助 :h(elp)或F1 打开总的帮助。 :help user-manual 打开用户手册。 命令帮助的格式为：第一行指明怎么使用那个命令； 然后是缩进的一段解释这个命令的作用，然后是进一步的信息。 :helptags somepath 为somepath中的文档生成索引。 :helpgrep 可以搜索整个帮助文档，匹配的列表显示在quickfix窗口中。 Ctrl+] 跳转到tag主题，Ctrl+t 跳回。 :ver 显示版本信息。 ","date":"2023-05-03T03:49:51+08:00","permalink":"https://qh.1357810.xyz/articles/linux/vimother/","title":"vim相关操作"},{"content":" 1. 概述篇 1.1. 大厂面试题 支付宝：\n支付宝三面：JVM 性能调优都做了什么？\n小米：\n有做过 JVM 内存优化吗？\n从 SQL、JVM、架构、数据库四个方面讲讲优化思路\n蚂蚁金服：\nJVM 的编译优化\njvm 性能调优都做了什么\nJVM 诊断调优工具用过哪些？\n二面：jvm 怎样调优，堆内存、栈空间设置多少合适\n三面：JVM 相关的分析工具使用过的有哪些？具体的性能调优步骤如何\n阿里：\n如何进行 JVM 调优？有哪些方法？\n如何理解内存泄漏问题？有哪些情况会导致内存泄漏？如何解决？\n字节跳动：\n三面：JVM 如何调优、参数怎么调？\n拼多多：\n从 SQL、JVM、架构、数据库四个方面讲讲优化思路\n京东：\nJVM 诊断调优工具用过哪些？\n每秒几十万并发的秒杀系统为什么会频繁发生 GC？\n日均百万级交易系统如何优化 JVM？\n线上生产系统 OOM 如何监控及定位与解决？\n高并发系统如何基于 G1 垃圾回收器优化性能？\n1.2. 背景说明 生产环境中的问题\n生产环境发生了内存溢出该如何处理？ 生产环境应该给服务器分配多少内存合适？ 如何对垃圾回收器的性能进行调优？ 生产环境 CPU 负载飙高该如何处理？ 生产环境应该给应用分配多少线程合适？ 不加 log，如何确定请求是否执行了某一行代码？ 不加 log，如何实时查看某个方法的入参与返回值？ 为什么要调优\n防止出现 OOM 解决 OOM 减少 Full GC 出现的频率 不同阶段的考虑\n上线前 项目运行阶段 线上出现 OOM 1.3. 调优概述 监控的依据\n运行日志 异常堆栈 GC 日志 线程快照 堆转储快照 调优的大方向\n合理地编写代码 充分并合理的使用硬件资源 合理地进行 JVM 调优 1.4. 性能优化的步骤 第 1 步：性能监控\nGC 频繁 cpu load 过高 OOM 内存泄露 死锁 程序响应时间较长 第 2 步：性能分析\n打印 GC 日志，通过 GCviewer 或者 http://gceasy.io 来分析异常信息 灵活运用命令行工具、jstack、jmap、jinfo 等 dump 出堆文件，使用内存分析工具分析文件 使用阿里 Arthas、jconsole、JVisualVM 来实时查看 JVM 状态 jstack 查看堆栈信息 第 3 步：性能调优\n适当增加内存，根据业务背景选择垃圾回收器 优化代码，控制内存使用 增加机器，分散节点压力 合理设置线程池线程数量 使用中间件提高程序效率，比如缓存、消息队列等 其他…… 1.5. 性能评价/测试指标 停顿时间（或响应时间）\n提交请求和返回该请求的响应之间使用的时间，一般比较关注平均响应时间。常用操作的响应时间列表：\n操作 响应时间 打开一个站点 几秒 数据库查询一条记录（有索引） 十几毫秒 机械磁盘一次寻址定位 4 毫秒 从机械磁盘顺序读取 1M 数据 2 毫秒 从 SSD 磁盘顺序读取 1M 数据 0.3 毫秒 从远程分布式换成 Redis 读取一个数据 0.5 毫秒 从内存读取 1M 数据 十几微妙 Java 程序本地方法调用 几微妙 网络传输 2Kb 数据 1 微妙 在垃圾回收环节中：\n暂停时间：执行垃圾收集时，程序的工作线程被暂停的时间。 -XX:MaxGCPauseMillis 吞吐量\n对单位时间内完成的工作量（请求）的量度 在 GC 中：运行用户代码的事件占总运行时间的比例（总运行时间：程序的运行时间+内存回收的时间） 吞吐量为 1-1/(1+n)，其中-XX::GCTimeRatio=n 并发数\n同一时刻，对服务器有实际交互的请求数 内存占用\nJava 堆区所占的内存大小 相互间的关系\n以高速公路通行状况为例\n吞吐量：每天通过高速公路收费站的车辆的数据 并发数：高速公路上正在行驶的车辆的数目 响应时间：车速 ","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/11895611/","title":"01-概述篇"},{"content":" 2. JVM 监控及诊断工具-命令行篇 2.1. 概述 性能诊断是软件工程师在日常工作中需要经常面对和解决的问题，在用户体验至上的今天，解决好应用的性能问题能带来非常大的收益。\nJava 作为最流行的编程语言之一，其应用性能诊断一直受到业界广泛关注。可能造成 Java 应用出现性能问题的因素非常多，例如线程控制、磁盘读写、数据库访问、网络 I/O、垃圾收集等。想要定位这些问题，一款优秀的性能诊断工具必不可少。\n体会 1：使用数据说明问题，使用知识分析问题，使用工具处理问题。\n体会 2：无监控、不调优！\n简单命令行工具\n在我们刚接触 java 学习的时候，大家肯定最先了解的两个命令就是 javac，java，那么除此之外，还有没有其他的命令可以供我们使用呢？\n我们进入到安装 jdk 的 bin 目录，发现还有一系列辅助工具。这些辅助工具用来获取目标 JVM 不同方面、不同层次的信息，帮助开发人员很好地解决 Java 应用程序的一些疑难杂症。\n官方源码地址：http://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/jdk.jcmd/share/classes/sun/tools 2.2. jps：查看正在运行的 Java 进程 jps(Java Process Status)：显示指定系统内所有的 HotSpot 虚拟机进程（查看虚拟机进程信息），可用于查询正在运行的虚拟机进程。\n说明：对于本地虚拟机进程来说，进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的，是唯一的。\n基本使用语法为：jps [options] [hostid]\n我们还可以通过追加参数，来打印额外的信息。\noptions 参数\n-q：仅仅显示 LVMID（local virtual machine id），即本地虚拟机唯一 id。不显示主类的名称等 -l：输出应用程序主类的全类名 或 如果进程执行的是 jar 包，则输出 jar 完整路径 -m：输出虚拟机进程启动时传递给主类 main()的参数 -v：列出虚拟机进程启动时的 JVM 参数。比如：-Xms20m -Xmx50m 是启动程序指定的 jvm 参数。 说明：以上参数可以综合使用。\n补充：如果某 Java 进程关闭了默认开启的 UsePerfData 参数（即使用参数-XX：-UsePerfData），那么 jps 命令（以及下面介绍的 jstat）将无法探知该 Java 进程。\nhostid 参数\nRMI 注册表中注册的主机名。如果想要远程监控主机上的 java 程序，需要安装 jstatd。\n对于具有更严格的安全实践的网络场所而言，可能使用一个自定义的策略文件来显示对特定的可信主机或网络的访问，尽管这种技术容易受到 IP 地址欺诈攻击。\n如果安全问题无法使用一个定制的策略文件来处理，那么最安全的操作是不运行 jstatd 服务器，而是在本地使用 jstat 和 jps 工具。\n2.3. jstat：查看 JVM 统计信息 jstat（JVM Statistics Monitoring Tool）：用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据。在没有 GUI 图形界面，只提供了纯文本控制台环境的服务器上，它将是运行期定位虚拟机性能问题的首选工具。常用于检测垃圾回收问题以及内存泄漏问题。\n官方文档：https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html 基本使用语法为：jstat -\u0026lt;option\u0026gt; [-t] [-h\u0026lt;lines\u0026gt;] \u0026lt;vmid\u0026gt; [\u0026lt;interval\u0026gt; [\u0026lt;count\u0026gt;]]\ninterval：多久(ms)打印一次，默认只打印一次 count：最多打印多少次 vmid：进程id -t：打印内容加上当前进程运行了多少时间(s) option：为打印主体内容是什么 -h：在周期性数据打印时，输出多少行数据后输出一个表头信息 查看命令相关参数：jstat-h 或 jstat-help\n其中 vmid 是进程 id 号，也就是 jps 之后看到的前面的号码，如下：\noption 参数\n选项 option 可以由以下值构成。\n类装载相关的：\n-class：显示 ClassLoader 的相关信息：类的装载、卸载数量、总空间、类装载所消耗的时间等 垃圾回收相关的：\n-gc：显示与 GC 相关的堆信息。包括 Eden 区、两个 Survivor 区、老年代、永久代等的容量、已用空间、GC 时间合计等信息。 -gccapacity：显示内容与-gc 基本相同，但输出主要关注 Java 堆各个区域使用到的最大、最小空间。 -gcutil：显示内容与-gc 基本相同，但输出主要关注已使用空间占总空间的百分比。 -gccause：与-gcutil 功能一样，但是会额外输出导致最后一次或当前正在发生的 GC 产生的原因。 -gcnew：显示新生代 GC 状况 -gcnewcapacity：显示内容与-gcnew 基本相同，输出主要关注使用到的最大、最小空间 -geold：显示老年代 GC 状况 -gcoldcapacity：显示内容与-gcold 基本相同，输出主要关注使用到的最大、最小空间 -gcpermcapacity：显示永久代使用到的最大、最小空间。 JIT 相关的：\n-compiler：显示 JIT 编译器编译过的方法、耗时等信息\n-printcompilation：输出已经被 JIT 编译的方法\njstat -class\njstat -compiler\njstat -printcompilation\njstat -gc\njstat -gccapacity\njstat -gcutil\njstat -gccause\njstat -gcnew\njstat -gcnewcapacity\njstat -gcold\njstat -gcoldcapacity\njstat -t\njstat -t -h\n表头 含义（字节） EC Eden 区的大小 EU Eden 区已使用的大小 S0C 幸存者 0 区的大小 S1C 幸存者 1 区的大小 S0U 幸存者 0 区已使用的大小 S1U 幸存者 1 区已使用的大小 MC 元空间的大小 MU 元空间已使用的大小 OC 老年代的大小 OU 老年代已使用的大小 CCSC 压缩类空间的大小 CCSU 压缩类空间已使用的大小 YGC 从应用程序启动到采样时 young gc 的次数 YGCT 从应用程序启动到采样时 young gc 消耗时间（秒） FGC 从应用程序启动到采样时 full gc 的次数 FGCT 从应用程序启动到采样时的 full gc 的消耗时间（秒） GCT 从应用程序启动到采样时 gc 的总时间 interval 参数： 用于指定输出统计数据的周期，单位为毫秒。即：查询间隔\ncount 参数： 用于指定查询的总次数\n-t 参数： 可以在输出信息前加上一个 Timestamp 列，显示程序的运行时间。单位：秒\n-h 参数： 可以在周期性数据输出时，输出多少行数据后输出一个表头信息\n补充： jstat 还可以用来判断是否出现内存泄漏。\n第 1 步：在长时间运行的 Java 程序中，我们可以运行 jstat 命令连续获取多行性能数据，并取这几行数据中 OU 列（即已占用的老年代内存）的最小值。\n第 2 步：然后，我们每隔一段较长的时间重复一次上述操作，来获得多组 OU 最小值。如果这些值呈上涨趋势，则说明该 Java 程序的老年代内存已使用量在不断上涨，这意味着无法回收的对象在不断增加，因此很有可能存在内存泄漏。\n2.4. jinfo：实时查看和修改 JVM 配置参数 jinfo(Configuration Info for Java)：查看虚拟机配置参数信息，也可用于调整虚拟机的配置参数。在很多情况卡，Java 应用程序不会指定所有的 Java 虚拟机参数。而此时，开发人员可能不知道某一个具体的 Java 虚拟机参数的默认值。在这种情况下，可能需要通过查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了 jinfo 工具，开发人员可以很方便地找到 Java 虚拟机参数的当前值。\n基本使用语法为：jinfo [options] pid\n说明：java 进程 ID 必须要加上\n选项 选项说明 no option 输出全部的参数和系统属性 -flag name 输出对应名称的参数 -flag [+-]name 开启或者关闭对应名称的参数 只有被标记为 manageable 的参数才可以被动态修改 -flag name=value 设定对应名称的参数 -flags 输出全部的参数 -sysprops 输出系统属性 jinfo -sysprops\n1 2 3 4 5 6 7 8 \u0026gt; jinfo -sysprops jboss.modules.system.pkgs = com.intellij.rt java.vendor = Oracle Corporation sun.java.launcher = SUN_STANDARD sun.management.compiler = HotSpot 64-Bit Tiered Compilers catalina.useNaming = true os.name = Windows 10 ... Copied! jinfo -flags\n1 2 3 \u0026gt; jinfo -flags 25592 Non-default VM flags: -XX:CICompilerCount=4 -XX:InitialHeapSize=333447168 -XX:MaxHeapSize=5324668928 -XX:MaxNewSize=1774714880 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=111149056 -XX:OldSize=222298112 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC Command line: -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:8040,suspend=y,server=n -Drebel.base=C:\\Users\\Vector\\.jrebel -Drebel.env.ide.plugin.version=2021.1.2 -Drebel.env.ide.version=2020.3.3 -Drebel.env.ide.product=IU -Drebel.env.ide=intellij -Drebel.notification.url=http://localhost:7976 -agentpath:C:\\Users\\Vector\\AppData\\Roaming\\JetBrains\\IntelliJIdea2020.3\\plugins\\jr-ide-idea\\lib\\jrebel6\\lib\\jrebel64.dll -Dmaven.home=D:\\eclipse\\env\\maven -Didea.modules.paths.file=C:\\Users\\Vector\\AppData\\Local\\JetBrains\\IntelliJIdea2020.3\\Maven\\idea-projects-state-596682c7.properties -Dclassworlds.conf=C:\\Users\\Vector\\AppData\\Local\\Temp\\idea-6755-mvn.conf -Dmaven.ext.class.path=D:\\IDEA\\plugins\\maven\\lib\\maven-event-listener.jar -javaagent:D:\\IDEA\\plugins\\java\\lib\\rt\\debugger-agent.jar -Dfile.encoding=UTF-8 Copied! jinfo -flag\n1 2 3 4 5 \u0026gt; jinfo -flag UseParallelGC 25592 -XX:+UseParallelGC \u0026gt; jinfo -flag UseG1GC 25592 -XX:-UseG1GC Copied! jinfo -flag name\n1 2 3 4 5 \u0026gt; jinfo -flag UseParallelGC 25592 -XX:+UseParallelGC \u0026gt; jinfo -flag UseG1GC 25592 -XX:-UseG1GC Copied! jinfo -flag [+-]name\n1 2 3 4 5 6 7 \u0026gt; jinfo -flag +PrintGCDetails 25592 \u0026gt; jinfo -flag PrintGCDetails 25592 -XX:+PrintGCDetails \u0026gt; jinfo -flag -PrintGCDetails 25592 \u0026gt; jinfo -flag PrintGCDetails 25592 -XX:-PrintGCDetails Copied! 拓展：\njava -XX:+PrintFlagsInitial 查看所有 JVM 参数启动的初始值\n1 2 3 4 5 6 [Global flags] intx ActiveProcessorCount = -1 {product} uintx AdaptiveSizeDecrementScaleFactor = 4 {product} uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} uintx AdaptiveSizePausePolicy = 0 {product} ... Copied! java -XX:+PrintFlagsFinal 查看所有 JVM 参数的最终值\n1 2 3 4 5 6 7 [Global flags] intx ActiveProcessorCount = -1 {product} ... intx CICompilerCount := 4 {product} uintx InitialHeapSize := 333447168 {product} uintx MaxHeapSize := 1029701632 {product} uintx MaxNewSize := 1774714880 {product} Copied! java -XX:+PrintCommandLineFlags 查看哪些已经被用户或者 JVM 设置过的详细的 XX 参数的名称和值\n1 -XX:InitialHeapSize=332790016 -XX:MaxHeapSize=5324640256 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC Copied! 修改参数 jinfo不仅可以查看某一个java虚拟机参数的实际取值，甚至可以在运行时修改部分参数，并使之生效。但是并非所有参数都支持动态修改，参数只有被标记为manageable的flag可以被实时修改，其实这个修改能力是极其有限的。 1 2 3 4 5 6 7 8 \u0026gt; jinfo -flag PrintGCDetails 3540 #查看 -XX:-PrintGCDetails \u0026gt; jinfo -flag +PrintGCDetails 3540 #修改 \u0026gt; jinfo -flag PrintGCDetails 3540 #再次查看 -XX:+PrintGCDetails \u0026gt; jinfo -flag MaxHeapFreeRatio=90 3540 #修改 Copied! 2.5. jmap：导出内存映像文件\u0026amp;内存使用情况 jmap（JVM Memory Map）：作用一方面是获取 dump 文件（堆转储快照文件，二进制文件），它还可以获取目标 Java 进程的内存相关信息，包括 Java 堆各区域的使用情况、堆中对象的统计信息、类加载信息等。开发人员可以在控制台中输入命令“jmap -help”查阅 jmap 工具的具体使用方式和一些标准选项配置。\n官方帮助文档：https://docs.oracle.com/en/java/javase/11/tools/jmap.html 基本使用语法为：\njmap [option] \u0026lt;pid\u0026gt; jmap [option] \u0026lt;executable \u0026lt;core\u0026gt; jmap [option] [server_id@] \u0026lt;remote server IP or hostname\u0026gt; 选项 作用 -dump 生成 dump 文件（Java 堆转储快照），-dump:live 只保存堆中的存活对象 -heap 输出整个堆空间的详细信息，包括 GC 的使用、堆配置信息，以及内存的使用信息等 -histo 输出堆空间中对象的统计信息，包括类、实例数量和合计容量，-histo:live 只统计堆中的存活对象 -J \u0026lt;flag\u0026gt; 传递参数给 jmap 启动的 jvm -finalizerinfo 显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象，仅 linux/solaris 平台有效 -permstat 以 ClassLoader 为统计口径输出永久代的内存状态信息，仅 linux/solaris 平台有效 -F 当虚拟机进程对-dump 选项没有任何响应时，强制执行生成 dump 文件，仅 linux/solaris 平台有效 说明：这些参数和 linux 下输入显示的命令多少会有不同，包括也受 jdk 版本的影响。\n1 \u0026gt; jmap -dump:format=b,file=\u0026lt;filename.hprof\u0026gt; \u0026lt;pid\u0026gt;\u0026gt; jmap -dump:live,format=b,file=\u0026lt;filename.hprof\u0026gt; \u0026lt;pid\u0026gt; Copied! 由于 jmap 将访问堆中的所有对象，为了保证在此过程中不被应用线程干扰，jmap 需要借助安全点机制，让所有线程停留在不改变堆中数据的状态。也就是说，由 jmap 导出的堆快照必定是安全点位置的。这可能导致基于该堆快照的分析结果存在偏差。\n举个例子，假设在编译生成的机器码中，某些对象的生命周期在两个安全点之间，那么:live 选项将无法探知到这些对象。\n另外，如果某个线程长时间无法跑到安全点，jmap 将一直等下去。与前面讲的 jstat 则不同，垃圾回收器会主动将 jstat 所需要的摘要数据保存至固定位置之中，而 jstat 只需直接读取即可。\n使用一：导出内存映射文件 注意：对于以上说明中的第1点是自动方式才会这样做，而手动不会在Full GC之后生成Dump使用手动方式生成dump文件，一般指令执行之后就会生成，不用等到快出现OOM的时候使用自动方式生成dump文件，当出现OOM之前先生成dump文件如果使用手动方式，一般使用第2种，毕竟生成堆中存活对象的dump文件是比较小的，我们通常关心的是GC没有回收的对象，便于传输和分析\n手动方式：\njmap -dump:format=b,file=\u0026lt;filename.hprof\u0026gt; jmap -dump:live,format=b,file=\u0026lt;filename.hprof\u0026gt; 说明：中的filename是文件名称，而.hprof是后缀名，\u0026lt;***\u0026gt;代表该值可以省略\u0026lt;\u0026gt;，当然后面的是进程id，需要通过jps查询出来format=b表示生成的是标准的dump文件，用来进行格式限定 具体例子如下：生成堆中所有对象的快照：生成堆中存活对象的快照： 其中file=后面的是生成的dump文件地址，最后的11696是进程id，可以通过jps查看 一般使用的是第二种方式，也就是生成堆中存活对象的快照，毕竟这种方式生成的dump文件更小，我们传输处理都更方便\n自动方式：\n* -XX:+HeapDumpOnOutOfMemoryError * -XX:HeapDumpPath=\u0026lt;filename.hprof\u0026gt; 使用二：显示堆内存相关信息，和jstat -gc 类似\njmap -heap 进程id\njmap -heap 进程id只是时间点上的堆信息，而jstat后面可以添加参数，可以指定时间动态观察数据改变情况，而图形化界面工具，例如jvisualvm等，它们可以用图表的方式动态展示出相关信息，更加直观明了例子如下：\njmap -histo 进程id\n输出堆中对象的同级信息，包括类、实例数量和合计容量，也是这一时刻的内存中的对象信息例子如下：\n使用三：其他作用\n查看系统的ClassLoader信息 jmap -permstat 进程id 查看堆积在finalizer队列中的对象 jmap -finalizerinfo 2.6. jhat：JDK 自带堆分析工具 jhat(JVM Heap Analysis Tool)：Sun JDK 提供的 jhat 命令与 jmap 命令搭配使用，用于分析 jmap 生成的 heap dump 文件（堆转储快照）。jhat 内置了一个微型的 HTTP/HTML 服务器，生成 dump 文件的分析结果后，用户可以在浏览器中查看分析结果（分析虚拟机转储快照信息）。\n使用了 jhat 命令，就启动了一个 http 服务，端口是 7000，即 http://localhost:7000/，就可以在浏览器里分析。\n说明：jhat 命令在 JDK9、JDK10 中已经被删除，官方建议用 VisualVM 代替。\n基本适用语法：jhat \u0026lt;option\u0026gt; \u0026lt;dumpfile\u0026gt;\noption 参数 作用 -stack false ｜ true 关闭｜打开对象分配调用栈跟踪 -refs false ｜ true 关闭｜打开对象引用跟踪 -port port-number 设置 jhat HTTP Server 的端口号，默认 7000 -exclude exclude-file 执行对象查询时需要排除的数据成员 -baseline exclude-file 指定一个基准堆转储 -debug int 设置 debug 级别 -version 启动后显示版本信息就退出 -J \u0026lt;flag\u0026gt; 传入启动参数，比如-J-Xmx512m 1 jhat \u0026lt;-option\u0026gt; ~/a.hprof Copied! 2.7. jstack：打印 JVM 中线程快照 jstack（JVM Stack Trace）：用于生成虚拟机指定进程当前时刻的线程快照（虚拟机堆栈跟踪）。线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合。\n生成线程快照的作用：可用于定位线程出现长时间停顿的原因，如线程间死锁、死循环、请求外部资源导致的长时间等待等问题。这些都是导致线程长时间停顿的常见原因。当线程出现停顿时，就可以用 jstack 显示各个线程调用的堆栈情况。\n官方帮助文档：https://docs.oracle.com/en/java/javase/11/tools/jstack.html 在 thread dump 中，要留意下面几种状态\n死锁，Deadlock（重点关注） 等待资源，Waiting on condition（重点关注） 等待获取监视器，Waiting on monitor entry（重点关注） 阻塞，Blocked（重点关注） 执行中，Runnable 暂停，Suspended 对象等待中，Object.wait() 或 TIMED＿WAITING 停止，Parked 1 jstack \u0026lt; -option \u0026gt; pid Copied! option 参数 作用 -F 当正常输出的请求不被响应时，强制输出线程堆栈 -l 除堆栈外，显示关于锁的附加信息 -m 如果调用本地方法的话，可以显示 C/C++的堆栈 2.8. jcmd：多功能命令行 在 JDK 1.7 以后，新增了一个命令行工具 jcmd。它是一个多功能的工具，可以用来实现前面除了 jstat 之外所有命令的功能。比如：用它来导出堆、内存使用、查看 Java 进程、导出线程信息、执行 GC、JVM 运行时间等。\n官方帮助文档：https://docs.oracle.com/en/java/javase/11/tools/jcmd.html jcmd 拥有 jmap 的大部分功能，并且在 Oracle 的官方网站上也推荐使用 jcmd 命令代 jmap 命令\n**jcmd -l：**列出所有的 JVM 进程\n**jcmd 进程号 help：**针对指定的进程，列出支持的所有具体命令\n**jcmd 进程号 具体命令：**显示指定进程的指令命令的数据\nThread.print 可以替换 jstack 指令 GC.class_histogram 可以替换 jmap 中的-histo 操作 GC.heap_dump 可以替换 jmap 中的-dump 操作 GC.run 可以查看 GC 的执行情况 VM.uptime 可以查看程序的总执行时间，可以替换 jstat 指令中的-t 操作 VM.system_properties 可以替换 jinfo -sysprops 进程 id VM.flags 可以获取 JVM 的配置参数信息 2.9. jstatd：远程主机信息收集 之前的指令只涉及到监控本机的 Java 应用程序，而在这些工具中，一些监控工具也支持对远程计算机的监控（如 jps、jstat）。为了启用远程监控，则需要配合使用 jstatd 工具。命令 jstatd 是一个 RMI 服务端程序，它的作用相当于代理服务器，建立本地计算机与远程监控工具的通信。jstatd 服务器将本机的 Java 应用程序信息传递到远程计算机。\n","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/14915117/","title":"02-JVM监控及诊断工具-命令行篇"},{"content":" 3. JVM 监控及诊断工具-GUI 篇 3.1. 工具概述 使用上一章命令行工具或组合能帮您获取目标 Java 应用性能相关的基础信息，但它们存在下列局限：\n1．无法获取方法级别的分析数据，如方法间的调用关系、各方法的调用次数和调用时间等（这对定位应用性能瓶颈至关重要）。 2．要求用户登录到目标 Java 应用所在的宿主机上，使用起来不是很方便。 3．分析数据通过终端输出，结果展示不够直观。 为此，JDK 提供了一些内存泄漏的分析工具，如 jconsole，jvisualvm 等，用于辅助开发人员定位问题，但是这些工具很多时候并不足以满足快速定位的需求。所以这里我们介绍的工具相对多一些、丰富一些。\nJDK 自带的工具\njconsole：JDK 自带的可视化监控工具。查看 Java 应用程序的运行概况、监控堆信息、永久区（或元空间）使用情况、类加载情况等\nVisual VM：Visual VM 是一个工具，它提供了一个可视界面，用于查看 Java 虚拟机上运行的基于 Java 技术的应用程序的详细信息。\nJMC：Java Mission Control，内置 Java Flight Recorder。能够以极低的性能开销收集 Java 虚拟机的性能数据。\n第三方工具\nMAT：MAT（Memory Analyzer Tool）是基于 Eclipse 的内存分析工具，是一个快速、功能丰富的 Java heap 分析工具，它可以帮助我们查找内存泄漏和减少内存消耗\nJProfiler：商业软件，需要付费。功能强大。\n3.2. JConsole jconsole：从 Java5 开始，在 JDK 中自带的 java 监控和管理控制台。用于对 JVM 中内存、线程和类等的监控，是一个基于 JMX（java management extensions）的 GUI 性能监控工具。\n官方地址：https://docs.oracle.com/javase/7/docs/technotes/guides/management/jconsole.html 3.3. Visual VM Visual VM 是一个功能强大的多合一故障诊断和性能监控的可视化工具。它集成了多个 JDK 命令行工具，使用 Visual VM 可用于显示虚拟机进程及进程的配置和环境信息（jps，jinfo），监视应用程序的 CPU、GC、堆、方法区及线程的信息（jstat、jstack）等，甚至代替 JConsole。在 JDK 6 Update 7 以后，Visual VM 便作为 JDK 的一部分发布（VisualVM 在 JDK／bin 目录下）即：它完全免费。\n主要功能：\n1.生成/读取堆内存/线程快照 2.查看 JVM 参数和系统属性 3.查看运行中的虚拟机进程 4.程序资源的实时监控 5.JMX 代理连接、远程环境监控、CPU 分析和内存分析 官方地址：https://visualvm.github.io/index.html 3.4. Eclipse MAT MAT（Memory Analyzer Tool）工具是一款功能强大的 Java 堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。MAT 是基于 Eclipse 开发的，不仅可以单独使用，还可以作为插件的形式嵌入在 Eclipse 中使用。是一款免费的性能分析工具，使用起来非常方便。\nMAT 可以分析 heap dump 文件。在进行内存分析时，只要获得了反映当前设备内存映像的 hprof 文件，通过 MAT 打开就可以直观地看到当前的内存信息。一般说来，这些内存信息包含：\n所有的对象信息，包括对象实例、成员变量、存储于栈中的基本类型值和存储于堆中的其他对象的引用值。 所有的类信息，包括 classloader、类名称、父类、静态变量等 GCRoot 到所有的这些对象的引用路径 线程信息，包括线程的调用栈及此线程的线程局部变量（TLS） MAT 不是一个万能工具，它并不能处理所有类型的堆存储文件。但是比较主流的厂家和格式，例如 Sun，HP，SAP 所采用的 HPROF 二进制堆存储文件，以及 IBM 的 PHD 堆存储文件等都能被很好的解析。\n最吸引人的还是能够快速为开发人员生成内存泄漏报表，方便定位问题和分析问题。虽然 MAT 有如此强大的功能，但是内存分析也没有简单到一键完成的程度，很多内存问题还是需要我们从 MAT 展现给我们的信息当中通过经验和直觉来判断才能发现。\n官方地址： https://www.eclipse.org/mat/downloads.php 3.5. JProfiler 在运行 Java 的时候有时候想测试运行时占用内存情况，这时候就需要使用测试工具查看了。在 eclipse 里面有 Eclipse Memory Analyzer tool（MAT）插件可以测试，而在 IDEA 中也有这么一个插件，就是 JProfiler。JProfiler 是由 ej-technologies 公司开发的一款 Java 应用性能诊断工具。功能强大，但是收费。\n特点：\n使用方便、界面操作友好（简单且强大） 对被分析的应用影响小（提供模板） CPU，Thread，Memory 分析功能尤其强大 支持对 jdbc，noSql，jsp，servlet，socket 等进行分析 支持多种模式（离线，在线）的分析 支持监控本地、远程的 JVM 跨平台，拥有多种操作系统的安装版本 主要功能：\n1-方法调用：对方法调用的分析可以帮助您了解应用程序正在做什么，并找到提高其性能的方法 2-内存分配：通过分析堆上对象、引用链和垃圾收集能帮您修复内存泄露问题，优化内存使用 3-线程和锁：JProfiler 提供多种针对线程和锁的分析视图助您发现多线程问题 4-高级子系统：许多性能问题都发生在更高的语义级别上。例如，对于 JDBC 调用，您可能希望找出执行最慢的 SQL 语句。JProfiler 支持对这些子系统进行集成分析 官网地址：https://www.ej-technologies.com/products/jprofiler/overview.html 数据采集方式：\nJProfier 数据采集方式分为两种：Sampling（样本采集）和 Instrumentation（重构模式）\nInstrumentation：这是 JProfiler 全功能模式。在 class 加载之前，JProfier 把相关功能代码写入到需要分析的 class 的 bytecode 中，对正在运行的 jvm 有一定影响。\n优点：功能强大。在此设置中，调用堆栈信息是准确的。 缺点：若要分析的 class 较多，则对应用的性能影响较大，CPU 开销可能很高（取决于 Filter 的控制）。因此使用此模式一般配合 Filter 使用，只对特定的类或包进行分析 Sampling：类似于样本统计，每隔一定时间（5ms）将每个线程栈中方法栈中的信息统计出来。\n优点：对 CPU 的开销非常低，对应用影响小（即使你不配置任何 Filter） 缺点：一些数据／特性不能提供（例如：方法的调用次数、执行时间） 注：JProfiler 本身没有指出数据的采集类型，这里的采集类型是针对方法调用的采集类型。因为 JProfiler 的绝大多数核心功能都依赖方法调用采集的数据，所以可以直接认为是 JProfiler 的数据采集类型。\n遥感监测 Telemetries\n内存视图 Live Memory\nLive memory 内存剖析：class／class instance 的相关信息。例如对象的个数，大小，对象创建的方法执行栈，对象创建的热点。\n所有对象 All Objects：显示所有加载的类的列表和在堆上分配的实例数。只有 Java 1.5（JVMTI）才会显示此视图。 记录对象 Record Objects：查看特定时间段对象的分配，并记录分配的调用堆栈。 分配访问树 Allocation Call Tree：显示一棵请求树或者方法、类、包或对已选择类有带注释的分配信息的 J2EE 组件。 分配热点 Allocation Hot Spots：显示一个列表，包括方法、类、包或分配已选类的 J2EE 组件。你可以标注当前值并且显示差异值。对于每个热点都可以显示它的跟踪记录树。 类追踪器 Class Tracker：类跟踪视图可以包含任意数量的图表，显示选定的类和包的实例与时间。 堆遍历 heap walker\ncpu 视图 cpu views\nJProfiler 提供不同的方法来记录访问树以优化性能和细节。线程或者线程组以及线程状况可以被所有的视图选择。所有的视图都可以聚集到方法、类、包或 J2EE 组件等不同层上。\n访问树 Call Tree：显示一个积累的自顶向下的树，树中包含所有在 JVM 中已记录的访问队列。JDBC，JMS 和 JNDI 服务请求都被注释在请求树中。请求树可以根据 Servlet 和 JSP 对 URL 的不同需要进行拆分。 热点 Hot Spots：显示消耗时间最多的方法的列表。对每个热点都能够显示回溯树。该热点可以按照方法请求，JDBC，JMS 和 JNDI 服务请求以及按照 URL 请求来进行计算。 访问图 Call Graph：显示一个从已选方法、类、包或 J2EE 组件开始的访问队列的图。 方法统计 Method Statistis：显示一段时间内记录的方法的调用时间细节。 线程视图 threads\nJProfiler 通过对线程历史的监控判断其运行状态，并监控是否有线程阻塞产生，还能将一个线程所管理的方法以树状形式呈现。对线程剖析。\n线程历史 Thread History：显示一个与线程活动和线程状态在一起的活动时间表。 线程监控 Thread Monitor：显示一个列表，包括所有的活动线程以及它们目前的活动状况。 线程转储 Thread Dumps：显示所有线程的堆栈跟踪。 线程分析主要关心三个方面：\n1．web 容器的线程最大数。比如：Tomcat 的线程容量应该略大于最大并发数。 2．线程阻塞 3．线程死锁 监控和锁 Monitors ＆Locks\n所有线程持有锁的情况以及锁的信息。观察 JVM 的内部线程并查看状态：\n死锁探测图表 Current Locking Graph：显示 JVM 中的当前死锁图表。 目前使用的监测器 Current Monitors：显示目前使用的监测器并且包括它们的关联线程。 锁定历史图表 Locking History Graph：显示记录在 JVM 中的锁定历史。 历史检测记录 Monitor History：显示重大的等待事件和阻塞事件的历史记录。 监控器使用统计 Monitor Usage Statistics：显示分组监测，线程和监测类的统计监测数据 3.6. Arthas 上述工具都必须在服务端项目进程中配置相关的监控参数，然后工具通过远程连接到项目进程，获取相关的数据。这样就会带来一些不便，比如线上环境的网络是隔离的，本地的监控工具根本连不上线上环境。并且类似于 Jprofiler 这样的商业工具，是需要付费的。\n那么有没有一款工具不需要远程连接，也不需要配置监控参数，同时也提供了丰富的性能监控数据呢？\n阿里巴巴开源的性能分析神器 Arthas 应运而生。\nArthas 是 Alibaba 开源的 Java 诊断工具，深受开发者喜爱。在线排查问题，无需重启；动态跟踪 Java 代码；实时监控 JVM 状态。Arthas 支持 JDK 6 ＋，支持 Linux／Mac／Windows，采用命令行交互模式，同时提供丰富的 Tab 自动补全功能，进一步方便进行问题的定位和诊断。当你遇到以下类似问题而束手无策时，Arthas 可以帮助你解决：\n这个类从哪个 jar 包加载的？为什么会报各种类相关的 Exception？ 我改的代码为什么没有执行到？难道是我没 commit？分支搞错了？ 遇到问题无法在线上 debug，难道只能通过加日志再重新发布吗？ 线上遇到某个用户的数据处理有问题，但线上同样无法 debug，线下无法重现！ 是否有一个全局视角来查看系统的运行状况？ 有什么办法可以监控到 JVM 的实时运行状态？ 怎么快速定位应用的热点，生成火焰图？ 官方地址：https://arthas.aliyun.com/doc/quick-start.html 安装方式：如果速度较慢，可以尝试国内的码云 Gitee 下载。\n1 2 wget https://io/arthas/arthas-boot.jar wget https://arthas/gitee/io/arthas-boot.jar Copied! Arthas 只是一个 java 程序，所以可以直接用 java -jar 运行。\n除了在命令行查看外，Arthas 目前还支持 Web Console。在成功启动连接进程之后就已经自动启动,可以直接访问 http://127.0.0.1:8563/ 访问，页面上的操作模式和控制台完全一样。\n基础指令\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 quit/exit 退出当前 Arthas客户端，其他 Arthas喜户端不受影响 stop/shutdown 关闭 Arthas服务端，所有 Arthas客户端全部退出 help 查看命令帮助信息 cat 打印文件内容，和linux里的cat命令类似 echo 打印参数，和linux里的echo命令类似 grep 匹配查找，和linux里的gep命令类似 tee 复制标隹输入到标准输出和指定的文件，和linux里的tee命令类似 pwd 返回当前的工作目录，和linux命令类似 cls 清空当前屏幕区域 session 查看当前会话的信息 reset 重置增强类，将被 Arthas增强过的类全部还原, Arthas服务端关闭时会重置所有增强过的类 version 输出当前目标Java进程所加载的 Arthas版本号 history 打印命令历史 keymap Arthas快捷键列表及自定义快捷键 Copied! jvm 相关\n1 2 3 4 5 6 7 8 9 10 11 12 dashboard 当前系统的实时数据面板 thread 查看当前JVM的线程堆栈信息 jvm 查看当前JVM的信息 sysprop 查看和修改JVM的系统属性 sysem 查看JVM的环境变量 vmoption 查看和修改JVM里诊断相关的option perfcounter 查看当前JVM的 Perf Counter信息 logger 查看和修改logger getstatic 查看类的静态属性 ognl 执行ognl表达式 mbean 查看 Mbean的信息 heapdump dump java heap，类似jmap命令的 heap dump功能 Copied! class/classloader 相关\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 sc 查看JVM已加载的类信息 -d 输出当前类的详细信息，包括这个类所加载的原始文件来源、类的声明、加载的Classloader等详细信息。如果一个类被多个Classloader所加载，则会出现多次 -E 开启正则表达式匹配，默认为通配符匹配 -f 输出当前类的成员变量信息（需要配合参数-d一起使用） -X 指定输出静态变量时属性的遍历深度，默认为0，即直接使用toString输出 sm 查看已加载类的方法信息 -d 展示每个方法的详细信息 -E 开启正则表达式匹配,默认为通配符匹配 jad 反编译指定已加载类的源码 mc 内存编译器，内存编译.java文件为.class文件 retransform 加载外部的.class文件, retransform到JVM里 redefine 加载外部的.class文件，redefine到JVM里 dump dump已加载类的byte code到特定目录 classloader 查看classloader的继承树，urts，类加载信息，使用classloader去getResource -t 查看classloader的继承树 -l 按类加载实例查看统计信息 -c 用classloader对应的hashcode来查看对应的 Jar urls Copied! monitor/watch/trace 相关\n1 2 3 4 5 6 7 8 9 10 11 12 monitor 方法执行监控，调用次数、执行时间、失败率 -c 统计周期，默认值为120秒 watch 方法执行观测，能观察到的范围为：返回值、抛出异常、入参，通过编写groovy表达式进行对应变量的查看 -b 在方法调用之前观察(默认关闭) -e 在方法异常之后观察(默认关闭) -s 在方法返回之后观察(默认关闭) -f 在方法结束之后(正常返回和异常返回)观察(默认开启) -x 指定输岀结果的属性遍历深度,默认为0 trace 方法内部调用路径,并输出方法路径上的每个节点上耗时 -n 执行次数限制 stack 输出当前方法被调用的调用路径 tt 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测 Copied! 其他\n1 2 3 4 5 6 7 8 9 jobs 列出所有job kill 强制终止任务 fg 将暂停的任务拉到前台执行 bg 将暂停的任务放到后台执行 grep 搜索满足条件的结果 plaintext 将命令的结果去除ANSI颜色 wc 按行统计输出结果 options 查看或设置Arthas全局开关 profiler 使用async-profiler对应用采样，生成火焰图 Copied! 3.7. Java Misssion Control 在 Oracle 收购 Sun 之前，Oracle 的 JRockit 虚拟机提供了一款叫做 JRockit Mission Control 的虚拟机诊断工具。\n在 Oracle 收购 sun 之后，Oracle 公司同时拥有了 Hotspot 和 JRockit 两款虚拟机。根据 Oracle 对于 Java 的战略，在今后的发展中，会将 JRokit 的优秀特性移植到 Hotspot 上。其中一个重要的改进就是在 Sun 的 JDK 中加入了 JRockit 的支持。\n在 Oracle JDK 7u40 之后，Mission Control 这款工具己经绑定在 Oracle JDK 中发布。\n自 Java11 开始，本节介绍的 JFR 己经开源。但在之前的 Java 版本，JFR 属于 Commercial Feature 通过 Java 虚拟机参数-XX:+UnlockCommercialFeatures 开启。\nJava Mission Control（简称 JMC) ， Java 官方提供的性能强劲的工具，是一个用于对 Java 应用程序进行管理、监视、概要分析和故障排除的工具套件。它包含一个 GUI 客户端以及众多用来收集 Java 虚拟机性能数据的插件如 JMX Console（能够访问用来存放虚拟机齐个于系统运行数据的 MXBeans）以及虚拟机内置的高效 profiling 工具 Java Flight Recorder（JFR）。\nJMC 的另一个优点就是：采用取样，而不是传统的代码植入技术，对应用性能的影响非常非常小，完全可以开着 JMC 来做压测（唯一影响可能是 full gc 多了）。\n官方地址：https://github.com/JDKMissionControl/jmc Java Flight Recorder\nJava Flight Recorder 是 JMC 的其中一个组件，能够以极低的性能开销收集 Java 虚拟机的性能数据。与其他工具相比，JFR 的性能开销很小，在默认配置下平均低于 1%。JFR 能够直接访问虚拟机内的敌据并且不会影响虚拟机的优化。因此它非常适用于生产环境下满负荷运行的 Java 程序。\nJava Flight Recorder 和 JDK Mission Control 共同创建了一个完整的工具链。JDK Mission Control 可对 Java Flight Recorder 连续收集低水平和详细的运行时信息进行高效、详细的分析。\n当启用时 JFR 将记录运行过程中发生的一系列事件。其中包括 Java 层面的事件如线程事件、锁事件，以及 Java 虚拟机内部的事件，如新建对象，垃圾回收和即时编译事件。按照发生时机以及持续时间来划分，JFR 的事件共有四种类型，它们分别为以下四种：\n瞬时事件（Instant Event) ，用户关心的是它们发生与否，例如异常、线程启动事件。\n持续事件(Duration Event) ，用户关心的是它们的持续时间，例如垃圾回收事件。\n计时事件(Timed Event) ，是时长超出指定阈值的持续事件。\n取样事件（Sample Event)，是周期性取样的事件。\n取样事件的其中一个常见例子便是方法抽样（Method Sampling），即每隔一段时问统计各个线程的栈轨迹。如果在这些抽样取得的栈轨迹中存在一个反复出现的方法，那么我们可以推测该方法是热点方法\n3.8. 其他工具 Flame Graphs（火焰图）\n在追求极致性能的场景下，了解你的程序运行过程中 cpu 在干什么很重要，火焰图就是一种非常直观的展示 CPU 在程序整个生命周期过程中时间分配的工具。火焰图对于现代的程序员不应该陌生，这个工具可以非常直观的显示出调用找中的 CPU 消耗瓶颈。\n网上的关于 Java 火焰图的讲解大部分来自于 Brenden Gregg 的博客 http://new.brendangregg.com/flamegraphs.html 火焰图，简单通过 x 轴横条宽度来度量时间指标，y 轴代表线程栈的层次。\nTprofiler\n案例： 使用 JDK 自身提供的工具进行 JVM 调优可以将下 TPS 由 2.5 提升到 20（提升了 7 倍），并准确 定位系统瓶颈。\n系统瓶颈有：应用里释态对象不是太多、有大量的业务线程在频繁创建一些生命周期很长的临时对象，代码里有问题。\n那么，如何在海量业务代码里边准确定位这些性能代码？这里使用阿里开源工具 Tprofiler 来定位 这些性能代码，成功解决掉了 GC 过于频繁的性能瓶预，并最终在上次优化的基础上将 TPS 再提升了 4 倍，即提升到 100。\nTprofiler 配置部署、远程操作、 日志阅谈都不太复杂，操作还是很简单的。但是其却是能够 起到一针见血、立竿见影的效果，帮我们解决了 GC 过于频繁的性能瓶预。 Tprofiler 最重要的特性就是能够统汁出你指定时间段内 JVM 的 top method 这些 top method 极有可能就是造成你 JVM 性能瓶颈的元凶。这是其他大多数 JVM 调优工具所不具备的，包括 JRockit Mission Control。JRokit 首席开发者 Marcus Hirt 在其私人博客《 Lom Overhead Method Profiling cith Java Mission Control》下的评论中曾明确指出 JRMC 井不支持 TOP 方法的统计。 官方地址：http://github.com/alibaba/Tprofiler Btrace\n常见的动态追踪工具有 BTrace、HouseHD（该项目己经停止开发）、Greys-Anatomy（国人开发 个人开发者）、Byteman（JBoss 出品），注意 Java 运行时追踪工具井不限干这几种，但是这几个是相对比较常用的。\nBTrace 是 SUN Kenai 云计算开发平台下的一个开源项目，旨在为 java 提供安全可靠的动态跟踪分析工具。先看一卜日 Trace 的官方定义：\n大概意思是一个 Java 平台的安全的动态追踪工具，可以用来动态地追踪一个运行的 Java 程序。BTrace 动态调整目标应用程序的类以注入跟踪代码（“字节码跟踪“）。\nYourKit\nJProbe\nSpring Insight\n3.9 区别 visualvm\n针对于实时观测的数据中，双击实例进不去对象内部，也没有出引用和入引用 针对于dump文件，可以双击实例进入对象内部，查看属性等，也没有出引用和入引用 mat\n针对于dump文件，什么都有\n内存泄漏检测报告\n支配树\n在对象引用图中，所有指向对象 B 的路径都经过对象 A，则认为对象 A 支配对象 B。如果对象 A 是离对象 B 最近的一个支配对象，则认为对象 A 为对象 B 的直接支配者\nOQL查询\n浅堆和深堆\n保留集 对象 A 的保留集可以被认为是只能通过对象 A 被直接或间接访问到的所有对象的集合\njprofiler\n功能强大 更细致 有出引用和入引用 能定位到某行代码 ","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/49167k49/","title":"03-JVM监控及诊断工具-GUI篇"},{"content":" 3. 运行时数据区及程序计数器 3.1. 运行时数据区 3.1.1. 概述 本节主要讲的是运行时数据区，也就是下图这部分，它是在类加载完成后的阶段\n当我们通过前面的：类的加载-\u0026gt; 验证 -\u0026gt; 准备 -\u0026gt; 解析 -\u0026gt; 初始化 这几个阶段完成后，就会用到执行引擎对我们的类进行使用，同时执行引擎将会使用到我们运行时数据区\n内存是非常重要的系统资源，是硬盘和 CPU 的中间仓库及桥梁，承载着操作系统和应用程序的实时运行 JVM 内存布局规定了 Java 在运行过程中内存申请、分配、管理的策略，保证了 JVM 的高效稳定运行。不同的 JVM 对于内存的划分方式和管理机制存在着部分差异。结合 JVM 虚拟机规范，来探讨一下经典的 JVM 内存布局。\n我们把大厨后面的东西（切好的菜，刀，调料），比作是运行时数据区。而厨师可以类比于执行引擎，将通过准备的东西进行制作成精美的菜品\n我们通过磁盘或者网络 IO 得到的数据，都需要先加载到内存中，然后 CPU 从内存中获取数据进行读取，也就是说内存充当了 CPU 和磁盘之间的桥梁\nJava 虚拟机定义了若干种程序运行期间会使用到的运行时数据区，其中有一些会随着虚拟机启动而创建，随着虚拟机退出而销毁。另外一些则是与线程一一对应的，这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。\n灰色的为单独线程私有的，红色的为多个线程共享的。即：\n每个线程：独立包括程序计数器、栈、本地栈。 线程间共享：堆、堆外内存（永久代或元空间、代码缓存） 每个 JVM 只有一个 Runtime 实例。即为运行时环境，相当于内存结构的中间的那个框框：运行时环境。\n3.1.2. 线程 线程是一个程序里的运行单元。JVM 允许一个应用有多个线程并行的执行。 在 Hotspot JVM 里，每个线程都与操作系统的本地线程直接映射。\n当一个 Java 线程准备好执行以后，此时一个操作系统的本地线程也同时创建。Java 线程执行终止后，本地线程也会回收。\n操作系统负责所有线程的安排调度到任何一个可用的 CPU 上。一旦本地线程初始化成功，它就会调用 Java 线程中的 run()方法。\n3.1.3. JVM 系统线程 如果你使用 console 或者是任何一个调试工具，都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main(String[] args)的 main 线程以及所有这个 main 线程自己创建的线程。\n这些主要的后台系统线程在 Hotspot JVM 里主要是以下几个：\n虚拟机线程：这种线程的操作是需要 JVM 达到安全点才会出现。这些操作必须在不同的线程中发生的原因是他们都需要 JVM 达到安全点，这样堆才不会变化。这种线程的执行类型包括\u0026quot;stop-the-world\u0026quot;的垃圾收集，线程栈收集，线程挂起以及偏向锁撤销。 周期任务线程：这种线程是时间周期事件的体现（比如中断），他们一般用于周期性操作的调度执行。 GC 线程：这种线程对在 JVM 里不同种类的垃圾收集行为提供了支持。 编译线程：这种线程在运行时会将字节码编译成到本地代码。 信号调度线程：这种线程接收信号并发送给 JVM，在它内部通过调用适当的方法进行处理。 3.2. 程序计数器(PC 寄存器) JVM 中的程序计数寄存器（Program Counter Register）中，Register 的命名源于 CPU 的寄存器，寄存器存储指令相关的现场信息。CPU 只有把数据装载到寄存器才能够运行。这里，并非是广义上所指的物理寄存器，或许将其翻译为 PC 计数器（或指令计数器）会更加贴切（也称为程序钩子），并且也不容易引起一些不必要的误会。JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟。\n作用\nPC 寄存器用来存储指向下一条指令的地址，也即将要执行的指令代码。由执行引擎读取下一条指令。\n它是一块很小的内存空间，几乎可以忽略不记。也是运行速度最快的存储区域。\n在 JVM 规范中，每个线程都有它自己的程序计数器，是线程私有的，生命周期与线程的生命周期保持一致。\n任何时间一个线程都只有一个方法在执行，也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址；或者，如果是在执行 native 方法，则是未指定值（undefined）。\n它是程序控制流的指示器，分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。\n字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。\n它是唯一一个在 Java 虚拟机规范中没有规定任何 OutofMemoryError 情况的区域。\n举例说明\n1 2 3 4 5 public int minus(){ intc = 3; intd = 4; return c - d; } Copied! 字节码文件：\n1 2 3 4 5 6 7 8 0: iconst_3 1: istore_1 2: iconst_4 3: istore_2 4: iload_1 5: iload_2 6: isub 7: ireturn Copied! 使用 PC 寄存器存储字节码指令地址有什么用呢？为什么使用 PC 寄存器记录当前线程的执行地址呢？\n因为 CPU 需要不停的切换各个线程，这时候切换回来以后，就得知道接着从哪开始继续执行。\nJVM 的字节码解释器就需要通过改变 PC 寄存器的值来明确下一条应该执行什么样的字节码指令。\nPC 寄存器为什么被设定为私有的？\n我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法，CPU 会不停地做任务切换，这样必然导致经常中断或恢复，如何保证分毫无差呢？为了能够准确地记录各个线程正在执行的当前字节码指令地址，最好的办法自然是为每一个线程都分配一个 PC 寄存器，这样一来各个线程之间便可以进行独立计算，从而不会出现相互干扰的情况。\n由于 CPU 时间片轮限制，众多线程在并发执行过程中，任何一个确定的时刻，一个处理器或者多核处理器中的一个内核，只会执行某个线程中的一条指令。\n这样必然导致经常中断或恢复，如何保证分毫无差呢？每个线程在创建后，都会产生自己的程序计数器和栈帧，程序计数器在各个线程之间互不影响。\nCPU 时间片\nCPU 时间片即 CPU 分配给各个程序的时间，每个线程被分配一个时间段，称作它的时间片。\n在宏观上：俄们可以同时打开多个应用程序，每个程序并行不悖，同时运行。\n但在微观上：由于只有一个 CPU，一次只能处理程序要求的一部分，如何处理公平，一种方法就是引入时间片，每个程序轮流执行。\n","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/84511h84/","title":"03-运行时数据区及程序计数器"},{"content":" 4. JVM 运行时参数 4.1. JVM 参数选项 官网地址：https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html 4.1.1. 类型一：标准参数选项 比较稳定，后续版本基本不会变化 直接在DOS窗口中运行java或者java -help可以看到所有的标准选项 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 \u0026gt; java -help 用法: java [-options] class [args...] (执行类) 或 java [-options] -jar jarfile [args...] (执行 jar 文件) 其中选项包括: -d32 使用 32 位数据模型 (如果可用) -d64 使用 64 位数据模型 (如果可用) -server 选择 \u0026#34;server\u0026#34; VM 默认 VM 是 server. -cp \u0026lt;目录和 zip/jar 文件的类搜索路径\u0026gt; -classpath \u0026lt;目录和 zip/jar 文件的类搜索路径\u0026gt; 用 ; 分隔的目录, JAR 档案 和 ZIP 档案列表, 用于搜索类文件。 -D\u0026lt;名称\u0026gt;=\u0026lt;值\u0026gt; 设置系统属性 -verbose:[class|gc|jni] 启用详细输出 -version 输出产品版本并退出 -version:\u0026lt;值\u0026gt; 警告: 此功能已过时, 将在 未来发行版中删除。 需要指定的版本才能运行 -showversion 输出产品版本并继续 -jre-restrict-search | -no-jre-restrict-search 警告: 此功能已过时, 将在 未来发行版中删除。 在版本搜索中包括/排除用户专用 JRE -? -help 输出此帮助消息 -X 输出非标准选项的帮助 -ea[:\u0026lt;packagename\u0026gt;...|:\u0026lt;classname\u0026gt;] -enableassertions[:\u0026lt;packagename\u0026gt;...|:\u0026lt;classname\u0026gt;] 按指定的粒度启用断言 -da[:\u0026lt;packagename\u0026gt;...|:\u0026lt;classname\u0026gt;] -disableassertions[:\u0026lt;packagename\u0026gt;...|:\u0026lt;classname\u0026gt;] 禁用具有指定粒度的断言 -esa | -enablesystemassertions 启用系统断言 -dsa | -disablesystemassertions 禁用系统断言 -agentlib:\u0026lt;libname\u0026gt;[=\u0026lt;选项\u0026gt;] 加载本机代理库 \u0026lt;libname\u0026gt;, 例如 -agentlib:hprof 另请参阅 -agentlib:jdwp=help 和 -agentlib:hprof=help -agentpath:\u0026lt;pathname\u0026gt;[=\u0026lt;选项\u0026gt;] 按完整路径名加载本机代理库 -javaagent:\u0026lt;jarpath\u0026gt;[=\u0026lt;选项\u0026gt;] 加载 Java 编程语言代理, 请参阅 java.lang.instrument -splash:\u0026lt;imagepath\u0026gt; 使用指定的图像显示启动屏幕 有关详细信息, 请参阅 http://www.oracle.com/technetwork/java/javase/documentation/index.html。 Copied! Server 模式和 Client 模式\nHotspot JVM 有两种模式，分别是 server 和 client，分别通过-server 和-client 模式设置\n32 位系统上，默认使用 Client 类型的 JVM。要想使用 Server 模式，机器配置至少有 2 个以上的 CPU 和 2G 以上的物理内存。client 模式适用于对内存要求较小的桌面应用程序，默认使用 Serial 串行垃圾收集器 64 位系统上，只支持 server 模式的 JVM，适用于需要大内存的应用程序，默认使用并行垃圾收集器 官网地址：https://docs.oracle.com/javase/8/docs/technotes/guides/vm/server-class.html 如何知道系统默认使用的是那种模式呢？\n通过 java -version 命令：可以看到 Server VM 字样，代表当前系统使用是 Server 模式\n1 2 3 4 \u0026gt; java -version java version \u0026#34;1.8.0_201\u0026#34; Java(TM) SE Runtime Environment (build 1.8.0_201-b09) Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode) Copied! 4.1.2. 类型二：-X 参数选项 非标准化参数 功能还是比较稳定的。但官方说后续版本可能会变更 直接在DOS窗口中运行java -X命令可以看到所有的X选项 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 37 38 39 40 41 42 \u0026gt; java -X -Xmixed 混合模式执行 (默认) -Xint 仅解释模式执行 -Xbootclasspath:\u0026lt;用 ; 分隔的目录和 zip/jar 文件\u0026gt; 设置搜索路径以引导类和资源 -Xbootclasspath/a:\u0026lt;用 ; 分隔的目录和 zip/jar 文件\u0026gt; 附加在引导类路径末尾 -Xbootclasspath/p:\u0026lt;用 ; 分隔的目录和 zip/jar 文件\u0026gt; 置于引导类路径之前 -Xdiag 显示附加诊断消息 -Xnoclassgc 禁用类垃圾收集 -Xincgc 启用增量垃圾收集 -Xloggc:\u0026lt;file\u0026gt; 将 GC 状态记录在文件中 (带时间戳) -Xbatch 禁用后台编译 -Xms\u0026lt;size\u0026gt; 设置初始 Java 堆大小 -Xmx\u0026lt;size\u0026gt; 设置最大 Java 堆大小 -Xss\u0026lt;size\u0026gt; 设置 Java 线程堆栈大小 -Xprof 输出 cpu 配置文件数据 -Xfuture 启用最严格的检查, 预期将来的默认值 -Xrs 减少 Java/VM 对操作系统信号的使用 (请参阅文档) -Xcheck:jni 对 JNI 函数执行其他检查 -Xshare:off 不尝试使用共享类数据 -Xshare:auto 在可能的情况下使用共享类数据 (默认) -Xshare:on 要求使用共享类数据, 否则将失败。 -XshowSettings 显示所有设置并继续 -XshowSettings:all 显示所有设置并继续 -XshowSettings:vm 显示所有与 vm 相关的设置并继续 -XshowSettings:properties 显示所有属性设置并继续 -XshowSettings:locale 显示所有与区域设置相关的设置并继续 -X 选项是非标准选项, 如有更改, 恕不另行通知。 以下选项为 Mac OS X 特定的选项: -XstartOnFirstThread 在第一个 (AppKit) 线程上运行 main() 方法 -Xdock:name=\u0026lt;应用程序名称\u0026gt;\u0026#34; 覆盖停靠栏中显示的默认应用程序名称 -Xdock:icon=\u0026lt;图标文件的路径\u0026gt; 覆盖停靠栏中显示的默认图标 Copied! 如何知道 JVM 默认使用的是混合模式呢？\n-Xint 只使用解释器：所有字节码都被解释执行，这个模式的速度是很慢的 -Xcomp 只使用编译器：所有字节码第一次使用就被编译成本地代码，然后在执行 -Xmixed 混合模式：这是默认模式，刚开始的时候使用解释器慢慢解释执行，后来让JIT即时编译器根据程序运行的情况，有选择地将某些热点代码提前编译并缓存在本地，在执行的时候效率就非常高了 同样地，通过 java -version 命令：可以看到 mixed mode 字样，代表当前系统使用的是混合模式\n-Xms 设置初始 Java 堆大小 等价于 -XX:InitialHeapSize -Xmx 设置最大 Java 堆大小 等价于 -XX:MaxHeapSize -Xss 设置 Java 线程堆栈大小 等价于 -XX:ThradeStackSize 4.1.3. 类型三：-XX 参数选项 非标准化参数 使用的最多的参数类型 这类选项属于实验性，不稳定 用于开发和调试JVM Boolean 类型格式\n1 2 -XX:+\u0026lt;option\u0026gt; 启用option属性 -XX:-\u0026lt;option\u0026gt; 禁用option属性 Copied! 非 Boolean 类型格式\n1 2 -XX:\u0026lt;option\u0026gt;=\u0026lt;number\u0026gt; 设置option数值，可以带单位如k/K/m/M/g/G -XX:\u0026lt;option\u0026gt;=\u0026lt;string\u0026gt; 设置option字符值 Copied! 特别的\n-XX:+PrintFlagsFinal 输出所有参数的名称和默认值 默认不包括Diagnostic和Experimental的参数 可以配合-XX:+UnlockDiagnosticVMOptions和-XX:UnlockExperimentalVMOptions使用 4.2 查看参数 java -XX:+PrintFlagsInitial 查看所有 JVM 参数启动的初始值\n1 2 3 4 5 6 [Global flags] intx ActiveProcessorCount = -1 {product} uintx AdaptiveSizeDecrementScaleFactor = 4 {product} uintx AdaptiveSizeMajorGCDecayTimeScale = 10 {product} uintx AdaptiveSizePausePolicy = 0 {product} ... Copied! java -XX:+PrintFlagsFinal 查看所有 JVM 参数的最终值\n1 2 3 4 5 6 7 [Global flags] intx ActiveProcessorCount = -1 {product} ... intx CICompilerCount := 4 {product} uintx InitialHeapSize := 333447168 {product} uintx MaxHeapSize := 1029701632 {product} uintx MaxNewSize := 1774714880 {product} Copied! java -XX:+PrintCommandLineFlags 查看哪些已经被用户或者 JVM 设置过的详细的 XX 参数的名称和值\n1 -XX:InitialHeapSize=332790016 -XX:MaxHeapSize=5324640256 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC Copied! 4.3 添加 JVM 参数选项 eclipse 和 idea 中配置不必多说，在 Run Configurations 中 VM Options 中配置即可，大同小异\n运行 jar 包\n1 java -Xms100m -Xmx100m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -jar demo.jar Copied! Tomcat 运行 war 包\n1 2 3 4 # linux下catalina.sh添加 JAVA_OPTS=\u0026#34;-Xms512M -Xmx1024M\u0026#34; # windows下catalina.bat添加 set \u0026#34;JAVA_OPTS=-Xms512M -Xmx1024M\u0026#34; Copied! 程序运行中\n1 2 3 4 # 设置Boolean类型参数 jinfo -flag [+|-]\u0026lt;name\u0026gt; \u0026lt;pid\u0026gt; # 设置非Boolean类型参数 jinfo -flag \u0026lt;name\u0026gt;=\u0026lt;value\u0026gt; \u0026lt;pid\u0026gt; Copied! 4.4. 常用的 JVM 参数选项 4.4.1. 打印设置的 XX 选项及值 1 2 3 4 -XX:+PrintCommandLineFlags #程序运行时JVM默认设置或用户手动设置的XX选项 -XX:+PrintFlagsInitial #打印所有XX选项的默认值 -XX:+PrintFlagsFinal #打印所有XX选项的实际值 -XX:+PrintVMOptions #打印JVM的参数 Copied! 4.4.2. 堆、栈、方法区等内存大小设置 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 # 栈 -Xss128k \u0026lt;==\u0026gt; -XX:ThreadStackSize=128k #设置线程栈的大小为128K # 堆 -Xms2048m \u0026lt;==\u0026gt; -XX:InitialHeapSize=2048m #设置JVM初始堆内存为2048M -Xmx2048m \u0026lt;==\u0026gt; -XX:MaxHeapSize=2048m #设置JVM最大堆内存为2048M -Xmn2g \u0026lt;==\u0026gt; -XX:NewSize=2g -XX:MaxNewSize=2g #设置年轻代大小为2G，官方推荐配置为整个堆大小的3/8 -XX:NewSize=1024m #设置年轻代初始值为1024M -XX:MaxNewSize=1024m #设置年轻代最大值为1024M -XX:SurvivorRatio=8 #设置Eden区与Survivor区的比值，默认为8 -XX:NewRatio=2 #设置老年代与年轻代(包括1个Eden区和2个Survivor区)的比例，默认为2 -XX:+UseAdaptiveSizePolicy #设置新生代中各区大小比例自适应，默认开启 #默认情况下UseAdaptiveSizePolicy开启，导致SurvivorRatio=8失效；如果要让SurvivorRatio生效，则需要关闭UseAdaptiveSizePolicy并且显式的给SurvivorRatio赋值才行：-XX:-UseAdaptiveSizePolicy -XX:SurvivorRatio=8 -XX:PretenureSizeThreadshold=1024 #设置让大于此阈值的对象直接分配在老年代，单位为字节，只对Serial、ParNew收集器有效 -XX:MaxTenuringThreshold=15 #设置新生代晋升老年代的年龄限制，默认为15 -XX:+PrintTenuringDistribution #让JVM在每次MinorGC后打印出当前使用的Survivor中对象的年龄分布 -XX:TargetSurvivorRatio #设置MinorGC结束后Survivor区中占用空间的期望比例 # 方法区 -XX:PermSize=256m #设置永久代初始值为256M -XX:MaxPermSize=256m #设置永久代最大值为256M -XX:MetaspaceSize #设置元空间初始值 -XX:MaxMetaspaceSize #设置元空间最大值，默认没有限制 -XX:+UseCompressedOops #普通对象指针压缩，默认开启 -XX:+UseCompressedClassPointers #类指针压缩，默认开启 #开启UseCompressedOops会默认开启UseCompressedClassPointers -XX:CompressedClassSpaceSize #设置Klass Metaspace的大小，默认1G # 直接内存 -XX:MaxDirectMemorySize #指定DirectMemory容量，默认等于Java堆最大值 Copied! 4.4.3. OutOfMemory 相关的选项 1 2 3 4 5 6 -XX:+HeapDumpOnOutMemoryError #内存出现OOM时生成Heap转储文件，以便后续分析 -XX:+HeapDumpBeforeFullGC #出现FullGC时生成Heap转储文件，以便后续分析 #-XX:+HeapDumpBeforeFullGC和-XX:+HeapDumpOnOutMemoryError只能设置1个，请注意FullGC可能出现多次，那么dump文件也会生成多个 -XX:HeapDumpPath=\u0026lt;path\u0026gt; #指定heap转储文件的存储路径，默认当前目录 -XX:OnOutOfMemoryError=\u0026lt;path\u0026gt; #指定可行性程序或脚本的路径，当发生OOM时执行脚本 Copied! 4.4.4. 垃圾收集器相关选项 首先需了解垃圾收集器之间的搭配使用关系\n红色虚线表示在 jdk8 时被 Deprecate，jdk9 时被删除 绿色虚线表示在 jdk14 时被 Deprecate 绿色虚框表示在 jdk9 时被 Deprecate，jdk14 时被删除 1 2 3 4 5 6 7 # Serial回收器(Serial使用复制算法，SerialOld使用标记整理算法) -XX:+UseSerialGC #年轻代使用Serial GC， 老年代使用SerialOldGC # ParNew回收器(使用复制算法) -XX:+UseParNewGC #年轻代使用ParNew GC 老年代默认为SerialOldGC，可以改为CMS -XX:ParallelGCThreads #设置年轻代并行收集器的线程数。 #一般地，最好与CPU数量相等，以避免过多的线程数影响垃圾收集性能。 Copied! $$ ParallelGCThreads = \\begin{cases} CPU_Count \u0026amp; \\text (CPU_Count \u0026lt;= 8) \\ 3 + (5 * CPU＿Count / 8) \u0026amp; \\text (CPU_Count \u0026gt; 8) \\end{cases} $$\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # Parallel回收器（JDK8默认使用的回收器，Parallel使用复制算法，ParallelOld使用标记整理算法） -XX:+UseParallelGC #年轻代使用 Parallel Scavenge GC，互相激活 -XX:+UseParallelOldGC #老年代使用 Parallel Old GC，互相激活 -XX:ParallelGCThreads #设置年轻代并行回收器的线程数，默认情况下： #CPU核心数小于8时，线程数==cpu核心数 #CPU核心数大于8时，线程数==3+(5*CPU核心数)/8 -XX:GCTimeRatio #垃圾收集时间占总时间的比例（1 / (N＋1)），用于衡量吞吐量的大小 #取值范围（0,100），默认值99，也就是垃圾回收时间不超过1％。 #与MaxGCPauseMillis参数有一定矛盾性。暂停时间越长，Radio参数就容易超过设定的比例。 #GCTimeRatio越大，吞吐量越大 -XX:MaxGCPauseMillis #设置垃圾收集器最大停顿时间（即STW的时间），单位是毫秒。 #为了尽可能地把停顿时间控制在MaxGCPauseMills以内，收集器在工作时会调整Java堆大小或者其他一些参数 #对于用户来讲，停顿时间越短体验越好；但是服务器端注重高并发，整体的吞吐量。 #所以服务器端适合Parallel，进行控制。该参数使用需谨慎。 -XX:+UseAdaptiveSizePolicy #设置Parallel Scavenge收集器具有自适应调节策略。 #在这种模式下，年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整，以达到在堆大小、吞吐量和停顿时间之间的平衡点。 #在手动调优比较困难的场合，可以直接使用这种自适应的方式，仅指定虚拟机的最大堆、目标的吞吐量（GCTimeRatio）和停顿时间（MaxGCPauseMills），让虚拟机自己完成调优工作。 Copied! 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 37 38 39 # CMS回收器(使用标记清除算法，ParNew+CMS) # JDK9中CMS被标记为Deprecate，JDK14中删除 -XX:+UseConcMarkSweepGC #老年代使用CMS GC。 #开启该参数后会自动将-XX：＋UseParNewGC打开。即：ParNew（Young区）+ CMS（Old区）+ Serial Old的组合 -XX:CMSInitiatingOccupanyFraction #设置堆内存使用率的阈值，一旦达到该阈值，便开始进行回收。JDK5及以前版本的默认值为68，DK6及以上版本默认值为92％。 #如果内存增长缓慢，则可以设置一个稍大的值，大的阈值可以有效降低CMS的触发频率，减少老年代回收的次数可以较为明显地改善应用程序性能。 #反之，如果应用程序内存使用率增长很快，则应该降低这个阈值，以避免频繁触发老年代串行收集器。 #因此通过该选项便可以有效降低Fu1l GC的执行次数。 -XX:+UseCMSInitiatingOccupancyOnly #是否动态可调，使CMS一直按CMSInitiatingOccupancyFraction设定的值启动；如果不指定，jvm仅在第一次使用设定值，后续会自动调整阈值，不要用 -XX:+UseCMSCompactAtFullCollection #用于指定在执行完Full GC后对内存空间进行压缩整理 #以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行，所带来的问题就是停顿时间变得更长了。 -XX:CMSFullGCsBeforeCompaction #设置在执行多少次Full GC后对内存空间进行压缩整理。 -XX:ParallelCMSThreads #设置CMS的线程数量。 #CMS 默认启动的线程数是(ParallelGCThreads＋3)/4，ParallelGCThreads 是年轻代并行收集器的线程数。 #当CPU 资源比较紧张时，受到CMS收集器线程的影响，应用程序的性能在垃圾回收阶段可能会非常糟糕。 ####补充参数： -XX:ConcGCThreads #设置并发垃圾收集的线程数，默认该值是基于ParallelGCThreads计算出来的 -XX:+CMSScavengeBeforeRemark #强制hotspot在cms remark阶段之前做一次minor gc，用于提高remark阶段的速度 -XX:+CMSClassUnloadingEnable #如果有的话，启用回收Perm 区（JDK8之前） -XX:+CMSParallelInitialEnabled #用于开启CMS initial-mark阶段采用多线程的方式进行标记 #用于提高标记速度，在Java8开始已经默认开启 -XX:+CMSParallelRemarkEnabled #用户开启CMS remark阶段采用多线程的方式进行重新标记，默认开启 -XX:+ExplicitGCInvokesConcurrent -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses #这两个参数用户指定hotspot虚拟在执行System.gc()时使用CMS周期 -XX:+CMSPrecleaningEnabled #指定CMS是否需要进行Pre cleaning阶段 Copied! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # G1回收器（使用分区复制算法，可以回收新生代和老年代） -XX:+UseG1GC #手动指定使用G1收集器执行内存回收任务。 -XX:G1HeapRegionSize #设置每个Region的大小。 #值是2的幂，范围是1MB到32MB之间，目标是根据最小的Java堆大小划分出约2048个区域。默认是堆内存的1/2000。 -XX:MaxGCPauseMillis #设置期望达到的最大GC停顿时间指标（JVM会尽力实现，但不保证达到）。默认值是200ms -XX:ParallelGCThread #设置STW时GC线程数的值。最多设置为8 -XX:ConcGCThreads #设置并发标记的线程数。将n设置为并行垃圾回收线程数（ParallelGCThreads）的1/4左右。 -XX:InitiatingHeapOccupancyPercent #设置触发并发GC周期的Java堆占用率阈值。超过此值，就触发GC。默认值是45。 #G1不要用 -Xmn和NewRatio设置新生代空间大小或比例，会影响默认设置的暂停时间 -XX:G1NewSizePercent #新生代占用整个堆内存的最小百分比（默认5％） -XX:G1MaxNewSizePercent #新生代占用整个堆内存的最大百分比（默认60％） -XX:G1ReservePercent=10 #默认10%。也就是老年代会预留10%的空间来给新生代的对象晋升，如果经常发生新生代晋升失败而导致Full GC，那么可以适当调高此阈值。但是调高此值同时也意味着降低了老年代的实际可用空间。 Copied! 怎么选择垃圾回收器？\n优先让 JVM 自适应，调整堆的大小 串行收集器：内存小于 100M；单核、单机程序，并且没有停顿时间的要求 并行收集器：多 CPU、高吞吐量、允许停顿时间超过 1 秒 并发收集器：多 CPU、追求低停顿时间、快速响应（比如延迟不能超过 1 秒，如互联网应用） 官方推荐 G1，性能高。现在互联网的项目，基本都是使用 G1 特别说明：\n没有最好的收集器，更没有万能的收集器 调优永远是针对特定场景、特定需求，不存在一劳永逸的收集器 4.4.5. GC 日志相关选项 1 2 3 4 5 6 7 8 9 10 -XX:+PrintGC \u0026lt;==\u0026gt; -verbose:gc #打印简要日志信息，可以独立使用 -XX:+PrintGCDetails #在发生垃圾回收时打印内存回收详细的日志，并在进程退出时输出当前内存各区域的分配情况，可以独立使用 -XX:+PrintGCTimeStamps #打印程序启动到GC发生的秒数，不可以可以独立使用，搭配-XX:+PrintGCDetails使用 -XX:+PrintGCDateStamps #打印GC发生时的时间戳(以日期的形式，例如：2013-05-04T21:53:59.234+0800)，不可以独立使用，搭配-XX:+PrintGCDetails使用 -XX:+PrintHeapAtGC #每一次GC前和GC后，都打印堆信息，可以独立使用，如下图 -Xloggc:\u0026lt;file\u0026gt; #把GC日志写入到一个文件中去，而不是打印到标准输出中 Copied! 1 2 3 4 5 6 7 8 -XX:+TraceClassLoading #监控 ，类的加载 -XX:+PrintGCApplicationStoppedTime #打印GC时线程的停顿时间 -XX:+PrintGCApplicationConcurrentTime #打印垃圾收集之前应用未中断的执行时间 -XX:+PrintReferenceGC #打印回收了多少种不同引用类型的引用 -XX:+PrintTenuringDistribution #打印JVM在每次MinorGC后当前使用的Survivor中对象的年龄分布 -XX:+UseGCLogFileRotation #启用GC日志文件的自动转储 -XX:NumberOfGCLogFiles=1 #设置GC日志文件的循环数目 -XX:GCLogFileSize=1M #设置GC日志文件的大小 Copied! 4.4.6. 其他参数 1 2 3 4 5 6 7 8 9 10 11 12 -XX:+DisableExplicitGC #禁用hotspot执行System.gc()，默认关闭 #指定代码缓存的大小 -XX:ReservedCodeCacheSize=\u0026lt;n\u0026gt;[g|m|k] -XX:InitialCodeCacheSize=\u0026lt;n\u0026gt;[g|m|k] -XX:+UseCodeCacheFlushing #放弃一些被编译的代码，避免代码缓存被占满时JVM切换到interpreted-only的情况，默认开启 -XX:+DoEscapeAnalysis #开启逃逸分析,64位jvm默认开启 -XX:+UseBiasedLocking #开启偏向锁,64位jvm默认开启 -XX:+UseLargePages #开启使用大页面 -XX:+PrintTLAB #打印TLAB的使用情况 -XX:TLABSize #设置TLAB大小 Copied! 4.5. 通过 Java 代码获取 JVM 参数 Java 提供了 java.lang.management 包用于监视和管理 Java 虚拟机和 Java 运行时中的其他组件，它允许本地或远程监控和管理运行的 Java 虚拟机。其中 ManagementFactory 类较为常用，另外 Runtime 类可获取内存、CPU 核数等相关的数据。通过使用这些 api，可以监控应用服务器的堆内存使用情况，设置一些阈值进行报警等处理。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class MemoryMonitor { public static void main(String[] args) { MemoryMXBean memorymbean = ManagementFactory.getMemoryMXBean(); MemoryUsage usage = memorymbean.getHeapMemoryUsage(); System.out.println(\u0026#34;INIT HEAP: \u0026#34; + usage.getInit() / 1024 / 1024 + \u0026#34;m\u0026#34;); System.out.println(\u0026#34;MAX HEAP: \u0026#34; + usage.getMax() / 1024 / 1024 + \u0026#34;m\u0026#34;); System.out.println(\u0026#34;USE HEAP: \u0026#34; + usage.getUsed() / 1024 / 1024 + \u0026#34;m\u0026#34;); System.out.println(\u0026#34;\\nFull Information:\u0026#34;); System.out.println(\u0026#34;Heap Memory Usage: \u0026#34; + memorymbean.getHeapMemoryUsage()); System.out.println(\u0026#34;Non-Heap Memory Usage: \u0026#34; + memorymbean.getNonHeapMemoryUsage()); System.out.println(\u0026#34;=======================通过java来获取相关系统状态============================ \u0026#34;); System.out.println(\u0026#34;当前堆内存大小totalMemory \u0026#34; + (int) Runtime.getRuntime().totalMemory() / 1024 / 1024 + \u0026#34;m\u0026#34;);// 当前堆内存大小 System.out.println(\u0026#34;空闲堆内存大小freeMemory \u0026#34; + (int) Runtime.getRuntime().freeMemory() / 1024 / 1024 + \u0026#34;m\u0026#34;);// 空闲堆内存大小 System.out.println(\u0026#34;最大可用总堆内存maxMemory \u0026#34; + Runtime.getRuntime().maxMemory() / 1024 / 1024 + \u0026#34;m\u0026#34;);// 最大可用总堆内存大小 } } Copied! ","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/45117945/","title":"04-JVM运行时参数"},{"content":" 5. 分析 GC 日志 5.1. GC 分类 针对 HotSpot VM 的实现，它里面的 GC 按照回收区域又分为两大种类型：一种是部分收集（Partial GC），一种是整堆收集（Full GC）\n部分收集（Partial GC）：不是完整收集整个 Java 堆的垃圾收集。其中又分为：\n新生代收集（Minor GC / Young GC）：只是新生代（Eden / S0, S1）的垃圾收集 老年代收集（Major GC / Old GC）：只是老年代的垃圾收集。目前，只有 CMS GC 会有单独收集老年代的行为。注意，很多时候 Major GC 会和 Full GC 混淆使用，需要具体分辨是老年代回收还是整堆回收。 混合收集（Mixed GC）：收集整个新生代以及部分老年代的垃圾收集。目前，只有 G1 GC 会有这种行为\n整堆收集（Full GC）：收集整个 java 堆和方法区的垃圾收集。\n发生FULL GC的情况： 5.2. GC 日志分类 MinorGC\nMinorGC（或 young GC 或 YGC）日志：\n1 [GC (Allocation Failure) [PSYoungGen: 31744K-\u0026gt;2192K (36864K) ] 31744K-\u0026gt;2200K (121856K), 0.0139308 secs] [Times: user=0.05 sys=0.01, real=0.01 secs] Copied! FullGC\n1 [Full GC (Metadata GC Threshold) [PSYoungGen: 5104K-\u0026gt;0K (132096K) ] [Par01dGen: 416K-\u0026gt;5453K (50176K) ]5520K-\u0026gt;5453K (182272K), [Metaspace: 20637K-\u0026gt;20637K (1067008K) ], 0.0245883 secs] [Times: user=0.06 sys=0.00, real=0.02 secs] Copied! 5.3. GC 日志结构剖析 透过日志看垃圾收集器\nSerial 收集器：新生代显示 \u0026ldquo;[DefNew\u0026rdquo;，即 Default New Generation\nParNew 收集器：新生代显示 \u0026ldquo;[ParNew\u0026rdquo;，即 Parallel New Generation\nParallel Scavenge 收集器：新生代显示\u0026quot;[PSYoungGen\u0026quot;，JDK1.7 使用的即 PSYoungGen\nParallel Old 收集器：老年代显示\u0026quot;[ParoldGen\u0026quot;\nG1 收集器：显示”garbage-first heap“\n透过日志看 GC 原因\nAllocation Failure：表明本次引起 GC 的原因是因为新生代中没有足够的区域存放需要分配的数据 Metadata GCThreshold：Metaspace 区不够用了 FErgonomics：JVM 自适应调整导致的 GC System：调用了 System.gc()方法 透过日志看 GC 前后情况\n通过图示，我们可以发现 GC 日志格式的规律一般都是：GC 前内存占用-＞ GC 后内存占用（该区域内存总大小）\n1 [PSYoungGen: 5986K-\u0026gt;696K (8704K) ] 5986K-\u0026gt;704K (9216K) Copied! 中括号内：GC 回收前年轻代堆大小，回收后大小，（年轻代堆总大小）\n括号外：GC 回收前年轻代和老年代大小，回收后大小，（年轻代和老年代总大小）\n注意：Minor GC 堆内存总容量 = 9/10 年轻代 + 老年代。原因是 Survivor 区只计算 from 部分，而 JVM 默认年轻代中 Eden 区和 Survivor 区的比例关系，Eden:S0:S1=8:1:1。\n透过日志看 GC 时间\nGC 日志中有三个时间：user，sys 和 real\nuser：进程执行用户态代码（核心之外）所使用的时间。这是执行此进程所使用的实际 CPU 时间，其他进程和此进程阻塞的时间并不包括在内。在垃圾收集的情况下，表示 GC 线程执行所使用的 CPU 总时间。 sys：进程在内核态消耗的 CPU 时间，即在内核执行系统调用或等待系统事件所使用的 CPU 时间 real：程序从开始到结束所用的时钟时间。这个时间包括其他进程使用的时间片和进程阻塞的时间（比如等待 I/O 完成）。对于并行 gc，这个数字应该接近（用户时间＋系统时间）除以垃圾收集器使用的线程数。 由于多核的原因，一般的 GC 事件中，real time 是小于 sys time ＋ user time 的，因为一般是多个线程并发的去做 GC，所以 real time 是要小于 sys ＋ user time 的。如果 real ＞ sys ＋ user 的话，则你的应用可能存在下列问题：IO 负载非常重或 CPU 不够用。\n5.4. GC 日志分析工具 GCEasy\nGCEasy 是一款在线的 GC 日志分析器，可以通过 GC 日志分析进行内存泄露检测、GC 暂停原因分析、JVM 配置建议优化等功能，大多数功能是免费的。\n官网地址：https://gceasy.io/ GCViewer\nGCViewer 是一款离线的 GC 日志分析器，用于可视化 Java VM 选项 -verbose:gc 和 .NET 生成的数据 -Xloggc:\u0026lt;file\u0026gt;。还可以计算与垃圾回收相关的性能指标（吞吐量、累积的暂停、最长的暂停等）。当通过更改世代大小或设置初始堆大小来调整特定应用程序的垃圾回收时，此功能非常有用。\n源码下载：https://github.com/chewiebug/GCViewer 运行版本下载：https://github.com/chewiebug/GCViewer/wiki/Changelog GChisto\n官网上没有下载的地方，需要自己从 SVN 上拉下来编译 不过这个工具似乎没怎么维护了，存在不少 bug HPjmeter\n工具很强大，但是只能打开由以下参数生成的 GC log，-verbose:gc -Xloggc:gc.log。添加其他参数生成的 gc.log 无法打开 HPjmeter 集成了以前的 HPjtune 功能，可以分析在 HP 机器上产生的垃圾回收日志文件 ","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/95611835/","title":"05-分析GC日志"},{"content":" 6. 堆 6.1. 堆（Heap）的核心概述 堆针对一个 JVM 进程来说是唯一的，也就是一个进程只有一个 JVM，但是进程包含多个线程，他们是共享同一堆空间的。\n一个 JVM 实例只存在一个堆内存，堆也是 Java 内存管理的核心区域。\nJava 堆区在 JVM 启动的时候即被创建，其空间大小也就确定了。是 JVM 管理的最大一块内存空间。\n堆内存的大小是可以调节的。 《Java 虚拟机规范》规定，堆可以处于物理上不连续的内存空间中，但在逻辑上它应该被视为连续的。\n所有的线程共享 Java 堆，在这里还可以划分线程私有的缓冲区（Thread Local Allocation Buffer，TLAB）。\n《Java 虚拟机规范》中对 Java 堆的描述是：所有的对象实例以及数组都应当在运行时分配在堆上。（The heap is the run-time data area from which memory for all class instances and arrays is allocated）\n数组和对象可能永远不会存储在栈上，因为栈帧中保存引用，这个引用指向对象或者数组在堆中的位置。\n在方法结束后，堆中的对象不会马上被移除，仅仅在垃圾收集的时候才会被移除。\n堆，是 GC（Garbage Collection，垃圾收集器）执行垃圾回收的重点区域。\n6.1.1. 堆内存细分 Java 7 及之前堆内存逻辑上分为三部分：新生区+养老区+永久区\nYoung Generation Space 新生区 Young/New 又被划分为 Eden 区和 Survivor 区 Tenure generation space 养老区 Old/Tenure Permanent Space 永久区 Perm Java 8 及之后堆内存逻辑上分为三部分：新生区+养老区+元空间\nYoung Generation Space 新生区 Young/New 又被划分为 Eden 区和 Survivor 区 Tenure generation space 养老区 Old/Tenure Meta Space 元空间 Meta 约定：新生区（代）\u0026lt;=\u0026gt;年轻代 、 养老区\u0026lt;=\u0026gt;老年区（代）、 永久区\u0026lt;=\u0026gt;永久代\n6.1.2. 堆空间内部结构（JDK7） 6.1.3. 堆空间内部结构（JDK8） 6.2. 设置堆内存大小与 OOM 6.2.1. 堆空间大小的设置 Java 堆区用于存储 Java 对象实例，那么堆的大小在 JVM 启动时就已经设定好了，大家可以通过选项\u0026quot;-Xmx\u0026quot;和\u0026quot;-Xms\u0026quot;来进行设置。\n“-Xms\u0026quot;用于表示堆区的起始内存，等价于-XX:InitialHeapSize “-Xmx\u0026quot;则用于表示堆区的最大内存，等价于-XX:MaxHeapSize 一旦堆区中的内存大小超过“-Xmx\u0026quot;所指定的最大内存时，将会抛出 OutOfMemoryError 异常。\n通常会将-Xms 和-Xmx 两个参数配置相同的值，其目的是为了能够在 ava 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小，从而提高性能。\n默认情况下\n初始内存大小：物理电脑内存大小 / 64 最大内存大小：物理电脑内存大小 / 4 6.2.2. OutOfMemory 举例 1 2 3 4 5 6 7 8 9 10 11 12 13 public class OOMTest { public static void main(String[]args){ ArrayList\u0026lt;Picture\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); while(true){ try { Thread.sleep(20); } catch (InterruptedException e){ e.printStackTrace(); } list.add(new Picture(new Random().nextInt(1024*1024))); } } } Copied! 1 2 3 Exception in thread \u0026#34;main\u0026#34; java.lang.OutofMemoryError: Java heap space at com.atguigu. java.Picture.\u0026lt;init\u0026gt;(OOMTest. java:25) at com.atguigu.java.O0MTest.main(OOMTest.java:16) Copied! 6.3. 年轻代与老年代 存储在 JVM 中的 Java 对象可以被划分为两类：\n一类是生命周期较短的瞬时对象，这类对象的创建和消亡都非常迅速 另外一类对象的生命周期却非常长，在某些极端的情况下还能够与 JVM 的生命周期保持一致 Java 堆区进一步细分的话，可以划分为年轻代（YoungGen）和老年代（oldGen）\n其中年轻代又可以划分为 Eden 空间、Survivor0 空间和 Survivor1 空间（有时也叫做 from 区、to 区）\n下面这参数开发中一般不会调：\n配置新生代与老年代在堆结构的占比。\n默认-XX:NewRatio=2，表示新生代占 1，老年代占 2，新生代占整个堆的 1/3 可以修改-XX:NewRatio=4，表示新生代占 1，老年代占 4，新生代占整个堆的 1/5 在 HotSpot 中，Eden 空间和另外两个 survivor 空间缺省所占的比例是 8：1：1\n当然开发人员可以通过选项“-xx:SurvivorRatio”调整这个空间比例。比如-xx:SurvivorRatio=8\n几乎所有的 Java 对象都是在 Eden 区被 new 出来的。绝大部分的 Java 对象的销毁都在新生代进行了。\nIBM 公司的专门研究表明，新生代中 80%的对象都是“朝生夕死”的。 可以使用选项\u0026quot;-Xmn\u0026ldquo;设置新生代最大内存大小，这个参数一般使用默认值就可以了。\n6.4. 图解对象分配过程 为新对象分配内存是一件非常严谨和复杂的任务，JVM 的设计者们不仅需要考虑内存如何分配、在哪里分配等问题，并且由于内存分配算法与内存回收算法密切相关，所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片。\nnew 的对象先放伊甸园区。此区有大小限制。\n当伊甸园的空间填满时，程序又需要创建对象，JVM 的垃圾回收器将对伊甸园区进行垃圾回收（MinorGC），将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区\n然后将伊甸园中的剩余对象移动到幸存者 0 区。\n如果再次触发垃圾回收，此时上次幸存下来的放到幸存者 0 区的，如果没有回收，就会放到幸存者 1 区。\n如果再次经历垃圾回收，此时会重新放回幸存者 0 区，接着再去幸存者 1 区。\n啥时候能去养老区呢？可以设置次数。默认是 15 次。\n可以设置参数：-Xx:MaxTenuringThreshold= N进行设置 在养老区，相对悠闲。当养老区内存不足时，再次触发 GC：Major GC，进行养老区的内存清理\n若养老区执行了 Major GC 之后，发现依然无法进行对象的保存，就会产生 OOM 异常。\n1 java.lang.OutofMemoryError: Java heap space Copied! 流程图\n总结\n针对幸存者 s0，s1 区的总结：复制之后有交换，谁空谁是 to 关于垃圾回收：频繁在新生区收集，很少在老年代收集，几乎不再永久代和元空间进行收集 常用调优工具（在 JVM 下篇：性能监控与调优篇会详细介绍）\nJDK 命令行 Eclipse:Memory Analyzer Tool Jconsole VisualVM Jprofiler Java Flight Recorder GCViewer GC Easy 大对象 虚拟机提供了一个 -XX:PretenureSizeThreshold=3m 参数，大于这个值的参数直接在老年代分配\n6.5. Minor GC，MajorGC、Full GC JVM 在进行 GC 时，并非每次都对上面三个内存区域一起回收的，大部分时候回收的都是指新生代。\n针对 Hotspot VM 的实现，它里面的 GC 按照回收区域又分为两大种类型：一种是部分收集（Partial GC），一种是整堆收集（FullGC）\n部分收集：不是完整收集整个 Java 堆的垃圾收集。其中又分为： 新生代收集（Minor GC / Young GC）：只是新生代的垃圾收集 老年代收集（Major GC / Old GC）：只是老年代的圾收集。 目前，只有 CMSGC 会有单独收集老年代的行为。 注意，很多时候 Major GC 会和 Full GC 混淆使用，需要具体分辨是老年代回收还是整堆回收。 混合收集（MixedGC）：收集整个新生代以及部分老年代的垃圾收集。 目前，只有 G1 GC 会有这种行为 整堆收集（Full GC）：收集整个 java 堆和方法区的垃圾收集。 6.5.1. 最简单的分代式 GC 策略的触发条件 年轻代 GC（Minor GC）触发机制 当年轻代空间不足时，就会触发 MinorGC，这里的年轻代满指的是 Eden 代满，Survivor 满不会引发 GC。（每次 Minor GC 会清理年轻代的内存。）\n因为Java 对象大多都具备朝生夕灭的特性.，所以 Minor GC 非常频繁，一般回收速度也比较快。这一定义既清晰又易于理解。\nMinor GC 会引发 STW，暂停其它用户的线程，等垃圾回收结束，用户线程才恢复运行\n老年代 GC（Major GC / Full GC）触发机制 指发生在老年代的 GC，对象从老年代消失时，我们说 “Major GC” 或 “Full GC” 发生了\n出现了 Major Gc，经常会伴随至少一次的 Minor GC（但非绝对的，在 Paralle1 Scavenge 收集器的收集策略里就有直接进行 MajorGC 的策略选择过程）\n也就是在老年代空间不足时，会先尝试触发 Minor Gc。如果之后空间还不足，则触发 Major GC Major GC 的速度一般会比 Minor GC 慢 10 倍以上，STW 的时间更长\n如果 Major GC 后，内存还不足，就报 OOM 了\nFull GC 触发机制（后面细讲）： 触发 Full GC 执行的情况有如下五种：\n调用 System.gc()时，系统建议执行 Full GC，但是不必然执行 老年代空间不足 方法区空间不足 通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存 由 Eden 区、survivor space0（From Space）区向 survivor space1（To Space）区复制时，对象大小大于 To Space 可用内存，则把该对象转存到老年代，且老年代的可用内存小于该对象大小 说明：Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些\n6.6. 堆空间分代思想 为什么要把 Java 堆分代？不分代就不能正常工作了吗？\n经研究，不同对象的生命周期不同。70%-99%的对象是临时对象。\n新生代：有 Eden、两块大小相同的 survivor（又称为 from/to，s0/s1）构成，to 总为空。 老年代：存放新生代中经历多次 GC 仍然存活的对象。 其实不分代完全可以，分代的唯一理由就是优化 GC 性能。如果没有分代，那所有的对象都在一块，就如同把一个学校的人都关在一个教室。GC 的时候要找到哪些对象没用，这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的，如果分代的话，把新创建的对象放到某一地方，当 GC 的时候先把这块存储“朝生夕死”对象的区域进行回收，这样就会腾出很大的空间出来。\n6.7. 内存分配策略 如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活，并且能被 Survivor 容纳的话，将被移动到 survivor 空间中，并将对象年龄设为 1。对象在 survivor 区中每熬过一次 MinorGC，年龄就增加 1 岁，当它的年龄增加到一定程度（默认为 15 岁，其实每个 JVM、每个 GC 都有所不同）时，就会被晋升到老年代\n对象晋升老年代的年龄阀值，可以通过选项-XX:MaxTenuringThreshold来设置\n针对不同年龄段的对象分配原则如下所示：\n优先分配到 Eden 大对象直接分配到老年代（尽量避免程序中出现过多的大对象） 长期存活的对象分配到老年代 动态对象年龄判断：如果 survivor 区中相同年龄的所有对象大小的总和大于 Survivor 空间的一半，年龄大于或等于该年龄的对象可以直接进入老年代，无须等到MaxTenuringThreshold中要求的年龄。 空间分配担保： -XX:HandlePromotionFailure 6.8. 为对象分配内存：TLAB 6.8.1. 为什么有 TLAB（Thread Local Allocation Buffer）？ 堆区是线程共享区域，任何线程都可以访问到堆区中的共享数据\n由于对象实例的创建在 JVM 中非常频繁，因此在并发环境下从堆区中划分内存空间是线程不安全的\n为避免多个线程操作同一地址，需要使用加锁等机制，进而影响分配速度。\n6.8.2. 什么是 TLAB？ 从内存模型而不是垃圾收集的角度，对 Eden 区域继续进行划分，JVM 为每个线程分配了一个私有缓存区域，它包含在 Eden 空间内。\n多线程同时分配内存时，使用 TLAB 可以避免一系列的非线程安全问题，同时还能够提升内存分配的吞吐量，因此我们可以将这种内存分配方式称之为快速分配策略。\n据我所知所有 OpenJDK 衍生出来的 JVM 都提供了 TLAB 的设计。\n6.8.3. TLAB 的再说明 尽管不是所有的对象实例都能够在 TLAB 中成功分配内存，但JVM 确实是将 TLAB 作为内存分配的首选。\n在程序中，开发人员可以通过选项“-XX:UseTLAB”设置是否开启 TLAB 空间。\n默认情况下，TLAB 空间的内存非常小，仅占有整个 Eden 空间的 1%，当然我们可以通过选项 “-XX:TLABWasteTargetPercent” 设置 TLAB 空间所占用 Eden 空间的百分比大小。\n一旦对象在 TLAB 空间分配内存失败时，JVM 就会尝试着通过使用加锁机制确保数据操作的原子性，从而直接在 Eden 空间中分配内存。\n6.9. 小结：堆空间的参数设置 官网地址：https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 详细的参数内容会在JVM下篇：性能监控与调优篇中进行详细介绍，这里先熟悉下 -XX:+PrintFlagsInitial //查看所有的参数的默认初始值 -XX:+PrintFlagsFinal //查看所有的参数的最终值（可能会存在修改，不再是初始值） jps //查看当前运行的进程 jinfo -flag SurvivorRatio 进程ID //查看某一个当前的参数 -Xms //初始堆空间内存（默认为物理内存的1/64） -Xmx //最大堆空间内存（默认为物理内存的1/4） -Xmn //设置新生代的大小。（初始值及最大值） -XX:NewRatio //配置新生代与老年代在堆结构的占比 -XX:SurvivorRatio //设置新生代中Eden和S0/S1空间的比例 -XX:MaxTenuringThreshold //设置新生代垃圾的最大年龄 -XX:+PrintGCDetails //输出详细的GC处理日志 //打印gc简要信息：①-Xx：+PrintGC ② - verbose:gc -XX:HandlePromotionFalilure：//是否设置空间分配担保 Copied! 在发生 Minor GC 之前，虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。\n如果大于，则此次 Minor GC 是安全的 如果小于，则虚拟机会查看-XX:HandlePromotionFailure设置值是否允担保失败。 如果HandlePromotionFailure=true，那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。 如果大于，则尝试进行一次 Minor GC，但这次 Minor GC 依然是有风险的； 如果小于，则改为进行一次 Full GC。 如果HandlePromotionFailure=false，则改为进行一次 Full Gc。 在 JDK6 Update24 之后，HandlePromotionFailure 参数不会再影响到虚拟机的空间分配担保策略，观察 openJDK 中的源码变化，虽然源码中还定义了 HandlePromotionFailure 参数，但是在代码中已经不会再使用它，默认为true。JDK6 Update 24 之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC，否则将进行 FullGC。\n6.X. 堆是分配对象的唯一选择么？ 在《深入理解 Java 虚拟机》中关于 Java 堆内存有这样一段描述：\n随着 JIT 编译期的发展与逃逸分析技术逐渐成熟，栈上分配、标量替换优化技术将会导致一些微妙的变化，所有的对象都分配到堆上也渐渐变得不那么“绝对”了。\n在 Java 虚拟机中，对象是在 Java 堆中分配内存的，这是一个普遍的常识。但是，有一种特殊情况，那就是如果经过逃逸分析（Escape Analysis）后发现，一个对象并没有逃逸出方法的话，那么就可能被优化成栈上分配.。这样就无需在堆上分配内存，也无须进行垃圾回收了。这也是最常见的堆外存储技术。\n此外，前面提到的基于 OpenJDK 深度定制的 TaoBaoVM，其中创新的 GCIH（GC invisible heap）技术实现 off-heap，将生命周期较长的 Java 对象从 heap 中移至 heap 外，并且 GC 不能管理 GCIH 内部的 Java 对象，以此达到降低 GC 的回收频率和提升 GC 的回收效率的目的。\n6.X.1. 逃逸分析概述 如何将堆上的对象分配到栈，需要使用逃逸分析手段。\n这是一种可以有效减少 Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。\n通过逃逸分析，Java Hotspot 编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。\n逃逸分析的基本行为就是分析对象动态作用域：\n当一个对象在方法中被定义后，对象只在方法内部使用，则认为没有发生逃逸。 当一个对象在方法中被定义后，它被外部方法所引用，则认为发生逃逸。例如作为调用参数传递到其他地方中。 举例 1\n1 2 3 4 5 6 public void my_method() { V v = new V(); // use v // .... v = null; } Copied! 没有发生逃逸的对象，则可以分配到栈上，随着方法执行的结束，栈空间就被移除，每个栈里面包含了很多栈帧\n1 2 3 4 5 6 public static StringBuffer createStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb; } Copied! 上述方法如果想要StringBuffer sb不发生逃逸，可以这样写\n1 2 3 4 5 6 public static String createStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb.toString(); } Copied! 举例 2\n1 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 public class EscapeAnalysis { public EscapeAnalysis obj; /** * 方法返回EscapeAnalysis对象，发生逃逸 * @return */ public EscapeAnalysis getInstance() { return obj == null ? new EscapeAnalysis() : obj; } /** * 为成员属性赋值，发生逃逸 */ public void setObj() { this.obj = new EscapeAnalysis(); } /** * 对象的作用于仅在当前方法中有效，没有发生逃逸 */ public void useEscapeAnalysis() { EscapeAnalysis e = new EscapeAnalysis(); } /** * 引用成员变量的值，发生逃逸 */ public void useEscapeAnalysis2() { EscapeAnalysis e = getInstance(); } } Copied! 参数设置\n在 JDK 6u23 版本之后，HotSpot 中默认就已经开启了逃逸分析\n如果使用的是较早的版本，开发人员则可以通过：\n选项“-XX:+DoEscapeAnalysis\u0026ldquo;显式开启逃逸分析 通过选项“-XX:+PrintEscapeAnalysis\u0026ldquo;查看逃逸分析的筛选结果 结论：开发中能使用局部变量的，就不要使用在方法外定义。\n6.X.2. 逃逸分析：代码优化 使用逃逸分析，编译器可以对代码做如下优化：\n一、栈上分配：将堆分配转化为栈分配。如果一个对象在子程序中被分配，要使指向该对象的指针永远不会发生逃逸，对象可能是栈上分配的候选，而不是堆上分配\n二、同步省略：如果一个对象被发现只有一个线程被访问到，那么对于这个对象的操作可以不考虑同步。\n三、分离对象或标量替换：有的对象可能不需要作为一个连续的内存结构存在也可以被访问到，那么对象的部分（或全部）可以不存储在内存，而是存储在 CPU 寄存器中。\n栈上分配 JIT 编译器在编译期间根据逃逸分析的结果，发现如果一个对象并没有逃逸出方法的话，就可能被优化成栈上分配。分配完成后，继续在调用栈内执行，最后线程结束，栈空间被回收，局部变量对象也被回收。这样就无须进行垃圾回收了。\n常见的栈上分配的场景\n在逃逸分析中，已经说明了。分别是给成员变量赋值、方法返回值、实例引用传递。\n栈上替换(On Stack Replacement)是JIT编译器编译后的代码放入栈上和替换原来的方法，和栈上分配不是同一个东西 同步省略-锁消除 线程同步的代价是相当高的，同步的后果是降低并发性和性能。\n在动态编译同步块的时候，JIT 编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。如果没有，那么 JIT 编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略，也叫锁消除。\n举例\n1 2 3 4 5 6 public void f() { Object hellis = new Object(); synchronized(hellis) { System.out.println(hellis); } } Copied! 代码中对 hellis 这个对象加锁，但是 hellis 对象的生命周期只在 f()方法中，并不会被其他线程所访问到，所以在 JIT 编译阶段就会被优化掉，优化成：\n1 2 3 4 public void f() { Object hellis = new Object(); System.out.println(hellis); } Copied! 标量替换 标量（scalar）是指一个无法再分解成更小的数据的数据。Java 中的原始数据类型就是标量。\n相对的，那些还可以分解的数据叫做聚合量（Aggregate），Java 中的对象就是聚合量，因为他可以分解成其他聚合量和标量。\n在 JIT 阶段，如果经过逃逸分析，发现一个对象不会被外界访问的话，那么经过 JIT 优化，就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。\n举例\n1 2 3 4 5 6 7 8 9 10 11 public static void main(String args[]) { alloc(); } private static void alloc() { Point point = new Point(1,2); System.out.println(\u0026#34;point.x\u0026#34; + point.x + \u0026#34;;point.y\u0026#34; + point.y); } class Point { private int x; private int y; } Copied! 以上代码，经过标量替换后，就会变成\n1 2 3 4 5 private static void alloc() { int x = 1; int y = 2; System.out.println(\u0026#34;point.x = \u0026#34; + x + \u0026#34;; point.y=\u0026#34; + y); } Copied! 可以看到，Point 这个聚合量经过逃逸分析后，发现他并没有逃逸，就被替换成两个标量了。那么标量替换有什么好处呢？就是可以大大减少堆内存的占用。因为一旦不需要创建对象了，那么就不再需要分配堆内存了。 标量替换为栈上分配提供了很好的基础。\n标量替换参数设置\n参数-XX:EliminateAllocations：开启了标量替换（默认打开），允许将对象打散分配到栈上。\n上述代码在主函数中进行了 1 亿次 alloc。调用进行对象创建，由于 User 对象实例需要占据约 16 字节的空间，因此累计分配空间达到将近 1.5GB。如果堆空间小于这个值，就必然会发生 GC。使用如下参数运行上述代码：\n1 -server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations Copied! 这里设置参数如下：\n参数-server：启动 Server 模式，因为在 server 模式下，才可以启用逃逸分析。 参数-XX:+DoEscapeAnalysis：启用逃逸分析 参数-Xmx10m：指定了堆空间最大为 10MB 参数-XX:+PrintGC：将打印 Gc 日志 参数-XX:+EliminateAllocations：开启了标量替换（默认打开），允许将对象打散分配在栈上，比如对象拥有 id 和 name 两个字段，那么这两个字段将会被视为两个独立的局部变量进行分配 6.X.3. 逃逸分析小结：逃逸分析并不成熟 关于逃逸分析的论文在 1999 年就已经发表了，但直到 JDK1.6 才有实现，而且这项技术到如今也并不是十分成熟。\n其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的，这其实也是一个相对耗时的过程。 一个极端的例子，就是经过逃逸分析之后，发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。\n虽然这项技术并不十分成熟，但是它也是即时编译器优化技术中一个十分重要的手段。\n注意到有一些观点，认为通过逃逸分析，JVM 会在栈上分配那些不会逃逸的对象，这在理论上是可行的，但是取决于 JVM 设计者的选择。据我所知，Oracle Hotspot JVM 中并未这么做，这一点在逃逸分析相关的文档里已经说明，所以可以明确所有的对象实例都是创建在堆上。\n目前很多书籍还是基于 JDK7 以前的版本，JDK 已经发生了很大变化，intern 字符串的缓存和静态变量曾经都被分配在永久代上，而永久代已经被元数据区取代。但是，intern 字符串缓存和静态变量并不是被转移到元数据区，而是直接在堆上分配，所以这一点同样符合前面一点的结论：对象实例都是分配在堆上。\n本章小结 年轻代是对象的诞生、成长、消亡的区域，一个对象在这里产生、应用，最后被垃圾回收器收集、结束生命。\n老年代放置长生命周期的对象，通常都是从 survivor 区域筛选拷贝过来的 Java 对象。当然，也有特殊情况，我们知道普通的对象会被分配在 TLAB 上；如果对象较大，JVM 会试图直接分配在 Eden 其他位置上；如果对象太大，完全无法在新生代找到足够长的连续空闲空间，JVM 就会直接分配到老年代。当 GC 只发生在年轻代中，回收年轻代对象的行为被称为 MinorGc。\n当 GC 发生在老年代时则被称为 MajorGc 或者 FullGC。一般的，MinorGc 的发生频率要比 MajorGC 高很多，即老年代中垃圾回收发生的频率将大大低于年轻代。\n","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/3491v834/","title":"06-堆"},{"content":" 8. 对象实例化及直接内存 8.1. 对象实例化 面试题\n美团：\n对象在 JVM 中是怎么存储的？\n对象头信息里面有哪些东西？\n蚂蚁金服：\nJava 对象头有什么？\n8.1.1. 创建对象的方式 new：最常见的方式、Xxx 的静态方法，XxxBuilder/XxxFactory 的静态方法 Class 的 newInstance 方法：反射的方式，只能调用空参的构造器，权限必须是 public Constructor 的 newInstance(XXX)：反射的方式，可以调用空参、带参的构造器，权限没有要求 使用 clone()：不调用任何的构造器，要求当前的类需要实现 Cloneable 接口，实现 clone() 使用序列化：从文件中、从网络中获取一个对象的二进制流 第三方库 Objenesis 8.1.2. 创建对象的步骤 前面所述是从字节码角度看待对象的创建过程，现在从执行步骤的角度来分析：\n1. 判断对象对应的类是否加载、链接、初始化 虚拟机遇到一条 new 指令，首先去检查这个指令的参数能否在 Metaspace 的常量池中定位到一个类的符号引用，并且检查这个符号引用代表的类是否已经被加载，解析和初始化（即判断类元信息是否存在）。\n如果没有，那么在双亲委派模式下，使用当前类加载器以 ClassLoader + 包名 + 类名为 key 进行查找对应的 .class 文件；\n如果没有找到文件，则抛出 ClassNotFoundException 异常 如果找到，则进行类加载，并生成对应的 Class 对象 2. 为对象分配内存 首先计算对象占用空间的大小，接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量，仅分配引用变量空间即可，即 4 个字节大小\n如果内存规整：虚拟机将采用的是指针碰撞法（Bump The Point）来为对象分配内存。\n意思是所有用过的内存在一边，空闲的内存放另外一边，中间放着一个指针作为分界点的指示器，分配内存就仅仅是把指针指向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是 Serial ，ParNew 这种基于压缩算法的，虚拟机采用这种分配方式。一般使用带 Compact（整理）过程的收集器时，使用指针碰撞。 如果内存不规整：虚拟机需要维护一个空闲列表（Free List）来为对象分配内存。\n已使用的内存和未使用的内存相互交错，那么虚拟机将采用的是空闲列表来为对象分配内存。意思是虚拟机维护了一个列表，记录上那些内存块是可用的，再分配的时候从列表中找到一块足够大的空间划分给对象实例，并更新列表上的内容。 选择哪种分配方式由 Java 堆是否规整所决定，而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。\n3. 处理并发问题 采用 CAS 失败重试、区域加锁保证更新的原子性 每个线程预先分配一块 TLAB：通过设置 -XX:+UseTLAB参数来设定 4. 初始化分配到的内存 所有属性设置默认值，保证对象实例字段在不赋值时可以直接使用\n5. 设置对象的对象头 将对象的所属类（即类的元数据信息）、对象的 HashCode 和对象的 GC 信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于 JVM 实现。\n6. 执行 init 方法进行初始化 在 Java 程序的视角看来，初始化才正式开始。初始化成员变量，执行实例化代码块，调用类的构造方法，并把堆内对象的首地址赋值给引用变量。\n因此一般来说（由字节码中跟随 invokespecial 指令所决定），new 指令之后会接着就是执行方法，把对象按照程序员的意愿进行初始化，这样一个真正可用的对象才算完成创建出来。\n给对象属性赋值的操作\n属性的默认初始化 显式初始化 代码块中初始化 构造器中初始化 对象实例化的过程\n加载类元信息 为对象分配内存 处理并发问题 属性的默认初始化（零值初始化） 设置对象头信息 属性的显示初始化、代码块中初始化、构造器中初始化 8.2. 对象内存布局 8.2.1. 对象头（Header） 对象头包含了两部分，分别是运行时元数据（Mark Word）和类型指针。如果是数组，还需要记录数组的长度\n运行时元数据 哈希值（HashCode） GC 分代年龄 锁状态标志 线程持有的锁 偏向线程 ID 翩向时间戳 类型指针 指向类元数据 InstanceKlass，确定该对象所属的类型。\n8.2.2. 实例数据（Instance Data） 它是对象真正存储的有效信息，包括程序代码中定义的各种类型的字段（包括从父类继承下来的和本身拥有的字段）\n相同宽度的字段总是被分配在一起 父类中定义的变量会出现在子类之前 如果 CompactFields 参数为 true（默认为 true）：子类的窄变量可能插入到父类变量的空隙 8.2.3. 对齐填充（Padding） 不是必须的，也没有特别的含义，仅仅起到占位符的作用\n举例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Customer{ int id = 1001; String name; Account acct; { name = \u0026#34;匿名客户\u0026#34;; } public Customer() { acct = new Account(); } } public class CustomerTest{ public static void main(string[] args){ Customer cust=new Customer(); } } Copied! 图示\n小结 8.3. 对象的访问定位 JVM 是如何通过栈帧中的对象引用访问到其内部的对象实例呢？\n8.3.1. 句柄访问 reference 中存储稳定句柄地址，对象被移动（垃圾收集时移动对象很普遍）时只会改变句柄中实例数据指针即可，reference 本身不需要被修改\n8.3.2. 直接指针（HotSpot 采用） 直接指针是局部变量表中的引用，直接指向堆中的实例，在对象实例中有类型指针，指向的是方法区中的对象类型数据\n8.4. 直接内存（Direct Memory） 8.4.1. 直接内存概述 不是虚拟机运行时数据区的一部分，也不是《Java 虚拟机规范》中定义的内存区域。直接内存是在 Java 堆外的、直接向系统申请的内存区间。来源于 NIO，通过存在堆中的 DirectByteBuffer 操作 Native 内存。通常，访问直接内存的速度会优于 Java 堆，即读写性能高。\n因此出于性能考虑，读写频繁的场合可能会考虑使用直接内存。 Java 的 NIO 库允许 Java 程序使用直接内存，用于数据缓冲区 8.4.2. 非直接缓存区 使用 IO 读写文件，需要与磁盘交互，需要由用户态切换到内核态。在内核态时，需要两份内存存储重复数据，效率低。\n8.4.3. 直接缓存区 使用 NIO 时，操作系统划出的直接缓存区可以被 java 代码直接访问，只有一份。NIO 适合对大文件的读写操作。\n也可能导致 OutOfMemoryError 异常\n1 2 3 4 5 Exception in thread \u0026#34;main\u0026#34; java.lang.OutOfMemoryError: Direct buffer memory at java.nio.Bits.reserveMemory(Bits.java:693) at java.nio.DirectByteBuffer.\u0026lt;init\u0026gt;(DirectByteBuffer.java:123) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) at com.atguigu.java.BufferTest2.main(BufferTest2.java:20) Copied! 由于直接内存在 Java 堆外，因此它的大小不会直接受限于-Xmx 指定的最大堆大小，但是系统内存是有限的，Java 堆和直接内存的总和依然受限于操作系统能给出的最大内存。\n分配回收成本较高 不受 JVM 内存回收管理 直接内存大小可以通过MaxDirectMemorySize设置。如果不指定，默认与堆的最大值-Xmx 参数值一致\n","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/16138956/","title":"08-对象实例化及直接内存"},{"content":" 10. StringTable 10.1. String 的基本特性 String：字符串，使用一对\u0026quot;\u0026ldquo;引起来表示 String 声明为 final 的，不可被继承 String 实现了 Serializable 接口：表示字符串是支持序列化的。 String 实现了 Comparable 接口：表示 string 可以比较大小 String 在 jdk8 及以前内部定义了 final char[] value 用于存储字符串数据。JDK9 时改为 byte[] 10.1.1. String 在 jdk9 中存储结构变更 官网地址：JEP 254: Compact Strings (java.net) Motivation The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.\nDescription We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field. The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.\nString-related classes such as AbstractStringBuilder, StringBuilder, and StringBuffer will be updated to use the same representation, as will the HotSpot VM\u0026rsquo;s intrinsic string operations.\nThis is purely an implementation change, with no changes to existing public interfaces. There are no plans to add any new public APIs or other interfaces.\nThe prototyping work done to date confirms the expected reduction in memory footprint, substantial reductions of GC activity, and minor performance regressions in some corner cases.\n动机\n目前 String 类的实现将字符存储在一个 char 数组中，每个字符使用两个字节（16 位）。从许多不同的应用中收集到的数据表明，字符串是堆使用的主要组成部分，此外，大多数字符串对象只包含 Latin-1 字符。这些字符只需要一个字节的存储空间，因此这些字符串对象的内部字符数组中有一半的空间没有被使用。\n说明\n我们建议将 String 类的内部表示方法从 UTF-16 字符数组改为字节数组加编码标志域。新的 String 类将根据字符串的内容，以 ISO-8859-1/Latin-1（每个字符一个字节）或 UTF-16（每个字符两个字节）的方式存储字符编码。编码标志将表明使用的是哪种编码。\nintern 是一个 native 方法，调用的是底层 C 的方法\n1 public native String intern(); Copied! 如果不是用双引号声明的 String 对象，可以使用 String 提供的 intern 方法，它会从字符串常量池中查询当前字符串是否存在，若不存在就会将当前字符串放入常量池中。\n1 String myInfo = new string(\u0026#34;I love atguigu\u0026#34;).intern(); Copied! 也就是说，如果在任意字符串上调用 String.intern 方法，那么其返回结果所指向的那个类实例，必须和直接以常量形式出现的字符串实例完全相同。因此，下列表达式的值必定是 true\n1 (\u0026#34;a\u0026#34;+\u0026#34;b\u0026#34;+\u0026#34;c\u0026#34;).intern() == \u0026#34;abc\u0026#34; Copied! 通俗点讲，Interned string 就是确保字符串在内存里只有一份拷贝，这样可以节约内存空间，加快字符串操作任务的执行速度。注意，这个值会被存放在字符串内部池（String Intern Pool）\n10.5.1. intern 的使用：JDK6 vs JDK7/8 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 public static void main(String[] args) { /** * 创建了两个对象: 堆空间中一个new对象 、字符串常量池中一个字符串常量\u0026#34;1\u0026#34;（注意：此时字符串常量池中已有\u0026#34;1\u0026#34;） * 由于字符串常量池中已存在\u0026#34;1\u0026#34; * s 指向的是堆空间中的对象地址 * s2 指向的是堆空间中常量池中\u0026#34;1\u0026#34;的地址 * 所以不相等 */ String s = new String(\u0026#34;1\u0026#34;); s.intern();//调用此方法之前，字符串常量池中已经存在了\u0026#34;1\u0026#34; String s2 = \u0026#34;1\u0026#34;; System.out.println(s == s2);//jdk6：false jdk7/8：false /** * ① String s3 = new String(\u0026#34;1\u0026#34;) + new String(\u0026#34;1\u0026#34;) * 等价于new String（\u0026#34;11\u0026#34;），但是，常量池中并不生成字符串\u0026#34;11\u0026#34;； * ② s3.intern() 由于此时常量池中并无\u0026#34;11\u0026#34;，所以把s3中记录的对象的地址存入常量池 * 所以s3 和 s4 指向的都是一个地址 */ String s3 = new String(\u0026#34;1\u0026#34;) + new String(\u0026#34;1\u0026#34;);//s3变量记录的地址为：new String(\u0026#34;11\u0026#34;) //执行完上一行代码以后，字符串常量池中，是否存在\u0026#34;11\u0026#34;呢？答案：不存在！！ s3.intern();//在字符串常量池中生成\u0026#34;11\u0026#34;。如何理解：jdk6:在字符串常量池中创建了一个新的对象\u0026#34;11\u0026#34;,也就有新的地址。 // jdk7:此时常量中并没有创建\u0026#34;11\u0026#34;,而是创建一个指向堆空间中new String(\u0026#34;11\u0026#34;)的地址 String s4 = \u0026#34;11\u0026#34;;//s4变量记录的地址：使用的是上一行代码代码执行时，在常量池中生成的\u0026#34;11\u0026#34;的地址 System.out.println(s3 == s4);//jdk6：false jdk7/8：true } Copied! 总结 String 的 intern()的使用：\nJDK1.6 中，将这个字符串对象尝试放入串池。\n如果串池中有，则并不会放入。返回已有的串池中的对象的地址 如果没有，会把此对象复制一份，放入串池，并返回串池中的对象地址 JDK1.7 起，将这个字符串对象尝试放入串池。\n如果串池中有，则并不会放入。返回已有的串池中的对象的地址 如果没有，则会把对象的引用地址复制一份，放入串池，并返回串池中的引用地址,字符串常量池底称为hashtable结构，所以这个时候就是 {字符串变量名:堆地址} 1 2 3 4 5 6 7 8 9 10 public static void main(String[] args) { String a = \u0026#34;a\u0026#34;;//常量池中创建\u0026#34;a\u0026#34; String b = \u0026#34;b\u0026#34;;//常量池中创建\u0026#34;b\u0026#34; String c = a + b;//堆中创建string对象\u0026#34;ab\u0026#34; String intern = c.intern();//jdk7/8:将堆的地址复制到常量池中，返回这个常量池中指向堆的地址 System.out.println(intern == c);//true String d = \u0026#34;ab\u0026#34;;//常量池中有\u0026#34;ab\u0026#34;，并且存的是指向堆中\u0026#34;ab\u0026#34;的地址，也就是c System.out.println(c==d);//true } Copied! 练习 1\n练习 2\n10.5.2 示例 1、\n1 2 3 4 5 6 7 8 public static void main(String[] args) { //JDK8 String a1 = new String(\u0026#34;a\u0026#34;).intern(); String a2 = new String(\u0026#34;a\u0026#34;).intern(); String a3 = \u0026#34;a\u0026#34;; System.out.println(a1 == a2);//true System.out.println(a1 == a3);//true } Copied! 2、\n1 2 3 4 5 6 7 public static void main(String[] args) { //JDK8 String s1 = new String(\u0026#34;a\u0026#34;)+new String(\u0026#34;b\u0026#34;); s1.intern(); String s2 = new String(\u0026#34;a\u0026#34;)+new String(\u0026#34;b\u0026#34;); System.out.println(s1 == s2);//false } Copied! 3、\n1 2 3 4 5 6 7 8 9 10 public static void main(String[] args) { //JDK8 String s1 = new String(\u0026#34;a\u0026#34;)+new String(\u0026#34;b\u0026#34;); String si1 = s1.intern();//s1==si1 堆中对象地址 String s2 = new String(\u0026#34;a\u0026#34;)+new String(\u0026#34;b\u0026#34;); String si2 = s2.intern();//si2==s1 堆中对象地址 System.out.println(si1 == si2);//true System.out.println(si2 == s1);//true System.out.println(s2 == si2);//false } Copied! 4、\n1 2 3 4 5 6 7 public static void main(String[] args) { //JDK8 String s1 = new String(\u0026#34;ab\u0026#34;);//会生成两个对象，一个为堆中实例，一个在字符串常量池中 s1.intern();//直接找到字符串常量池中数据，不会在常量池中创建新的对象 String s2 = \u0026#34;ab\u0026#34;; System.out.println(s1 == s2);//false } Copied! 10.5.3. intern 的效率测试：空间角度 我们通过测试一下，使用了 intern 和不使用的时候，其实相差还挺多的\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class StringIntern2 { static final int MAX_COUNT = 1000 * 10000; static final String[] arr = new String[MAX_COUNT]; public static void main(String[] args) { Integer[] data = new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; long start = System.currentTimeMillis(); for (int i = 0; i \u0026lt; MAX_COUNT; i++) { // arr[i] = new String(String.valueOf(data[i%data.length])); arr[i] = new String(String.valueOf(data[i % data.length])).intern(); } long end = System.currentTimeMillis(); System.out.println(\u0026#34;花费的时间为：\u0026#34; + (end - start)); try { Thread.sleep(1000000); } catch (Exception e) { e.getStackTrace(); } } }// 运行结果不使用intern：7256ms使用intern：1395ms Copied! 结论：对于程序中大量使用存在的字符串时，尤其存在很多已经重复的字符串时，使用 intern()方法能够节省内存空间。\n大的网站平台，需要内存中存储大量的字符串。比如社交网站，很多人都存储：北京市、海淀区等信息。这时候如果字符串都调用 intern()方法，就会很明显降低内存的大小。\n10.6. StringTable 的垃圾回收 1 2 3 4 5 6 7 8 9 10 public class StringGCTest { /** * -Xms15m -Xmx15m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails */ public static void main(String[] args) { for (int i = 0; i \u0026lt; 100000; i++) { String.valueOf(i).intern(); } } } Copied! 运行结果\n可知现在字符串常量池中只有6万多数据，不足十万\n10.7. G1 中的 String 去重操作 官网地址：JEP 192: String Deduplication in G1 (java.net) Motivation Many large-scale Java applications are currently bottlenecked on memory. Measurements have shown that roughly 25% of the Java heap live data set in these types of applications is consumed by String objects. Further, roughly half of those String objects are duplicates, where duplicates means string1.equals(string2) is true. Having duplicate String objects on the heap is, essentially, just a waste of memory. This project will implement automatic and continuous String deduplication in the G1 garbage collector to avoid wasting memory and reduce the memory footprint.\n目前，许多大规模的 Java 应用程序在内存上遇到了瓶颈。测量表明，在这些类型的应用程序中，大约 25%的 Java 堆实时数据集被String'对象所消耗。此外，这些 \u0026quot;String \u0026quot;对象中大约有一半是重复的，其中重复意味着 \u0026quot;string1.equals(string2) \u0026quot;是真的。在堆上有重复的String\u0026rsquo;对象，从本质上讲，只是一种内存的浪费。这个项目将在 G1 垃圾收集器中实现自动和持续的`String\u0026rsquo;重复数据删除，以避免浪费内存，减少内存占用。\n","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/117q4511/","title":"10-StringTable"},{"content":" JVM：String底层 常量池包括class文件常量池、运行时常量池和字符串常量池。\n*常量池查看方法参考“JVM：类加载\u0026amp;类加载器”/实验\n运行时常量池：一般意义上所指的常量池。是InstanceKlass的一个属性。存储于方法区（元空间）。\n/openjdk/hotspot/src/share/vm/oops/instanceKlass.hpp\n1 2 3 4 5 class InstanceKlass : public Klass { … ConstantPool* _constants; …. } Copied! Class****文件常量池： 可通过 javap –verbose 对象全限定名查看constant pool。存储在硬盘。\n字符串常量池**(String Pool)**：底层是StringTable。存储在堆区。继承链：HashTable – StringTable – String Pool\n/openjdk/hotspot/src/share/vm/classfile/symbolTable.hpp\n1 2 3 class StringTable : public RehashableHashtable\u0026lt;oop, mtSymbol\u0026gt; { … } Copied! hashtable如何存储字符串\n- hashtable的底层是数组+链表。\n- 用hash算法对字符串对象计算得到hashValue，按照hashValue将key和value放入hashtable中，如果有冲突的放进该hashValue的链表中。\ne.g.name=”ziya”; sex=”man”; zhiye=”teacher”\nname/sex /zhiye的hash value为11/13/11\n根据key从hashtable查找数据：\n-\u0026gt; 用hash算法对key计算得到hashValue (e.g. key:name -\u0026gt; hashValue 11)\n-\u0026gt; 根据hashValue区hashtable中找，如果index=hashValue的元素只有1个，直接返回；\n-\u0026gt; 如果元素有多个，根据链表进行遍历比对key\njava字符串在jvm中的存储\nStringTable中key的生成方式\n-\u0026gt; 根据字符串(name)和字符串长度计算出hashValue\n-\u0026gt; 根据hashValue计算出index（作为key）\n/openjdk/hotspot/src/share/vm/classfile/symbolTable.cpp\n1 2 3 4 5 6 7 oop StringTable::basic_add(int index_arg, Handle string, jchar* name, int len, unsigned int hashValue_arg, TRAPS) { … hashValue = hash_string(name, len); index = hash_to_index(hashValue); … } Copied! StringTable中value的生成方式\n-\u0026gt; 调用new_entry()将Java的String类实例instanceOopDesc封装成HashtableEntry\n* instanceOopDesc: OOP(ordinary object pointer)体系是Java对象在jvm中的存在形式 //相对于Klass是Java类在jvm中的存在形式\n/openjdk/hotspot/src/share/vm/classfile/symbolTable.cpp\n1 2 3 4 5 6 7 8 //string()就是instanceOopDesc oop StringTable::basic_add(int index_arg, Handle string, jchar* name, int len, unsigned int hashValue_arg, TRAPS) { … HashtableEntry\u0026lt;oop, mtSymbol\u0026gt;* entry = new_entry(hashValue, string()); add_entry(index, entry); … } Copied! new_entry()包含有关HashtableEntry的一些链表操作。\n/openjdk/hotspot/src/share/vm/utilities/hashtable.cpp\n1 2 3 template \u0026lt;MEMFLAGS F\u0026gt; BasicHashtableEntry\u0026lt;F\u0026gt;* BasicHashtable\u0026lt;F\u0026gt;::new_entry(unsigned int hashValue) { … } Copied! HashtableEntry是一个单向链表结点结构。value对应要封装的字符串对象InstanceOopDesc，key对应hashValue。\n/openjdk/jdk/src/windows/native/sun/windows/Hashtable.h\n1 2 3 4 5 6 struct HashtableEntry { INT_PTR hash; void* key; void* value; HashtableEntry* next; }; Copied! 创建String的底层实现\n实验1\n1 2 3 4 5 6 7 8 9 10 public class Test { public static void main(String[] args) { String s1=\u0026#34;11\u0026#34;; String s2=new String(\u0026#34;11\u0026#34;); System.out.println(s1.hashCode()); System.out.println(s2.hashCode()); System.out.println(s1==s2); System.out.println(s1.equals(s2)); } } Copied! 结果：两次输出hashCode值相同，s1==s2为false, s1.equals(s2)为true。\n原因：1) 因为hashCode就是根据字符串值计算得到的，字符串值一样hashCode就会一样。\n​ \\2) s1==s2比较的是地址。\n* String重写了Object中的hashCode方法。\nString的值是存储在字符数组char[] value中的。\n基本数据类型数组的对象生成的实例为TypeArrayOopDesc（对应Klass体系中基本数据类型数组的元信息存放在TypeArrayKlass）。\n实验2：证明字符数组在jvm中以TypeArrayOopDesc形式存在\n1 2 3 4 5 6 public class Test { public static void main(String[] args) { char[] arr=new char[]{\u0026#39;1\u0026#39;, \u0026#39;2\u0026#39;}; while (true); } } Copied! -\u0026gt; 代码中声明字符数组\n-\u0026gt; HSDB attach到对应进程，查看main线程堆栈，找到[C的内存地址\n-\u0026gt; Inspector查看证明元信息存储在TypeArrayKlass，对象为OOP(TypeArrayOopDesc)。\n实验3 String s1 = “1”;语句生成了几个OOP？2个。\n1 2 3 4 5 6 7 8 public class Test { public static void main(String[] args) { test3(); } public static void test3() { String s1=\u0026#34;11\u0026#34;; } } Copied! \\1) TypeArrayOopDesc – char数组\n\\2) InstanceOop – String对象\n证明：在语句处设置断点， idea debug模式运行程序（Debug模式单步验证）\n-\u0026gt; 勾选memory，memory layout点击load classes\n-\u0026gt; 执行完String s1=“11”时，String和char[]的count都增1；\n原因：\n//底层\n* 因为是字面量，该String值会放在字符串常量池\n-\u0026gt; 在字符串常量池中找有没有该value(“11”)，如果有则直接返回对应的String对象;\n-\u0026gt; 如果没有找到，创建该value的typeArrayOopDesc，再创建String， String中包含char数组，char数组指向该typeArrayOopDesc；\n-\u0026gt; 在字符串常量池表创建HashTableEntry指向String（将String对象对应的InstanceOopDesc封装成HashTableEntry作为StringTable的value存储）。\n*面试题：创建了几个对象\n-\u0026gt; 先问清楚问的是String对象还是OOP对象\n-\u0026gt; 如果问创建了几个String对象-\u0026gt; 1个。\n-\u0026gt; 如果问创建了几个OOP对象 -\u0026gt; 2个: 1个char数组，1个String对象对应的OOP。\n//HashTableEntry是C++对象，不算入OOP对象。\n实验4 String s1 = “11”; String s2=”11”;语句生成了几个OOP？ 2个。\n证明：\n1 2 3 4 5 6 7 8 9 public class Test { public static void main(String[] args) { test4(); } public static void test4() { String s1=\u0026#34;11\u0026#34;; String s2=\u0026#34;11\u0026#34;; } } Copied! Debug模式单步验证，\n-\u0026gt; 执行完String s1=“11”时，String和char[]都增1；\n-\u0026gt; 执行完String s2=“11”时，count没有增加；\n原因：\n-\u0026gt; s1的创建参考实验1；\n-\u0026gt; 创建s2时，在字符串常量池中有找到该值，不需要再创建，S2直接和S1指向同一个String对象。\n*面试题：创建了几个对象\n-\u0026gt; 先问清楚问的是String对象还是OOP对象\n-\u0026gt; 如果问创建了几个String对象-\u0026gt; 1个。\n-\u0026gt; 如果问创建了几个OOP对象 -\u0026gt; 2个: 1个char数组，1个String对象（对应的OOP）。\n实验5 String s1 = new String(“11”)语句生成了几个OOP？ 3个。\n证明：\n1 2 3 4 5 6 7 8 public class Test { public static void main(String[] args) { test5(); } public static void test5() { String s1=new String(\u0026#34;11\u0026#34;); } } Copied! Debug模式单步验证，\n-\u0026gt; 执行完String s1=new String(“11”)时，String增2，char[]增1；\n原因：\n-\u0026gt; 在字符串常量池中找，发现没有该value(“11”)；\n-\u0026gt; 创建HashTableEntry指向String，String指向typeArrayOopDesc；\n-\u0026gt; 因为new，又在堆区再创建一个String对象，其char数组直接指向已创建的typeArrayOopDesc。\n实验6 String s1 = new String(“11”); String s2 = new String(“11”);语句生成了几个OOP？ 4个。\n证明：\n1 2 3 4 5 6 7 8 9 public class Test { public static void main(String[] args) { test6(); } public static void test6() { String s1=new String(\u0026#34;11\u0026#34;); String s2=new String(\u0026#34;11\u0026#34;); } } Copied! Debug模式单步验证，\n-\u0026gt; 执行完String s1=new String(“11”)时，String增2，char[]增1；\n-\u0026gt; 执行完String s2=new String(“11”)时，String增1。\n原因：\n-\u0026gt; s1的创建参考实验5；\n-\u0026gt; 创建s2时，因为new，再创建一个String对象指向同一个typeArrayOopDesc。\n实验7 String s1 = “11”; String s2=”22”;语句生成了几个OOP？ 4个。\n证明：\n1 2 3 4 5 6 7 8 9 public class Test { public static void main(String[] args) { test7(); } public static void test7() { String s1=\u0026#34;11\u0026#34;; String s2=\u0026#34;22\u0026#34;; } } Copied! 创建了几个对象？\n-\u0026gt; 如果问创建了几个String对象？-\u0026gt; 2个。\n-\u0026gt; 如果问创建了几个OOP对象? -\u0026gt; 4个。\nString拼接\n实验8\n1 2 3 4 5 6 7 8 9 10 public class Test { public static void main(String[] args) { test8(); } public static void test8() { String s1=\u0026#34;1\u0026#34;; String s2=\u0026#34;1\u0026#34;; String s = s1+s2; } } Copied! Debug模式单步验证，\n-\u0026gt; 执行完String s1=”1”时，char[]和String的count都增1；\n-\u0026gt; 执行完String s2=”1”时，count没有增加；\n-\u0026gt; 执行完String s=s1+s2时，char[]和String的count都增1。\n//因为语句3底层调用StringBuilder.toString()==调用String构造方法String(value, offset, count)，不会在常量池生成记录，只创建了1个String对象。（参考：String的两种构造方法）\n所以总共创建了2个String，4个OOP（2个char数组，2个String）。\n对应字节码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 0 ldc #2 \u0026lt;1\u0026gt; 2 astore_0 3 ldc #2 \u0026lt;1\u0026gt; 5 astore_1 6 new #3 \u0026lt;java/lang/StringBuilder\u0026gt; 9 dup 10 invokespecial #4 \u0026lt;java/lang/StringBuilder.\u0026lt;init\u0026gt;\u0026gt; 13 aload_0 14 invokevirtual #5 \u0026lt;java/lang/StringBuilder.append\u0026gt; 17 aload_1 18 invokevirtual #5 \u0026lt;java/lang/StringBuilder.append\u0026gt; 21 invokevirtual #6 \u0026lt;java/lang/StringBuilder.toString\u0026gt; 24 astore_2 25 return Copied! 说明拼接语句String s=s1+s2底层是用new StringBuilder().append(“1”).apend(“1”).toString()实现的。\nString的两种构造方法\nStringBuilder的toString()调用了String(char[] value, int offset, int count)的构造方法。\nStringBuilder.class\n1 2 3 Public String toString() { return new String(this.value, 0, this.count); } Copied! String(value)会创建2个String，3个OOP (实验运行String s2=new String(“22”)，结果char[]增1，String增2)。\nString(value, offset, count)会创建1个String，2个OOP（可实验运行String s1=new String(new char[]{‘1’,’1’},0,count); 结果char[]和String都增1证明）。该构造方法不会在常量池生成记录。\n*从结果来看，String(value, offset, count)创建了2个OOP，但从过程来讲则创建了3个OOP（1个String和2个char数组）。因为：\nString(value, offset, count) 用到了copyOfRange()；\nString.class\n1 2 3 4 5 Public String(char[], int offset, int count) { … this.value = Arrays.copyOfRange(value, offset, offset + count); … } Copied! copyOfRange()底层又重新生成了char[]；\nArrays.class\n1 2 3 4 5 Public static char[] copyOfRange(char[] original, int from, int to) { … char[] copy = new char[newLength]; … } Copied! String.intern()\n**intern()**去常量池中找字符串，如果有直接返回，如果没有就把String对应的instanceOopDesc封装成HashTableEntry存储（写入常量池）。\n实验9\n1 2 3 4 5 6 7 8 9 10 11 12 13 public class Test { public static void main(String[] args) { test9(); } public static void test9() { String s1=\u0026#34;1\u0026#34;; String s2=\u0026#34;1\u0026#34;; String s = s1+s2; s.intern(); String str=\u0026#34;11\u0026#34;; System.out.println(s==str); } } Copied! 结果：有执行intern输出true，没有执行intern输出false\n原因：\n如果没有调用intern，String s=s1+s2执行时不会在常量池中生成记录，所以String str=”11”执行时依然会生成新String；\n如果有调用intern，intern()将s=”11”写入常量池，后面str就会在常量池中找到该值，直接指向s所创建的String对象。\nDebug模式单步验证，\n如果没有调用intern，执行完String str=”11”时，String和char[]的count都增1；\n如果有调用intern，执行完String str=”11”时，count没有增加。\n实验10\n1 2 3 4 5 6 7 8 9 10 11 12 13 public class Test { public static void main(String[] args) { test10(); } public static void test10() { final String s1=\u0026#34;3\u0026#34;; final String s2=\u0026#34;3\u0026#34;; String s = s1+s2; s.intern(); String str=\u0026#34;33\u0026#34;; System.out.println(s==str); } } Copied! 结果：有没有执行intern都输出true。\n原因：因为s1和s2都是常量，编译优化时已经将String s=s1+s2变成String s=”33”，33”会被存储到常量池。\n（没有执行intern版本）字节码：\n//常量池17位为CONSTANT_String_info “33”。\nDebug模式单步验证，（如果没有调用intern方法）\n-\u0026gt; 执行完final String s1=”3”时String和char[]的count都增1；\n-\u0026gt; 执行完final String s2=”3”时count没有增加；\n-\u0026gt; 执行完String s=s1+s2时String和char[]的count都增1；\n-\u0026gt; 执行完String str=”33”时count没有增加 （因为已经能够在常量池找到）。\n实验11\n1 2 3 4 5 6 7 8 9 10 11 12 13 public class Test { public static void main(String[] args) { test11(); } public static void test11() { final String s1=new String(\u0026#34;5\u0026#34;); final String s2=new String(\u0026#34;5\u0026#34;); String s = s1+s2; //s.intern(); String str=\u0026#34;55\u0026#34;; System.out.println(s==str); } } Copied! 结果：没有执行intern输出false。\n原因：new得到的对象不是常量（类似uuid的动态生成，见”JVM: 类加载\u0026amp;类加载器”）。\u0026ndash; 虽然用了final修饰，只能表示引用是final，引用指向的值并不是final。\n（没有执行intern版本）字节码：\n-\u0026mdash;\n练习1\n1 String s1 = “11”+new String(“22”); Copied! 结果：实验显示该语句新增4个String和3个char数组。\n原因：可以拆开来分析：\n\\1) “11” -\u0026gt; 1个String，1个char数组；\n\\2) new String(“22”) -\u0026gt; 2个String，1个char数组；\n\\3) 拼接-\u0026gt;底层调用StringBuilder.toString()-\u0026gt;底层调用String(value, offset, count)-\u0026gt; 1个String，1个char数组。\n所以总共生成4个String，3个char数组。\n练习2\n1 2 String s1 = “11”+”11”; String s2 = “11”+new String(“22”); Copied! 结果：即使前面有语句1，语句2仍然新增4个String和3个char数组。\n原因：语句1在编译时已经被计算替换为s1=“1111”（查看字节码可证）；语句2过程分析同练习1。\ne.g.\n","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/89561389/","title":"10-StringTable"},{"content":" 一、JVM 1、JVM虚拟机基于栈的结构，相比于寄存器结构指令集空间小，但是执行一个操作的指令数比较多，效率比较差\n2、Classic,没有即时编译器(JIT)-\u0026gt;HotSpot(sun，包含JIT，在响应时间和性能方面取平衡、方法区、元空间)-\u0026gt;JRockit(BEA，不包含JIT)-\u0026gt;J9(IBM)\n3、即时编译器(JIT) -热点代码探测技术 ，如果全用JIT，效率会变差\n4、JVM并不是把所有的类一次性全部加载到JVM中的，java虚拟机对class文件采取的是按需加载的方式，也不是每次用到一个类的时候都去查找，对于JVM级别的类加载器在启动时就会把默认的JAVA_HOME/lib里的class文件加载到JVM中，因为这些是系统常用的类，并初始化sun.misc.Launcher从而创建Extension ClassLoader和Application ClassLoader的实例。其他类使用到该类才会加载到内存中生成class对象，所以就需要双亲委派机制。\n1 2 3 4 5 public class Test { public static void main(String[] args) { String a = \u0026#34;sd\u0026#34;; } } Copied! 当前类Test是一个用户自定义的加载器，所以加载这个类就要用到系统类加载器，然后加载到里面的String类的时候，当前系统类加载器会交给上级加载，所以就用引导类加载器加载了。类里面的使用的其他类，会用加载当前类的类加载器去加载。\n5、执行引擎=解释器+JIT即时编译器+垃圾回收\n6、程序计数器：存储当前线程该执行的指令地址\n7、类加载过程中的链接过程里面的Resolution（解析）：将常量池中的符号引用解析为直接引用，这时应该只能处理一些简单的能确定的引用，而向多态这种运行时才知道实际引用的情况，应该用的是虚拟机栈的动态链接\n8、启动HotSpot Debugger [HSDB] 查看虚拟机内存状态\n1 java -cp ./sa-jdi.jar sun.jvm.hotspot.HSDB Copied! 9、Error和GC\n内存区域 ERROR GC 程序计数器 no no 虚拟机栈 yes no 本地方法栈 yes no 堆 yes yes 方法区 yes yes 二、栈 1、分配的栈内存越大越好吗？\n内存越多，会让stackoverflowerror延后出现，但是不会避免，还会挤占其他线程的空间\n2、方法中定义的局部变量是否线程安全？\n具体问题具体分析；\n何为线程安全？如果只有一个线程操作此数据，则线程安全；如果数据被多个线程共享，则不安全\n1 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 37 38 39 40 41 public class Test{ public static void method1(){ //stringbuilder 是线程不安全的类 //但是这个s1变量是线程安全的，因为只有一个线程访问 StringBuilder s1 = new StringBuilder(); s1.append(\u0026#34;a\u0026#34;); s1.append(\u0026#34;b\u0026#34;); //... } //s2的操作过程是线程不安全的 public static void method2(StringBuilder s2){ s2.append(\u0026#34;a\u0026#34;); s2.append(\u0026#34;b\u0026#34;); //... } //s3的操作过程有可能线程不安全,因为返回值有可能被其他线程抢用 public static StringBuilder method3(){ StringBuilder s3 = new StringBuilder(); s3.append(\u0026#34;a\u0026#34;); s3.append(\u0026#34;b\u0026#34;); return s3; } public static void main(String[] args){ StringBuilder s = new StringBuilder(); new Thread(()-\u0026gt;{ s.append(\u0026#34;a\u0026#34;); s.append(\u0026#34;b\u0026#34;); }).start(); //线程不安全 method2(s); } } Copied! 二、堆和垃圾回收 1、new的对象先放伊甸园区。此区有大小限制。\n2、当伊甸园的空间填满时，程序又需要创建对象，JVM的垃圾回收器将对伊甸园区进行垃圾回收（MinorGC），将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区\n3、然后将伊甸园中的剩余对象移动到幸存者0区。\n4、如果再次触发垃圾回收，此时上次幸存下来的放到幸存者0区的，如果没有回收，就会放到幸存者1区。\n5、如果再次经历垃圾回收，此时会重新放回幸存者0区，接着再去幸存者1区。\n6、啥时候能去养老区呢？可以设置次数。默认是15次。\n可以设置参数：进行设置-Xx:MaxTenuringThreshold= N 7、在养老区，相对悠闲。当养老区内存不足时，再次触发GC：Major GC，进行养老区的内存清理\n8、若养老区执行了Major GC之后，发现依然无法进行对象的保存，就会产生OOM异常。\n1、设置堆空间大小 -Xms -Xmx\n默认：起始堆空间大小为计算机物理内存的64分之一，最大堆空间大小为计算机物理内存的4分之一\n开发中建议初始堆内存和最大堆内存设置为一样的数值，减少频繁的扩容和释放，造成系统压力\n1 2 Runtime.getRuntime().maxMemory(); 获取最大堆内存，发现比 -Xmx中设置的值少了一部分，是因为伊甸园区的s0 s1 区域只能使用一个 Copied! 堆空间大小，无论是默认的还是自己设置的，都不包括 永久代(JDK7) 和 元空间(JDK8)\n2、设置老年代和新生代的比例：-XX:NewRatio=2 ，默认值为2\n设置新生代中eden和survivor比例：默认为8:1:1，但是默认情况下启动应用，用jvisualVM发现比例是6:1:1，这是因为jvm做了内存的自适应，如果要确切用8:1:1，则需显示的配置jvm参数： -XX:SurvivorRatio=8\n几乎所有对象都是在Eden区被new出来的\n绝大部分java对象的销毁都在新生代进行\n-Xmn 可以设置新生代最大内存大小，和-XX:NewRatio=2冲突时，以-Xmn为准\n3、YGC==minorGC，当Eden区满的时候会触发YGC/minorGC，回收Eden区和survivor中的From区中的垃圾；但是当某个survivor满的时候是不会触发YGC/minorGC，这种情况有可能直接复制到老年代。\nsurvivor区中的from和to区，谁空谁是to\n4、垃圾回收经验：\nYGC之后 伊甸园区一定是空的；\nOOM几乎都发生在老年代中；\n新生代频繁回收，老年代很少回收，几乎不在永久代回收\n5、垃圾回收是有一个GC线程的，和用户线程分开\n6. Minor GC，MajorGC、Full GC JVM在进行GC时，并非每次都对上面三个内存区域一起回收的，大部分时候回收的都是指新生代。\n针对Hotspot VM的实现，它里面的GC按照回收区域又分为两大种类型：一种是部分收集（Partial GC），一种是整堆收集（FullGC）\n部分收集：不是完整收集整个Java堆的垃圾收集。其中又分为：\n新生代收集（Minor GC / Young GC）：只是新生代(Eden\\s0\\s1)的垃圾收集 老年代收集（Major GC / Old GC）：只是老年代的圾收集。 目前，只有CMSGC会有单独收集老年代的行为。 注意，很多时候Major GC会和Full GC混淆使用，需要具体分辨是老年代回收还是整堆回收。 混合收集（MixedGC）：收集整个新生代以及部分老年代的垃圾收集。 目前，只有G1 GC会有这种行为 整堆收集（Full GC）：收集整个java堆和方法区的垃圾收集。 新生代+老年代+方法区(永久代/元空间)\n年轻代 当年轻代空间不足时，就会触发MinorGC，这里的年轻代满指的是Eden代满，Survivor满不会引发GC。（每次Minor GC会清理年轻代的内存。）\n因为Java对象大多都具备朝生夕灭的特性.，所以Minor GC非常频繁，一般回收速度也比较快。这一定义既清晰又易于理解。\nMinor GC会引发STW，暂停其它用户的线程，等垃圾回收结束，用户线程才恢复运行\n老年代GC（Major GC / Full GC）触发机制 指发生在老年代的GC，对象从老年代消失时，我们说 “Major GC” 或 “Full GC” 发生了\n出现了Major Gc，经常会伴随至少一次的Minor GC（但非绝对的，在Paralle1 Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程）\n也就是在老年代空间不足时，会先尝试触发Minor Gc。如果之后空间还不足，则触发Major GC 并发GC的触发条件就不太一样。以CMS GC为例，它主要是定时去检查old gen的使用量，当使用量超过了触发比例就会启动一次CMS GC，对old gen做并发收集。 Major GC的速度一般会比Minor GC慢10倍以上，STW的时间更长\n如果Major GC后，内存还不足，就报OOM了\nFull GC触发机制： 触发Full GC执行的情况有如下五种：\n调用System.gc()时，系统建议执行Full GC，但是不必然执行\n老年代空间不足\n方法区空间不足\n通过Minor GC后进入老年代的平均大小大于老年代的可用内存，空间分配担保\n由Eden区、survivor space0（From Space）区向survivor space1（To Space）区复制时，对象大小大于To Space可用内存，则把该对象转存到老年代，且老年代的可用内存小于该对象大小\n说明：Full GC 是开发或调优中尽量要避免的。这样暂时时间会短一些\n7、方法区的实现 8以前叫永久代，8叫元空间\n8、显示堆空间信息 -XX:+PrintGCDetails\n9、为什么要把Java堆分代？不分代就不能正常工作了吗？ 经研究，不同对象的生命周期不同。70%-99%的对象是临时对象。\n新生代：有Eden、两块大小相同的survivor（又称为from/to，s0/s1）构成，to总为空。\n老年代：存放新生代中经历多次GC仍然存活的对象。\n其实不分代完全可以，分代的唯一理由就是优化GC性能。如果没有分代，那所有的对象都在一块，就如同把一个学校的人都关在一个教室。GC的时候要找到哪些对象没用，这样就会对堆的所有区域进行扫描。而很多对象都是朝生夕死的，如果分代的话，把新创建的对象放到某一地方，当GC的时候先把这块存储“朝生夕死”对象的区域进行回收，这样就会腾出很大的空间出来。\n10、内存分配策略 如果对象在Eden出生并经过第一次Minor GC后仍然存活，并且能被Survivor容纳的话，将被移动到survivor空间中，并将对象年龄设为1。对象在survivor区中每熬过一次MinorGC，年龄就增加1岁，当它的年龄增加到一定程度（默认为15岁，其实每个JVM、每个GC都有所不同）时，就会被晋升到老年代\n对象晋升老年代的年龄阀值，可以通过选项-XX:MaxTenuringThreshold来设置\n针对不同年龄段的对象分配原则如下所示：\n优先分配到Eden\n大对象直接分配到老年代（尽量避免在开发中出现过多的大对象）\n长期存活的对象分配到老年代\n动态对象年龄判断：如果survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半，年龄大于或等于该年龄的对象可以直接进入老年代，无须等到MaxTenuringThreshold中要求的年龄。\n真实情况：年龄1+年龄2+年龄3+年龄N的对象加起来的空间，大于survivor区域的一半，就会让年龄N和年龄N以上的对象进入老年代。动态年龄判断应该是这样子的。说的通俗一点：就是年龄从小到大对象的占据空间的累加和，而不是某一个特定年龄对象占据的空间。-XX:TargetSurvivorRatio 设置目标存活率，默认为50%，年龄从小到大进行累加，当加入某个年龄段后，累加和超过survivor区域*TargetSurvivorRatio的时候，就从这个年龄段网上的年龄的对象进行晋升\n空间分配担保： -XX:HandlePromotionFailure\n11、TLAB的再说明 尽管不是所有的对象实例都能够在TLAB中成功分配内存，但JVM确实是将TLAB作为内存分配的首选。\n在程序中，开发人员可以通过选项“-XX:UseTLAB”设置是否开启TLAB空间。\nhotSpot虚拟机默认开启TLAB\n默认情况下，TLAB空间的内存非常小，仅占有整个Eden空间的1%，当然我们可以通过选项 -XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。\n一旦对象在TLAB空间分配内存失败时，JVM就会尝试着通过使用加锁机制确保数据操作的原子性，从而直接在Eden空间中分配内存。\n","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/38956h38/","title":"JVM"},{"content":" 补充：浅堆深堆与内存泄露 1. 浅堆（Shallow Heap） 浅堆是指一个对象所消耗的内存。在 32 位系统中，一个对象引用会占据 4 个字节，一个 int 类型会占据 4 个字节，long 型变量会占据 8 个字节，每个对象头需要占用 8 个字节。根据堆快照格式不同，对象的大小可能会同 8 字节进行对齐。\n以 String 为例：2 个 int 值共占 8 字节，对象引用占用 4 字节，对象头 8 字节，合计 20 字节，向 8 字节对齐，故占 24 字节。（jdk7 中）\nint hash32 0 int hash 0 ref value C:\\Users\\Administrat 这 24 字节为 String 对象的浅堆大小。它与 String 的 value 实际取值无关，无论字符串长度如何，浅堆大小始终是 24 字节。\n2. 保留集（Retained Set） 对象 A 的保留集指当对象 A 被垃圾回收后，可以被释放的所有的对象集合（包括对象 A 本身），即对象 A 的保留集可以被认为是只能通过对象 A 被直接或间接访问到的所有对象的集合。通俗地说，就是指仅被对象 A 所持有的对象的集合。\n3. 深堆（Retained Heap） 深堆是指对象的保留集中所有的对象的浅堆大小之和。\n注意：浅堆指对象本身占用的内存，不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的（直接或间接）所有对象的浅堆之和，即对象被回收后，可以释放的真实空间。\n4. 对象的实际大小 这里，对象的实际大小定义为一个对象所能触及的所有对象的浅堆大小之和，也就是通常意义上我们说的对象大小。与深堆相比，似乎这个在日常开发中更为直观和被人接受，但实际上，这个概念和垃圾回收无关。\n下图显示了一个简单的对象引用关系图，对象 A 引用了 C 和 D，对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身，不含 C 和 D，而 A 的实际大小为 A、C、D 三者之和。而 A 的深堆大小为 A 与 D 之和，由于对象 C 还可以通过对象 B 访问到，因此不在对象 A 的深堆范围内。\n5. 支配树（Dominator Tree） 支配树的概念源自图论。MAT 提供了一个称为支配树（Dominator Tree）的对象图。支配树体现了对象实例间的支配关系。在对象引用图中，所有指向对象 B 的路径都经过对象 A，则认为对象 A 支配对象 B。如果对象 A 是离对象 B 最近的一个支配对象，则认为对象 A 为对象 B 的直接支配者。支配树是基于对象间的引用图所建立的，它有以下基本性质：\n对象 A 的子树（所有被对象 A 支配的对象集合）表示对象 A 的保留集（retained set），即深堆。 如果对象 A 支配对象 B，那么对象 A 的直接支配者也支配对象 B。 支配树的边与对象引用图的边不直接对应。 如下图所示：左图表示对象引用图，右图表示左图所对应的支配树。对象 A 和 B 由根对象直接支配，由于在到对象 C 的路径中，可以经过 A，也可以经过 B，因此对象 C 的直接支配者也是根对象。对象 F 与对象 D 相互引用，因为到对象 F 的所有路径必然经过对象 D，因此，对象 D 是对象 F 的直接支配者。而到对象 D 的所有路径中，必然经过对象 C，即使是从对象 F 到对象 D 的引用，从根节点出发，也是经过对象 C 的，所以，对象 D 的直接支配者为对象 C。同理，对象 E 支配对象 G。到达对象 H 的可以通过对象 D，也可以通过对象 E，因此对象 D 和 E 都不能支配对象 H，而经过对象 C 既可以到达 D 也可以到达 E，因此对象 C 为对象 H 的直接支配者。\n6. 内存泄漏（memory leak） 可达性分析算法来判断对象是否是不再使用的对象，本质都是判断一个对象是否还被引用。那么对于这种情况下，由于代码的实现不同就会出现很多种内存泄漏问题（让 JVM 误以为此对象还在引用中，无法回收，造成内存泄漏）。\n＞ 是否还被使用？是\n＞ 是否还被需要？否\n严格来说，只有对象不会再被程序用到了，但是 GC 又不能回收他们的情况，才叫内存泄漏。但实际情况很多时候一些不太好的实践（或疏忽）会导致对象的生命周期变得很长甚至导致 00M，也可以叫做宽泛意义上的“内存泄漏”。\n如下图，当 Y 生命周期结束的时候，X 依然引用着 Y，这时候，垃圾回收期是不会回收对象 Y 的；如果对象 X 还引用着生命周期比较短的 A、B、C，对象 A 又引用着对象 a、b、c，这样就可能造成大量无用的对象不能被回收，进而占据了内存资源，造成内存泄漏，直到内存溢出。\n申请了内存用完了不释放，比如一共有 1024M 的内存，分配了 512M 的内存一直不回收，那么可以用的内存只有 512M 了，仿佛泄露掉了一部分；通俗一点讲的话，内存泄漏就是【占着茅坑不拉 shi】\n7. 内存溢出（out of memory） 申请内存时，没有足够的内存可以使用；通俗一点儿讲，一个厕所就三个坑，有两个站着茅坑不走的（内存泄漏），剩下最后一个坑，厕所表示接待压力很大，这时候一下子来了两个人，坑位（内存）就不够了，内存泄漏变成内存溢出了。可见，内存泄漏和内存溢出的关系：内存泄漏的增多，最终会导致内存溢出。\n泄漏的分类\n经常发生：发生内存泄露的代码会被多次执行，每次执行，泄露一块内存； 偶然发生：在某些特定情况下才会发生 一次性：发生内存泄露的方法只会执行一次； 隐式泄漏：一直占着内存不释放，直到执行结束；严格的说这个不算内存泄漏，因为最终释放掉了，但是如果执行时间特别长，也可能会导致内存耗尽。 8. Java 中内存泄露的 8 种情况 8.1. 静态集合类 静态集合类，如 HashMap、LinkedList 等等。如果这些容器为静态的，那么它们的生命周期与 JVM 程序一致，则容器中的对象在程序结束之前将不能被释放，从而造成内存泄漏。简单而言，长生命周期的对象持有短生命周期对象的引用，尽管短生命周期的对象不再使用，但是因为长生命周期对象持有它的引用而导致不能被回收。\n1 2 3 4 5 6 7 public class MemoryLeak { static List list = new ArrayList(); public void oomTests(){ Object obj＝new Object();//局部变量 list.add(obj); } } Copied! 8.2. 单例模式 单例模式，和静态集合导致内存泄露的原因类似，因为单例的静态特性，它的生命周期和 JVM 的生命周期一样长，所以如果单例对象如果持有外部对象的引用，那么这个外部对象也不会被回收，那么就会造成内存泄漏。\n8.3. 内部类持有外部类 内部类持有外部类，如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了，即使那个外部类实例对象不再被使用，但由于内部类持有外部类的实例对象，这个外部类对象将不会被垃圾回收，这也会造成内存泄漏。\n8.4. 各种连接，如数据库连接、网络连接和 IO 连接等 在对数据库进行操作的过程中，首先需要建立与数据库的连接，当不再使用时，需要调用 close 方法来释放与数据库的连接。只有连接被关闭后，垃圾回收器才会回收对应的对象。否则，如果在访问数据库的过程中，对 Connection、Statement 或 ResultSet 不显性地关闭，将会造成大量的对象无法被回收，从而引起内存泄漏。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main(String[] args) { try{ Connection conn =null; Class.forName(\u0026#34;com.mysql.jdbc.Driver\u0026#34;); conn =DriverManager.getConnection(\u0026#34;url\u0026#34;,\u0026#34;\u0026#34;,\u0026#34;\u0026#34;); Statement stmt =conn.createStatement(); ResultSet rs =stmt.executeQuery(\u0026#34;....\u0026#34;); } catch（Exception e）{//异常日志 } finally { // 1．关闭结果集 Statement // 2．关闭声明的对象 ResultSet // 3．关闭连接 Connection } } Copied! 8.5. 变量不合理的作用域 变量不合理的作用域。一般而言，一个变量的定义的作用范围大于其使用范围，很有可能会造成内存泄漏。另一方面，如果没有及时地把对象设置为 null，很有可能导致内存泄漏的发生。\n1 2 3 4 5 6 7 public class UsingRandom { private String msg; public void receiveMsg(){ readFromNet();//从网络中接受数据保存到msg中 saveDB();//把msg保存到数据库中 } } Copied! 如上面这个伪代码，通过 readFromNet 方法把接受的消息保存在变量 msg 中，然后调用 saveDB 方法把 msg 的内容保存到数据库中，此时 msg 已经就没用了，由于 msg 的生命周期与对象的生命周期相同，此时 msg 还不能回收，因此造成了内存泄漏。实际上这个 msg 变量可以放在 receiveMsg 方法内部，当方法使用完，那么 msg 的生命周期也就结束，此时就可以回收了。还有一种方法，在使用完 msg 后，把 msg 设置为 null，这样垃圾回收器也会回收 msg 的内存空间。\n8.6. 改变哈希值 改变哈希值，当一个对象被存储进 HashSet 集合中以后，就不能修改这个对象中的那些参与计算哈希值的字段了。\n否则，对象修改后的哈希值与最初存储进 HashSet 集合中时的哈希值就不同了，在这种情况下，即使在 contains 方法使用该对象的当前引用作为的参数去 HashSet 集合中检索对象，也将返回找不到对象的结果，这也会导致无法从 HashSet 集合中单独删除当前对象，造成内存泄漏。\n这也是 String 为什么被设置成了不可变类型，我们可以放心地把 String 存入 HashSet，或者把 String 当做 HashMap 的 key 值；\n当我们想把自己定义的类保存到散列表的时候，需要保证对象的 hashCode 不可变。\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 /** * 例1 */ public class ChangeHashCode { public static void main(String[] args) { HashSet set = new HashSet(); Person p1 = new Person(1001, \u0026#34;AA\u0026#34;); Person p2 = new Person(1002, \u0026#34;BB\u0026#34;); set.add(p1); set.add(p2); p1.name = \u0026#34;CC\u0026#34;;//导致了内存的泄漏 set.remove(p1); //删除失败 System.out.println(set); set.add(new Person(1001, \u0026#34;CC\u0026#34;)); System.out.println(set); set.add(new Person(1001, \u0026#34;AA\u0026#34;)); System.out.println(set); } } class Person { int id; String name; public Person(int id, String name) { this.id = id; this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Person)) return false; Person person = (Person) o; if (id != person.id) return false; return name != null ? name.equals(person.name) : person.name == null; } @Override public int hashCode() { int result = id; result = 31 * result + (name != null ? name.hashCode() : 0); return result; } @Override public String toString() { return \u0026#34;Person{\u0026#34; + \u0026#34;id=\u0026#34; + id + \u0026#34;, name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } Copied! 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 /** * 例2 */ public class ChangeHashCode1 { public static void main(String[] args) { HashSet\u0026lt;Point\u0026gt; hs = new HashSet\u0026lt;Point\u0026gt;(); Point cc = new Point(); cc.setX(10);//hashCode = 41 hs.add(cc); cc.setX(20);//hashCode = 51 此行为导致了内存的泄漏 System.out.println(\u0026#34;hs.remove = \u0026#34; + hs.remove(cc));//false hs.add(cc); System.out.println(\u0026#34;hs.size = \u0026#34; + hs.size());//size = 2 System.out.println(hs); } } class Point { int x; public int getX() { return x; } public void setX(int x) { this.x = x; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + x; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Point other = (Point) obj; if (x != other.x) return false; return true; } @Override public String toString() { return \u0026#34;Point{\u0026#34; + \u0026#34;x=\u0026#34; + x + \u0026#39;}\u0026#39;; } } Copied! 8.7. 缓存泄露 内存泄漏的另一个常见来源是缓存，一旦你把对象引用放入到缓存中，他就很容易遗忘。比如：之前项目在一次上线的时候，应用启动奇慢直到夯死，就是因为代码中会加载一个表中的数据到缓存（内存）中，测试环境只有几百条数据，但是生产环境有几百万的数据。\n对于这个问题，可以使用 WeakHashMap 代表缓存，此种 Map 的特点是，当除了自身有对 key 的引用外，此 key 没有其他引用那么此 map 会自动丢弃此值。\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class MapTest { static Map wMap = new WeakHashMap(); static Map map = new HashMap(); public static void main(String[] args) { init(); testWeakHashMap(); testHashMap(); } public static void init() { String ref1 = new String(\u0026#34;obejct1\u0026#34;); String ref2 = new String(\u0026#34;obejct2\u0026#34;); String ref3 = new String(\u0026#34;obejct3\u0026#34;); String ref4 = new String(\u0026#34;obejct4\u0026#34;); wMap.put(ref1, \u0026#34;cacheObject1\u0026#34;); wMap.put(ref2, \u0026#34;cacheObject2\u0026#34;); map.put(ref3, \u0026#34;cacheObject3\u0026#34;); map.put(ref4, \u0026#34;cacheObject4\u0026#34;); System.out.println(\u0026#34;init\u0026#34;);//ref1的引用声明在这个方法中，出栈的时候，就没有其他引用指向ref1了 } public static void testWeakHashMap() { System.out.println(\u0026#34;WeakHashMap GC之前\u0026#34;); for (Object o : wMap.entrySet()) { System.out.println(o); } try { System.gc(); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(\u0026#34;WeakHashMap GC之后\u0026#34;); for (Object o : wMap.entrySet()) { System.out.println(o); } } public static void testHashMap() { System.out.println(\u0026#34;HashMap GC之前\u0026#34;); for (Object o : map.entrySet()) { System.out.println(o); } try { System.gc(); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(\u0026#34;HashMap GC之后\u0026#34;); for (Object o : map.entrySet()) { System.out.println(o); } } } Copied! 上面代码和图示主演演示 WeakHashMap 如何自动释放缓存对象，当 init 函数执行完成后，局部变量字符串引用 weakd1，weakd2，d1，d2 都会消失，此时只有静态 map 中保存中对字符串对象的引用，可以看到，调用 gc 之后，HashMap 的没有被回收，而 WeakHashMap 里面的缓存被回收了。\n8.8. 监听器和其他回调 内存泄漏第三个常见来源是监听器和其他回调，如果客户端在你实现的 API 中注册回调，却没有显示的取消，那么就会积聚。\n需要确保回调立即被当作垃圾回收的最佳方法是只保存它的弱引用，例如将他们保存成为 WeakHashMap 中的键。\n9. 内存泄露案例分析 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 public class Stack { private Object[] elements; private int size = 0; private static final int DEFAULT_INITIAL_CAPACITY = 16; public Stack() { elements = new Object[DEFAULT_INITIAL_CAPACITY]; } public void push(Object e) { //入栈 ensureCapacity(); elements[size++] = e; } public Object pop() { //出栈 if (size == 0) throw new EmptyStackException(); return elements[--size]; } private void ensureCapacity() { if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1); } } Copied! 上述程序并没有明显的错误，但是这段程序有一个内存泄漏，随着 GC 活动的增加，或者内存占用的不断增加，程序性能的降低就会表现出来，严重时可导致内存泄漏，但是这种失败情况相对较少。\n代码的主要问题在 pop 函数，下面通过这张图示展现。假设这个栈一直增长，增长后如下图所示\n当进行大量的 pop 操作时，由于引用未进行置空，gc 是不会释放的，如下图所示\n从上图中看以看出，如果栈先增长，再收缩，那么从栈中弹出的对象将不会被当作垃圾回收，即使程序不再使用栈中的这些队象，他们也不会回收，因为栈中仍然保存这对象的引用，俗称过期引用，这个内存泄露很隐蔽。\n将代码中的 pop()方法变成如下方法：\n1 2 3 4 5 6 7 public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; return result; } Copied! 一旦引用过期，清空这些引用，将引用置空。\n","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/67349167/","title":"补充：浅堆深堆与内存泄露"},{"content":" 补充：使用 OQL 语言查询对象信息 MAT 支持一种类似于 SQL 的查询语言 OQL（Object Query Language）。OQL 使用类 SQL 语法，可以在堆中进行对象的查找和筛选。\n1. SELECT 子句 在 MAT 中，Select 子句的格式与 SQL 基本一致，用于指定要显示的列。Select 子句中可以使用“＊”，查看结果对象的引用实例（相当于 outgoing references）。\n1 SELECT * FROM java.util.Vector v Copied! 使用“OBJECTS”关键字，可以将返回结果集中的项以对象的形式显示。\n1 2 3 SELECT objects v.elementData FROM java.util.Vector v SELECT OBJECTS s.value FROM java.lang.String s Copied! 在 Select 子句中，使用“AS RETAINED SET”关键字可以得到所得对象的保留集。\n1 SELECT AS RETAINED SET *FROM com.atguigu.mat.Student Copied! “DISTINCT”关键字用于在结果集中去除重复对象。\n1 SELECT DISTINCT OBJECTS classof(s) FROM java.lang.String s Copied! 2. FROM 子句 From 子句用于指定查询范围，它可以指定类名、正则表达式或者对象地址。\n1 SELECT * FROM java.lang.String s Copied! 使用正则表达式，限定搜索范围，输出所有 com.atguigu 包下所有类的实例\n1 SELECT * FROM \u0026#34;com\\.atguigu\\..*\u0026#34; Copied! 使用类的地址进行搜索。使用类的地址的好处是可以区分被不同 ClassLoader 加载的同一种类型。\n1 select * from 0x37a0b4d Copied! 3. WHERE 子句 Where 子句用于指定 OQL 的查询条件。OQL 查询将只返回满足 Where 子句指定条件的对象。Where 子句的格式与传统 SQL 极为相似。\n返回长度大于 10 的 char 数组。\n1 SELECT *FROM Ichar[] s WHERE s.@length\u0026gt;10 Copied! 返回包含“java”子字符串的所有字符串，使用“LIKE”操作符，“LIKE”操作符的操作参数为正则表达式。\n1 SELECT * FROM java.lang.String s WHERE toString(s) LIKE \u0026#34;.*java.*\u0026#34; Copied! 返回所有 value 域不为 null 的字符串，使用“＝”操作符。\n1 SELECT * FROM java.lang.String s where s.value!=null Copied! 返回数组长度大于 15，并且深堆大于 1000 字节的所有 Vector 对象。\n1 SELECT * FROM java.util.Vector v WHERE v.elementData.@length\u0026gt;15 AND v.@retainedHeapSize\u0026gt;1000 Copied! 4. 内置对象与方法 OQL 中可以访问堆内对象的属性，也可以访问堆内代理对象的属性。访问堆内对象的属性时，格式如下，其中 alias 为对象名称：\n[ \u0026lt;alias\u0026gt;. ] \u0026lt;field\u0026gt; . \u0026lt;field\u0026gt;. \u0026lt;field\u0026gt;\n访问 java.io.File 对象的 path 属性，并进一步访问 path 的 value 属性：\n1 SELECT toString(f.path.value) FROM java.io.File f Copied! 显示 String 对象的内容、objectid 和 objectAddress。\n1 SELECT s.toString(),s.@objectId, s.@objectAddress FROM java.lang.String s Copied! 显示 java.util.Vector 内部数组的长度。\n1 SELECT v.elementData.@length FROM java.util.Vector v Copied! 显示所有的 java.util.Vector 对象及其子类型\n1 select * from INSTANCEOF java.util.Vector Copied! ","date":"2023-05-03T03:17:33+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/16835116/","title":"补充：使用OQL语言查询对象信息"},{"content":" 1. Class 文件结构 1.1. Class 字节码文件结构 类型 名称 说明 长度 数量 魔数 u4 magic 魔数,识别Class文件格式 4个字节 1 版本号 u2 minor_version 副版本号(小版本) 2个字节 1 u2 major_version 主版本号(大版本) 2个字节 1 常量池集合 u2 constant_pool_count 常量池计数器 2个字节 1 cp_info constant_pool 常量池表 n个字节 constant_pool_count - 1 访问标识 u2 access_flags 访问标识 2个字节 1 索引集合 u2 this_class 类索引 2个字节 1 u2 super_class 父类索引 2个字节 1 u2 interfaces_count 接口计数器 2个字节 1 u2 interfaces 接口索引集合 2个字节 interfaces_count 字段表集合 u2 fields_count 字段计数器 2个字节 1 field_info fields 字段表 n个字节 fields_count 方法表集合 u2 methods_count 方法计数器 2个字节 1 method_info methods 方法表 n个字节 methods_count 属性表集合 u2 attributes_count 属性计数器 2个字节 1 attribute_info attributes 属性表 n个字节 attributes_count 1.2. Class 文件数据类型 数据类型 定义 说明 无符号数 无符号数可以用来描述数字、索引引用、数量值或按照 utf-8 编码构成的字符串值。 其中无符号数属于基本的数据类型。 以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节 表 表是由多个无符号数或其他表构成的复合数据结构。 所有的表都以“_info”结尾。 由于表没有固定长度，所以通常会在其前面加上个数说明。 1.3. 魔数 Magic Number（魔数）\n每个 Class 文件开头的 4 个字节的无符号整数称为魔数（Magic Number） 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的 Class 文件。即：魔数是 Class 文件的标识符。 魔数值固定为 0xCAFEBABE。不会改变。 如果一个 Class 文件不以 0xCAFEBABE 开头，虚拟机在进行文件校验的时候就会直接抛出以下错误： 1 2 Error: A JNI error has occurred, please check your installation and try again Exception in thread \u0026#34;main\u0026#34; java.lang.ClassFormatError: Incompatible magic value 1885430635 in class file StringTest Copied! 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑，因为文件扩展名可以随意地改动。 1.4. 文件版本号 紧接着魔数的 4 个字节存储的是 Class 文件的版本号。同样也是 4 个字节。第 5 个和第 6 个字节所代表的含义就是编译的副版本号 minor_version，而第 7 个和第 8 个字节就是编译的主版本号 major_version。\n它们共同构成了 class 文件的格式版本号。譬如某个 Class 文件的主版本号为 M，副版本号为 m，那么这个 Class 文件的格式版本号就确定为 M.m。\n版本号和 Java 编译器的对应关系如下表：\n1.4.1. Class 文件版本号对应关系 主版本（十进制） 副版本（十进制） 编译器版本 45 3 1.1 46 0 1.2 47 0 1.3 48 0 1.4 49 0 1.5 50 0 1.6 51 0 1.7 52 0 1.8 53 0 1.9 54 0 1.10 55 0 1.11 Java 的版本号是从 45 开始的，JDK1.1 之后的每个 JDK 大版本发布主版本号向上加 1。\n不同版本的 Java 编译器编译的 Class 文件对应的版本是不一样的。目前，高版本的 Java 虚拟机可以执行由低版本编译器生成的 Class 文件，但是低版本的 Java 虚拟机不能执行由高版本编译器生成的 Class 文件。否则 JVM 会抛出 java.lang.UnsupportedClassVersionError 异常。（向下兼容）\n在实际应用中，由于开发环境和生产环境的不同，可能会导致该问题的发生。因此，需要我们在开发时，特别注意开发编译的 JDK 版本和生产环境中的 JDK 版本是否一致。\n虚拟机 JDK 版本为 1.k（k\u0026gt;=2）时，对应的 class 文件格式版本号的范围为 45.0 - 44+k.0（含两端）。 1.5. 常量池集合 常量池是 Class 文件中内容最为丰富的区域之一。常量池对于 Class 文件中的字段和方法解析也有着至关重要的作用。\n随着 Java 虚拟机的不断发展，常量池的内容也日渐丰富。可以说，常量池是整个 Class 文件的基石。\n在版本号之后，紧跟着的是常量池的数量，以及若干个常量池表项。\n常量池中常量的数量是不固定的，所以在常量池的入口需要放置一项 u2 类型的无符号数，代表常量池容量计数值（constant_pool_count）。与 Java 中语言习惯不一样的是，这个容量计数是从 1 而不是 0 开始的。\n类型 名称 数量 u2（无符号数） constant_pool_count 1 cp_info（表） constant_pool constant_pool_count - 1 由上表可见，Class 文件使用了一个前置的容量计数器（constant_pool_count）加若干个连续的数据项（constant_pool）的形式来描述常量池内容。我们把这一系列连续常量池数据称为常量池集合。\n常量池表项中，用于存放编译时期生成的各种字面量和符号引用，这部分内容将在类加载后进入方法区的运行时常量池中存放 1.5.1. 常量池计数器 constant_pool_count（常量池计数器）\n由于常量池的数量不固定，时长时短，所以需要放置两个字节来表示常量池容量计数值。 常量池容量计数值（u2 类型）：从 1 开始，表示常量池中有多少项常量。即 constant_pool_count=1 表示常量池中有 0 个常量项。 Demo 的值为： 其值为 0x0016，掐指一算，也就是 22。需要注意的是，这实际上只有 21 项常量。索引为范围是 1-21。为什么呢？\n通常我们写代码时都是从 0 开始的，但是这里的常量池却是从 1 开始，因为它把第 0 项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义，这种情况可用索引值 0 来表示。\n1.5.2. 常量池表 constant_pool 是一种表结构，以 1 ~ constant_pool_count - 1 为索引。表明了后面有多少个常量项。\n常量池主要存放两大类常量：字面量（Literal）和符号引用（Symbolic References）\n它包含了 class 文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第 1 个字节作为类型标记，用于确定该项的格式，这个字节称为 tag byte（标记字节、标签字节）。\n类型 标志(或标识) 描述 CONSTANT_Utf8_info 1 UTF-8 编码的字符串 CONSTANT_Integer_info 3 整型字面量 CONSTANT_Float_info 4 浮点型字面量 CONSTANT_Long_info 5 长整型字面量 CONSTANT_Double_info 6 双精度浮点型字面量 CONSTANT_Class_info 7 类或接口的符号引用 CONSTANT_String_info 8 字符串类型字面量 CONSTANT_Fieldref_info 9 字段的符号引用 CONSTANT_Methodref_info 10 类中方法的符号引用 CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用 CONSTANT_NameAndType_info 12 字段或方法的符号引用 CONSTANT_MethodHandle_info 15 表示方法句柄 CONSTANT_MethodType_info 16 标志方法类型 CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点 Ⅰ. 字面量和符号引用 在对这些常量解读前，我们需要搞清楚几个概念。\n常量池主要存放两大类常量：字面量（Literal）和符号引用（Symbolic References）。如下表：\n常量 具体的常量 字面量 文本字符串 声明为 final 的常量值 符号引用 类和接口的全限定名 字段的名称和描述符 方法的名称和描述符 全限定名\ncom/atguigu/test/Demo 这个就是类的全限定名，仅仅是把包名的“.“替换成”/”，为了使连续的多个全限定名之间不产生混淆，在使用时最后一般会加入一个“;”表示全限定名结束。\n简单名称\n简单名称是指没有类型和参数修饰的方法或者字段名称，上面例子中的类的 add()方法和 num 字段的简单名称分别是 add 和 num。\n描述符\n描述符的作用是用来描述字段的数据类型、方法的参数列表（包括数量、类型以及顺序）和返回值。根据描述符规则，基本数据类型（byte、char、double、float、int、long、short、boolean）以及代表无返回值的 void 类型都用一个大写字符来表示，而对象类型则用字符 L 加对象的全限定名来表示，详见下表：\n标志符 含义 B 基本数据类型 byte C 基本数据类型 char D 基本数据类型 double F 基本数据类型 float I 基本数据类型 int J 基本数据类型 long S 基本数据类型 short Z 基本数据类型 boolean V 代表 void 类型 L 对象类型，比如：Ljava/lang/Object; [ 数组类型，代表一维数组。比如：`double[] is [D 用描述符来描述方法时，按照先参数列表，后返回值的顺序描述，参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法 java.lang.String tostring()的描述符为()Ljava/lang/String; ，方法 int abc(int[]x, int y)的描述符为([II)I。\n补充说明：\n虚拟机在加载 Class 文件时才会进行动态链接，也就是说，Class 文件中不会保存各个方法和字段的最终内存布局信息。因此，这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时，需要从常量池中获得对应的符号引用，再在类加载过程中的解析阶段将其替换为直接引用，并翻译到具体的内存地址中。\n这里说明下符号引用和直接引用的区别与关联：\n符号引用：符号引用以一组符号来描述所引用的目标，符号可以是任何形式的字面量，只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关，引用的目标并不一定已经加载到了内存中。 直接引用：直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的，同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用，那说明引用的目标必定已经存在于内存之中了。 Ⅱ. 常量类型和结构 常量池中每一项常量都是一个表，J0K1.7 之后共有 14 种不同的表结构数据。如下表格所示：\n根据上图每个类型的描述我们也可以知道每个类型是用来描述常量池中哪些内容（主要是字面量、符号引用）的。比如: CONSTANT_Integer_info 是用来描述常量池中字面量信息的，而且只是整型字面量信息。\n标志为 15、16、18 的常量项类型是用来支持动态语言调用的（jdk1.7 时才加入的）。\n细节说明:\nCONSTANT_Class_info 结构用于表示类或接口 CONSTAT_Fieldref_info、CONSTAHT_Methodref_infoF 和 lCONSTANIT_InterfaceMethodref_info 结构表示字段、方汇和按口小法 CONSTANT_String_info 结构用于表示示 String 类型的常量对象，指向CONSTANT_Utf8_info中的字符串 CONSTANT_Integer_info 和 CONSTANT_Float_info 表示 4 字节（int 和 float）的数值常量 CONSTANT_Long_info 和 CONSTAT_Double_info 结构表示 8 字作（long 和 double）的数值常量 在 class 文件的常最池表中，所行的 a 字节常借均占两个表成员（项）的空问。如果一个 CONSTAHT_Long_info 和 CNSTAHT_Double_info 结构在常量池中的索引位 n，则常量池中一个可用的索引位 n+2，此时常量池长中索引为 n+1 的项仍然有效但必须视为不可用的。 CONSTANT_NameAndType_info 结构用于表示字段或方法，但是和之前的 3 个结构不同，CONSTANT_NameAndType_info 结构没有指明该字段或方法所属的类或接口。 CONSTANT_Utf8_info 用于表示字符常量的值 CONSTANT_MethodHandle_info 结构用于表示方法句柄 CONSTANT_MethodType_info 结构表示方法类型 CONSTANT_InvokeDynamic_info 结构表示 invokedynamic 指令所用到的引导方法(bootstrap method)、引导方法所用到的动态调用名称(dynamic invocation name)、参数和返回类型，并可以给引导方法传入一系列称为静态参数（static argument）的常量。 解析方法：\n一个字节一个字节的解析 使用 javap 命令解析：javap-verbose Demo.class 或 jclasslib 工具会更方便。 总结 1：\n这 14 种表（或者常量项结构）的共同点是：表开始的第一位是一个 u1 类型的标志位（tag），代表当前这个常量项使用的是哪种表结构，即哪种常量类型。 在常量池列表中，CONSTANT_Utf8_info 常量项是一种使用改进过的 UTF-8 编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息。 这 14 种常量项结构还有一个特点是，其中 13 个常量项占用的字节固定，只有 CONSTANT_Utf8_info 占用字节不固定，其大小由 length 决定。为什么呢？因为从常量池存放的内容可知，其存放的是字面量和符号引用，最终这些内容都会是一个字符串，这些字符串的大小是在编写程序时才确定，比如你定义一个类，类名可以取长取短，所以在没编译前，大小不固定，编译后，通过 utf-8 编码，就可以知道其长度。 总结 2：\n常量池：可以理解为 Class 文件之中的资源仓库，它是 Class 文件结构中与其他项目关联最多的数据类型（后面的很多数据类型都会指向此处），也是占用 Class 文件空间最大的数据项目之一。 常量池中为什么要包含这些内容？Java 代码在进行 Javac 编译的时候，并不像 C 和 C++那样有“连接”这一步骤，而是在虚拟机加载 C1ass 文件的时候进行动态链接。也就是说，在 Class 文件中不会保存各个方法、字段的最终内存布局信息，因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址，也就无法直接被虚拟机使用。当虚拟机运行时，需要从常量池获得对应的符号引用，再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态链接的内容，在虚拟机类加载过程时再进行详细讲解 1.6. 访问标志 访问标识（access_flag、访问标志、访问标记）\n在常量池后，紧跟着访问标记。该标记使用两个字节表示，用于识别一些类或者接口层次的访问信息，包括：这个 Class 是类还是接口；是否定义为 public 类型；是否定义为 abstract 类型；如果是类的话，是否被声明为 final 等。各种访问标记如下所示：\n标志名称 标志值 含义 ACC_PUBLIC 0x0001 标志为 public 类型 ACC_FINAL 0x0010 标志被声明为 final，只有类可以设置 ACC_SUPER 0x0020 标志允许使用 invokespecial 字节码指令的新语义，JDK1.0.2 之后编译出来的类的这个标志默认为真。（使用增强的方法调用父类方法） ACC_INTERFACE 0x0200 标志这是一个接口 ACC_ABSTRACT 0x0400 是否为 abstract 类型，对于接口或者抽象类来说，次标志值为真，其他类型为假 ACC_SYNTHETIC 0x1000 标志此类并非由用户代码产生（即：由编译器产生的类，没有源码对应） ACC_ANNOTATION 0x2000 标志这是一个注解 ACC_ENUM 0x4000 标志这是一个枚举 类的访问权限通常为 ACC_开头的常量。\n每一种类型的表示都是通过设置访问标记的 32 位中的特定位来实现的。比如，若是 public final 的类，则该标记为 ACC_PUBLIC | ACC_FINAL。\n使用 ACC_SUPER 可以让类更准确地定位到父类的方法 super.method()，现代编译器都会设置并且使用这个标记。\n补充说明：\n带有 ACC_INTERFACE 标志的 class 文件表示的是接口而不是类，反之则表示的是类而不是接口。\n如果一个 class 文件被设置了 ACC_INTERFACE 标志，那么同时也得设置 ACC_ABSTRACT 标志。同时它不能再设置 ACC_FINAL、ACC_SUPER 或 ACC_ENUM 标志。 如果没有设置 ACC_INTERFACE 标志，那么这个 class 文件可以具有上表中除 ACC_ANNOTATION 外的其他所有标志。当然，ACC_FINAL 和 ACC_ABSTRACT 这类互斥的标志除外。这两个标志不得同时设置。 ACC_SUPER 标志用于确定类或接口里面的 invokespecial 指令使用的是哪一种执行语义。针对 Java 虚拟机指令集的编译器都应当设置这个标志。对于 Java SE 8 及后续版本来说，无论 class 文件中这个标志的实际值是什么，也不管 class 文件的版本号是多少，Java 虚拟机都认为每个 class 文件均设置了 ACC_SUPER 标志。\nACC_SUPER 标志是为了向后兼容由旧 Java 编译器所编译的代码而设计的。目前的 ACC_SUPER 标志在由 JDK1.0.2 之前的编译器所生成的 access_flags 中是没有确定含义的，如果设置了该标志，那么 0racle 的 Java 虚拟机实现会将其忽略。 ACC_SYNTHETIC 标志意味着该类或接口是由编译器生成的，而不是由源代码生成的。\n注解类型必须设置 ACC_ANNOTATION 标志。如果设置了 ACC_ANNOTATION 标志，那么也必须设置 ACC_INTERFACE 标志。\nACC_ENUM 标志表明该类或其父类为枚举类型。\n1.7. 类索引、父类索引、接口索引 在访问标记后，会指定该类的类别、父类类别以及实现的接口，格式如下：\n长度 含义 u2 this_class u2 super_class u2 interfaces_count u2 interfaces[interfaces_count] 这三项数据来确定这个类的继承关系：\n类索引用于确定这个类的全限定名 父类索引用于确定这个类的父类的全限定名。由于 Java 语言不允许多重继承，所以父类索引只有一个，除了 java.1ang.Object 之外，所有的 Java 类都有父类，因此除了 java.lang.Object 外，所有 Java 类的父类索引都不为 e。 接口索引集合就用来描述这个类实现了哪些接口，这些被实现的接口将按 implements 语句（如果这个类本身是一个接口，则应当是 extends 语句）后的接口顺序从左到右排列在接口索引集合中。 1.7.1. this_class（类索引） 2 字节无符号整数，指向常量池的索引。它提供了类的全限定名，如 com/atguigu/java1/Demo。this_class 的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为 CONSTANT_Class_info 类型结构体，该结构体表示这个 class 文件所定义的类或接口。\n1.7.2. super_class（父类索引） 2 字节无符号整数，指向常量池的索引。它提供了当前类的父类的全限定名。如果我们没有继承任何类，其默认继承的是 java/lang/object 类。同时，由于 Java 不支持多继承，所以其父类只有一个。\nsuper_class 指向的父类不能是 final。\n1.7.3. interfaces 指向常量池索引集合，它提供了一个符号引用到所有已实现的接口\n由于一个类可以实现多个接口，因此需要以数组形式保存多个接口的索引，表示接口的每个索引也是一个指向常量池的 CONSTANT_Class（当然这里就必须是接口，而不是类）。\nⅠ. interfaces_count（接口计数器） interfaces_count 项的值表示当前类或接口的直接超接口数量。\nⅡ. interfaces[]（接口索引集合） interfaces[]中每个成员的值必须是对常量池表中某项的有效索引值，它的长度为 interfaces_count。每个成员 interfaces[i]必须为 CONSTANT_Class_info 结构，其中 0 \u0026lt;= i \u0026lt; interfaces_count。在 interfaces[]中，各成员所表示的接口顺序和对应的源代码中给定的接口顺序（从左至右）一样，即 interfaces[0]对应的是源代码中最左边的接口。\n1.8. 字段表集合 fields\n用于描述接口或类中声明的变量。字段（field）包括类级变量以及实例级变量，但是不包括方法内部、代码块内部声明的局部变量。\n字段叫什么名字、字段被定义为什么数据类型，这些都是无法固定的，只能引用常量池中的常量来描述。\n它指向常量池索引集合，它描述了每个字段的完整信息。比如字段的标识符、访问修饰符（public、private 或 protected）、是类变量还是实例变量（static 修饰符）、是否是常量（final 修饰符）等。\n注意事项：\n字段表集合中不会列出从父类或者实现的接口中继承而来的字段，但有可能列出原本 Java 代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性，会自动添加指向外部类实例的字段。 在 Java 语言中字段是无法重载的，两个字段的数据类型、修饰符不管是否相同，都必须使用不一样的名称，但是对于字节码来讲，如果两个字段的描述符不一致，那字段重名就是合法的。 1.8.1. 字段计数器 fields_count（字段计数器）\nfields_count 的值表示当前 class 文件 fields 表的成员个数。使用两个字节来表示。\nfields 表中每个成员都是一个 field_info 结构，用于表示该类或接口所声明的所有类字段或者实例字段，不包括方法内部声明的变量，也不包括从父类或父接口继承的那些字段。\n标志名称 标志值 含义 数量 u2 access_flags 访问标志 1 u2 name_index 字段名索引 1 u2 descriptor_index 描述符索引 1 u2 attributes_count 属性计数器 1 attribute_info attributes 属性集合 attributes_count 1.8.2. 字段表 Ⅰ. 字段表访问标识 我们知道，一个字段可以被各种关键字去修饰，比如：作用域修饰符（public、private、protected）、static 修饰符、final 修饰符、volatile 修饰符等等。因此，其可像类的访问标志那样，使用一些标志来标记字段。字段的访问标志有如下这些：\n标志名称 标志值 含义 ACC_PUBLIC 0x0001 字段是否为 public ACC_PRIVATE 0x0002 字段是否为 private ACC_PROTECTED 0x0004 字段是否为 protected ACC_STATIC 0x0008 字段是否为 static ACC_FINAL 0x0010 字段是否为 final ACC_VOLATILE 0x0040 字段是否为 volatile ACC_TRANSTENT 0x0080 字段是否为 transient ACC_SYNCHETIC 0x1000 字段是否为由编译器自动产生 ACC_ENUM 0x4000 字段是否为 enum Ⅱ. 描述符索引 描述符的作用是用来描述字段的数据类型、方法的参数列表（包括数量、类型以及顺序）和返回值。根据描述符规则，基本数据类型（byte，char，double，float，int，long，short，boolean）及代表无返回值的 void 类型都用一个大写字符来表示，而对象则用字符 L 加对象的全限定名来表示，如下所示：\n标志符 含义 B 基本数据类型 byte C 基本数据类型 char D 基本数据类型 double F 基本数据类型 float I 基本数据类型 int J 基本数据类型 long S 基本数据类型 short Z 基本数据类型 boolean V 代表 void 类型 L 对象类型，比如：Ljava/lang/Object; [ 数组类型，代表一维数组。比如：`double[][][] is [[[D Ⅲ. 属性表集合 一个字段还可能拥有一些属性，用于存储更多的额外信息。比如初始化值、一些注释信息等。属性个数存放在 attribute_count 中，属性具体内容存放在 attributes 数组中。\n1 2 3 4 5 6 // 以常量属性为例，结构为： ConstantValue_attribute{ u2 attribute_name_index; u4 attribute_length; u2 constantvalue_index; } Copied! 说明：对于常量属性而言，attribute_length 值恒为 2。\n1.9. 方法表集合 methods：指向常量池索引集合，它完整描述了每个方法的签名。\n在字节码文件中，每一个 method_info 项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符（public、private 或 protected），方法的返回值类型以及方法的参数信息等。 如果这个方法不是抽象的或者不是 native 的，那么字节码中会体现出来。 一方面，methods 表只描述当前类或接口中声明的方法，不包括从父类或父接口继承的方法。另一方面，methods 表有可能会出现由编译器自动添加的方法，最典型的便是编译器产生的方法信息（比如：类（接口）初始化方法\u0026lt;clinit\u0026gt;()和实例初始化方法\u0026lt;init\u0026gt;()）。 使用注意事项：\n在 Java 语言中，要重载（Overload）一个方法，除了要与原方法具有相同的简单名称之外，还要求必须拥有一个与原方法不同的特征签名，特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合，也就是因为返回值不会包含在特征签名之中，因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在 Class 文件格式中，特征签名的范围更大一些，只要描述符不是完全一致的两个方法就可以共存。也就是说，如果两个方法有相同的名称和特征签名，但返回值不同，那么也是可以合法共存于同一个 class 文件中。\n也就是说，尽管 Java 语法规范并不允许在一个类或者接口中声明多个方法签名相同的方法，但是和 Java 语法规范相反，字节码文件中却恰恰允许存放多个方法签名相同的方法，唯一的条件就是这些方法之间的返回值不能相同。\n1.9.1. 方法计数器 methods_count（方法计数器）\nmethods_count 的值表示当前 class 文件 methods 表的成员个数。使用两个字节来表示。\nmethods 表中每个成员都是一个 method_info 结构。\n1.9.2. 方法表 methods[]（方法表）\nmethods 表中的每个成员都必须是一个 method_info 结构，用于表示当前类或接口中某个方法的完整描述。如果某个 method_info 结构的 access_flags 项既没有设置 ACC_NATIVE 标志也没有设置 ACC_ABSTRACT 标志，那么该结构中也应包含实现这个方法所用的 Java 虚拟机指令。\nmethod_info 结构可以表示类和接口中定义的所有方法，包括实例方法、类方法、实例初始化方法和类或接口初始化方法\n方法表的结构实际跟字段表是一样的，方法表结构如下：\n标志名称 标志值 含义 数量 u2 access_flags 访问标志 1 u2 name_index 方法名索引 1 u2 descriptor_index 描述符索引 1 u2 attributes_count 属性计数器 1 attribute_info attributes 属性集合 attributes_count 方法表访问标志\n跟字段表一样，方法表也有访问标志，而且他们的标志有部分相同，部分则不同，方法表的具体访问标志如下：\n标志名称 标志值 含义 ACC_PUBLIC 0x0001 public，方法可以从包外访问 ACC_PRIVATE 0x0002 private，方法只能本类访问 ACC_PROTECTED 0x0004 protected，方法在自身和子类可以访问 ACC_STATIC 0x0008 static，静态方法 1.10. 属性表集合 方法表集合之后的属性表集合，指的是 class 文件所携带的辅助信息，比如该 class 文件的源文件的名称。以及任何带有 RetentionPolicy.CLASS 或者 RetentionPolicy.RUNTIME 的注解。这类信息通常被用于 Java 虚拟机的验证和运行，以及 Java 程序的调试，一般无须深入了解。\n此外，字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息。\n属性表集合的限制没有那么严格，不再要求各个属性表具有严格的顺序，并且只要不与已有的属性名重复，任何人实现的编译器都可以向属性表中写入自己定义的属性信息，但 Java 虚拟机运行时会忽略掉它不认识的属性。\n1.10.1. 属性计数器 attributes_count（属性计数器）\nattributes_count 的值表示当前 class 文件属性表的成员个数。属性表中每一项都是一个 attribute_info 结构。\n1.10.2. 属性表 attributes[]（属性表）\n属性表的每个项的值必须是 attribute_info 结构。属性表的结构比较灵活，各种不同的属性只要满足以下结构即可。\n属性的通用格式\n类型 名称 数量 含义 u2 attribute_name_index 1 属性名索引 u4 attribute_length 1 属性长度 u1 info attribute_length 属性表 属性类型\n属性表实际上可以有很多类型，上面看到的 Code 属性只是其中一种，Java8 里面定义了 23 种属性。下面这些是虚拟机中预定义的属性：\n属性名称 使用位置 含义 Code 方法表 Java 代码编译成的字节码指令 ConstantValue 字段表 final 关键字定义的常量池 Deprecated 类，方法，字段表 被声明为 deprecated 的方法和字段 Exceptions 方法表 方法抛出的异常 EnclosingMethod 类文件 仅当一个类为局部类或者匿名类时才能拥有这个属性，这个属性用于标识这个类所在的外围方法 InnerClass 类文件 内部类列表 LineNumberTable Code 属性 Java 源码的行号与字节码指令的对应关系 LocalVariableTable Code 属性 方法的局部变量描述 StackMapTable Code 属性 JDK1.6 中新增的属性，供新的类型检查检验器和处理目标方法的局部变量和操作数有所需要的类是否匹配 Signature 类，方法表，字段表 用于支持泛型情况下的方法签名 SourceFile 类文件 记录源文件名称 SourceDebugExtension 类文件 用于存储额外的调试信息 Synthetic 类，方法表，字段表 标志方法或字段为编译器自动生成的 LocalVariableTypeTable 类 是哟很难过特征签名代替描述符，是为了引入泛型语法之后能描述泛型参数化类型而添加 RuntimeVisibleAnnotations 类，方法表，字段表 为动态注解提供支持 RuntimeInvisibleAnnotations 类，方法表，字段表 用于指明哪些注解是运行时不可见的 RuntimeVisibleParameterAnnotation 方法表 作用与 RuntimeVisibleAnnotations 属性类似，只不过作用对象或方法 RuntimeInvisibleParameterAnnotation 方法表 作用与 RuntimeInvisibleAnnotations 属性类似，只不过作用对象或方法 AnnotationDefault 方法表 用于记录注解类元素的默认值 BootstrapMethods 类文件 用于保存 invokeddynamic 指令引用的引导方法限定符 或者（查看官网）\n部分属性详解\n① ConstantValue 属性\nConstantValue 属性表示一个常量字段的值。位于 field_info 结构的属性表中。\n1 2 3 4 5 ConstantValue_attribute{ u2 attribute_name_index; u4 attribute_length; u2 constantvalue_index;//字段值在常量池中的索引，常量池在该索引处的项给出该属性表示的常量值。（例如，值是1ong型的，在常量池中便是CONSTANT_Long） } Copied! ② Deprecated 属性\nDeprecated 属性是在 JDK1.1 为了支持注释中的关键词@deprecated 而引入的。\n1 2 3 4 Deprecated_attribute{ u2 attribute_name_index; u4 attribute_length; } Copied! ③ Code 属性\nCode 属性就是存放方法体里面的代码。但是，并非所有方法表都有 Code 属性。像接口或者抽象方法，他们没有具体的方法体，因此也就不会有 Code 属性了。Code 属性表的结构，如下图：\n类型 名称 数量 含义 u2 attribute_name_index 1 属性名索引 u4 attribute_length 1 属性长度 u2 max_stack 1 操作数栈深度的最大值 u2 max_locals 1 局部变量表所需的存续空间 u4 code_length 1 字节码指令的长度 u1 code code_lenth 存储字节码指令 u2 exception_table_length 1 异常表长度 exception_info exception_table exception_length 异常表 u2 attributes_count 1 属性集合计数器 attribute_info attributes attributes_count 属性集合 可以看到：Code 属性表的前两项跟属性表是一致的，即 Code 属性表遵循属性表的结构，后面那些则是他自定义的结构。\n④ InnerClasses 属性\n为了方便说明特别定义一个表示类或接口的 Class 格式为 C。如果 C 的常量池中包含某个 CONSTANT_Class_info 成员，且这个成员所表示的类或接口不属于任何一个包，那么 C 的 ClassFile 结构的属性表中就必须含有对应的 InnerClasses 属性。InnerClasses 属性是在 JDK1.1 中为了支持内部类和内部接口而引入的，位于 ClassFile 结构的属性表。\n⑤ LineNumberTable 属性\nLineNumberTable 属性是可选变长属性，位于 Code 结构的属性表。\nLineNumberTable 属性是用来描述 Java 源码行号与字节码行号之间的对应关系。这个属性可以用来在调试的时候定位代码执行的行数。\nstart_pc，即字节码行号；1ine_number，即 Java 源代码行号。 在 Code 属性的属性表中，LineNumberTable 属性可以按照任意顺序出现，此外，多个 LineNumberTable 属性可以共同表示一个行号在源文件中表示的内容，即 LineNumberTable 属性不需要与源文件的行一一对应。\n1 2 3 4 5 6 7 8 9 10 // LineNumberTable属性表结构： LineNumberTable_attribute{ u2 attribute_name_index; u4 attribute_length; u2 line_number_table_length; { u2 start_pc; u2 line_number; } line_number_table[line_number_table_length]; } Copied! ⑥ LocalVariableTable 属性\njavac -g 才会在字节码中生成局部变量表，不加-g则没有 默认情况下，eclipse、IDEA 在编译时会帮你生成局部变量表、指令和代码行偏移量映射等信息的 LocalVariableTable 是可选变长属性，位于 Code 属性的属性表中。它被调试器用于确定方法在执行过程中局部变量的信息。在 Code 属性的属性表中，LocalVariableTable 属性可以按照任意顺序出现。Code 属性中的每个局部变量最多只能有一个 LocalVariableTable 属性。\nstart pc + length 表示这个变量在字节码中的生命周期起始和结束的偏移位置（this 生命周期从头 e 到结尾 10） index 就是这个变量在局部变量表中的槽位（槽位可复用） name 就是变量名 Descriptor 表示局部变量类型描述 1 2 3 4 5 6 7 8 9 10 11 12 13 // LocalVariableTable属性表结构： LocalVariableTable_attribute{ u2 attribute_name_index; u4 attribute_length; u2 local_variable_table_length; { u2 start_pc; u2 length; u2 name_index; u2 descriptor_index; u2 index; } local_variable_table[local_variable_table_length]; } Copied! ⑦ Signature 属性\nSignature 属性是可选的定长属性，位于 ClassFile，field_info 或 method_info 结构的属性表中。在 Java 语言中，任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量（Type Variables）或参数化类型（Parameterized Types），则 Signature 属性会为它记录泛型签名信息。\n⑧ SourceFile 属性\nSourceFile 属性结构\n类型 名称 数量 含义 u2 attribute_name_index 1 属性名索引 u4 attribute_length 1 属性长度 u2 sourcefile index 1 源码文件素引 可以看到，其长度总是固定的 8 个字节。\n⑨ 其他属性\nJava 虚拟机中预定义的属性有 20 多个，这里就不一一介绍了，通过上面几个属性的介绍，只要领会其精髓，其他属性的解读也是易如反掌。\n","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/61189461/","title":"01-class文件结构"},{"content":" 1.JVM 与 Java 体系结构 1.1. 前言 作为 Java 工程师的你曾被伤害过吗？你是否也遇到过这些问题？\n运行着的线上系统突然卡死，系统无法访问，甚至直接 OOM 想解决线上 JVM GC 问题，但却无从下手 新项目上线，对各种 JVM 参数设置一脸茫然，直接默认吧然后就 JJ 了 每次面试之前都要重新背一遍 JVM 的一些原理概念性的东西，然而面试官却经常问你在实际项目中如何调优 VM 参数，如何解决 GC、OOM 等问题，一脸懵逼 大部分 Java 开发人员，除会在项目中使用到与 Java 平台相关的各种高精尖技术，对于 Java 技术的核心 Java 虚拟机了解甚少。\n开发人员如何看待上层框架\n一些有一定工作经验的开发人员，打心眼儿里觉得 SSM、微服务等上层技术才是重点，基础技术并不重要，这其实是一种本末倒置的“病态”。\n如果我们把核心类库的 API 比做数学公式的话，那么 Java 虚拟机的知识就好比公式的推导过程。\n计算机系统体系对我们来说越来越远，在不了解底层实现方式的前提下，通过高级语言很容易编写程序代码。但事实上计算机并不认识高级语言\n我们为什么要学习 JVM？\n面试的需要（BATJ、TMD，PKQ 等面试都爱问） 中高级程序员必备技能 项目管理、调优的需求 追求极客的精神 比如：垃圾回收算法、JIT、底层原理 Java vs C++\n垃圾收集机制为我们打理了很多繁琐的工作，大大提高了开发的效率，但是，垃圾收集也不是万能的，懂得 JVM 内部的内存结构、工作机制，是设计高扩展性应用和诊断运行时问题的基础，也是 Java 工程师进阶的必备能力。\n1.2. 面向人群及参考书目 1.3. Java 及 JVM 简介 TIOBE 语言热度排行榜：index | TIOBE - The Software Quality Company Programming Language 2021 2016 2011 2006 2001 1996 1991 1986 C 1 2 2 2 1 1 1 1 Java 2 1 1 1 3 26 - - Python 3 5 6 8 27 19 - - C++ 4 3 3 3 2 2 2 8 C# 5 4 5 7 13 - - - Visual Basic 6 13 - - - - - - JavaScript 7 8 10 9 10 32 - - PHP 8 6 4 4 11 - - - SQL 9 - - - - - - - R 10 17 31 - - - - - Lisp 34 27 13 14 17 7 4 2 Ada 36 28 17 16 20 8 5 3 (Visual) Basic - - 7 6 4 3 3 5 世界上没有最好的编程语言，只有最适用于具体应用场景的编程语言\nJVM：跨语言的平台\nJava 是目前应用最为广泛的软件开发平台之一。随着 Java 以及 Java 社区的不断壮大 Java 也早已不再是简简单单的一门计算机语言了，它更是一个平台、一种文化、一个社区。\n作为一个平台，Java 虚拟机扮演着举足轻重的作用 Groovy、Scala、JRuby、Kotlin 等都是 Java 平台的一部分 作为灯种文化，Java 几乎成为了“开源”的代名词。 第三方开源软件和框架。如 Tomcat、Struts，MyBatis，Spring 等。 就连 JDK 和 JVM 自身也有不少开源的实现，如 openJDK、Harmony。 作为一个社区，Java 拥有全世界最多的技术拥护者和开源社区支持，有数不清的论坛和资料。从桌面应用软件、嵌入式开发到企业级应用、后台服务器、中间件，都可以看到 Java 的身影。其应用形式之复杂、参与人数之众多也令人咋舌。 每个语言都需要转换成字节码文件，最后转换的字节码文件都能通过 Java 虚拟机进行运行和处理\n随着 Java7 的正式发布，Java 虚拟机的设计者们通过 JSR-292 规范基本实现在Java 虚拟机平台上运行非 Java 语言编写的程序。 Java 虚拟机根本不关心运行在其内部的程序到底是使用何种编程语言编写的，它只关心“字节码”文件。也就是说 Java 虚拟机拥有语言无关性，并不会单纯地与 Java 语言“终身绑定”，只要其他编程语言的编译结果满足并包含 Java 虚拟机的内部指令集、符号表以及其他的辅助信息，它就是一个有效的字节码文件，就能够被虚拟机所识别并装载运行。 字节码\n我们平时说的 java 字节码，指的是用 java 语言编译成的字节码。准确的说任何能在 jvm 平台上执行的字节码格式都是一样的。所以应该统称为：jvm 字节码。 不同的编译器，可以编译出相同的字节码文件，字节码文件也可以在不同的 JVM 上运行。 Java 虚拟机与 Java 语言并没有必然的联系，它只与特定的二进制文件格式—Class 文件格式所关联，Class 文件中包含了 Java 虚拟机指令集（或者称为字节码、Bytecodes）和符号表，还有一些其他辅助信息。 多语言混合编程\nJava 平台上的多语言混合编程正成为主流，通过特定领域的语言去解决特定领域的问题是当前软件开发应对日趋复杂的项目需求的一个方向。 试想一下，在一个项目之中，并行处理用 Clojure 语言编写，展示层使用 JRuby/Rails，中间层则是 Java，每个应用层都将使用不同的编程语言来完成，而且，接口对每一层的开发者都是透明的，各种语言之间的交互不存在任何困难，就像使用自己语言的原生 API 一样方便，因为它们最终都运行在一个虚拟机之上。 对这些运行于 Java 虚拟机之上、Java 之外的语言，来自系统级的、底层的支持正在迅速增强，以 JSR-292 为核心的一系列项目和功能改进（如 Da Vinci Machine 项目、Nashorn 引擎、InvokeDynamic 指令、java.lang.invoke 包等），推动 Java 虚拟机从“Java 语言的虚拟机”向 “多语言虚拟机”的方向发展。 如何真正搞懂 JVM？\nJava 虚拟机非常复杂，要想真正理解它的工作原理，最好的方式就是自己动手编写一个！\n自己动手写一个 Java 虚拟机，难吗？\n天下事有难易乎？\n为之，则难者亦易矣；不为，则易者亦难矣\n1.4. Java 发展的重大事件 1990 年，在 Sun 计算机公司中，由 Patrick Naughton、MikeSheridan 及 James Gosling 领导的小组 Green Team，开发出的新的程序语言，命名为 oak，后期命名为 Java 1995 年，Sun 正式发布 Java 和 HotJava 产品，Java 首次公开亮相。 1996 年 1 月 23 日，Sun Microsystems 发布了 JDK 1.0。 1998 年，JDK1.2 版本发布。同时，sun 发布了 JSP/Servlet、EJB 规范，以及将 Java 分成了 J2EE、J2SE 和 J2ME。这表明了 Java 开始向企业、桌面应用和移动设备应用 3 大领域挺进。 2000 年，JDK1.3 发布，Java HotSpot Virtual Machine 正式发布，成为 Java 的默认虚拟机。 2002 年，JDK1.4 发布，古老的 Classic 虚拟机退出历史舞台。 2003 年年底，Java 平台的 Scala 正式发布，同年 Groovy 也加入了 Java 阵营。 2004 年，JDK1.5 发布。同时 JDK1.5 改名为 JavaSE5.0。 2006 年，JDK6 发布。同年，Java 开源并建立了 OpenJDK。顺理成章，Hotspot 虚拟机也成为了 openJDK 中的默认虚拟机。 2007 年，Java 平台迎来了新伙伴 Clojure。 2008 年，Oracle 收购了 BEA，得到了 JRockit 虚拟机。 2009 年，Twitter 宣布把后台大部分程序从 Ruby 迁移到 Scala，这是 Java 平台的又一次大规模应用。 2010 年，Oracle 收购了 Sun，获得 Java 商标和最真价值的 HotSpot 虚拟机。此时，Oracle 拥有市场占用率最高的两款虚拟机 HotSpot 和 JRockit，并计划在未来对它们进行整合：HotRockit 2011 年，JDK7 发布。在 JDK1.7u4 中，正式启用了新的垃圾回收器 G1。 2017 年，JDK9 发布。将 G1 设置为默认 Gc，替代 CMS 同年，IBM 的 J9 开源，形成了现在的 Open J9 社区 2018 年，Android 的 Java 侵权案判决，Google 赔偿 Oracle 计 88 亿美元 同年，Oracle 宣告 JavaEE 成为历史名词 JDBC、JMS、Servlet 赠予 Eclipse 基金会 同年，JDK11 发布，LTS 版本的 JDK，发布革命性的 ZGC，调整 JDK 授权许可 2019 年，JDK12 发布，加入 RedHat 领导开发的shenandoah GC 在 JDK11 之前，OracleJDK 中还会存在一些 OpenJDK 中没有的、闭源的功能。但在 JDK11 中，我们可以认为 OpenJDK 和 OracleJDK 代码实质上已经完全一致的程度。\n不过，主流的 JDK 8 在 2019 年 01 月之后就被宣布停止更新了。另外， JDK 11 及以后的版本也不再提供免费的长期支持（LTS），而且 JDK 15 和 JDK 16 也不是一个长期支持的版本，最新的 JDK 15 只支持 6 个月时间，到 2021 年 3 月，所以千万不要把 JDK 15 等非长期支持版本用在生产。\n1.5. 虚拟机与 Java 虚拟机 虚拟机\n所谓虚拟机（Virtual Machine），就是一台虚拟的计算机。它是一款软件，用来执行一系列虚拟计算机指令。大体上，虚拟机可以分为系统虚拟机和程序虚拟机。\n大名鼎鼎的 Visual Box，Mware 就属于系统虚拟机，它们完全是对物理计算机的仿真，提供了一个可运行完整操作系统的软件平台。 程序虚拟机的典型代表就是 Java 虚拟机，它专门为执行单个计算机程序而设计，在 Java 虚拟机中执行的指令我们称为 Java 字节码指令。 无论是系统虚拟机还是程序虚拟机，在上面运行的软件都被限制于虚拟机提供的资源中。\nJava 虚拟机\nJava 虚拟机是一台执行 Java 字节码的虚拟计算机，它拥有独立的运行机制，其运行的 Java 字节码也未必由 Java 语言编译而成。 JVM 平台的各种语言可以共享 Java 虚拟机带来的跨平台性、优秀的垃圾回器，以及可靠的即时编译器。 Java 技术的核心就是 Java 虚拟机（JVM，Java Virtual Machine），因为所有的 Java 程序都运行在 Java 虚拟机内部。 作用\nJava 虚拟机就是二进制字节码的运行环境，负责装载字节码到其内部，解释/编译为对应平台上的机器指令执行。每一条 Java 指令，Java 虚拟机规范中都有详细定义，如怎么取操作数，怎么处理操作数，处理结果放在哪里。 特点\n一次编译，到处运行 自动内存管理 自动垃圾回收功能 JVM 的位置\nJVM 是运行在操作系统之上的，它与硬件没有直接的交互 1.6. JVM 的整体结构 HotSpot VM 是目前市面上高性能虚拟机的代表作之一。 它采用解释器与即时编译器并存的架构。 在今天，Java 程序的运行性能早已脱胎换骨，已经达到了可以和 C/C++程序一较高下的地步。 1.7. Java 代码执行流程 1.8. JVM 的架构模型 Java 编译器输入的指令流基本上是一种基于栈的指令集架构，另外一种指令集架构则是基于寄存器的指令集架构。\n具体来说：这两种架构之间的区别：\n基于栈式架构的特点\n设计和实现更简单，适用于资源受限的系统 避开了寄存器的分配难题：使用零地址指令方式分配 指令流中的指令大部分是零地址指令，其执行过程依赖于操作栈。指令集更小，编译器容易实现 不需要硬件支持，可移植性更好，更好实现跨平台 基于寄存器架构的特点\n典型的应用是 x86 的二进制指令集：比如传统的 PC 以及 Android 的 Davlik 虚拟机 指令集架构则完全依赖硬件，可移植性差 性能优秀和执行更高效 花费更少的指令去完成一项操作 在大部分情况下，基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主，而基于栈式架构的指令集却是以零地址指令为主 举例 1\n同样执行 2+3 这种逻辑操作，其指令分别如下：\n基于栈的计算流程（以 Java 虚拟机为例）：\n1 2 3 4 5 6 7 8 iconst_2 //常量2入栈 istore_1 iconst_3 // 常量3入栈 istore_2 iload_1 iload_2 iadd //常量2/3出栈，执行相加 istore_0 // 结果5入栈 Copied! 而基于寄存器的计算流程\n1 2 mov eax,2 //将eax寄存器的值设为1 add eax,3 //使eax寄存器的值加3 Copied! 举例 2\n1 2 3 4 5 6 public int calc(){ int a=100; int b=200; int c=300; return (a + b) * c; } Copied! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026gt; javap -c Test.class ... public int calc(); Code: Stack=2,Locals=4,Args_size=1 0: bipush 100 2: istore_1 3: sipush 200 6: istore_2 7: sipush 300 10: istore_3 11: iload_1 12: iload_2 13: iadd 14: iload_3 15: imul 16: ireturn } Copied! 总结\n由于跨平台性的设计，Java 的指令都是根据栈来设计的。不同平台 CPU 架构不同，所以不能设计为基于寄存器的。优点是跨平台，指令集小，编译器容易实现，缺点是性能下降，实现同样的功能需要更多的指令。\n时至今日，尽管嵌入式平台已经不是 Java 程序的主流运行平台了（准确来说应该是 HotSpotVM 的宿主环境已经不局限于嵌入式平台了），那么为什么不将架构更换为基于寄存器的架构呢？\n1.9. JVM 的生命周期 虚拟机的启动\nJava 虚拟机的启动是通过引导类加载器（bootstrap class loader）创建一个初始类（initial class）来完成的，这个类是由虚拟机的具体实现指定的。\n虚拟机的执行\n一个运行中的 Java 虚拟机有着一个清晰的任务：执行 Java 程序。 程序开始执行时他才运行，程序结束时他就停止。 执行一个所谓的 Java 程序的时候，真真正正在执行的是一个叫做 Java 虚拟机的进程。 虚拟机的退出\n有如下的几种情况：\n程序正常执行结束 程序在执行过程中遇到了异常或错误而异常终止 由于操作系统用现错误而导致 Java 虚拟机进程终止 某线程调用 Runtime 类或 system 类的 exit 方法，或 Runtime 类的 halt 方法，并且 Java 安全管理器也允许这次 exit 或 halt 操作。 除此之外，JNI（Java Native Interface）规范描述了用 JNI Invocation API 来加载或卸载 Java 虚拟机时，Java 虚拟机的退出情况。 X. JVM 的发展历程 Sun Classic VM 早在 1996 年 Java1.0 版本的时候，Sun 公司发布了一款名为 sun classic VM 的 Java 虚拟机，它同时也是世界上第一款商用 Java 虚拟机，JDK1.4 时完全被淘汰。 这款虚拟机内部只提供解释器。现在还有及时编译器，因此效率比较低，而及时编译器会把热点代码缓存起来，那么以后使用热点代码的时候，效率就比较高。 如果使用 JIT 编译器，就需要进行外挂。但是一旦使用了 JIT 编译器，JIT 就会接管虚拟机的执行系统。解释器就不再工作。解释器和编译器不能配合工作。 现在 hotspot 内置了此虚拟机。 Exact VM 为了解决上一个虚拟机问题，jdk1.2 时，Sun 提供了此虚拟机。 Exact Memory Management：准确式内存管理 也可以叫 Non-Conservative/Accurate Memory Management 虚拟机可以知道内存中某个位置的数据具体是什么类型。 具备现代高性能虚拟机的维形 热点探测 编译器与解释器混合工作模式 只在 solaris 平台短暂使用，其他平台上还是 classic vm 英雄气短，终被 Hotspot 虚拟机替换 HotSpot VM HotSpot 历史 最初由一家名为“Longview Technologies”的小公司设计 1997 年，此公司被 sun 收购；2009 年，Sun 公司被甲骨文收购。 JDK1.3 时，HotSpot VM 成为默认虚拟机 目前 Hotspot 占有绝对的市场地位，称霸武林。 不管是现在仍在广泛使用的 JDK6，还是使用比例较多的 JDK8 中，默认的虚拟机都是 HotSpot Sun / Oracle JDK 和 OpenJDK 的默认虚拟机 因此本课程中默认介绍的虚拟机都是 HotSpot，相关机制也主要是指 HotSpot 的 Gc 机制。（比如其他两个商用虚机都没有方法区的概念） 从服务器、桌面到移动端、嵌入式都有应用。 名称中的 HotSpot 指的就是它的热点代码探测技术。 通过计数器找到最具编译价值代码，触发即时编译或栈上替换 通过编译器与解释器协同工作，在最优化的程序响应时间与最佳执行性能中取得平衡 JRockit 专注于服务器端应用\n它可以不太关注程序启动速度，因此 JRockit 内部不包含解析器实现，全部代码都靠即时编译器编译后执行。 大量的行业基准测试显示，JRockit JVM 是世界上最快的 JVM。\n使用 JRockit 产品，客户已经体验到了显著的性能提高（一些超过了 70%）和硬件成本的减少（达 50%）。 优势：全面的 Java 运行时解决方案组合\nJRockit 面向延迟敏感型应用的解决方案 JRockit Real Time 提供以毫秒或微秒级的 JVM 响应时间，适合财务、军事指挥、电信网络的需要 MissionControl 服务套件，它是一组以极低的开销来监控、管理和分析生产环境中的应用程序的工具。 2008 年，JRockit 被 oracle 收购。\nOracle 表达了整合两大优秀虚拟机的工作，大致在 JDK8 中完成。整合的方式是在 HotSpot 的基础上，移植 JRockit 的优秀特性。\n高斯林：目前就职于谷歌，研究人工智能和水下机器人\nIBM 的 J9 全称：IBM Technology for Java Virtual Machine，简称 IT4J，内部代号：J9\n市场定位与 HotSpot 接近，服务器端、桌面应用、嵌入式等多用途 VM\n广泛用于 IBM 的各种 Java 产品。\n目前，有影响力的三大商用虚拟机之一，也号称是世界上最快的 Java 虚拟机。\n2017 年左右，IBM 发布了开源 J9VM，命名为 openJ9，交给 EClipse 基金会管理，也称为 Eclipse OpenJ9\nKVM 和 CDC / CLDC Hotspot Oracle 在 Java ME 产品线上的两款虚拟机为：CDC/CLDC HotSpot Implementation VM\nKVM（Kilobyte）是 CLDC-HI 早期产品\n目前移动领域地位尴尬，智能机被 Android 和 iOS 二分天下。\nKVM 简单、轻量、高度可移植，面向更低端的设备上还维持自己的一片市场\n智能控制器、传感器 老人手机、经济欠发达地区的功能手机 所有的虚拟机的原则：一次编译，到处运行。\nAzul VM 前面三大“高性能 Java 虚拟机”使用在通用硬件平台上这里 Azul VW 和 BEA Liquid VM 是与特定硬件平台绑定、软硬件配合的专有虚拟机\n高性能 Java 虚拟机中的战斗机。 Azul VM 是 Azul Systems 公司在 HotSpot 基础上进行大量改进，运行于 Azul Systems 公司的专有硬件 Vega 系统上的 Java 虚拟机。\n每个 Azul VM 实例都可以管理至少数十个 CPU 和数百 GB 内存的硬件资源，并提供在巨大内存范围内实现可控的 GC 时间的垃圾收集器、专有硬件优化的线程调度等优秀特性。\n2010 年，AzulSystems 公司开始从硬件转向软件，发布了自己的 Zing JVM，可以在通用 x86 平台上提供接近于 Vega 系统的特性。\nLiquid VM 高性能 Java 虚拟机中的战斗机。\nBEA 公司开发的，直接运行在自家 Hypervisor 系统上\nLiquid VM 即是现在的 JRockit VE（Virtual Edition），Liquid VM 不需要操作系统的支持，或者说它自己本身实现了一个专用操作系统的必要功能，如线程调度、文件系统、网络支持等。\n随着 JRockit 虚拟机终止开发，Liquid vM 项目也停止了。\nApache Harmony Apache 也曾经推出过与 JDK1.5 和 JDK1.6 兼容的 Java 运行平台 Apache Harmony。\n它是 IBM 和 Intel 联合开发的开源 JVM，受到同样开源的 OpenJDK 的压制，Sun 坚决不让 Harmony 获得 JCP 认证，最终于 2011 年退役，IBM 转而参与 OpenJDK\n虽然目前并没有 Apache Harmony 被大规模商用的案例，但是它的 Java 类库代码吸纳进了 Android SDK。\nMicorsoft JVM 微软为了在 IE3 浏览器中支持 Java Applets，开发了 Microsoft JVM。\n只能在 Windows 平台下运行。但确是当时 Windows 下性能最好的 Java VM。\n1997 年，Sun 以侵犯商标、不正当竞争罪名指控微软成功，赔了 Sun 很多钱。微软 WindowsXP SP3 中抹掉了其 VM。现在 Windows 上安装的 jdk 都是 HotSpot。\nTaobao JVM 由 AliJVM 团队发布。阿里，国内使用 Java 最强大的公司，覆盖云计算、金融、物流、电商等众多领域，需要解决高并发、高可用、分布式的复合问题。有大量的开源产品。\n基于 OpenJDK 开发了自己的定制版本 AlibabaJDK，简称 AJDK。是整个阿里 Java 体系的基石。\n基于 OpenJDK Hotspot VM 发布的国内第一个优化、深度定制且开源的高性能服务器版 Java 虚拟机。\n创新的 GCIH（GC invisible heap）技术实现了 off-heap，即将生命周期较长的 Java 对象从 heap 中移到 heap 之外，并且 GC 不能管理 GCIH 内部的 Java 对象，以此达到降低 GC 的回收频率和提升 GC 的回收效率的目的。 GCIH 中的对象还能够在多个 Java 虚拟机进程中实现共享 使用 crc32 指令实现 JVM intrinsic 降低 JNI 的调用开销 PMU hardware 的 Java profiling tool 和诊断协助功能 针对大数据场景的 ZenGc taobao vm 应用在阿里产品上性能高，硬件严重依赖 intel 的 cpu，损失了兼容性，但提高了性能\n目前已经在淘宝、天猫上线，把 oracle 官方 JvM 版本全部替换了。 Dalvik VM 谷歌开发的，应用于 Android 系统，并在 Android2.2 中提供了 JIT，发展迅猛。\nDalvik VM 只能称作虚拟机，而不能称作“Java 虚拟机”，它没有遵循 Java 虚拟机规范，不能直接执行 Java 的 Class 文件\n基于寄存器架构，不是 jvm 的栈架构。\n执行的是编译以后的 dex（Dalvik Executable）文件。执行效率比较高。\n它执行的 dex（Dalvik Executable）文件可以通过 class 文件转化而来，使用 Java 语法编写应用程序，可以直接使用大部分的 Java API 等。 Android 5.0 使用支持提前编译（Ahead of Time Compilation，AoT）的 ART VM 替换 Dalvik VM。\nGraal VM 2018 年 4 月，oracle Labs 公开了 Graal VM，号称 \u0026ldquo;Run Programs Faster Anywhere\u0026rdquo;，野心勃勃。与 1995 年 java 的”write once，run anywhere\u0026quot;遥相呼应。\nGraal VM 在 HotSpot VM 基础上增强而成的跨语言全栈虚拟机，可以作为“任何语言” 的运行平台使用。语言包括：Java、Scala、Groovy、Kotlin；C、C++、Javascript、Ruby、Python、R 等\n支持不同语言中混用对方的接口和对象，支持这些语言使用已经编写好的本地库文件\n工作原理是将这些语言的源代码或源代码编译后的中间格式，通过解释器转换为能被 Graal VM 接受的中间表示。Graal VM 提供 Truffle 工具集快速构建面向一种新语言的解释器。在运行时还能进行即时编译优化，获得比原生编译器更优秀的执行效率。\n如果说 HotSpot 有一天真的被取代，Graal VM 希望最大。但是 Java 的软件生态没有丝毫变化。\n总结 具体 JVM 的内存结构，其实取决于其实现，不同厂商的 JVM，或者同一厂商发布的不同版本，都有可能存在一定差异。主要以 Oracle HotSpot VM 为默认虚拟机。\n","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/11894611/","title":"01-JVM与Java体系结构"},{"content":" 2. 类加载子系统 2.1. 内存结构概述 Class 文件 类加载子系统 运行时数据区 方法区 堆 程序计数器 虚拟机栈 本地方法栈 执行引擎 本地方法接口 本地方法库 如果自己想手写一个 Java 虚拟机的话，主要考虑哪些结构呢？\n类加载器 执行引擎 2.2. 类加载器与类的加载过程 类加载器子系统作用\n类加载器子系统负责从文件系统或者网络中加载 Class 文件，class 文件在文件开头有特定的文件标识。 ClassLoader 只负责 class 文件的加载，至于它是否可以运行，则由 Execution Engine 决定。 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外，方法区中还会存放运行时常量池信息，可能还包括字符串字面量和数字常量（这部分常量信息是 Class 文件中常量池部分的内存映射） 类加载器 ClasLoader 角色\nclass file 存在于本地硬盘上，可以理解为设计师画在纸上的模板，而最终这个模板在执行的时候是要加载到 JVM 当中来根据这个文件实例化出 n 个一模一样的实例。 class file 加载到 JVM 中，被称为 DNA 元数据模板，放在方法区。 在.class 文件-\u0026gt;JVM-\u0026gt;最终成为元数据模板，此过程就要一个运输工具（类装载器 Class Loader），扮演一个快递员的角色。 类的加载过程\n1 2 3 4 5 6 7 8 /** *示例代码 */ public class HelloLoader { public static void main(String[] args) { System.out.println(\u0026#34;Hello World!\u0026#34;); } } Copied! 用流程图表示上述示例代码：\n加载阶段 通过一个类的全限定名获取定义此类的二进制字节流 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 在内存中生成一个代表这个类的 java.lang.Class 对象，作为方法区这个类的各种数据的访问入口 补充：加载 class 文件的方式\n从本地系统中直接加载 通过网络获取，典型场景：Web Applet 从 zip压缩包中读取，成为日后 jar、war 格式的基础 运行时计算生成，使用最多的是：动态代理技术 由其他文件生成，典型场景：JSP 应用 从专有数据库中提取.class 文件，比较少见 从加密文件中获取，典型的防 Class 文件被反编译的保护措施 链接阶段 验证（Verify）： 目的在子确保 Class 文件的字节流中包含信息符合当前虚拟机要求，保证被加载类的正确性，不会危害虚拟机自身安全。 主要包括四种验证，文件格式验证，元数据验证，字节码验证，符号引用验证。 准备（Prepare）： 为类变量(静态变量static)分配内存并且设置该类变量的默认初始值，即零值。 这里不包含用 final 修饰的 static，因为 final 在编译的时候就会分配了，准备阶段会显式初始化； 这里不会为实例变量分配初始化，类变量会分配在方法区中，而实例变量是会随着对象一起分配到 Java 堆中。 解析（Resolve）： 将常量池内的符号引用转换为直接引用的过程。 事实上，解析操作往往会伴随着 JVM 在执行完初始化之后再执行。 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java 虚拟机规范》的 Class 文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_info，CONSTANT_Fieldref_info、CONSTANT_Methodref_info 等。 初始化阶段 初始化阶段就是执行类构造器方法\u0026lt;clinit\u0026gt;()的过程。 此方法不需定义，是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。 构造器方法中指令按语句在源文件中出现的顺序执行。 \u0026lt;clinit\u0026gt;()不同于类的构造器。（关联：构造器是虚拟机视角下的\u0026lt;init\u0026gt;()） 若该类具有父类，JVM 会保证子类的\u0026lt;clinit\u0026gt;()执行前，父类的\u0026lt;clinit\u0026gt;()已经执行完毕。 虚拟机必须保证一个类的\u0026lt;clinit\u0026gt;()方法在多线程下被同步加锁。 2.3. 类加载器分类 JVM 支持两种类型的类加载器 。分别为引导类加载器（Bootstrap ClassLoader）和自定义类加载器（User-Defined ClassLoader）。\n从概念上来讲，自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器，但是 Java 虚拟机规范却没有这么定义，而是将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器。\n无论类加载器的类型如何划分，在程序中我们最常见的类加载器始终只有 3 个，如下所示：\n这里的四者之间的关系是包含关系。不是上层下层，也不是子父类的继承关系。\n2.3.1. 虚拟机自带的加载器 启动类加载器（引导类加载器，Bootstrap ClassLoader）\n这个类加载使用 C/C++语言实现的，嵌套在 JVM 内部。 它用来加载 Java 的核心库（JAVA_HOME/jre/lib/rt.jar、resources.jar 或 sun.boot.class.path 路径下的内容），用于提供 JVM 自身需要的类 并不继承自 ava.lang.ClassLoader，没有父加载器。 加载扩展类和应用程序类加载器，并指定为他们的父类加载器。 出于安全考虑，Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类 扩展类加载器（Extension ClassLoader）\nJava 语言编写，由 sun.misc.Launcher$ExtClassLoader 实现。 派生于 ClassLoader 类 父类加载器为启动类加载器 从 java.ext.dirs 系统属性所指定的目录中加载类库，或从 JDK 的安装目录的 jre/1ib/ext 子目录（扩展目录）下加载类库。如果用户创建的 JAR 放在此目录下，也会自动由扩展类加载器加载。 应用程序类加载器（系统类加载器，AppClassLoader）\njava 语言编写，由 sun.misc.LaunchersAppClassLoader 实现 派生于 ClassLoader 类 父类加载器为扩展类加载器 它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库 该类加载是程序中默认的类加载器，一般来说，Java 应用的类都是由它来完成加载 通过 ClassLoader#getSystemclassLoader() 方法可以获取到该类加载器 2.3.2. 用户自定义类加载器 在 Java 的日常应用程序开发中，类的加载几乎是由上述 3 种类加载器相互配合执行的，在必要时，我们还可以自定义类加载器，来定制类的加载方式。 为什么要自定义类加载器？\n隔离加载类 修改类加载的方式 扩展加载源 防止源码泄漏 用户自定义类加载器实现步骤：\n开发人员可以通过继承抽象类 ava.lang.ClassLoader 类的方式，实现自己的类加载器，以满足一些特殊的需求 在 JDK1.2 之前，在自定义类加载器时，总会去继承 ClassLoader 类并重写 loadClass() 方法，从而实现自定义的类加载类，但是在 JDK1.2 之后已不再建议用户去覆盖 loadclass() 方法，而是建议把自定义的类加载逻辑写在 findClass()方法中 在编写自定义类加载器时，如果没有太过于复杂的需求，可以直接继承 URLClassLoader 类，这样就可以避免自己去编写 findClass() 方法及其获取字节码流的方式，使自定义类加载器编写更加简洁。 2.4. ClassLoader 的使用说明 ClassLoader 类是一个抽象类，其后所有的类加载器都继承自 ClassLoader（不包括启动类加载器）\nsun.misc.Launcher 它是一个 java 虚拟机的入口应用\n获取 ClassLoader 的途径\n方式一：获取当前 ClassLoader\n1 clazz.getClassLoader() Copied! 方式二：获取当前线程上下文的 ClassLoader\n1 Thread.currentThread().getContextClassLoader() Copied! 方式三：获取系统的 ClassLoader\n1 ClassLoader.getSystemClassLoader() Copied! 方式四：获取调用者的 ClassLoader\n1 DriverManager.getCallerClassLoader() Copied! 2.5. 双亲委派机制 Java 虚拟机对 class 文件采用的是按需加载的方式，也就是说当需要使用该类时才会将它的 class 文件加载到内存生成 class 对象。而且加载某个类的 class 文件时，Java 虚拟机采用的是双亲委派模式，即把请求交由父类处理，它是一种任务委派模式。\n工作原理\n1）如果一个类加载器收到了类加载请求，它并不会自己先去加载，而是把这个请求委托给父类的加载器去执行； 2）如果父类加载器还存在其父类加载器，则进一步向上委托，依次递归，请求最终将到达顶层的启动类加载器； 3）如果父类加载器可以完成类加载任务，就成功返回，倘若父类加载器无法完成此加载任务，子加载器才会尝试自己去加载，这就是双亲委派模式。 举例\n当我们加载 jdbc.jar 用于实现数据库连接的时候，首先我们需要知道的是 jdbc.jar 是基于 SPI 接口进行实现的，所以在加载的时候，会进行双亲委派，最终从根加载器中加载 SPI 核心类，然后在加载 SPI 接口类，接着在进行反向委派，通过线程上下文类加载器进行实现类 jdbc.jar 的加载。\n优势\n避免类的重复加载 保护程序安全，防止核心 API 被随意篡改 自定义类：java.lang.String 自定义类：java.lang.ShkStart（报错：阻止创建 java.lang 开头的类） 沙箱安全机制\n自定义 String 类，但是在加载自定义 String 类的时候会率先使用引导类加载器加载，而引导类加载器在加载的过程中会先加载 jdk 自带的文件（rt.jar 包中 java\\lang\\String.class），报错信息说没有 main 方法，就是因为加载的是 rt.jar 包中的 string 类。这样可以保证对 java 核心源代码的保护，这就是沙箱安全机制。\n2.6. 其他 如何判断两个 class 对象是否相同\n在 JVM 中表示两个 class 对象是否为同一个类存在两个必要条件：\n类的完整类名必须一致，包括包名。 加载这个类的 ClassLoader（指 ClassLoader 实例对象）必须相同。 换句话说，在 JVM 中，即使这两个类对象（class 对象）来源同一个 Class 文件，被同一个虚拟机所加载，但只要加载它们的 ClassLoader 实例对象不同，那么这两个类对象也是不相等的。\n对类加载器的引用\nJVM 必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的，那么 JVM 会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候，JVM 需要保证这两个类型的类加载器是相同的。\n类的主动使用和被动使用\nJava 程序对类的使用方式分为：主动使用和被动使用。\n主动使用，又分为七种情况：\n创建类的实例\n访问某个类或接口的静态变量，或者对该静态变量赋值\n调用类的静态方法\n反射（比如：Class.forName（\u0026ldquo;com.atguigu.Test\u0026rdquo;））\n初始化一个类的子类\nJava 虚拟机启动时被标明为启动类的类\nJDK 7 开始提供的动态语言支持：\njava.lang.invoke.MethodHandle 实例的解析结果\nREF_getStatic、REF_putStatic、REF_invokeStatic 句柄对应的类没有初始化，则初始化\n除了以上七种情况，其他使用 Java 类的方式都被看作是对类的被动使用，都不会导致类的初始化。\n内部类 静态内部类 和 java文件中不被public修饰的类 不会自动加载\n类似下面：\n1 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 37 package com.atguigu.java; import java.io.IOException; import java.lang.reflect.Field; import java.util.Vector; /** * @author shkstart shkstart@126.com * @create 2020 14:57 */ public class LocalVarGC { public static LocalVarGC getInstance() { SingletonHolder singletonHolder = new SingletonHolder(); return singletonHolder.INSTANCE; } public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException { // LocalVarGC local = new LocalVarGC(); // local.localvarGC1(); // System.in.read(); Assd a = new Assd(); a.setA(2); a.setB(4); ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); Field f = ClassLoader.class.getDeclaredField(\u0026#34;classes\u0026#34;); f.setAccessible(true); Vector classes = (Vector) f.get(systemClassLoader);//没有SingletonHolder LocalVarGC instance = getInstance(); Vector classes2 = (Vector) f.get(systemClassLoader);//有SingletonHolder System.out.println(); } } class SingletonHolder { public final LocalVarGC INSTANCE = new LocalVarGC(); } Copied! ","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/49157139/","title":"02-类加载子系统"},{"content":" 0、javap javac javac javac -g 才会在字节码中生成局部变量表，不加-g则没有 默认情况下，eclipse、IDEA 在编译时会帮你生成局部变量表、指令和代码行偏移量映射等信息的 javap javap -v -p **.class 可以打印出最全的信息\n1. 概述 2. 加载与存储指令 2.1. 局部变量压栈指令 iload 从局部变量中装载int类型值\nlload 从局部变量中装载long类型值\nfload 从局部变量中装载float类型值\ndload 从局部变量中装载double类型值\naload 从局部变量中装载引用类型值（refernce）\niload_0 从局部变量0中装载int类型值\niload_1 从局部变量1中装载int类型值\niload_2 从局部变量2中装载int类型值\niload_3 从局部变量3中装载int类型值\nlload_0 从局部变量0中装载long类型值\nlload_1 从局部变量1中装载long类型值\nlload_2 从局部变量2中装载long类型值\nlload_3 从局部变量3中装载long类型值\nfload_0 从局部变量0中装载float类型值\nfload_1 从局部变量1中装载float类型值\nfload_2 从局部变量2中装载float类型值\nfload_3 从局部变量3中装载float类型值\ndload_0 从局部变量0中装载double类型值\ndload_1 从局部变量1中装载double类型值\ndload_2 从局部变量2中装载double类型值\ndload_3 从局部变量3中装载double类型值\naload_0 从局部变量0中装载引用类型值\naload_1 从局部变量1中装载引用类型值\naload_2 从局部变量2中装载引用类型值\naload_3 从局部变量3中装载引用类型值\niaload 从数组中装载int类型值\nlaload 从数组中装载long类型值\nfaload 从数组中装载float类型值\ndaload 从数组中装载double类型值\naaload 从数组中装载引用类型值\nbaload 从数组中装载byte类型或boolean类型值\ncaload 从数组中装载char类型值\nsaload 从数组中装载short类型值\n局部变量压栈常用指令集 xload_n xload_0 xload_1 xload_2 xload_3 iload_n iload_0 iload_1 iload_2 iload_3 lload_n lload_0 lload_1 lload_2 lload_3 fload_n fload_0 fload_1 fload_2 fload_3 dload_n dload_0 dload_1 dload_2 dload_3 aload_n aload_0 aload_1 aload_2 aload_3 局部变量压栈指令剖析 1 2 3 4 5 6 7 public void load(int num, Object obj, long count, boolean flag, short[] arr) { System.out.println(num); System.out.println(obj); System.out.println(count); System.out.println(flag); System.out.println(arr); } Copied! 2.2. 常量入栈指令 aconst_null 将null对象引用压入栈\niconst_m1 将int类型常量-1压入栈\niconst_0 将int类型常量0压入栈\niconst_1 将int类型常量1压入栈\niconst_2 将int类型常量2压入栈\niconst_3 将int类型常量3压入栈\niconst_4 将int类型常量4压入栈\niconst_5 将int类型常量5压入栈\nlconst_0 将long类型常量0压入栈\nlconst_1 将long类型常量1压入栈\nfconst_0 将float类型常量0压入栈\nfconst_1 将float类型常量1压入栈\ndconst_0 将double类型常量0压入栈\ndconst_1 将double类型常量1压入栈\nbipush 将一个8位带符号整数压入栈\nsipush 将16位带符号整数压入栈\nldc 把常量池中的项压入栈\nldc_w 把常量池中的项压入栈（使用宽索引）\nldc2_w 把常量池中long类型或者double类型的项压入栈（使用宽索引）\n常量入栈常用指令集 xconst_n 范围 xconst_null xconst_m1 xconst_0 xconst_1 xconst_2 xconst_3 xconst_4 xconst_5 iconst_n [-1, 5] iconst_m1 iconst_0 iconst_1 iconst_2 iconst_3 iconst_4 iconst_5 lconst_n 0, 1 lconst_0 lconst_1 fconst_n 0, 1, 2 fconst_0 fconst_1 fconst_2 dconst_n 0, 1 dconst_0 dconst_1 aconst_n null, String literal, Class literal aconst_null bipush 一个字节，2^8^，[-2^7^, 2^7^ - 1]，即[-128, 127] sipush 两个字节，2^16^，[-2^15^, 2^15^ - 1]，即[-32768, 32767] ldc 四个字节，2^32^，[-2^31^, 2^31^ - 1] ldc_w 宽索引 ldc2_w 宽索引，long或double 常量入栈指令剖析 类型 常数指令 范围 int(boolean,byte,char,short) iconst [-1, 5] bipush [-128, 127] sipush [-32768, 32767] ldc any int value long lconst 0, 1 ldc any long value float fconst 0, 1, 2 ldc any float value double dconst 0, 1 ldc any double value reference aconst null ldc String literal, Class literal 2.3. 出栈装入局部变量表指令 istore 将int类型值存入局部变量\nlstore 将long类型值存入局部变量\nfstore 将float类型值存入局部变量\ndstore 将double类型值存入局部变量\nastore 将将引用类型或returnAddress类型值存入局部变量\nistore_0 将int类型值存入局部变量0\nistore_1 将int类型值存入局部变量1\nistore_2 将int类型值存入局部变量2\nistore_3 将int类型值存入局部变量3\nlstore_0 将long类型值存入局部变量0\nlstore_1 将long类型值存入局部变量1\nlstore_2 将long类型值存入局部变量2\nlstore_3 将long类型值存入局部变量3\nfstore_0 将float类型值存入局部变量0\nfstore_1 将float类型值存入局部变量1\nfstore_2 将float类型值存入局部变量2\nfstore_3 将float类型值存入局部变量3\ndstore_0 将double类型值存入局部变量0\ndstore_1 将double类型值存入局部变量1\ndstore_2 将double类型值存入局部变量2\ndstore_3 将double类型值存入局部变量3\nastore_0 将引用类型或returnAddress类型值存入局部变量0\nastore_1 将引用类型或returnAddress类型值存入局部变量1\nastore_2 将引用类型或returnAddress类型值存入局部变量2\nastore_3 将引用类型或returnAddress类型值存入局部变量3\niastore 将int类型值存入数组中\nlastore 将long类型值存入数组中\nfastore 将float类型值存入数组中\ndastore 将double类型值存入数组中\naastore 将引用类型值存入数组中\nbastore 将byte类型或者boolean类型值存入数组中\ncastore 将char类型值存入数组中\nsastore 将short类型值存入数组中\nwide指令\nwide 使用附加字节扩展局部变量索引\n出栈装入局部变量表常用指令集 xstore_n xstore_0 xstore_1 xstore_2 xstore_3 istore_n istore_0 istore_1 istore_2 istore_3 lstore_n lstore_0 lstore_1 lstore_2 lstore_3 fstore_n fstore_0 fstore_1 fstore_2 fstore_3 dstore_n dstore_0 dstore_1 dstore_2 dstore_3 astore_n astore_0 astore_1 astore_2 astore_3 出栈装入局部变量表指令剖析 3. 算术指令 整数运算 iadd 执行int类型的加法\nladd 执行long类型的加法\nisub 执行int类型的减法\nlsub 执行long类型的减法\nimul 执行int类型的乘法\nlmul 执行long类型的乘法\nidiv 执行int类型的除法\nldiv 执行long类型的除法\nirem 计算int类型除法的余数\nlrem 计算long类型除法的余数\nineg 对一个int类型值进行取反操作\nlneg 对一个long类型值进行取反操作\niinc 把一个常量值加到一个int类型的局部变量上\n逻辑运算 移位操作 ishl 执行int类型的向左移位操作\nlshl 执行long类型的向左移位操作\nishr 执行int类型的向右移位操作\nlshr 执行long类型的向右移位操作\niushr 执行int类型的向右逻辑移位操作\nlushr 执行long类型的向右逻辑移位操作\n按位布尔运算 iand 对int类型值进行“逻辑与”操作\nland 对long类型值进行“逻辑与”操作\nior 对int类型值进行“逻辑或”操作\nlor 对long类型值进行“逻辑或”操作\nixor 对int类型值进行“逻辑异或”操作\nlxor 对long类型值进行“逻辑异或”操作\n浮点运算 fadd 执行float类型的加法\ndadd 执行double类型的加法\nfsub 执行float类型的减法\ndsub 执行double类型的减法\nfmul 执行float类型的乘法\ndmul 执行double类型的乘法\nfdiv 执行float类型的除法\nddiv 执行double类型的除法\nfrem 计算float类型除法的余数\ndrem 计算double类型除法的余数\nfneg 将一个float类型的数值取反\ndneg 将一个double类型的数值取反\n算术指令集 算数指令 int(boolean,byte,char,short) long float double 加法指令 iadd ladd fadd dadd 减法指令 isub lsub fsub dsub 乘法指令 imul lmul fmul dmul 除法指令 idiv ldiv fdiv ddiv 求余指令 irem lrem frem drem 取反指令 ineg lneg fneg dneg 自增指令 iinc 位运算指令 按位或指令 ior lor 按位或指令 ior lor 按位与指令 iand land 按位异或指令 ixor lxor 比较指令 lcmp fcmpg / fcmpl dcmpg / dcmpl 1 2 int x =10; int y = x/0;//报算数异常 Copied! 注意：NaN(Not a Number)表示不是一个数字\n算术指令举例 举例1 1 2 3 public static int bar(int i) { return ((i + 1) - 2) * 3 / 4; } Copied! 举例2 1 2 3 4 5 public void add() { byte i = 15; int j = 8; int k = i + j; } Copied! 举例3 1 2 3 4 5 6 7 public static void main(String[] args) { int x = 500; int y = 100; int a = x / y; int b = 50; System.out.println(a + b); } Copied! 4. 类型转换指令 宽化类型转换 i2l 把int类型的数据转化为long类型\ni2f 把int类型的数据转化为float类型\ni2d 把int类型的数据转化为double类型\nl2f 把long类型的数据转化为float类型\nl2d 把long类型的数据转化为double类型\nf2d 把float类型的数据转化为double类型\n窄化类型转换 i2b 把int类型的数据转化为byte类型\ni2c 把int类型的数据转化为char类型\ni2s 把int类型的数据转化为short类型\nl2i 把long类型的数据转化为int类型\nf2i 把float类型的数据转化为int类型\nf2l 把float类型的数据转化为long类型\nd2i 把double类型的数据转化为int类型\nd2l 把double类型的数据转化为long类型\nd2f 把double类型的数据转化为float类型\nbyte char short int long float double int i2b i2c i2s ○ i2l i2f i2d long l2i i2b l2i i2c l2i i2s l2i ○ l2f l2d float f2i i2b f2i i2c f2i i2s f2i f2l ○ f2d double d2i i2b d2i i2c d2i i2s d2i d2l d2f ○ 类型转换指令可以将两种不同的数值类型进行相互转换。这些转换操作一般用于实现用户代码中的显式类型转換操作，或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题。\n4.1. 宽化类型转换剖析 宽化类型转换( Widening Numeric Conversions)\n转换规则 Java虚拟机直接支持以下数值的宽化类型转换（ widening numeric conversion,小范围类型向大范围类型的安全转换）。也就是说，并不需要指令执行，包括\n从int类型到long、float或者 double类型。对应的指令为：i21、i2f、i2d\n从long类型到float、 double类型。对应的指令为：i2f、i2d\n从float类型到double类型。对应的指令为：f2d\n简化为：int\u0026ndash;\u0026gt;long\u0026ndash;\u0026gt;float-\u0026gt; double\n精度损失问题 2.1. 宽化类型转换是不会因为超过目标类型最大值而丢失信息的，例如，从int转换到long,或者从int转换到double,都不会丢失任何信息，转换前后的值是精确相等的。\n2.2. 从int、long类型数值转换到float,或者long类型数值转换到double时，将可能发生精度丢失一一可能丢失掉几个最低有效位上的值，转换后的浮点数值是根据IEEE754最接近含入模式所得到的正确整数值。\n尽管宽化类型转换实际上是可能发生精度丢失的，但是这种转换永远不会导致Java虚拟机抛出运行时异常\n补充说明 从byte、char和 short类型到int类型的宽化类型转换实际上是不存在的。对于byte类型转为int,拟机并没有做实质性的转化处理，只是简单地通过操作数栈交換了两个数据。而将byte转为long时，使用的是i2l,可以看到在内部，byte在这里已经等同于int类型处理，类似的还有 short类型，这种处理方式有两个特点：\n一方面可以减少实际的数据类型，如果为 short和byte都准备一套指令，那么指令的数量就会大増，而虚拟机目前的设计上，只愿意使用一个字节表示指令，因此指令总数不能超过256个，为了节省指令资源，将 short和byte当做int处理也在情理之中。\n另一方面，由于局部变量表中的槽位固定为32位，无论是byte或者 short存入局部变量表，都会占用32位空间。从这个角度说，也没有必要特意区分这几种数据类型。\n4.2. 窄化类型转换剖析 窄化类型转换( Narrowing Numeric Conversion)\n转换规则 Java虚拟机也直接支持以下窄化类型转换：\n从主int类型至byte、 short或者char类型。对应的指令有：i2b、i2c、i2s\n从long类型到int类型。对应的指令有：l2i\n从float类型到int或者long类型。对应的指令有：f2i、f2l\n从double类型到int、long或者float类型。对应的指令有：d2i、d2l、d2f\n精度损失问题 窄化类型转换可能会导致转换结果具备不同的正负号、不同的数量级，因此，转换过程很可能会导致数值丢失精度。\n尽管数据类型窄化转换可能会发生上限溢出、下限溢出和精度丢失等情况，但是Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常\n补充说明 3.1. 当将一个浮点值窄化转换为整数类型T(T限于int或long类型之一)的时候，将遵循以下转换规则：\n如果浮点值是NaN,那转换结果就是int或long类型的0.\n如果浮点值不是无穷大的话，浮点值使用IEEE754的向零含入模式取整，获得整数值Vv如果v在目标类型T(int或long)的表示范围之内，那转换结果就是v。否则，将根据v的符号，转换为T所能表示的最大或者最小正数\n3.2. 当将一个double类型窄化转换为float类型时，将遵循以下转换规则\n通过向最接近数舍入模式舍入一个可以使用float类型表示的数字。最后结果根据下面这3条规则判断\n如果转换结果的绝对值太小而无法使用float来表示，将返回float类型的正负零\n如果转换结果的绝对值太大而无法使用float来表示，将返回float类型的正负无穷大。\n对于double类型的NaN值将按规定转換为float类型的NaN值。\n5. 对象的创建与访问指令 对象操作指令 new 创建一个新对象\ngetfield 从对象中获取字段\nputfield 设置对象中字段的值\ngetstatic 从类中获取静态字段\nputstatic 设置类中静态字段的值\ncheckcast 确定对象为所给定的类型。后跟目标类，判断栈顶元素是否为目标类 / 接口的实例。如果不是便抛出异常\ninstanceof 判断对象是否为给定的类型。后跟目标类，判断栈顶元素是否为目标类 / 接口的实例。是则压入 1，否则压入 0\n数组操作指令 newarray 分配数据成员类型为基本上数据类型的新数组\nanewarray 分配数据成员类型为引用类型的新数组\narraylength 获取数组长度\nmultianewarray 分配新的多维数组\nJava是面向对象的程序设计语言，虚拟机平台从字节码层面就对面向对象做了深层次的支持。有一系列指令专门用于对象操作，可进一步细分为创建指令、字段访问指令、数组操作指令、类型检查指令。\n5.1. 创建指令 创建指令 含义 new 创建类实例 newarray 创建基本类型数组 anewarray 创建引用类型数组 multilanewarra 创建多维数组 5.2. 字段访问指令 字段访问指令 含义 getstatic、putstatic 访问类字段（static字段，或者称为类变量）的指令 getfield、 putfield 访问类实例字段（非static字段，或者称为实例变量）的指令 5.3. 数组操作指令 数组指令 byte(boolean) char short long long float double reference xaload baload caload saload iaload laload faload daload aaload xastore bastore castore sastore iastore lastore fastore dastore aastore 5.4. 类型检查指令 类型检查指令 含义 instanceof 检查类型强制转换是否可以进行 checkcast 判断给定对象是否是某一个类的实例 6. 方法调用与返回指令 方法调用指令 invokcvirtual 运行时按照对象的类来调用实例方法\ninvokespecial 根据编译时类型来调用实例方法\ninvokestatic 调用类（静态）方法\ninvokcinterface 调用接口方法\n方法返回指令 ireturn 从方法中返回int类型的数据\nlreturn 从方法中返回long类型的数据\nfreturn 从方法中返回float类型的数据\ndreturn 从方法中返回double类型的数据\nareturn 从方法中返回引用类型的数据\nreturn 从方法中返回，返回值为void\n6.1. 方法调用指令 方法调用指令 含义 invokevirtual 调用对象的实例方法 invokeinterface 调用接口方法 invokespecial 调用一些需要特殊处理的实例方法，包括实例初始化方法（构造器）、私有方法和父类方法 invokestatic 调用命名类中的类方法（static方法） invokedynamic 调用动态绑定的方法 6.2. 方法返回指令 方法返回指令 void int long float double reference xreturn return ireturn lreturn freutrn dreturn areturn 1 2 3 4 5 6 7 public int methodReturn() { int i = 500; int j = 200; int k = 50; return (i + j) / k; } Copied! 7. 操作数栈管理指令 通用(无类型）栈操作 nop 不做任何操作\npop 弹出栈顶端一个字长的内容\npop2 弹出栈顶端两个字长的内容\ndup 复制栈顶部一个字长内容\ndup_x1 复制栈顶部一个字长的内容，然后将复制内容及原来弹出的两个字长的内容压入栈\ndup_x2 复制栈顶部一个字长的内容，然后将复制内容及原来弹出的三个字长的内容压入栈\ndup2 复制栈顶部两个字长内容\ndup2_x1 复制栈顶部两个字长的内容，然后将复制内容及原来弹出的三个字长的内容压入栈\ndup2_x2 复制栈顶部两个字长的内容，然后将复制内容及原来弹出的四个字长的内容压入栈\nswap 交换栈顶部两个字长内容\n8. 控制转移指令 比较指令 lcmp 比较long类型值\nfcmpl 比较float类型值（当遇到NaN时，返回-1）\nfcmpg 比较float类型值（当遇到NaN时，返回1）\ndcmpl 比较double类型值（当遇到NaN时，返回-1）\ndcmpg 比较double类型值（当遇到NaN时，返回1）\n条件分支指令 ifeq 如果等于0，则跳转\nifne 如果不等于0，则跳转\niflt 如果小于0，则跳转\nifge 如果大于等于0，则跳转\nifgt 如果大于0，则跳转\nifle 如果小于等于0，则跳转\n比较条件分支指令 if_icmpeq 如果两个int值相等，则跳转\nif_icmpne 如果两个int类型值不相等，则跳转\nif_icmplt 如果一个int类型值小于另外一个int类型值，则跳转\nif_icmpge 如果一个int类型值大于或者等于另外一个int类型值，则跳转\nif_icmpgt 如果一个int类型值大于另外一个int类型值，则跳转\nif_icmple 如果一个int类型值小于或者等于另外一个int类型值，则跳转\nifnull 如果等于null，则跳转\nifnonnull 如果不等于null，则跳转\nif_acmpeq 如果两个对象引用相等，则跳转\nif_acmpne 如果两个对象引用不相等，则跳转\n多条件分支跳转指令 tableswitch 通过索引访问跳转表，并跳转\nlookupswitch 通过键值匹配访问跳转表，并执行跳转操作\n无条件跳转指令 goto 无条件跳转\ngoto_w 无条件跳转（宽索引）\n8.1. 比较指令 比较指令的作用是比较占栈顶两个元素的大小，并将比较结果入栽。\n比较指令有： dcmpg,dcmpl、 fcmpg、fcmpl、lcmp\n与前面讲解的指令类似，首字符d表示double类型，f表示float,l表示long.\n对于double和float类型的数字，由于NaN的存在，各有两个版本的比较指令。以float为例，有fcmpg和fcmpl两个指令，它们的区别在于在数字比较时，若遇到NaN值，处理结果不同。\n指令dcmpl和 dcmpg也是类似的，根据其命名可以推测其含义，在此不再赘述。\n举例\n指令 fcmp和fcmpl都从中弹出两个操作数，并将它们做比较，设栈顶的元素为v2,顶顺位第2位的元素为v1,若v1=v2,则压入0:若v1\u0026gt;v2则压入1:若v1\u0026lt;v2则压入-1.\n两个指令的不同之处在于，如果遇到NaN值， fcmpg会压入1,而fcmpl会压入-1\n8.2. 条件跳转指令 \u0026lt; \u0026lt;= == != \u0026gt;= \u0026gt; null not null iflt ifle ifeq ifng ifge ifgt ifnull ifnonnull 8.3. 比较条件跳转指令 \u0026lt; \u0026lt;= == != \u0026gt;= \u0026gt; if_icmplt if_icmple if_icmpeq、if_acmpeq if_icmpne、if_acmpne if_icmpge if_icmpgt 8.4. 多条件分支跳转 8.5. 无条件跳转 9. 异常处理指令 异常处理指令 athrow 抛出异常或错误。将栈顶异常抛出\njsr 跳转到子例程\njsr_w 跳转到子例程（宽索引）\nrct 从子例程返回\n10. 同步控制指令 线程同步 montiorenter 进入并获取对象监视器。即：为栈顶对象加锁\nmonitorexit 释放并退出对象监视器。即：为栈顶对象解锁\nJava虚拟机支持两种同步结构：方法级的同步和方法内部一段指令序列的同步，这两种同步都是使用monitor来支持的\n10.1. 方法级的同步 1 2 3 4 private int i = 0; public synchronized void add() { i++; } Copied! 10.2. 方法内指令指令序列的同步 ","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/95613895/","title":"02-字节码指令集"},{"content":" 1. 概述 在 Java 中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义，引用数据类型则需要进行类的加载。\n按照 Java 虚拟机规范，从 class 文件到加载到内存中的类，到类卸载出内存为止，它的整个生命周期包括如下 7 个阶段：\n其中，验证、准备、解析 3 个部分统称为链接（Linking）\n从程序中类的使用过程看\n大厂面试题 蚂蚁金服：\n描述一下 JVM 加载 Class 文件的原理机制？\n一面：类加载过程\n百度：\n类加载的时机\njava 类加载过程？\n简述 java 类加载机制？\n腾讯：\nJVM 中类加载机制，类加载过程？\n滴滴：\nJVM 类加载机制\n美团：\nJava 类加载过程\n描述一下 jvm 加载 class 文件的原理机制\n京东：\n什么是类的加载？\n哪些情况会触发类的加载？\n讲一下 JVM 加载一个类的过程 JVM 的类加载机制是什么？\n2. 过程一：Loading（加载）阶段 2.1. 加载完成的操作 加载的理解\n$\\color{red}{所谓加载，简而言之就是将Java类的字节码文件加载到机器内存中，并在内存中构建出Java类的原型——类模板对象。}$所谓类模板对象，其实就是 Java 类在]VM 内存中的一个快照，JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中，这样]VM 在运行期便能通过类模板而获取 Java 类中的任意信息，能够对 Java 类的成员变量进行遍历，也能进行 Java 方法的调用。\n反射的机制即基于这一基础。如果 JVM 没有将 Java 类的声明信息存储起来，则 JVM 在运行期也无法反射。\n加载完成的操作\n$\\color{red}{加载阶段，简言之，查找并加载类的二进制数据，生成Class的实例。}$\n在加载类时，Java 虚拟机必须完成以下 3 件事情：\n通过类的全名，获取类的二进制数据流。\n解析类的二进制数据流为方法区内的数据结构（Java 类模型）\n创建 java.lang.Class 类的实例，表示该类型。作为方法区这个类的各种数据的访问入口\n2.2. 二进制流的获取方式 对于类的二进制数据流，虚拟机可以通过多种途径产生或获得。（只要所读取的字节码符合 JVM 规范即可）\n虚拟机可能通过文件系统读入一个 class 后缀的文件$\\color{red}{（最常见）}$ 读入 jar、zip 等归档数据包，提取类文件。 事先存放在数据库中的类的二进制数据 使用类似于 HTTP 之类的协议通过网络进行加载 在运行时生成一段 class 的二进制信息等 在获取到类的二进制信息后，Java 虚拟机就会处理这些数据，并最终转为一个 java.lang.Class 的实例。 如果输入数据不是 ClassFile 的结构，则会抛出 ClassFormatError。\n2.3. 类模型与 Class 实例的位置 类模型的位置\n加载的类在 JVM 中创建相应的类结构，类结构会存储在方法区（JDKl.8 之前：永久代；J0Kl.8 及之后：元空间）。\nClass 实例的位置\n类将.class 文件加载至元空间后，会在堆中创建一个 Java.lang.Class 对象，用来封装类位于方法区内的数据结构，该 Class 对象是在加载类的过程中创建的，每个类都对应有一个 Class 类型的对象。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Class clazz = Class.forName(\u0026#34;java.lang.String\u0026#34;); //获取当前运行时类声明的所有方法 Method[] ms = clazz.getDecla#FF0000Methods(); for (Method m : ms) { //获取方法的修饰符 String mod = Modifier.toString(m.getModifiers()); System.out.print(mod + \u0026#34;\u0026#34;); //获取方法的返回值类型 String returnType = (m.getReturnType()).getSimpleName(); System.out.print(returnType + \u0026#34;\u0026#34;); //获取方法名 System.out.print(m.getName() + \u0026#34;(\u0026#34;); //获取方法的参数列表 Class\u0026lt;?\u0026gt;[] ps = m.getParameterTypes(); if (ps.length == 0) { System.out.print(\u0026#39;)\u0026#39;); } for (int i = 0; i \u0026lt; ps.length; i++) { char end = (i == ps.length - 1) ? \u0026#39;)\u0026#39; : \u0026#39;,\u0026#39;; //获取参教的类型 System.out.print(ps[i].getSimpleName() + end); } } Copied! 2.4. 数组类的加载 创建数组类的情况稍微有些特殊，因为数组类本身并不是由类加载器负责创建，而是由 JVM 在运行时根据需要而直接创建的，但数组的元素类型仍然需要依靠类加载器去创建。创建数组类（下述简称 A）的过程：\n如果数组的元素类型是引用类型，那么就遵循定义的加载过程递归加载和创建数组 A 的元素类型； JVM 使用指定的元素类型和数组维度来创建新的数组类。 如果数组的元素类型是引用类型，数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为 public。\n3. 过程二：Linking（链接）阶段 3.1. 环节 1：链接阶段之 Verification（验证） 当类加载到系统后，就开始链接操作，验证是链接操作的第一步。\n$\\color{red}{它的目的是保证加载的字节码是合法、合理并符合规范的。}$\n验证的步骤比较复杂，实际要验证的项目也很繁多，大体上 Java 虚拟机需要做以下检查，如图所示。\n整体说明：\n验证的内容则涵盖了类数据信息的格式验证、语义检查、字节码验证，以及符号引用验证等。\n$\\color{red}{其中格式验证会和加载阶段一起执行}$。验证通过之后，类加载器才会成功将类的二进制数据信息加载到方法区中。 $\\color{red}{格式验证之外的验证操作将会在方法区中进行}$。 链接阶段的验证虽然拖慢了加载速度，但是它避免了在字节码运行时还需要进行各种检查。（磨刀不误砍柴工）\n具体说明：\n格式验证：是否以魔数 0XCAFEBABE 开头，主版本和副版本号是否在当前 Java 虚拟机的支持范围内，数据中每一个项是否都拥有正确的长度等。\n语义检查：Java 虚拟机会进行字节码的语义检查，但凡在语义上不符合规范的，虚拟机也不会给予验证通过。比如：\n是否所有的类都有父类的存在（在 Java 里，除了 object 外，其他类都应该有父类） 是否一些被定义为 final 的方法或者类被重写或继承了 非抽象类是否实现了所有抽象方法或者接口方法 字节码验证：Java 虚拟机还会进行字节码验证，$\\color{red}{字节码验证也是验证过程中最为复杂的一个过程}$。它试图通过对字节码流的分析，判断字节码是否可以被正确地执行。比如：\n在字节码的执行过程中，是否会跳转到一条不存在的指令 函数的调用是否传递了正确类型的参数 变量的赋值是不是给了正确的数据类型等 栈映射帧（StackMapTable）就是在这个阶段，用于检测在特定的字节码处，其局部变量表和操作数栈是否有着正确的数据类型。但遗憾的是，100%准确地判断一段字节码是否可以被安全执行是无法实现的，因此，该过程只是尽可能地检查出可以预知的明显的问题。如果在这个阶段无法通过检查，虚拟机也不会正确装载这个类。但是，如果通过了这个阶段的检查，也不能说明这个类是完全没有问题的。\n$\\color{red}{在前面3次检查中，已经排除了文件格式错误、语义错误以及字节码的不正确性。但是依然不能确保类是没有问题的。}$\n符号引用的验证：校验器还将进符号引用的验证。Class 文件在其常量池会通过字符串记录自己将要使用的其他类或者方法。因此，在验证阶段，$\\color{red}{虚拟机就会检查这些类或者方法确实是存在的}$，并且当前类有权限访问这些数据，如果一个需要使用类无法在系统中找到，则会抛出 NoClassDefFoundError，如果一个方法无法被找到，则会抛出 NoSuchMethodError。此阶段在解析环节才会执行。\n3.2. 环节 2：链接阶段之 Preparation（准备） $\\color{red}{准备阶段（Preparation），简言之，为类的静态变分配内存，并将其初始化为默认值。}$\n当一个类验证通过时，虚拟机就会进入准备阶段。在这个阶段，虚拟机就会为这个类分配相应的内存空间，并设置默认初始值。Java 虚拟机为各类型变量默认的初始值如表所示。\n类型 默认初始值 byte (byte)0 short (short)0 int 0 long 0L float 0.0f double 0.0 char \\u0000 boolean false reference null Java 并不支持 boolean 类型，对于 boolean 类型，内部实现是 int，由于 int 的默认值是 0，故对应的，boolean 的默认值就是 false。\n注意\n$\\color{red}{这里不包含基本数据类型的字段用static final修饰的情况，因为final在编译的时候就会分配了，准备阶段会显式赋值。}$\n1 2 3 4 // 一般情况：static final修饰的基本数据类型、字符串类型字面量会在准备阶段赋值 private static final String str = \u0026#34;Hello world\u0026#34;; // 特殊情况：static final修饰的引用类型不会在准备阶段赋值，而是在初始化阶段赋值 private static final String str = new String(\u0026#34;Hello world\u0026#34;); Copied! 注意这里不会为实例变量分配初始化，类变量会分配在方法区中，而实例变量是会随着对象一起分配到 Java 堆中。\n在这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行。\n3.3. 环节 3：链接阶段之 Resolution（解析） 在准备阶段完成后，就进入了解析阶段。解析阶段（Resolution），简言之，将类、接口、字段和方法的符号引用转为直接引用。\n具体描述：\n符号引用就是一些字面量的引用，和虚拟机的内部数据结构和和内存布局无关。比较容易理解的就是在 Class 类文件中，通过常量池进行了大量的符号引用。但是在程序实际运行时，只有符号引用是不够的，比如当如下 println()方法被调用时，系统需要明确知道该方法的位置。\n举例：\n输出操作 System.out.println()对应的字节码：\n1 invokevirtual #24 \u0026lt;java/io/PrintStream.println\u0026gt; Copied! 以方法为例，Java 虚拟机为每个类都准备了一张方法表，将其所有的方法都列在表中，当需要调用一个类的方法的时候，只要知道这个方法在方法表中的偏移量就可以直接调用该方法。$\\color{red}{通过解析操作，符号引用就可以转变为目标方法在类中方法表中的位置，从而使得方法被成功调用。}$\n4. 过程三：Initialization（初始化）阶段 4.1. static 与 final 的搭配问题 说明：使用 static+ final 修饰的字段的显式赋值的操作，到底是在哪个阶段进行的赋值？\n情况 1：在链接阶段的准备环节赋值\n情况 2：在初始化阶段\u0026lt;clinit\u0026gt;()中赋值\n结论： 在链接阶段的准备环节赋值的情况：\n对于基本数据类型的字段来说，如果使用 static final 修饰，则显式赋值(直接赋值常量，而非调用方法通常是在链接阶段的准备环节进行\n对于 String 来说，如果使用字面量的方式赋值，使用 static final 修饰的话，则显式赋值通常是在链接阶段的准备环节进行\n在初始化阶段\u0026lt;clinit\u0026gt;()中赋值的情况： 排除上述的在准备环节赋值的情况之外的情况。\n最终结论：使用 static+final 修饰，且显示赋值中不涉及到方法或构造器调用的基本数据类到或 String 类型的显式财值，是在链接阶段的准备环节进行。\n1 2 3 4 5 6 7 8 9 10 public static final int INT_CONSTANT = 10; // 在链接阶段的准备环节赋值 public static final int NUM1 = new Random().nextInt(10); // 在初始化阶段clinit\u0026gt;()中赋值 public static int a = 1; // 在初始化阶段\u0026lt;clinit\u0026gt;()中赋值 public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100); // 在初始化阶段\u0026lt;clinit\u0026gt;()中赋值 public static Integer INTEGER_CONSTANT2 = Integer.valueOf(100); // 在初始化阶段\u0026lt;clinit\u0026gt;()中概值 public static final String s0 = \u0026#34;helloworld0\u0026#34;; // 在链接阶段的准备环节赋值 public static final String s1 = new String(\u0026#34;helloworld1\u0026#34;); // 在初始化阶段\u0026lt;clinit\u0026gt;()中赋值 public static String s2 = \u0026#34;hellowrold2\u0026#34;; // 在初始化阶段\u0026lt;clinit\u0026gt;()中赋值 Copied! 4.2. \u0026lt;clinit\u0026gt;()的线程安全性 对于\u0026lt;clinit\u0026gt;()方法的调用，也就是类的初始化，虚拟机会在内部确保其多线程环境中的安全性。\n虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步，如果多个线程同时去初始化一个类，那么只会有一个线程去执行这个类的\u0026lt;clinit\u0026gt;()方法，其他线程都需要阻塞等待，直到活动线程执行\u0026lt;clinit\u0026gt;()方法完毕。\n正是因为$\\color{red}{函数()带锁线程安全的}$，因此，如果在一个类的\u0026lt;clinit\u0026gt;()方法中有耗时很长的操作，就可能造成多个线程阻塞，引发死锁。并且这种死锁是很难发现的，因为看起来它们并没有可用的锁信息。\n如果之前的线程成功加载了类，则等在队列中的线程就没有机会再执行\u0026lt;clinit\u0026gt;()方法了。那么，当需要使用这个类时，虚拟机会直接返回给它已经准备好的信息。\n4.3. 类的初始化情况：主动使用 vs 被动使用 Java 程序对类的使用分为两种：主动使用和被动使用。\n主动使用\nClass 只有在必须要首次使用的时候才会被装载，Java 虚拟机不会无条件地装载 Class 类型。Java 虚拟机规定，一个类或接口在初次使用前，必须要进行初始化。这里指的“使用”，是指主动使用，主动使用只有下列几种情况：（即：如果出现如下的情况，则会对类进行初始化操作。而初始化操作之前的加载、验证、准备已经完成。\n实例化：当创建一个类的实例时，比如使用 new 关键字，或者通过反射、克隆、反序列化。\n1 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 37 38 39 40 /** * 反序列化 */ Class Order implements Serializable { static { System.out.println(\u0026#34;Order类的初始化\u0026#34;); } } public void test() { ObjectOutputStream oos = null; ObjectInputStream ois = null; try { // 序列化 oos = new ObjectOutputStream(new FileOutputStream(\u0026#34;order.dat\u0026#34;)); oos.writeObject(new Order()); // 反序列化 ois = new ObjectInputStream(new FileOutputStream(\u0026#34;order.dat\u0026#34;)); Order order = ois.readObject(); } catch (IOException e){ e.printStackTrace(); } catch (ClassNotFoundException e){ e.printStackTrace(); } finally { try { if (oos != null) { oos.close(); } if (ois != null) { ois.close(); } } catch (IOException e){ e.printStackTrace(); } } } Copied! 静态方法：当调用类的静态方法时，即当使用了字节码 invokestatic 指令。\n静态字段：当使用类、接口的静态字段时（final 修饰特殊考虑），比如，使用 getstatic 或者 putstatic 指令。（对应访问变量、赋值变量操作）\n1 2 3 4 5 6 7 8 9 10 11 12 13 public class ActiveUse { @Test public void test() { System.out.println(User.num); } } class User { static { System.out.println(\u0026#34;User类的初始化\u0026#34;); } public static final int num = 1; } Copied! 反射：当使用 java.lang.reflect 包中的方法反射类的方法时。比如：Class.forName(\u0026ldquo;com.atguigu.java.Test\u0026rdquo;)\n继承：当初始化子类时，如果发现其父类还没有进行过初始化，则需要先触发其父类的初始化。\n当 Java 虚拟机初始化一个类时，要求它的所有父类都已经被初始化，但是这条规则并不适用于接口。\n在初始化一个类时，并不会先初始化它所实现的接口 在初始化一个接口时，并不会先初始化它的父接口 因此，一个父接口并不会因为它的子接口或者实现类的初始化而初始化。只有当程序首次使用特定接口的静态字段时，才会导致该接口的初始化。 default 方法：如果一个接口定义了 default 方法，那么直接实现或者间接实现该接口的类的初始化，该接口要在其之前被初始化。\n1 2 3 4 5 6 7 interface Compare { public static final Thread t = new Thread() { { System.out.println(\u0026#34;Compare接口的初始化\u0026#34;); } } } Copied! main 方法：当虚拟机启动时，用户需要指定一个要执行的主类（包含 main()方法的那个类），虚拟机会先初始化这个主类。\nVM 启动的时候通过引导类加载器加载一个初始类。这个类在调用 public static void main(String[])方法之前被链接和初始化。这个方法的执行将依次导致所需的类的加载，链接和初始化。\nMethodHandle：当初次调用 MethodHandle 实例时，初始化该 MethodHandle 指向的方法所在的类。（涉及解析 REF getStatic、REF_putStatic、REF invokeStatic 方法句柄对应的类）\n被动使用\n除了以上的情况属于主动使用，其他的情况均属于被动使用。$\\color{red}{被动使用不会引起类的初始化。}$\n也就是说：$\\color{red}{并不是在代码中出现的类，就一定会被加载或者初始化。}$如果不符合主动使用的条件，类就不会初始化。\n静态字段：当通过子类引用父类的静态变量，不会导致子类初始化，只有真正声明这个字段的类才会被初始化。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class PassiveUse { @Test public void test() { System.out.println(Child.num); } } class Child extends Parent { static { System.out.println(\u0026#34;Child类的初始化\u0026#34;); } } class Parent { static { System.out.println(\u0026#34;Parent类的初始化\u0026#34;); } public static int num = 1; } Copied! 数组定义：通过数组定义类引用，不会触发此类的初始化\n1 2 3 4 Parent[] parents= new Parent[10]; System.out.println(parents.getClass()); // new的话才会初始化 parents[0] = new Parent(); Copied! 引用常量：引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class PassiveUse { public static void main(String[] args) { System.out.println(Serival.num); // 但引用其他类的话还是会初始化 System.out.println(Serival.num2); } } interface Serival { public static final Thread t = new Thread() { { System.out.println(\u0026#34;Serival初始化\u0026#34;); } }; public static int num = 10; public static final int num2 = new Random().nextInt(10); } Copied! loadClass 方法：调用 ClassLoader 类的 loadClass()方法加载一个类，并不是对类的主动使用，不会导致类的初始化。\n1 Class clazz = ClassLoader.getSystemClassLoader().loadClass(\u0026#34;com.test.java.Person\u0026#34;); Copied! 扩展\n-XX:+TraceClassLoading：追踪打印类的加载信息\n5. 过程四：类的 Using（使用） 任何一个类型在使用之前都必须经历过完整的加载、链接和初始化 3 个类加载步骤。一旦一个类型成功经历过这 3 个步骤之后，便“厉事俱备只欠东风”，就等着开发者使用了。\n开发人员可以在程序中访问和调用它的静态类成员信息（比如：静态字段、静态方法），或者使用 new 关键字为其创建对象实例。\n6. 过程五：类的 Unloading（卸载） 6.1. 类、类的加载器、类的实例之间的引用关系 在类加载器的内部实现中，用一个 Java 集合来存放所加载类的引用。另一方面，一个 Class 对象总是会引用它的类加载器，调用 Class 对象的 getClassLoader()方法，就能获得它的类加载器。由此可见，代表某个类的 Class 实例与其类的加载器之间为双向关联关系。\n一个类的实例总是引用代表这个类的 Class 对象。在 Object 类中定义了 getClass()方法，这个方法返回代表对象所属类的 Class 对象的引用。此外，所有的 java 类都有一个静态属性 class，它引用代表这个类的 Class 对象。\n6.2.类的生命周期 当 Sample 类被加载、链接和初始化后，它的生命周期就开始了。当代表 Sample 类的 Class 对象不再被引用，即不可触及时，Class 对象就会结束生命周期，Sample 类在方法区内的数据也会被卸载，从而结束 Sample 类的生命周期。\n$\\color{red}{一个类何时结束生命周期，取决于代表它的Class对象何时结束生命周期。}$\n6.3. 具体例子 loader1 变量和 obj 变量间接应用代表 Sample 类的 Class 对象，而 objClass 变量则直接引用它。\n如果程序运行过程中，将上图左侧三个引用变量都置为 null，此时 Sample 对象结束生命周期，MyClassLoader 对象结束生命周期，代表 Sample 类的 Class 对象也结束生命周期，Sample 类在方法区内的二进制数据被卸载。\n当再次有需要时，会检查 Sample 类的 Class 对象是否存在，如果存在会直接使用，不再重新加载；如果不存在 Sample 类会被重新加载，在 Java 虚拟机的堆区会生成一个新的代表 Sample 类的 Class 实例（可以通过哈希码查看是否是同一个实例）\n6.4. 类的卸载 （1）启动类加载器加载的类型在整个运行期间是不可能被卸载的（jvm 和 jls 规范）\n（2）被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载，因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到，其达到 unreachable 的可能性极小。\n（3）被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载，而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想，稍微复杂点的应用场景中（比如：很多时候用户在开发自定义类加载器实例的时候采用缓存的策略以提高系统性能），被加载的类型在运行期间也是几乎不太可能被卸载的（至少卸载的时间是不确定的）。\n综合以上三点，一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。同时我们可以看的出来，开发者在开发代码时候，不应该对虚拟机的类型卸载做任何假设的前提下，来实现系统中的特定功能。\n回顾：方法区的垃圾回收 方法区的垃圾收集主要回收两部分内容：常量池中废弃的常量和不再使用的类型。\nHotSpot 虚拟机对常量池的回收策略是很明确的，只要常量池中的常量没有被任何地方引用，就可以被回收。\n判定一个常量是否“废弃”还是相对简单，而要判定一个类型是否属于“不再使用的类”的条件就比较苛刻了。需要同时满足下面三个条件：\n$\\color{blue}{该类所有的实例都已经被回收。也就是Java堆中不存在该类及其任何派生子类的实例。}$ $\\color{blue}{加载该类的类加载器已经被回收。这个条件除非是经过精心设计的可替换类加载器的场景，如OSGi、JSP的重加载等，否则通常是很难达成的。}$ $\\color{blue}{该类对应的java.lang.Class对象没有在任何地方被引用，无法在任何地方通过反射访问该类的方法。}$ Java 虚拟机被允许对满足上述三个条件的无用类进行回收，这里说的仅仅是“被允许”，而并不是和对象一样，没有引用了就必然会回收。\n","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/7f491673/","title":"03-类的加载过程（类的生命周期）详解"},{"content":" 4. 虚拟机栈 4.1. 虚拟机栈概述 4.1.1. 虚拟机栈出现的背景 由于跨平台性的设计，Java 的指令都是根据栈来设计的。不同平台 CPU 架构不同，所以不能设计为基于寄存器的。\n优点是跨平台，指令集小，编译器容易实现，缺点是性能下降，实现同样的功能需要更多的指令。\n4.1.2. 初步印象 有不少 Java 开发人员一提到 Java 内存结构，就会非常粗粒度地将 JVM 中的内存区理解为仅有 Java 堆（heap）和 Java 栈（stack）？为什么？\n4.1.3. 内存中的栈与堆 栈是运行时的单位，而堆是存储的单位\n栈解决程序的运行问题，即程序如何执行，或者说如何处理数据。 堆解决的是数据存储的问题，即数据怎么放，放哪里 4.1.4. 虚拟机栈基本内容 Java 虚拟机栈是什么？ Java 虚拟机栈（Java Virtual Machine Stack），早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈，其内部保存一个个的栈帧（Stack Frame），对应着一次次的 Java 方法调用，是线程私有的。\n生命周期 生命周期和线程一致\n作用 主管 Java 程序的运行，它保存方法的局部变量、部分结果，并参与方法的调用和返回。\n栈的特点 栈是一种快速有效的分配存储方式，访问速度仅次于罹序计数器。\nJVM 直接对 Java 栈的操作只有两个：\n每个方法执行，伴随着进栈（入栈、压栈） 执行结束后的出栈工作 对于栈来说不存在垃圾回收问题（栈存在溢出的情况）\n面试题：开发中遇到哪些异常？ 栈中可能出现的异常\nJava 虚拟机规范允许Java 栈的大小是动态的或者是固定不变的。\n如果采用固定大小的 Java 虚拟机栈，那每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量，Java 虚拟机将会抛出一个StackOverflowError 异常。\n如果 Java 虚拟机栈可以动态扩展，并且在尝试扩展的时候无法申请到足够的内存，或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈，那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。\n1 2 3 4 5 6 7 8 public static void main(String[] args) { test(); } public static void test() { test(); } //抛出异常：Exception in thread\u0026#34;main\u0026#34;java.lang.StackoverflowError //程序不断的进行递归调用，而且没有退出条件，就会导致不断地进行压栈。 Copied! 设置栈内存大小\n我们可以使用参数 -Xss 选项来设置线程的最大栈空间，栈的大小直接决定了函数调用的最大可达深度\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class StackDeepTest{ private static int count=0; public static void recursion(){ count++; recursion(); } public static void main(String args[]){ try{ recursion(); } catch (Throwable e){ System.out.println(\u0026#34;deep of calling=\u0026#34;+count); e.printstackTrace(); } } } Copied! 4.2. 栈的存储单位 4.2.1. 栈中存储什么？ 每个线程都有自己的栈，栈中的数据都是以栈帧（Stack Frame）的格式存在。\n在这个线程上正在执行的每个方法都各自对应一个栈帧（Stack Frame）。\n栈帧是一个内存区块，是一个数据集，维系着方法执行过程中的各种数据信息。\n4.2.2. 栈运行原理 JVM 直接对 Java 栈的操作只有两个，就是对栈帧的压栈和出栈，遵循“先进后出”/“后进先出”原则。\n在一条活动线程中，一个时间点上，只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧（栈顶栈帧）是有效的，这个栈帧被称为当前栈帧（Current Frame），与当前栈帧相对应的方法就是当前方法（Current Method），定义这个方法的类就是当前类（Current Class）。\n执行引擎运行的所有字节码指令只针对当前栈帧进行操作。\n如果在该方法中调用了其他方法，对应的新的栈帧会被创建出来，放在栈的顶端，成为新的当前帧。\n不同线程中所包含的栈帧是不允许存在相互引用的，即不可能在一个栈帧之中引用另外一个线程的栈帧。\n如果当前方法调用了其他方法，方法返回之际，当前栈帧会传回此方法的执行结果给前一个栈帧，接着，虚拟机会丢弃当前栈帧，使得前一个栈帧重新成为当前栈帧。\nJava 方法有两种返回函数的方式，一种是正常的函数返回，使用 return 指令；另外一种是抛出异常。不管使用哪种方式，都会导致栈帧被弹出。\n1 2 3 4 5 6 7 8 9 public class CurrentFrameTest{ public void methodA(){ system.out.println（\u0026#34;当前栈帧对应的方法-\u0026gt;methodA\u0026#34;); methodB(); system.out.println（\u0026#34;当前栈帧对应的方法-\u0026gt;methodA\u0026#34;); } public void methodB(){ System.out.println（\u0026#34;当前栈帧对应的方法-\u0026gt;methodB\u0026#34;); } Copied! 4.2.3. 栈帧的内部结构 每个栈帧中存储着：\n局部变量表（Local Variables） 操作数栈（operand Stack）（或表达式栈） 动态链接（DynamicLinking）（或指向运行时常量池的方法引用） 方法返回地址（Return Address）（或方法正常退出或者异常退出的定义） 一些附加信息 并行每个线程下的栈都是私有的，因此每个线程都有自己各自的栈，并且每个栈里面都有很多栈帧，栈帧的大小主要由局部变量表 和 操作数栈决定的\n4.3. 局部变量表(Local Variables) 局部变量表也被称之为局部变量数组或本地变量表\n定义为一个数字数组，主要用于存储方法参数和定义在方法体内的局部变量，这些数据类型包括各类基本数据类型、对象引用（reference），以及 returnAddress 类型。\n由于局部变量表是建立在线程的栈上，是线程的私有数据，因此不存在数据安全问题\n局部变量表所需的容量大小是在编译期确定下来的，并保存在方法的 Code 属性的 maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的。\n方法嵌套调用的次数由栈的大小决定。一般来说，栈越大，方法嵌套调用次数越多。对一个函数而言，它的参数和局部变量越多，使得局部变量表膨胀，它的栈帧就越大，以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间，导致其嵌套调用次数就会减少。\n局部变量表中的变量只在当前方法调用中有效。在方法执行时，虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后，随着方法栈帧的销毁，局部变量表也会随之销毁。\n4.3.1. 关于 Slot 的理解 局部变量表，最基本的存储单元是 Slot（变量槽）\n参数值的存放总是在局部变量数组的 index0 开始，到数组长度-1 的索引结束。\n局部变量表中存放编译期可知的各种基本数据类型（8 种），引用类型（reference），returnAddress 类型的变量。\n在局部变量表里，32 位以内的类型只占用一个 slot（包括 returnAddress 类型），64 位的类型（long 和 double）占用两个 slot。\nbyte、short、char 在存储前被转换为 int，boolean 也被转换为 int，0 表示 false，非 0 表示 true。\nJVM 会为局部变量表中的每一个 Slot 都分配一个访问索引，通过这个索引即可成功访问到局部变量表中指定的局部变量值\n当一个实例方法被调用的时候，它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个 slot 上\n如果需要访问局部变量表中一个 64bit 的局部变量值时，只需要使用前一个索引即可。（比如：访问 long 或 doub1e 类型变量）\n如果当前帧是由构造方法或者实例方法创建的，那么该对象引用 this 将会存放在 index 为 0 的 slot 处，其余的参数按照参数表顺序继续排列。\n4.3.2. Slot 的重复利用 栈帧中的局部变量表中的槽位是可以重用的，如果一个局部变量过了其作用域，那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位，从而达到节省资源的目的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class SlotTest { public void localVarl() { int a = 0; System.out.println(a); int b = 0; } public void localVar2() { { int a = 0; System.out.println(a); } //此时的就会复用a的槽位 int b = 0; } } Copied! 4.3.3. 静态变量与局部变量的对比 参数表分配完毕之后，再根据方法体内定义的变量的顺序和作用域分配。\n我们知道类变量表有两次初始化的机会，第一次是在“准备阶段”，执行系统初始化，对类变量设置零值，另一次则是在“初始化”阶段，赋予程序员在代码中定义的初始值。\n和类变量初始化不同的是，局部变量表不存在系统初始化的过程，这意味着一旦定义了局部变量则必须人为的初始化，否则无法使用。\n1 2 3 4 public void test(){ int i; System. out. println(i); } Copied! 这样的代码是错误的，没有赋值不能够使用。\n4.3.4. 补充说明 在栈帧中，与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时，虚拟机使用局部变量表完成方法的传递。\n局部变量表中的变量也是重要的垃圾回收根节点，只要被局部变量表中直接或间接引用的对象都不会被回收。\n4.4. 操作数栈（Operand Stack） 每一个独立的栈帧除了包含局部变量表以外，还包含一个后进先出（Last-In-First-Out）的 操作数栈，也可以称之为表达式栈（Expression Stack）\n操作数栈，在方法执行过程中，根据字节码指令，往栈中写入数据或提取数据，即入栈（push）和 出栈（pop）\n某些字节码指令将值压入操作数栈，其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈 比如：执行复制、交换、求和等操作 代码举例\n1 2 3 4 5 public void testAddOperation(){ byte i = 15; int j = 8; int k = i + j; } Copied! 字节码指令信息\n1 2 3 4 5 6 7 8 9 10 11 public void testAddOperation(); Code: 0: bipush 15 2: istore_1 3: bipush 8 5: istore_2 6:iload_1 7:iload_2 8:iadd 9:istore_3 10:return Copied! 操作数栈，主要用于保存计算过程的中间结果，同时作为计算过程中变量临时的存储空间。\n操作数栈就是 JVM 执行引擎的一个工作区，当一个方法刚开始执行的时候，一个新的栈帧也会随之被创建出来，这个方法的操作数栈是空的。\n每一个操作数栈都会拥有一个明确的栈深度用于存储数值，其所需的最大深度在编译期就定义好了，保存在方法的 Code 属性中，为 max_stack 的值。\n栈中的任何一个元素都是可以任意的 Java 数据类型\n32bit 的类型占用一个栈单位深度 64bit 的类型占用两个栈单位深度 操作数栈并非采用访问索引的方式来进行数据访问的，而是只能通过标准的入栈和出栈操作来完成一次数据访问\n如果被调用的方法带有返回值的话，其返回值将会被压入当前栈帧的操作数栈中，并更新 PC 寄存器中下一条需要执行的字节码指令。\n操作数栈中元素的数据类型必须与字节码指令的序列严格匹配，这由编译器在编译器期间进行验证，同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。\n另外，我们说 Java 虚拟机的解释引擎是基于栈的执行引擎，其中的栈指的就是操作数栈。\njavap -v 反编译代码看到的某个方法的stack深度即为操作数栈的深度 4.5. 代码追踪 1 2 3 4 5 public void testAddOperation() { byte i = 15; int j = 8; int k = i + j; } Copied! 使用 javap 命令反编译 class 文件： javap -v 类名.class\n1 public void testAddoperation(); Code:\t0: bipush 15 2: istore_1 3: bipush 8\t5: istore_2\t6: iload_1\t7: iload_2\t8: iadd\t9: istore_3 10: return Copied! 程序员面试过程中，常见的 i++和++i 的区别，放到字节码篇章时再介绍。\n4.6. 栈顶缓存技术（Top Of Stack Cashing）技术 前面提过，基于栈式架构的虚拟机所使用的零地址指令更加紧凑，但完成一项操作的时候必然需要使用更多的入栈和出栈指令，这同时也就意味着将需要更多的指令分派（instruction dispatch）次数和内存读/写次数。\n由于操作数是存储在内存中的，因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题，HotSpot JVM 的设计者们提出了栈顶缓存（Tos，Top-of-Stack Cashing）技术，将栈顶元素全部缓存在物理 CPU 的寄存器中，以此降低对内存的读/写次数，提升执行引擎的执行效率。\n4.7. 动态链接（Dynamic Linking） 动态链接、方法返回地址、附加信息 ： 有些地方被称为帧数据区\n每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接（Dynamic Linking）。比如：invokedynamic 指令\n在 Java 源文件被编译到字节码文件中时，所有的变量和方法引用都作为符号引用（Symbolic Reference）保存在 class 文件的常量池里。比如：描述一个方法调用了另外的其他方法时，就是通过常量池中指向方法的符号引用来表示的，那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。\n为什么需要运行时常量池呢？\n常量池的作用：就是为了提供一些符号和常量，便于指令的识别\n4.8. 方法的调用：解析与分配 在 JVM 中，将符号引用转换为调用方法的直接引用与方法的绑定机制相关\n4.8.1. 静态链接 当一个字节码文件被装载进 JVM 内部时，如果被调用的目标方法在编译期可知，且运行期保持不变时，这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接\n4.8.2. 动态链接 如果被调用的方法在编译期无法被确定下来，只能够在程序运行期将调用的方法的符号转换为直接引用，由于这种引用转换过程具备动态性，因此也被称之为动态链接。\n静态链接和动态链接不是名词，而是动词，这是理解的关键。\n随着高级语言的横空出世，类似于 Java 一样的基于面向对象的编程语言如今越来越多，尽管这类编程语言在语法风格上存在一定的差别，但是它们彼此之间始终保持着一个共性，那就是都支持封装、继承和多态等面向对象特性，既然这一类的编程语言具备多态特悄，那么自然也就具备早期绑定和晚期绑定两种绑定方式。\nJava 中任何一个普通的方法其实都具备虚函数的特征，它们相当于 C++语言中的虚函数（C++中则需要使用关键字 virtual 来显式定义）。如果在 Java 程序中不希望某个方法拥有虚函数的特征时，则可以使用关键字 final 来标记这个方法。\n","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/57138451/","title":"04-虚拟机栈"},{"content":" 1. 概述 类加载器是JVM执行类加载机制的前提。\nClassLoader的作用：\nClassLoader是Java的核心组件，所有的Class都是由ClassLoader进行加载的，ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部，转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此，ClassLoader在整个装载阶段，只能影响到类的加载，而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行，则由Execution Engine决定。\n1.1. 大厂面试题 蚂蚁金服：\n深入分析ClassLoader，双亲委派机制\n类加载器的双亲委派模型是什么？一面：双亲委派机制及使用原因\n百度：\n都有哪些类加载器，这些类加载器都加载哪些文件？\n手写一个类加载器Demo\nClass的forName（“java.lang.String”）和Class的getClassLoader（）的Loadclass（“java.lang.String”）有什么区别？\n腾讯：\n什么是双亲委派模型？\n类加载器有哪些？\n小米：\n双亲委派模型介绍一下\n滴滴：\n简单说说你了解的类加载器一面：讲一下双亲委派模型，以及其优点\n字节跳动：\n什么是类加载器，类加载器有哪些？\n京东：\n类加载器的双亲委派模型是什么？\n双亲委派机制可以打破吗？为什么\n1.2. 类加载器的分类 类的加载分类：显式加载 vs 隐式加载\nclass文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式。\n显式加载指的是在代码中通过调用ClassLoader加载class对象，如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象，而是通过虚拟机自动加载到内存中，如在加载某个类的class文件时，该类的class文件中引用了另外一个类的对象，此时额外引用的类将通过JVM自动加载到内存中。 在日常开发以上两种方式一般会混合使用。\n1 2 3 4 5 6 //隐式加载 User user=new User(); //显式加载，并初始化 Class clazz=Class.forName(\u0026#34;com.test.java.User\u0026#34;); //显式加载，但不初始化 ClassLoader.getSystemClassLoader().loadClass(\u0026#34;com.test.java.Parent\u0026#34;); Copied! 1.3. 类加载器的必要性 一般情况下，Java开发人员并不需要在程序中显式地使用类加载器，但是了解类加载器的加载机制却显得至关重要。从以下几个方面说：\n避免在开发中遇到java.lang.ClassNotFoundException异常或java.lang.NoClassDefFoundError异常时，手足无措。只有了解类加载器的 加载机制才能够在出现异常的时候快速地根据错误异常日志定位问题和解决问题 需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时，就需要与类加载器打交道了。 开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则，以便实现一些自定义的处理逻辑。 1.4. 命名空间 何为类的唯一性？\n$\\color{red}{对于任意一个类，都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。}$每一个类加载器，都拥有一个独立的类名称空间：$\\color{red}{比较两个类是否相等，只有在这两个类是由同一个类加载器加载的前提下才有意义。}$否则，即使这两个类源自同一个Class文件，被同一个虚拟机加载，只要加载他们的类加载器不同，那这两个类就必定不相等。\n命名空间\n每个类加载器都有自己的命名空间，命名空间由该加载器及所有的父加载器所加载的类组成\n在同一命名空间中，不会出现类的完整名字（包括类的包名）相同的两个类\n在不同的命名空间中，有可能会出现类的完整名字（包括类的包名）相同的两个类\n在大型应用中，我们往往借助这一特性，来运行同一个类的不同版本。\n1.5. 类加载机制的基本特征 双亲委派模型。但不是所有类加载都遵守这个模型，有的时候，启动类加载器所加载的类型，是可能要加载用户代码的，比如JDK内部的ServiceProvider/ServiceLoader机制，用户可以在标准API框架上，提供自己的实现，JDK也需要提供些默认的参考实现。例如，Java中JNDI、JDBC、文件系统、Cipher等很多方面，都是利用的这种机制，这种情况就不会用双亲委派模型去加载，而是利用所谓的上下文加载器。\n可见性，子类加载器可以访问父加载器加载的类型，但是反过来是不允许的。不然，因为缺少必要的隔离，我们就没有办法利用类加载器去实现容器的逻辑。\n单一性，由于父加载器的类型对于子加载器是可见的，所以父加载器中加载过的类型，就不会在子加载器中重复加载。但是注意，类加载器“邻居”间，同一类型仍然可以被加载多次，因为互相并不可见。\n1.6. 类加载器之间的关系 Launcher类核心代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 Launcher.ExtClassLoader var1; try { var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError(\u0026#34;Could not create extension class loader\u0026#34;, var10); } try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError(\u0026#34;Could not create application class loader\u0026#34;, var9); } Thread.currentThread().setContextClassLoader(this.loader); Copied! ExtClassLoader的Parent类是null\nAppClassLoader的Parent类是ExtClassLoader\n当前线程的ClassLoader是AppClassLoader\n$\\color{red}{注意，这里的Parent类并不是Java语言意义上的继承关系，而是一种包含关系}$\n2. 类的加载器分类 JVM支持两种类型的类加载器，分别为引导类加载器（Bootstrap ClassLoader）和自定义类加载器（User-Defined ClassLoader）。\n从概念上来讲，自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器，但是Java虚拟机规范却没有这么定义，而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分，在程序中我们最常见的类加载器结构主要是如下情况：\n除了顶层的启动类加载器外，其余的类加载器都应当有自己的“父类”加戟器。 不同类加载器看似是继承（Inheritance）关系，实际上是包含关系。在下层加载器中，包含着上层加载器的引用。 父类加载器和子类加载器的关系：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class ClassLoader{ ClassLoader parent;//父类加载器 public ClassLoader(ClassLoader parent){ this.parent = parent; } } class ParentClassLoader extends ClassLoader{ public ParentClassLoader(ClassLoader parent){ super(parent); } } class ChildClassLoader extends ClassLoader{ public ChildClassLoader(ClassLoader parent){ //parent = new ParentClassLoader(); super(parent); } } Copied! 正是由于子类加载器中包含着父类加载器的引用，所以可以通过子类加载器的方法获取对应的父类加载器\n注意：\n启动类加载器通过C/C++语言编写，而自定义类加载器都是由Java语言编写的，虽然扩展类加载器和应用程序类加载器是被jdk开发人员使用java语言来编写的，但是也是由java语言编写的，所以也被称为自定义类加载器\n2.1. 引导类加载器 启动类加载器（引导类加载器，Bootstrap ClassLoader）\n这个类加载使用C/C++语言实现的，嵌套在JVM内部。\n它用来加载Java的核心库（JAVAHOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容）。用于提供JVM自身需要的类。\n并不继承自java.lang.ClassLoader，没有父加载器。\n出于安全考虑，Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类\n加载扩展类和应用程序类加载器，并指定为他们的父类加载器。\n使用-XX:+TraceClassLoading参数得到。\n启动类加载器使用C++编写的？Yes！\nC/C++：指针函数\u0026amp;函数指针、C++支持多继承、更加高效 Java：由C++演变而来，（C++）–版，单继承 1 2 3 4 5 6 7 8 9 System.out.println(\u0026#34;＊＊＊＊＊＊＊＊＊＊启动类加载器＊＊＊＊＊＊＊＊＊＊\u0026#34;); // 获取BootstrapclassLoader能够加载的api的路径 URL[] urLs = sun.misc.Launcher.getBootstrapcLassPath().getURLs(); for (URL element : urLs) { System.out.println(element.toExternalForm()); } // 从上面的路径中随意选择一个类，来看看他的类加载器是什么：引导类加载器 ClassLoader classLoader = java.security.Provider.class.getClassLoader(); System.out.println(classLoader); Copied! 执行结果： 2.2. 扩展类加载器 扩展类加载器（Extension ClassLoader）\nJava语言编写，由sun.misc.Launcher$ExtClassLoader实现。\n继承于ClassLoader类\n父类加载器为启动类加载器\n从java.ext.dirs系统属性所指定的目录中加载类库，或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下，也会自动由扩展类加载器加载。\n1 2 3 4 5 6 7 8 9 System.out.println(\u0026#34;＊＊＊＊＊＊＊＊＊＊＊扩展类加载器＊＊＊＊＊＊＊＊＊＊＊\u0026#34;); String extDirs =System.getProperty(\u0026#34;java.ext.dirs\u0026#34;); for (String path :extDirs.split( regex:\u0026#34;;\u0026#34;)){ System.out.println(path); } // 从上面的路径中随意选择一个类，来看看他的类加载器是什么：扩展类加载器 lassLoader classLoader1 = sun.security.ec.CurveDB.class.getClassLoader(); System.out.print1n(classLoader1); //sun.misc. Launcher$ExtCLassLoader@1540e19d Copied! 执行结果：\n2.3. 系统类加载器 应用程序类加载器（系统类加载器，AppClassLoader）\njava语言编写，由sun.misc.Launcher$AppClassLoader实现 继承于ClassLoader类 父类加载器为扩展类加载器 它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库 $\\color{red}{应用程序中的类加载器默认是系统类加载器。}$ 它是用户自定义类加载器的默认父加载器 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器 2.4. 用户自定义类加载器 用户自定义类加载器\n在Java的日常应用程序开发中，类的加载几乎是由上述3种类加载器相互配合执行的。在必要时，我们还可以自定义类加载器，来定制类的加载方式。 体现Java语言强大生命力和巨大魅力的关键因素之一便是，Java开发者可以自定义类加载器来实现类库的动态加载，加载源可以是本地的JAR包，也可以是网络上的远程资源。 $\\color{red}{通过类加载器可以实现非常绝妙的插件机制}$，这方面的实际应用案例举不胜举。例如，著名的OSGI组件框架，再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制，这种机制无须重新打包发布应用程序就能实现。 同时，$\\color{red}{自定义加载器能够实现应用隔离}$，例如Tomcat，Spring等中间件和组件框架都在内部实现了自定义的加载器，并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多，想不修改C/C++程序就能为其新增功能，几乎是不可能的，仅仅一个兼容性便能阻挡住所有美好的设想。 自定义类加载器通常需要继承于ClassLoader。 3. 测试不同的类的加载器 每个Class对象都会包含一个定义它的ClassLoader的一个引用。 获取ClassLoader的途径\n1 2 3 4 5 6 // 获得当前类的ClassLoader clazz.getClassLoader() // 获得当前线程上下文的ClassLoader Thread.currentThread().getContextClassLoader() // 获得系统的ClassLoader ClassLoader.getSystemClassLoader() Copied! 说明：\n站在程序的角度看，引导类加载器与另外两种类加载器（系统类加载器和扩展类加载器）并不是同一个层次意义上的加 载器，引导类加载器是使用C++语言编写而成的，而另外两种类加载器则是使用Java语言编写而成的。由于引导类加载 器压根儿就不是一个Java类，因此在Java程序中只能打印出空值。 数组类的Class对象，不是由类加载器去创建的，而是在Java运行期JVM根据需要自动创建的。对于数组类的类加载器 来说，是通过Class.getClassLoader()返回的，与数组当中元素类型的类加载器是一样的；如果数组当中的元素类型 是基本数据类型，数组类是没有类加载器的。 1 2 3 4 5 6 7 8 9 10 11 // 运行结果：null String[] strArr = new String[6]; System.out.println(strArr.getClass().getClassLoader()); // 运行结果：sun．misc．Launcher＄AppCLassLoader＠18b4aac2 ClassLoaderTest[] test=new ClassLoaderTest[1]; System.out.println(test.getClass().getClassLoader()); // 运行结果：null int[]ints =new int[2]; System.out.println(ints.getClass().getClassLoader()); Copied! 代码：\n1 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 class ClassLoaderTest1{ public static void main(String[] args) { //获取系统该类加载器 ClassLoader systemClassLoader=ClassLoader.getSystemCLassLoader(); System.out.print1n(systemClassLoader);//sun.misc.Launcher$AppCLassLoader@18b4aac2 //获取扩展类加载器 ClassLoader extClassLoader =systemClassLoader.getParent(); System.out.println(extClassLoader);//sun.misc. Launcher$ExtCLassLoader@1540e19d //试图获取引导类加载器：失败 ClassLoader bootstrapClassLoader =extClassLoader.getParent(); System.out.print1n(bootstrapClassLoader);//null //################################## try{ ClassLoader classLoader =Class.forName(\u0026#34;java.lang.String\u0026#34;).getClassLoader(); System.out.println(classLoader); //自定义的类默认使用系统类加载器 ClassLoader classLoader1=Class.forName(\u0026#34;com.atguigu.java.ClassLoaderTest1\u0026#34;).getClassLoader(); System.out.println(classLoader1); //关于数组类型的加载：使用的类的加载器与数组元素的类的加载器相同 String[] arrstr = new String[10]; System.out.println(arrstr.getClass().getClassLoader());//null：表示使用的是引导类加载器 ClassLoaderTest1[] arr1 =new ClassLoaderTest1[10]; System.out.println(arr1.getClass().getClassLoader());//sun.misc. Launcher$AppcLassLoader@18b4aac2 int[] arr2 = new int[10]; System.out.println(arr2.getClass().getClassLoader());//null:不需要类加载器，基本数据类型是定义好的，不需要类加载器加载 } catch (ClassNotFoundException e) { e.printStackTrace(); } } } Copied! 4. ClassLoader源码解析 ClassLoader与现有类的关系：\n除了以上虚拟机自带的加载器外，用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader，所有用户自定义的类加载器都应该继承ClassLoader类。\n4.1. ClassLoader的主要方法 抽象类ClassLoader的主要方法：（内部没有抽象方法）\n1 public final ClassLoader getParent() Copied! 返回该类加载器的超类加载器\n1 public Class\u0026lt;?\u0026gt; loadClass(String name) throws ClassNotFoundException Copied! 加载名称为name的类，返回结果为java.lang.Class类的实例。如果找不到类，则返回 ClassNotFoundException异常。该方法中的逻辑就是双亲委派模式的实现。\n1 protected Class\u0026lt;?\u0026gt; findClass(String name) throws ClassNotFoundException Copied! 查找二进制名称为name的类，返回结果为java.lang.Class类的实例。这是一个受保护的方法，JVM鼓励我们重写此方法，需要自定义加载器遵循双亲委托机制，该方法会在检查完父类加载器之后被loadClass()方法调用。\n在JDK1.2之前，在自定义类加载时，总会去继承ClassLoader类并重写loadClass方法，从而实现自定义的类加载类。但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法，而是建议把自定义的类加载逻辑写在findClass()方法中，从前面的分析可知，findClass()方法是在loadClass()方法中被调用的，当loadClass()方法中父加载器加载失败后，则会调用自己的findClass()方法来完成类加载，这样就可以保证自定义的类加载器也符合双亲委托模式。\n需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑，取而代之的是抛出ClassNotFoundException异常，同时应该知道的是findClass方法通常是和defineClass方法一起使用的。$\\color{red}{一般情况下，在自定义类加载器时，会直接覆盖ClassLoader的findClass()方法并编写加载规则，取得要加载类的字节码后转换成流，然后调用defineClass()方法生成类的Class对象。}$\n1 protected final Class\u0026lt;?\u0026gt; defineClass(String name, byte[] b,int off,int len) Copied! 根据给定的字节数组b转换为Class的实例，off和len参数表示实际Class信息在byte数组中的位置和长度，其中byte数组b是ClassLoader从外部获取的。这是受保护的方法，只有在自定义ClassLoader子类中可以使用。\ndefineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象（ClassLoader中已实现该方法逻辑），通过这个方法不仅能够通过class文件实例化class对象，也可以通过其他方式实例化class对象，如通过网络接收一个类的字节码，然后转换为byte字节流创建对应的Class对象。\n$\\color{red}{defineClass()方法通常与findClass()方法一起使用，一般情况下，在自定义类加载器时，会直接覆盖ClassLoader的findClass()方法并编写加载规则，取得要加载类的字节码后转换成流，然后调用defineClass()方法生成类的Class对象}$\n简单举例：\n1 2 3 4 5 6 7 8 9 10 protected Class\u0026lt;?\u0026gt; findClass(String name) throws ClassNotFoundException { // 获取类的字节数组 byte[] classData =getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else{ //使用defineClass生成class对象 return defineClass(name,classData,θ,classData.length); } } Copied! 1 protected final void resolveClass(Class\u0026lt;?\u0026gt; c) Copied! 链接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要是对字节码进行验证，为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。\n1 protected final Class\u0026lt;?\u0026gt; findLoadedClass(String name) Copied! 查找名称为name的已经被加载过的类，返回结果为java.lang.Class类的实例。这个方法是final方法，无法被修改。\n1 private final ClassLoader parent; Copied! 它也是一个ClassLoader的实例，这个字段所表示的ClassLoader也称为这个ClassLoader的双亲。在类加载的过程中，ClassLoader可能会将某些请求交予自己的双亲处理。\n4.2. SecureClassLoader与URLClassLoader 接着SecureClassLoader扩展了ClassLoader，新增了几个与使用相关的代码源（对代码源的位置及其证书的验证）和权限定义类验证（主要指对class源码的访问权限）的方法，一般我们不会直接跟这个类打交道，更多是与它的子类URLClassLoader有所关联。\n前面说过，ClassLoader是一个抽象类，很多方法是空的没有实现，比如findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现。并新增了URLClassPath类协助取得Class字节码流等功能。$\\color{red}{在编写自定义类加载器时，如果没有太过于复杂的需求，可以直接继承URLClassLoader类}$，这样就可以避免自己去编写findClass()方法及其获取字节码流的方式，使自定义类加载器编写更加简洁。\n4.3. ExtClassLoader与AppClassLoader 了解完URLClassLoader后接着看看剩余的两个类加载器，即拓展类加载器ExtClassLoader和系统类加载器AppClassLoader，这两个类都继承自URLClassLoader，是sun.misc.Launcher的静态内部类。\nsun.misc.Launcher主要被系统用于启动主应用程序，ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的，其类主要类结构如下：\n我们发现ExtClassLoader并没有重写loadClass()方法，这足矣说明其遵循双亲委派模式，而AppClassLoader重载了loadClass()方法，但最终调用的还是父类loadClass()方法，因此依然遵守双亲委派模式。\n4.4. Class.forName()与ClassLoader.loadClass() Class.forName()\nClass.forName()：是一个静态方法，最常用的是Class.forName(String className);\n根据传入的类的全限定名返回一个Class对象。该方法在将Class文件加载到内存的同时，会执行类的初始化。\n1 Class.forName(\u0026#34;com.atguigu.java.Helloworld\u0026#34;); Copied! ClassLoader.loadClass()\nClassLoader.loadClass()：这是一个实例方法，需要一个ClassLoader对象来调用该方法。\n该方法将Class文件加载到内存时，并不会执行类的初始化，直到这个类第一次使用时才进行初始化。该方法因为需要得到一个ClassLoader对象，所以可以根据需要指定使用哪个类加载器。\n1 Classloader cl = ......; cl.loadClass(\u0026#34;com.atguigu.java.Helloworld\u0026#34;); Copied! 5. 双亲委派模型 5.1. 定义与本质 类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始，类的加载过程采用双亲委派机制，这种机制能更好地保证Java平台的安全。\n定义\n如果一个类加载器在接到加载类的请求时，它首先不会自己尝试去加载这个类，而是把这个请求任务委托给父类加载器去完成，依次递归，如果父类加载器可以完成类加载任务，就成功返回。只有父类加载器无法完成此加载任务时，才自己去加载。\n本质\n规定了类加载的顺序是：引导类加载器先加载，若加载不到，由扩展类加载器加载，若还加载不到，才会由系统类加载器或自定义的类加载器进行加载。\n5.2. 优势与劣势 双亲委派机制优势\n避免类的重复加载，确保一个类的全局唯一性\n$\\color{red}{Java类随着它的类加载器一起具备了一种带有优先级的层次关系，通过这种层级关可以避免类的重复加载，当父亲已经加载了该类时，就没有必要子ClassLoader再加载一次。}$\n保护程序安全，防止核心API被随意篡改\n代码支持\n双亲委派机制在java.lang.ClassLoader.loadClass(String，boolean)接口中体现。该接口的逻辑如下：\n（1）先在当前加载器的缓存中查找有无目标类，如果有，直接返回。\n（2）判断当前加载器的父加载器是否为空，如果不为空，则调用parent.loadClass(name，false)接口进行加载。\n（3）反之，如果当前加载器的父类加载器为空，则调用findBootstrapClassorNull(name)接口，让引导类加载器进行加载。\n（4）如果通过以上3条路径都没能成功加载，则调用findClass(name)接口进行加载。该接口最终会调用java.lang.ClassLoader接口的defineClass系列的native接口加载目标Java类。\n双亲委派的模型就隐藏在这第2和第3步中。\n举例\n假设当前加载的是java.lang.Object这个类，很显然，该类属于JDK中核心得不能再核心的一个类，因此一定只能由引导类加载器进行加载。当]VM准备加载javaJang.Object时，JVM默认会使用系统类加载器去加载，按照上面4步加载的逻辑，在第1步从系统类的缓存中肯定查找不到该类，于是进入第2步。由于从系统类加载器的父加载器是扩展类加载器，于是扩展类加载器继续从第1步开始重复。由于扩展类加载器的缓存中也一定查找不到该类，因此进入第2步。扩展类的父加载器是null，因此系统调用findClass（String），最终通过引导类加载器进行加载。\n思考\n如果在自定义的类加载器中重写java.lang.ClassLoader.loadClass(String)或java.lang.ClassLoader.loadclass(String，boolean)方法，抹去其中的双亲委派机制，仅保留上面这4步中的第l步与第4步，那么是不是就能够加载核心类库了呢？\n这也不行！因为JDK还为核心类库提供了一层保护机制。不管是自定义的类加载器，还是系统类加载器抑或扩展类加载器，最终都必须调用 java.lang.ClassLoader.defineclass(String，byte[]，int，int，ProtectionDomain)方法，而该方法会执行preDefineClass()接口，该接口中提供了对JDK核心类库的保护。\n弊端\n检查类是否加载的委托过程是单向的，这个方式虽然从结构上说比较清晰，使各个ClassLoader的职责非常明确，但是同时会带来一个问题，即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。\n通常情况下，启动类加载器中的类为系统核心类，包括一些重要的系统接口，而在应用类加载器中，为应用类。按照这种模式，应用类访问系统类自然是没有问题，但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口，该接口需要在应用类中得以实现，该接口还绑定一个工厂方法，用于创建该接口的实例，而接口和工厂方法都在启动类加载器中。这时，就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。\n结论\n$\\color{red}{由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型，只是建议采用这种方式而已。}$比如在Tomcat中，类加载器所采用的加载机制就和传统的双亲委派模型有一定区别，当缺省的类加载器接收到一个类的加载任务时，首先会由它自行加载，当它加载失败时，才会将类的加载任务委派给它的超类加载器去执行，这同时也是Serylet规范推荐的一种做法。\n5.3. 破坏双亲委派机制 双亲委派模型并不是一个具有强制性约束的模型，而是Java设计者推荐给开发者们的类加载器实现方式。\n在Java的世界中大部分的类加载器都遵循这个模型，但也有例外的情况，直到Java模块化出现为止，双亲委派模型主要出现过3次较大规模“被破坏”的情况。\n第一次破坏双亲委派机制\n双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前一—即JDK1.2面世以前的“远古”时代。\n由于双亲委派模型在JDK 1.2之后才被引入，但是类加载器的概念和抽象类java.lang.ClassLoader则在Java的第一个版本中就已经存在，面对经存在的用户自定义类加载器的代码，Java设计者们引入双亲委派模型时不得不做出一些妥协，$\\color{red}{为了兼容这些已有代码，无法再以技术手段避免loadClass()被子类覆盖的可能性}$，只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass()，并引导用户编写的类加载逻辑时尽可能去重写这个方法，而不是在loadClass()中编写代码。上节我们已经分析过loadClass()方法，双亲委派的具体逻辑就实现在这里面，按照loadClass()方法的逻辑，如果父类加载失败，会自动调用自己的findClass()方法来完成加载，这样既不影响用户按照自己的意愿去加载类，又可以保证新写出来的类加载器是符合双亲委派规则的。\n第二次破坏双亲委派机制：线程上下文类加载器\n双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的，双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题（$\\color{red}{越基础的类由越上层的加载器进行加载}$），基础类型之所以被称为“基础”，是因为它们总是作为被用户代码继承、调用的API存在，但程序设计往往没有绝对不变的完美规则，如果有$\\color{red}{基础类型又要调用回用户的代码，那该怎么办呢？}$\n这并非是不可能出现的事情，一个典型的例子便是JNDI服务，JNDI现在已经是Java的标准服务，它的代码由启动类加载器来完成加载（在JDK 1.3时加入到rt.jar的），肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理，它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口（Service Provider Interface，SPI）的代码，现在问题来了，$\\color{red}{启动类加载器是绝不可能认识、加载这些代码的，那该怎么办？}$（SPI：在Java平台中，通常把核心类rt.jar中提供外部服务、可由应用层自行实现的接口称为SPI）\n为了解决这个困境，Java的设计团队只好引入了一个不太优雅的设计：$\\color{red}{线程上下文类加载器（Thread Context ClassLoader）}$。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置，如果创建线程时还未设置，它将会从父线程中继承一个，如果在应用程序的全局范围内都没有设置过的话，那这个类加载器默认就是应用程序类加载器。\n有了线程上下文类加载器，程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码，$\\color{red}{这是一种父类加载器去请求子类加载器完成类加载的行为，这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器，已经违背了双亲委派模型的一般性原则}$，但也是无可奈何的事情。 ，例如JNDI、JDBC、JCE、JAXB和JBI等。不过，当SPI的服务提供者多于一个的时候，代码就只能根据具体提供者的类型来硬编码判断，为了消除这种极不优雅的实现方式，在JDK6时，JDK提供了java.util.ServiceLoader类，以META-INF/services中的配置信息，辅以责任链模式，这才算是给SPI的加载提供了一种相对合理的解决方案。\n默认上下文加载器就是应用类加载器，这样以上下文加载器为中介，使得启动类加载器中的代码也可以访问应用类加载器中的类。\n第三次破坏双亲委派机制\n双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。如：**代码热替换(Hot Swap)、模块热部署(Hot Deployment)**等\nIBM公司主导的JSR-291(即OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现，每一个程序模块(osGi中称为Bundle)都有一个自己的类加载器，当需要更换一个Bundle时，就把Bund1e连同类加载器一起换掉以实现代码的热替换。在oSGi环境下，类加载器不再双亲委派模型推荐的树状结构，而是进一步发展为更加复杂的网状结构。\n当收到类加载请求时，OSGi将按照下面的顺序进行类搜索：\n1）$\\color{red}{将以java.*开头的类，委派给父类加载器加载。}$\n2）$\\color{red}{否则，将委派列表名单内的类，委派给父类加载器加载。}$\n3）否则，将Import列表中的类，委派给Export这个类的Bundle的类加载器加载。\n4）否则，查找当前Bundle的ClassPath，使用自己的类加载器加载。\n5）否则，查找类是否在自己的Fragment Bundle中，如果在，则委派给Fragment Bundle的类加载器加载。\n6）否则，查找Dynamic Import列表的Bundle，委派给对应Bund1e的类加载器加载。\n7）否则，类查找失败。\n说明：只有开头两点仍然符合双亲委派模型的原则，其余的类查找都是在平级的类加载器中进行的\n小结：这里，我们使用了“被破坏”这个词来形容上述不符合双亲委派模型原则的行为，但这里“被破坏”并不一定是带有贬义的。只要有明确的目的和充分的理由，突破旧有原则无疑是一种创新。\n正如：OSGi中的类加载器的设计不符合传统的双亲委派的类加载器架构，且业界对其为了实现热部署而带来的额外的高复杂度还存在不少争议，但对这方面有了解的技术人员基本还是能达成一个共识，认为OSGi中对类加载器的运用是值得学习的，完全弄懂了OSGi的实现，就算是掌握了类加载器的精粹。\n5.4. 热替换的实现 热替换是指在程序的运行过程中，不停止服务，只通过替换程序文件来修改程序的行为。$\\color{red}{热替换的关键需求在于服务不能中断，修改必须立即表现正在运行的系统之中。}$基本上大部分脚本语言都是天生支持热替换的，比如：PHP，只要替换了PHP源文件，这种改动就会立即生效，而无需重启Web服务器。\n但对Java来说，热替换并非天生就支持，如果一个类已经加载到系统中，通过修改类文件，并无法让系统再来加载并重定义这个类。因此，在Java中实现这一功能的一个可行的方法就是灵活运用ClassLoader。\n注意：由不同ClassLoader加载的同名类属于不同的类型，不能相互转换和兼容。即两个不同的ClassLoader加载同一个类，在虚拟机内部，会认为这2个类是完全不同的。\n根据这个特点，可以用来模拟热替换的实现，基本思路如下图所示：\n6. 沙箱安全机制 沙箱安全机制\n保证程序安全 保护Java原生的JDK代码 $\\color{red}{Java安全模型的核心就是Java沙箱（sandbox）}$。什么是沙箱？沙箱是一个限制程序运行的环境。\n沙箱机制就是将Java代码$\\color{red}{限定在虚拟机（JVM）特定的运行范围中，并且严格限制代码对本地系统资源访问}$。通过这样的措施来保证对代码的有限隔离，防止对本地系统造成破坏。\n沙箱主要限制系统资源访问，那系统资源包括什么？CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。\n所有的Java程序运行都可以指定沙箱，可以定制安全策略。\n6.1. JDK1.0时期 在Java中将执行程序分成本地代码和远程代码两种，本地代码默认视为可信任的，而远程代码则被看作是不受信的。对于授信的本地代码，可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中，安全依赖于沙箱（Sandbox）机制。如下图所示JDK1.0安全模型\n6.2. JDK1.1时期 JDK1.0中如此严格的安全机制也给程序的功能扩展带来障碍，比如当用户希望远程代码访问本地系统的文件时候，就无法实现。\n因此在后续的Java1.1版本中，针对安全机制做了改进，增加了安全策略。允许用户指定代码对本地资源的访问权限。\n如下图所示JDK1.1安全模型\n6.3. JDK1.2时期 在Java1.2版本中，再次改进了安全机制，增加了代码签名。不论本地代码或是远程代码，都会按照用户的安全策略设定，由类加载器加载到虚拟机中权限不同的运行空间，来实现差异化的代码执行权限控制。如下图所示JDK1.2安全模型：\n6.4. JDK1.6时期 当前最新的安全机制实现，则引入了**域（Domain）**的概念。\n虚拟机会把所有代码加载到不同的系统域和应用域。$\\color{red}{系统域部分专门负责与关键资源进行交互}$，而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域（Protected Domain），对应不一样的权限（Permission）。存在于不同域中的类文件就具有了当前域的全部权限，如下图所示，最新的安全模型（jdk1.6）\n7. 自定义类的加载器 7.1. 为什么要自定义类加载器？ $\\color{red}{隔离加载类}$\n在某些框架内进行中间件与应用的模块隔离，把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器，内部自定义了好几种类加载器，用于隔离同一个Web应用服务器上的不同应用程序。\n$\\color{red}{修改类加载的方式}$\n类的加载模型并非强制，除Bootstrap外，其他的加载并非一定要引入，或者根据实际情况在某个时间点进行按需进行动态加载\n$\\color{red}{扩展加载源}$\n比如从数据库、网络、甚至是电视机机顶盒进行加载\n$\\color{red}{防止源码泄漏}$\nJava代码容易被编译和篡改，可以进行编译加密。那么类加载也需要自定义，还原加密的字节码。\n常见的场景\n实现类似进程内隔离，类加载器实际上用作不同的命名空间，以提供类似容器、模块化的效果。例如，两个模块依赖于某个类库的不同版本，如果分别被不同的容器加载，就可以互不干扰。这个方面的集大成者是JavaEE和OSGI、JPMS等框架。 应用需要从不同的数据源获取类定义信息，例如网络数据源，而不是本地文件系统。或者是需要自己操纵字节码，动态修改或者生成类型。 注意\n在一般情况下，使用不同的类加载器去加载不同的功能模块，会提高应用程序的安全性。但是，如果涉及Java类型转换，则加载器反而容易产生不美好的事情。在做Java类型转换时，只有两个类型都是由同一个加载器所加载，才能进行类型转换，否则转换时会发生异常。\n7.2. 实现方式 Java提供了抽象类java.lang.ClassLoader，所有用户自定义的类加载器都应该继承ClassLoader类。\n在自定义ClassLoader的子类时候，我们常见的会有两种做法:\n方式一:重写loadClass()方法 方式二:重写findclass()方法 对比\n这两种方法本质上差不多，毕竟loadClass()也会调用findClass()，但是从逻辑上讲我们最好不要直接修改loadClass()的内部逻辑。建议的做法是只在findClass()里重写自定义类的加载方法，根据参数指定类的名字，返回对应的Class对象的引用。 loadclass()这个方法是实现双亲委派模型逻辑的地方，擅自修改这个方法会导致模型被破坏，容易造成问题。$\\color{red}{因此我们最好是在双亲委派模型框架内进行小范围的改动，不破坏原有的稳定结构}$。同时，也避免了自己重写loadClass()方法的过程中必须写双亲委托的重复代码，从代码的复用性来看，不直接修改这个方法始终是比较好的选择。 当编写好自定义类加载器后，便可以在程序中调用loadClass()方法来实现类加载操作。 说明\n其父类加载器是系统类加载器 JVM中的所有类加载都会使用java.lang.ClassLoader.loadClass(String)接口(自定义类加载器并重写java.lang.ClassLoader.loadClass(String)接口的除外)，连JDK的核心类库也不能例外。 8. Java9新特性 为了保证兼容性，JDK9没有从根本上改变三层类加载器架构和双亲委派模型，但为了模块化系统的顺利运行，仍然发生了一些值得被注意的变动。\n扩展机制被移除，扩展类加载器由于向后兼容性的原因被保留，不过被重命名为平台类加载器(platform class loader)。可以通过classLoader的新方法getPlatformClassLoader()来获取。\nJDK9时基于模块化进行构建(原来的rt.jar和tools.jar被拆分成数十个JMOD文件)，其中的Java类库就已天然地满足了可扩展的需求，那自然无须再保留\u0026lt;JAVA_HOME\u0026gt;\\lib\\ext目录，此前使用这个目录或者java.ext.dirs系统变量来扩展JDK功能的机制已经没有继续存在的价值了。\n平台类加载器和应用程序类加载器都不再继承自java.net.URLClassLoader。\n现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader。\n​\t如果有程序直接依赖了这种继承关系，或者依赖了URLClassLoader类的特定方法，那代码很可能会在JDK9及更高版本的JDK中崩溃。\n在Java9中，类加载器有了名称。该名称在构造方法中指定，可以通过getName()方法来获取。平台类加载器的名称是platform，应用类加载器的名称是app。类加载器的名称在调试与类加载器相关的问题时会非常有用。 启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器（以前是C++实现），但为了与之前代码兼容，在获取启动类加载器的场景中仍然会返回null，而不会得到BootClassLoader实例。 类加载的委派关系也发生了变动。当平台及应用程序类加载器收到类加载请求，在委派给父加载器加载前，要先判断该类是否能够归属到某一个系统模块中，如果可以找到这样的归属关系，就要优先委派给负责那个模块的加载器完成加载。 代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class ClassLoaderTest { public static void main(String[] args) { System.out.println(ClassLoaderTest.class.getClassLoader()); System.out.println(ClassLoaderTest.class.getClassLoader().getParent()); System.out.println(ClassLoaderTest.class.getClassLoader().getParent().getParent()); //获取系统类加载器 System.out.println(ClassLoader.getSystemClassLoader()); //获取平台类加载器 System.out.println(ClassLoader.getPlatformClassLoader()); //获取类的加载器的名称 System.out.println(ClassLoaderTest.class.getClassLoader().getName()); } } Copied! ","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/13895tg3/","title":"04-再谈类的加载器"},{"content":" 5. 本地方法接口和本地方法栈 5.1. 什么是本地方法？ 简单地讲，一个 Native Method 是一个 Java 调用非 Java 代码的接囗。一个 Native Method 是这样一个 Java 方法：该方法的实现由非 Java 语言实现，比如 C。这个特征并非 Java 所特有，很多其它的编程语言都有这一机制，比如在 C++中，你可以用 extern \u0026ldquo;c\u0026rdquo; 告知 c++编译器去调用一个 c 的函数。\nA native method is a Java method whose implementation is provided by non-java code.\n在定义一个 native method 时，并不提供实现体（有些像定义一个 Java interface），因为其实现体是由非 java 语言在外面实现的。\n本地接口的作用是融合不同的编程语言为 Java 所用，它的初衷是融合 C/C++程序。\n举例\n1 2 3 4 5 6 public class IHaveNatives{ public native void methodNative1(int x); public native static long methodNative2(); private native synchronized float methodNative3(Object o); native void methodNative4(int[] ary) throws Exception; } Copied! 标识符 native 可以与其它 java 标识符连用，但是 abstract 除外\n5.2. 为什么使用 Native Method？ Java 使用起来非常方便，然而有些层次的任务用 Java 实现起来不容易，或者我们对程序的效率很在意时，问题就来了。\n与 Java 环境的交互\n有时 Java 应用需要与 Java 外面的环境交互，这是本地方法存在的主要原因。你可以想想 Java 需要与一些底层系统，如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制：它为我们提供了一个非常简洁的接口，而且我们无需去了解 Java 应用之外的繁琐的细节。\n与操作系统的交互\nJVM 支持着 Java 语言本身和运行时库，它是 Java 程序赖以生存的平台，它由一个解释器（解释字节码）和一些连接到本地代码的库组成。然而不管怎样，它毕竟不是一个完整的系统，它经常依赖于一底层系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法，我们得以用 Java 实现了 jre 的与底层系统的交互，甚至 JVM 的一些部分就是用 c 写的。还有，如果我们要使用一些 Java 语言本身没有提供封装的操作系统的特性时，我们也需要使用本地方法。\nSun\u0026rsquo;s Java\nSun 的解释器是用 C 实现的，这使得它能像一些普通的 C 一样与外部交互。jre 大部分是用 Java 实现的，它也通过一些本地方法与外界交互。例如：类 java.lang.Thread 的 setPriority()方法是用 Java 实现的，但是它实现调用的是该类里的本地方法 setPriority()。这个本地方法是用 C 实现的，并被植入 JVM 内部，在 Windows 95 的平台上，这个本地方法最终将调用 Win32 setPriority() ApI。这是一个本地方法的具体实现由 JVM 直接提供，更多的情况是本地方法由外部的动态链接库（external dynamic link library）提供，然后被 JVw 调用。\n现状\n目前该方法使用的越来越少了，除非是与硬件有关的应用，比如通过 Java 程序驱动打印机或者 Java 系统管理生产设备，在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达，比如可以使用 Socket 通信，也可以使用 Web Service 等等，不多做介绍。\n5.2. 本地方法栈 Java 虚拟机栈于管理 Java 方法的调用，而本地方法栈用于管理本地方法的调用。\n本地方法栈，也是线程私有的。\n允许被实现成固定或者是可动态扩展的内存大小。（在内存溢出方面是相同的）\n如果线程请求分配的栈容量超过本地方法栈允许的最大容量，Java 虚拟机将会抛出一个 StackOverflowError 异常。 如果本地方法栈可以动态扩展，并且在尝试扩展的时候无法申请到足够的内存，或者在创建新的线程时没有足够的内存去创建对应的本地方法栈，那么 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。 本地方法是使用 C 语言实现的。\n它的具体做法是 Native Method Stack 中登记 native 方法，在 Execution Engine 执行时加载本地方法库。\n当某个线程调用一个本地方法时，它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。\n本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。 它甚至可以直接使用本地处理器中的寄存器 直接从本地内存的堆中分配任意数量的内存。 并不是所有的 JVM 都支持本地方法。因为 Java 虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果 JVM 产品不打算支持 native 方法，也可以无需实现本地方法栈。\n在 Hotspot JVM 中，直接将本地方法栈和虚拟机栈合二为一。\n","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/4q117845/","title":"05-本地方法接口和本地方法栈"},{"content":" 7. 方法区 从线程共享与否的角度来看\n7.1. 栈、堆、方法区的交互关系 7.2. 方法区的理解 官方文档：Chapter 2. The Structure of the Java Virtual Machine (oracle.com) 7.2.1. 方法区在哪里？ 《Java 虚拟机规范》中明确说明：“尽管所有的方法区在逻辑上是属于堆的一部分，但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。”但对于 HotSpotJVM 而言，方法区还有一个别名叫做 Non-Heap（非堆），目的就是要和堆分开。\n所以，方法区看作是一块独立于 Java 堆的内存空间。\n7.2.2. 方法区的基本理解 方法区（Method Area）与 Java 堆一样，是各个线程共享的内存区域。 方法区在 JVM 启动的时候被创建，并且它的实际的物理内存空间中和 Java 堆区一样都可以是不连续的。 方法区的大小，跟堆空间一样，可以选择固定大小或者可扩展。 方法区的大小决定了系统可以保存多少个类，如果系统定义了太多的类，导致方法区溢出，虚拟机同样会抛出内存溢出错误：java.lang.OutOfMemoryError: PermGen space 或者java.lang.OutOfMemoryError: Metaspace 加载大量的第三方的 jar 包；Tomcat 部署的工程过多（30~50 个）；大量动态的生成反射类 关闭 JVM 就会释放这个区域的内存。 7.2.3. HotSpot 中方法区的演进 在 jdk7 及以前，习惯上把方法区，称为永久代。jdk8 开始，使用元空间取代了永久代。\n本质上，方法区和永久代并不等价。仅是对 hotspot 而言的。《Java 虚拟机规范》对如何实现方法区，不做统一要求。例如：BEA JRockit / IBM J9 中不存在永久代的概念。\n现在来看，当年使用永久代，不是好的 idea。导致 Java 程序更容易 OOM（超过-XX:MaxPermsize上限）\n而到了 JDK8，终于完全废弃了永久代的概念，改用与 JRockit、J9 一样在本地内存中实现的元空间（Metaspace）来代替\n元空间的本质和永久代类似，都是对 JVM 规范中方法区的实现。不过元空间与永久代最大的区别在于：元空间不在虚拟机设置的内存中，而是使用本地内存\n永久代、元空间二者并不只是名字变了，内部结构也调整了\n根据《Java 虚拟机规范》的规定，如果方法区无法满足新的内存分配需求时，将抛出 OOM 异常\n7.3. 设置方法区大小与 OOM 7.3.1. 设置方法区内存的大小 方法区的大小不必是固定的，JVM 可以根据应用的需要动态调整。\njdk7 及以前\n通过-XX:Permsize来设置永久代初始分配空间。默认值是 20.75M 通过-XX:MaxPermsize来设定永久代最大可分配空间。32 位机器默认是 64M，64 位机器模式是 82M 当 JVM 加载的类信息容量超过了这个值，会报异常OutOfMemoryError:PermGen space。 JDK8 以后\n元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定 默认值依赖于平台。windows 下，-XX:MetaspaceSize=21M -XX:MaxMetaspaceSize=-1//即没有限制。 与永久代不同，如果不指定大小，默认情况下，虚拟机会耗尽所有的可用系统内存。如果元数据区发生溢出，虚拟机一样会抛出异常OutOfMemoryError:Metaspace -XX:MetaspaceSize：设置初始的元空间大小。对于一个 64 位的服务器端 JVM 来说，其默认的-XX:MetaspaceSize值为 21MB。这就是初始的高水位线，一旦触及这个水位线，Full GC 将会被触发并卸载没用的类（即这些类对应的类加载器不再存活），然后这个高水位线将会重置。新的高水位线的值取决于 GC 后释放了多少元空间。如果释放的空间不足，那么在不超过MaxMetaspaceSize时，适当提高该值。如果释放空间过多，则适当降低该值。 如果初始化的高水位线设置过低，上述高水位线调整情况会发生很多次。通过垃圾回收器的日志可以观察到 Full GC 多次调用。为了避免频繁地 GC，建议将-XX:MetaspaceSize设置为一个相对较高的值。 举例 1：《深入理解 Java 虚拟机》的例子\n举例 2\n1 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 /** * jdk8中： * -XX:MetaspaceSize=10m-XX:MaxMetaspaceSize=10m * jdk6中： * -XX:PermSize=10m-XX:MaxPermSize=10m */ public class OOMTest extends ClassLoader{ public static void main(String[] args){ int j = 0; try{ OOMTest test = new OOMTest(); for (int i=0;i\u0026lt;10000;i++){ //创建Classwriter对象，用于生成类的二进制字节码 ClassWriter classWriter = new ClassWriter(0); //指明版本号，public，类名，包名，父类，接口 classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, \u0026#34;Class\u0026#34; + i, nu1l, \u0026#34;java/lang/Object\u0026#34;, null); //返回byte[] byte[] code = classWriter.toByteArray(); //类的加载 test.defineClass(\u0026#34;Class\u0026#34; + i, code, 0, code.length); //CLass对象 j++; } } finally{ System.out.println(j); } } } Copied! 7.3.2. 如何解决这些 OOM 要解决 OOM 异常或 heap space 的异常，一般的手段是首先通过内存映像分析工具（如 Eclipse Memory Analyzer）对 dump 出来的堆转储快照进行分析，重点是确认内存中的对象是否是必要的，也就是要先分清楚到底是出现了内存泄漏（Memory Leak）还是内存溢出（Memory Overflow）\n如果是内存泄漏，可进一步通过工具查看泄漏对象到 GC Roots 的引用链。于是就能找到泄漏对象是通过怎样的路径与 GCRoots 相关联并导致垃圾收集器无法自动回收它们的。掌握了泄漏对象的类型信息，以及 GCRoots 引用链的信息，就可以比较准确地定位出泄漏代码的位置。\n如果不存在内存泄漏，换句话说就是内存中的对象确实都还必须存活着，那就应当检查虚拟机的堆参数（-Xmx与-Xms），与机器物理内存对比看是否还可以调大，从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况，尝试减少程序运行期的内存消耗。\n7.4. 方法区的内部结构 7.4.1. 方法区（Method Area）存储什么？ 《深入理解 Java 虚拟机》书中对方法区（Method Area）存储内容描述如下：\n它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。\n7.4.2. 方法区的内部结构 类型信息 对每个加载的类型（类 class、接口 interface、枚举 enum、注解 annotation），JVM 必须在方法区中存储以下类型信息：\n这个类型的完整有效名称（全名=包名.类名） 这个类型直接父类的完整有效名（对于 interface 或是 java.lang.object，都没有父类） 这个类型的修饰符（public，abstract，final 的某个子集） 这个类型直接接口的一个有序列表 域（Field）信息 JVM 必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。\n域的相关信息包括：域名称、域类型、域修饰符（public，private，protected，static，final，volatile，transient 的某个子集）\n方法（Method）信息 JVM 必须保存所有方法的以下信息，同域信息一样包括声明顺序：\n方法名称 方法的返回类型（或 void） 方法参数的数量和类型（按顺序） 方法的修饰符（public，private，protected，static，final，synchronized，native，abstract 的一个子集） 方法的字节码（bytecodes）、操作数栈、局部变量表及大小（abstract 和 native 方法除外） 异常表（abstract 和 native 方法除外） 每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引 non-final 的类变量 静态变量和类关联在一起，随着类的加载而加载，他们成为类数据在逻辑上的一部分 类变量被类的所有实例共享，即使没有类实例时，你也可以访问它 1 2 3 4 5 6 7 8 9 10 11 12 13 public class MethodAreaTest { public static void main(String[] args) { Order order = new Order(); order.hello(); System.out.println(order.count); } } class Order { public static int count = 1; public static void hello() { System.out.println(\u0026#34;hello!\u0026#34;); } } Copied! 补充说明：全局常量（static final） 被声明为 final 的类变量的处理方法则不同，每个全局常量在编译的时候就会被分配了。\n7.4.3. 运行时常量池 VS 常量池 方法区，内部包含了运行时常量池 字节码文件，内部包含了常量池 要弄清楚方法区，需要理解清楚 ClassFile，因为加载类的信息都在方法区。 要弄清楚方法区的运行时常量池，需要理解清楚 ClassFile 中的常量池。 官方文档：https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html 一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外，还包含一项信息就是常量池表（Constant Pool Table），包括各种字面量和对类型、域和方法的符号引用\n为什么需要常量池？ 一个 java 源文件中的类、接口，编译后产生一个字节码文件。而 Java 中的字节码需要数据支持，通常这种数据会很大以至于不能直接存到字节码里，换另一种方式，可以存到常量池，这个字节码包含了指向常量池的引用。在动态链接的时候会用到运行时常量池，之前有介绍。\n比如：如下的代码：\n1 2 3 4 5 public class SimpleClass { public void sayHello() { System.out.println(\u0026#34;hello\u0026#34;); } } Copied! 虽然只有 194 字节，但是里面却使用了 String、System、PrintStream 及 Object 等结构。这里的代码量其实很少了，如果代码多的话，引用的结构将会更多，这里就需要用到常量池了。\n常量池中有什么? 击中常量池内存储的数据类型包括：\n数量值 字符串值 类引用 字段引用 方法引用 例如下面这段代码：\n1 2 3 4 5 public class MethodAreaTest2 { public static void main(String args[]) { Object obj = new Object(); } } Copied! Object obj = new Object();将会被翻译成如下字节码：\n1 2 3 0: new #2 // Class java/lang/Object 1: dup 2: invokespecial // Method java/lang/Object \u0026#34;\u0026lt;init\u0026gt;\u0026#34;() V Copied! 小结 常量池、可以看做是一张表，虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型\n7.4.4. 运行时常量池 运行时常量池（Runtime Constant Pool）是方法区的一部分。 常量池表（Constant Pool Table）是 Class 文件的一部分，用于存放编译期生成的各种字面量与符号引用，这部分内容将在类加载后存放到方法区的运行时常量池中。 运行时常量池，在加载类和接口到虚拟机后，就会创建对应的运行时常量池。 JVM 为每个已加载的类型（类或接口）都维护一个常量池。池中的数据项像数组项一样，是通过索引访问的。 运行时常量池中包含多种不同的常量，包括编译期就已经明确的数值字面量，也包括到运行期解析后才能够获得的方法或者字段引用。此时不再是常量池中的符号地址了，这里换为真实地址。 运行时常量池，相对于 Class 文件常量池的另一重要特征是：具备动态性。 string.intern() 运行时常量池类似于传统编程语言中的符号表（symboltable），但是它所包含的数据却比符号表要更加丰富一些。 当创建类或接口的运行时常量池时，如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值，则 JVM 会抛 OutOfMemoryError 异常。 7.5. 方法区使用举例 1 2 3 4 5 6 7 8 9 public class MethodAreaDemo { public static void main(String args[]) { int x = 500; int y = 100; int a = x / y; int b = 50; System.out.println(a+b); } } Copied! 7.6. 方法区的演进细节 首先明确：只有 Hotspot 才有永久代。BEA JRockit、IBMJ9 等来说，是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节，不受《Java 虚拟机规范》管束，并不要求统一 Hotspot 中方法区的变化： JDK1.6 及之前 有永久代（permanet），静态变量和字符串常量池存储在永久代上 JDK1.7 有永久代，但已经逐步 “去永久代”，字符串常量池，静态变量移除，保存在堆中 JDK1.8 无永久代，类型信息，字段，方法，常量保存在本地内存的元空间，但字符串常量池、静态变量仍然在堆中。 静态变量引用的对象始终在堆空间中，发生变化的是静态变量这个引用存放位置\n7.6.1. 为什么永久代要被元空间替代？ 官网地址：JEP 122: Remove the Permanent Generation (java.net) JRockit 是和 HotSpot 融合后的结果，因为 JRockit 没有永久代，所以他们不需要配置永久代\n随着 Java8 的到来，HotSpot VM 中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域，这个区域叫做元空间（Metaspace）。\n由于类的元数据分配在本地内存中，元空间的最大可分配空间就是系统可用内存空间。\n这项改动是很有必要的，原因有：\n为永久代设置空间大小是很难确定的。在某些场景下，如果动态加载类过多，容易产生 Perm 区的 oom。比如某个实际 Web 工 程中，因为功能点比较多，在运行过程中，要不断动态加载很多类，经常出现致命错误。\n1 \u0026#34;Exception in thread \u0026#39;dubbo client x.x connector\u0026#39; java.lang.OutOfMemoryError:PermGen space\u0026#34; Copied! 而元空间和永久代之间最大的区别在于：元空间并不在虚拟机中，而是使用本地内存。 因此，默认情况下，元空间的大小仅受本地内存限制。\n对永久代进行调优是很困难的。\n有些人认为方法区（如 HotSpot 虚拟机中的元空间或者永久代）是没有垃圾收集行为的，其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的，提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在（如 JDK 11 时期的 ZGC 收集器就不支持类卸载）。 一般来说这个区域的回收效果比较难令人满意，尤其是类型的卸载，条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 Sun 公司的 Bug 列表中，曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏\n方法区的垃圾收集主要回收两部分内容：常量池中废弃的常量和不再使用的类型\n7.6.2. StringTable 为什么要调整位置？ jdk7 中将 StringTable 放到了堆空间中。因为永久代的回收效率很低，在 full gc 的时候才会触发。而 full gc 是老年代的空间不足、永久代不足时才会触发。\n这就导致 StringTable 回收效率不高。而我们开发中会有大量的字符串被创建，回收效率低，导致永久代内存不足。放到堆里，能及时回收内存。\n7.6.3. 静态变量存放在那里？ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** * 静态引用对应的对象实体始终都存在堆空间 * jdk7: * -Xms200m -Xmx200m -XX:PermSize=300m -XX:MaxPermSize=300m -XX:+PrintGCDetails * jdk8: * -Xms200m -Xmx200m-XX:MetaspaceSize=300m -XX:MaxMetaspaceSize=300m -XX:+PrintGCDetails */ public class StaticFieldTest { private static byte[] arr = new byte[1024 * 1024 * 100]; public static void main(String[] args) { System.out.println(StaticFieldTest.arr); try { Thread.sleep(1000000); } catch (InterruptedException e){ e.printStackTrace(); } } } Copied! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /** * staticobj、instanceobj、Localobj存放在哪里？ */ public class StaticobjTest { static class Test { static ObjectHolder staticobj = new ObjectHolder(); ObjectHolder instanceobj = new ObjectHolder(); void foo() { ObjectHolder localobj = new ObjectHolder(); System.out.println(\u0026#34;done\u0026#34;); } } private static class ObjectHolder { public static void main(String[] args) { Test test = new StaticobjTest.Test(); test.foo(); } } } Copied! 使用 JHSDB 工具进行分析，这里细节略掉\nstaticobj 随着 Test 的类型信息存放在方法区，instanceobj 随着 Test 的对象实例存放在 Java 堆，localobject 则是存放在 foo()方法栈帧的局部变量表中。\n测试发现：三个对象的数据在内存中的地址都落在 Eden 区范围内，所以结论：只要是对象实例必然会在 Java 堆中分配。\n接着，找到了一个引用该 staticobj 对象的地方，是在一个 java.lang.Class 的实例里，并且给出了这个实例的地址，通过 Inspector 查看该对象实例，可以清楚看到这确实是一个 java.lang.Class 类型的对象实例，里面有一个名为 staticobj 的实例字段：\n从《Java 虚拟机规范》所定义的概念模型来看，所有 Class 相关的信息都应该存放在方法区之中，但方法区该如何实现，《Java 虚拟机规范》并未做出规定，这就成了一件允许不同虚拟机自己灵活把握的事情。JDK7 及其以后版本的 HotSpot 虚拟机选择把静态变量与类型在 Java 语言一端的映射 class 对象存放在一起，存储于 Java 堆之中，从我们的实验中也明确验证了这一点\n7.7. 方法区的垃圾回收 有些人认为方法区（如 Hotspot 虚拟机中的元空间或者永久代）是没有垃圾收集行为的，其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的，提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在（如 JDK11 时期的 zGC 收集器就不支持类卸载）。\n一般来说这个区域的回收效果比较难令人满意，尤其是类型的卸载，条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 sun 公司的 Bug 列表中，曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。\n方法区的垃圾收集主要回收两部分内容：常量池中废弃的常量和不再使用的类型。\n先来说说方法区内常量池之中主要存放的两大类常量：字面量和符号引用。字面量比较接近 Java 语言层次的常量概念，如文本字符串、被声明为 final 的常量值等。而符号引用则属于编译原理方面的概念，包括下面三类常量：\n类和接口的全限定名 字段的名称和描述符 方法的名称和描述符 HotSpot 虚拟机对常量池的回收策略是很明确的，只要常量池中的常量没有被任何地方引用，就可以被回收。\n回收废弃常量与回收 Java 堆中的对象非常类似。\n判定一个常量是否“废弃”还是相对简单，而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。需要同时满足下面三个条件：\n该类所有的实例都已经被回收，也就是 Java 堆中不存在该类及其任何派生子类的实例。\n加载该类的类加载器已经被回收，这个条件除非是经过精心设计的可替换类加载器的场景，如 OSGi、JSP 的重加载等，否则通常是很难达成的。\n该类对应的 java.lang.Class 对象没有在任何地方被引用，无法在任何地方通过反射访问该类的方法。\nJava 虚拟机被允许对满足上述三个条件的无用类进行回收，这里说的仅仅是“被允许”，而并不是和对象一样，没有引用了就必然会回收。关于是否要对类型进行回收，HotSpot 虚拟机提供了-Xnoclassgc参数进行控制，还可以使用-verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载和卸载信息\n在大量使用反射、动态代理、CGLib 等字节码框架，动态生成 JSP 以及 OSGi 这类频繁自定义类加载器的场景中，通常都需要 Java 虚拟机具备类型卸载的能力，以保证不会对方法区造成过大的内存压力。\n7.8 class对象和方法区 我们在写 Java 代码的时候，我们会面对着无数个接口，类，对象和方法。但我们有木有想过，Java 中的这些对象、类和方法，在 HotSpot JVM 中的结构又是怎么样呢？HotSpot JVM 底层都是 C++ 实现的，那么 Java 的对象模型与 C++ 对象模型之间又有什么关系呢？今天就来分析一下 HotSpot JVM 中的对象模型：oop-klass model，它们的源码位于 openjdk-8/openjdk/hotspot/src/share/vm/oops 文件夹内。\nHotSpot JVM 并没有根据 Java 实例对象直接通过虚拟机映射到新建的 C++ 对象，而是设计了一个 oop-klass model。\n当时第一次看到 oop，我的第一反应就是 object-oriented programming，其实这里的 oop 指的是 Ordinary Object Pointer（普通对象指针），它用来表示对象的实例信息，看起来像个指针实际上是藏在指针里的对象。而 klass 则包含 元数据和方法信息，用来描述 Java 类。\n那么为何要设计这样一个一分为二的对象模型呢？这是因为 HotSopt JVM 的设计者不想让每个对象中都含有一个 vtable（虚函数表），所以就把对象模型拆成 klass 和 oop，其中 oop 中不含有任何虚函数，而 klass 就含有虚函数表，可以进行 method dispatch。这个模型其实是参照的 Strongtalk VM 底层的对象模型。\nKlass klass是JVM内部建立的Java类的对等模型，里面有Java类中的类变量，成员变量、成员方法和继承信息等，正是因为JVM在内部建立了一个和Java类对等的C++类模型，才能在程序运行过程动态反射出类的全部信息。除此之外klass模型还提供了虚函数列表，里面包含了一些函数的调用接口，在访问Java类时是通过handle类获得oop实例，在通过oop实例里的指向klass的指针得到klass实例，来完成函数调用。\n非数组\nInstanceKlass 普通的类在JVM对应的C++类 存储类的元信息 在方法区 InstanceMirrorKlass 用于表示java.lang.Class，Java代码中获取到的Class对象，实际上就是这个C++类的实例，存储在堆区，学名镜像类 数组\n基本类型数组 boolean、byte、char、short、int、float、long、double 对应 TypeArrayKlass 引用类型数组 对应 ObjArrayKlass Class对象是存放在堆区的，不是方法区，这点很多人容易犯错。类的元数据（元数据并不是类的Class对象！Class对象是加载的最终产品，类的方法代码，变量名，方法名，访问权限，返回值等等都是在方法区的）才是存在方法区的。\njvm为每个加载的类型(译者：包括类和接口)都创建一个java.lang.Class的实例。而jvm必须以某种方式把Class的这个实例和存储在方法区中的类型数据联系起来。\noop oop前面说了就是普通对象指针，存放Java类的实际数据，JVM内部定义了许多___oopDesc*类型的指针，这些指针都由GC（garbage collection垃圾回收）管理，这里的oop指针前面要加上“普通”两个字，原因是区分开与SmallTalk语言中的“直接指针”，所谓直接指针就是将实际数据直接存放到指针变量里，并不会在GC堆上分配，这样对象在离开了函数的作用域后就会直接从对阵上被释放，而无需GC区回收。普通对象指针由于GC堆管理，垃圾回收需要GC来处理。来看看oop体系里面都有哪些类型变量： 上图所示就是整个oop体系的结构，每一个类型代表JVM内部一个特定的对象模型，也就是说在Java程序运行时，每创建一个新的对象，都会有对应的OopDesc对象生成。\n创建对象时符号引用指向了方法区的Class数据，还是堆内存中Class对象？ 首先要分清楚方法区中的类数据和堆中Class对象的区别。\n**堆Class对象本质上是对方法区类型数据的一个访问接口。**在Java类文件（除了数组类型）的加载过程中，首先会把.class二进制文件转化为方法区的运行时数据结构，然后会在Java堆内存中实例化一个java.lang.Class类的对象，用来访问方法区中的类型数据。因此，堆中的Class并不存储静态变量、常量、方法等实际信息。创建对象时符号表引用指向的类肯定是方法区中的类数据，因为没有必要通过Class对象来间接访问方法区，这样需要两次引用解析，开销更大。\n创建好的对象的对象头里存放的类型指针指向的是方法区中类型数据还是堆内存的Class对象？ 首先要搞清楚，对象为什么要引用方法区中的类型数据？\n进行类型强转（cast）操作或者instanceof判断时，虚拟机需要查看目标类型是不是当前对象的类型或者父类之一。 当调用实例方法时，需要进行动态绑定，动态绑定的过程需要类的信息。 和上一问一样，我们需要引用的最终目标是方法区中类有关的信息，所以类型指针直接指向方法区中的类型数据。\n如果类型指针指向的是方法区中的类数据，那么这个在堆中的Class对象又有什么用？ Class对象为程序员提供了查看方法区类型信息的接口, 如类名，当前对象的父类，方法，变量等。对于同一个ClassLoader, 只存在一个Class对象。Class对象可以通过两种方法获得：\n根据实例对象获得：ref.getClass() 根据类名获得：ClassName.class , 基本类型只可以通过这种方式获得Class对象。 new操作返回的instanceOopDesc类型指针指向instanceKlass，而instanceKlass指向了对应的类型的Class实例的instanceOopDesc；既然已经指向了方法区的类数据，那为什么还要指回Class实例？ 因为对象指向的是方法区，所以要想得到Class实例的引用，就必须通过方法区的数据，instanceKlass保留对Class实例的引用是必要的。\n总结 常见面试题 百度：\n说一下 JVM 内存模型吧，有哪些区？分别干什么的？\n蚂蚁金服：\nJava8 的内存分代改进 JVM 内存分哪几个区，每个区的作用是什么？\n一面：JVM 内存分布/内存结构？栈和堆的区别？堆的结构？为什么两个 survivor 区？\n二面：Eden 和 survior 的比例分配\n小米：\njvm 内存分区，为什么要有新生代和老年代\n字节跳动：\n二面：Java 的内存分区\n二面：讲讲 vm 运行时数据库区 什么时候对象会进入老年代？\n京东：\nJVM 的内存结构，Eden 和 Survivor 比例。\nJVM 内存为什么要分成新生代，老年代，持久代。\n新生代中为什么要分为 Eden 和 survivor。\n天猫：\n一面：Jvm 内存模型以及分区，需要详细到每个区放什么。\n一面：JVM 的内存模型，Java8 做了什么改\n拼多多：\nJVM 内存分哪几个区，每个区的作用是什么？\n美团：\njava 内存分配 jvm 的永久代中会发生垃圾回收吗？\n一面：jvm 内存分区，为什么要有新生代和老年代？\n","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/78451178/","title":"07-方法区"},{"content":" 9. 执行引擎 9.1. 执行引擎概述 执行引擎属于 JVM 的下层，里面包括解释器、及时编译器、垃圾回收器\n执行引擎是 Java 虚拟机核心的组成部分之一。\n“虚拟机”是一个相对于“物理机”的概念，这两种机器都有代码执行能力，其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的，而虚拟机的执行引擎则是由软件自行实现的，因此可以不受物理条件制约地定制指令集与执行引擎的结构体系，能够执行那些不被硬件直接支持的指令集格式。\nJVM 的主要任务是负责装载字节码到其内部，但字节码并不能够直接运行在操作系统之上，因为字节码指令并非等价于本地机器指令，它内部包含的仅仅只是一些能够被 JVM 所识别的字节码指令、符号表，以及其他辅助信息。\n那么，如果想要让一个 Java 程序运行起来，执行引擎（Execution Engine）的任务就是将字节码指令解释/编译为对应平台上的本地机器指令.才可以。简单来说，JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。\n[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H3dqdi5T-1620741818957)(https://gitee.com/vectorx/ImageCloud/raw/master/img/20210511090655.png)]\n9.1.1. 执行引擎的工作流程 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于 PC 寄存器。 每当执行完一项指令操作后，PC 寄存器就会更新下一条需要被执行的指令地址。 当然方法在执行的过程中，执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在 Java 堆区中的对象实例信息，以及通过对象头中的元数据指针定位到目标对象的类型信息。 从外观上来看，所有的 Java 虚拟机的执行引擎输入，输出都是一致的：输入的是字节码二进制流，处理过程是字节码解析执行的等效过程，输出的是执行过程。\n9.2. Java 代码编译和执行过程 大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前，都需要经过上图中的各个步骤\nJava 代码编译是由 Java 源码编译器（前端编译器）来完成，流程图如下所示：\nJava 字节码的执行是由 JVM 执行引擎（后端编译器）来完成，流程图 如下所示\n9.2.1. 什么是解释器（Interpreter）？什么是 JIT 编译器？ 解释器：当 Java 虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行，将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。\nJIT（Just In Time Compiler）编译器：就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。\n9.2.2. 为什么 Java 是半编译半解释型语言？ JDK1.0 时代，将 Java 语言定位为“解释执行”还是比较准确的。再后来，Java 也发展出可以直接生成本地代码的编译器。现在 JVM 在执行 Java 代码的时候，通常都会将解释执行与编译执行二者结合起来进行。\n图示\n9.3. 机器码、指令、汇编语言 9.3.1. 机器码 各种用二进制编码方式表示的指令，叫做机器指令码。开始，人们就用它采编写程序，这就是机器语言。\n机器语言虽然能够被计算机理解和接受，但和人们的语言差别太大，不易被人们理解和记忆，并且用它编程容易出差错。\n用它编写的程序一经输入计算机，CPU 直接读取运行，因此和其他语言编的程序相比，执行速度最快。\n机器指令与 CPU 紧密相关，所以不同种类的 CPU 所对应的机器指令也就不同。\n9.3.2. 指令 由于机器码是有 0 和 1 组成的二进制序列，可读性实在太差，于是人们发明了指令。\n指令就是把机器码中特定的 0 和 1 序列，简化成对应的指令（一般为英文简写，如 mov，inc 等），可读性稍好\n由于不同的硬件平台，执行同一个操作，对应的机器码可能不同，所以不同的硬件平台的同一种指令（比如 mov），对应的机器码也可能不同。\n9.3.3. 指令集 不同的硬件平台，各自支持的指令，是有差别的。因此每个平台所支持的指令，称之为对应平台的指令集。 如常见的\nx86 指令集，对应的是 x86 架构的平台 ARM 指令集，对应的是 ARM 架构的平台 9.3.4. 汇编语言 由于指令的可读性还是太差，于是人们又发明了汇编语言。\n在汇编语言中，用助记符（Mnemonics）代替机器指令的操作码，用\u0026lt;mark 地址符号（Symbol）或标号（Label）代替指令或操作数的地址。在不同的硬件平台，汇编语言对应着不同的机器语言指令集，通过汇编过程转换成机器指令。\n由于计算机只认识指令码，所以用汇编语言编写的程序还必须翻译成机器指令码，计算机才能识别和执行。\n9.3.5. 高级语言 为了使计算机用户编程序更容易些，后来就出现了各种高级计算机语言。高级语言比机器语言、汇编语言更接近人的语言\n当计算机执行高级语言编写的程序时，仍然需要把程序解释和编译成机器的指令码。完成这个过程的程序就叫做解释程序或编译程序。\n高级语言也不是直接翻译成机器指令，而是翻译成汇编语言码，如下面说的 C 和 C++\nC、C++源程序执行过程 编译过程又可以分成两个阶段：编译和汇编。\n编译过程：是读取源程序（字符流），对之进行词法和语法的分析，将高级语言指令转换为功能等效的汇编代码\n汇编过程：实际上指把汇编语言代码翻译成目标机器指令的过程。\n9.3.6. 字节码 字节码是一种中间状态（中间码）的二进制代码（文件），它比机器码更抽象，需要直译器转译后才能成为机器码\n字节码主要为了实现特定软件运行和软件环境、与硬件环境无关。\n字节码的实现方式是通过编译器和虚拟机器。编译器将源码编译成字节码，特定平台上的虚拟机器将字节码转译为可以直接执行的指令。字节码典型的应用为：Java bytecode\n9.4. 解释器 JVM 设计者们的初衷仅仅只是单纯地为了满足 Java 程序实现跨平台特性，因此避免采用静态编译的方式直接生成本地机器指令，从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。\n为什么 Java 源文件不直接翻译成 JMV，而是翻译成字节码文件？可能是因为直接翻译的代价是比较大的\n9.4.1. 解释器工作机制 解释器真正意义上所承担的角色就是一个运行时“翻译者”，将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。\n当一条字节码指令被解释执行完成后，接着再根据 PC 寄存器中记录的下一条需要被执行的字节码指令执行解释操作。\n9.4.2. 解释器分类 在 Java 的发展历史里，一共有两套解释执行器，即古老的字节码解释器、现在普遍使用的模板解释器。\n字节码解释器在执行时通过纯软件代码模拟字节码的执行，效率非常低下。 而模板解释器将每一条字节码和一个模板函数相关联，模板函数中直接产生这条字节码执行时的机器码，从而很大程度上提高了解释器的性能。 在 HotSpot VM 中，解释器主要由 Interpreter 模块和 Code 模块构成。\nInterpreter 模块：实现了解释器的核心功能 Code 模块：用于管理 HotSpot VM 在运行时生成的本地机器指令 9.4.3. 现状 由于解释器在设计和实现上非常简单，因此除了 Java 语言之外，还有许多高级语言同样也是基于解释器执行的，比如 Python、Perl、Ruby 等。但是在今天，基于解释器执行已经沦落为低效的代名词，并且时常被一些 C/C++程序员所调侃。\n为了解决这个问题，JVM 平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行，而是将整个函数体编译成为机器码，每次函数执行时，只执行编译后的机器码即可，这种方式可以使执行效率大幅度提升。\n不过无论如何，基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献。\n9.5. JIT 编译器 9.5.1. Java 代码的执行分类 第一种是将源代码编译成字节码文件，然后在运行时通过解释器将字节码文件转为机器码执行\n第二种是编译执行（直接编译成机器码，但是要知道不同机器上编译的机器码是不一样，而字节码是可以跨平台的）。现代虚拟机为了提高执行效率，会使用即时编译技术（JIT，Just In Time）将方法编译成机器码后再执行\nHotSpot VM 是目前市面上高性能虚拟机的代表作之一。它采用解释器与即时编译器并存的架构。在 Java 虚拟机运行时，解释器和即时编译器能够相互协作，各自取长补短，尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。\n在今天，Java 程序的运行性能早已脱胎换骨，已经达到了可以和 C/C++ 程序一较高下的地步。\n问题来了\n有些开发人员会感觉到诧异，既然 HotSpot VM 中已经内置 JIT 编译器了，那么为什么还需要再使用解释器来“拖累”程序的执行性能呢？比如 JRockit VM 内部就不包含解释器，字节码全部都依靠即时编译器编译后执行。\n首先明确： 当程序启动后，解释器可以马上发挥作用，省去编译的时间，立即执行。 编译器要想发挥作用，把代码编译成本地代码，需要一定的执行时间。但编译为本地代码后，执行效率高。\n所以： 尽管 JRockit VM 中程序的执行性能会非常高效，但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说，启动时间并非是关注重点，但对于那些看中启动时间的应用场景而言，或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。在此模式下，当 Java 虚拟器启动时，解释器可以首先发挥作用，而不必等待即时编译器全部编译完成后再执行，这样可以省去许多不必要的编译时间。随着时间的推移，编译器发挥作用，把越来越多的代码编译成本地代码，获得更高的执行效率。\n同时，解释执行在编译器进行激进优化不成立的时候，作为编译器的“逃生门”。\n9.5.2. HotSpot JVM 执行方式 当虚拟机启动的时候，解释器可以首先发挥作用，而不必等待即时编译器全部编译完成再执行，这样可以省去许多不必要的编译时间。并且随着程序运行时间的推移，即时编译器逐渐发挥作用，根据热点探测功能，将有价值的字节码编译为本地机器指令，以换取更高的程序执行效率。\n案例来了\n注意解释执行与编译执行在线上环境微妙的辩证关系。机器在热机状态可以承受的负载要大于冷机状态。如果以热机状态时的流量进行切流，可能使处于冷机状态的服务器因无法承载流量而假死。\n在生产环境发布过程中，以分批的方式进行发布，根据机器数量划分成多个批次，每个批次的机器数至多占到整个集群的 1/8。曾经有这样的故障案例：某程序员在发布平台进行分批发布，在输入发布总批数时，误填写成分为两批发布。如果是热机状态，在正常情况下一半的机器可以勉强承载流量，但由于刚启动的 JVM 均是解释执行，还没有进行热点代码统计和 JIT 动态编译，导致机器启动之后，当前 1/2 发布成功的服务器马上全部宕机，此故障说明了 JIT 的存在。—阿里团队\n9.5.3. 概念解释 Java 语言的“编译期”其实是一段“不确定”的操作过程，因为它可能是指一个前端编译器（其实叫“编译器的前端”更准确一些）把.java 文件转变成.class 文件的过程；\n也可能是指虚拟机的后端运行期编译器（JIT 编译器，Just In Time Compiler）把字节码转变成机器码的过程。\n还可能是指使用静态提前编译器（AOT 编译器，Ahead of Time Compiler）直接把.java 文件编译成本地机器代码的过程。\n前端编译器：Sun 的 Javac、Eclipse JDT 中的增量式编译器（ECJ）。\nJIT 编译器：HotSpot VM 的 C1、C2 编译器。\nAOT 编译器：GNU Compiler for the Java（GCJ）、Excelsior JET。\n9.5.4. 热点代码及探测技术 当然是否需要启动 JIT 编译器将字节码直接编译为对应平台的本地机器指令，则需要根据代码被调用执行的频率而定。关于那些需要被编译为本地代码的字节码，也被称之为“热点代码”，JIT 编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化，将其直接编译为对应平台的本地机器指令，以此提升 Java 程序的执行性能。\n一个被多次调用的方法，或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”，因此都可以通过 JIT 编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中，因此被称之为栈上替换，或简称为OSR（On Stack Replacement）编译。\n一个方法究竟要被调用多少次，或者一个循环体究竟需要执行多少次循环才可以达到这个标准？必然需要一个明确的阈值，JIT 编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。\n目前 HotSpot VM 所采用的热点探测方式是基于计数器的热点探测。\nJIT编译的代码缓存会放到方法区里面 采用基于计数器的热点探测，HotSpot VM 将会为每一个方法都建立 2 个不同类型的计数器，分别为方法调用计数器（Invocation Counter）和回边计数器（Back Edge Counter）。\n方法调用计数器用于统计方法的调用次数 回边计数器则用于统计循环体执行的循环次数 方法调用计数器 这个计数器就用于统计方法被调用的次数，它的默认阀值在 Client 模式下是 1500 次，在 Server 模式下是 10000 次。超过这个阈值，就会触发 JIT 编译。\n这个阀值可以通过虚拟机参数 -XX:CompileThreshold来人为设定。\n当一个方法被调用时，会先检查该方法是否存在被 JIT 编译过的版本，如果存在，则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本，则将此方法的调用计数器值加 1，然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阀值。如果已超过阈值，那么将会向即时编译器提交一个该方法的代码编译请求。\n热点衰减 如果不做任何设置，方法调用计数器统计的并不是方法被调用的绝对次数，而是一个相对的执行频率，即一段时间之内方法被调用的次数。当超过一定的时间限度，如果方法的调用次数仍然不足以让它提交给即时编译器编译，那这个方法的调用计数器就会被减少一半，这个过程称为方法调用计数器热度的衰减（Counter Decay），而这段时间就称为此方法统计的半衰周期（Counter Half Life Time）\n进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的，可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭热度衰减，让方法计数器统计方法调用的绝对次数，这样，只要系统运行时间足够长，绝大部分方法都会被编译成本地代码。\n另外，可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间，单位是秒。\n回边计数器 它的作用是统计一个方法中循环体代码执行的次数，在字节码中遇到控制流向后跳转的指令称为“回边”（Back Edge）。显然，建立回边计数器统计的目的就是为了触发 OSR 编译。\n9.5.5. HotSpotVM 可以设置程序执行方法 缺省情况下 HotSpot VM 是采用解释器与即时编译器并存的架构，当然开发人员可以根据具体的应用场景，通过命令显式地为 Java 虚拟机指定在运行时到底是完全采用解释器执行，还是完全采用即时编译器执行。如下所示：\n-Xint：完全采用解释器模式执行程序； -Xcomp：完全采用即时编译器模式执行程序。如果即时编译出现问题，解释器会介入执行 -Xmixed：采用解释器+即时编译器的混合模式共同执行程序。 9.5.6. HotSpotVM 中 JIT 分类 JIT 的编译器还分为了两种，分别是 C1 和 C2，在 HotSpot VM 中内嵌有两个 JIT 编译器，分别为 Client Compiler 和 Server Compiler，但大多数情况下我们简称为 C1 编译器 和 C2 编译器。开发人员可以通过如下命令显式指定 Java 虚拟机在运行时到底使用哪一种即时编译器，如下所示：\n-client：指定 Java 虚拟机运行在 Client 模式下，并使用 C1 编译器；C1 编译器会对字节码进行简单和可靠的优化，耗时短，以达到更快的编译速度。 -server：指定 Java 虚拟机运行在 server 模式下，并使用 C2 编译器。C2进行耗时较长的优化，以及激进优化，但优化的代码执行效率更高。 分层编译（Tiered Compilation）策略：程序解释执行（不开启性能监控）可以触发 C1 编译，将字节码编译成机器码，可以进行简单优化，也可以加上性能监控，C2 编译会根据性能监控信息进行激进优化。\n不过在 Java7 版本之后，一旦开发人员在程序中显式指定命令“-server\u0026quot;时，默认将会开启分层编译策略，由 C1 编译器和 C2 编译器相互协作共同来执行编译任务。\nC1 和 C2 编译器不同的优化策略 在不同的编译器上有不同的优化策略，C1 编译器上主要有方法内联、去虚拟化、冗余消除。\n方法内联：将引用的函数代码编译到引用点处，这样可以减少栈帧的生成，减少参数传递以及跳转过程 去虚拟化：对唯一的实现类进行内联 冗余消除：在运行期间把一些不会执行的代码折叠掉 C2 的优化主要是在全局层面，逃逸分析（前面讲过，并不成熟）是优化的基础。基于逃逸分析在 C2 上有如下几种优化：\n标量替换：用标量值代替聚合对象的属性值 栈上分配：对于未逃逸的对象分配对象在栈而不是堆 同步消除：清除同步操作，通常指 synchronized 总结 一般来讲，JIT 编译出来的机器码性能比解释器高。C2 编译器启动时长比 C1 慢，系统稳定执行以后，C2 编译器执行速度远快于 C1 编译器\n写到最后 1 自 JDK10 起，HotSpot 又加入了一个全新的及时编译器：Graal 编译器 编译效果短短几年时间就追评了 C2 编译器，未来可期 目前，带着实验状态标签，需要使用开关参数-XX:+UnlockExperimentalvMOptions -XX:+UseJVMCICompiler去激活才能使用 写到最后 2：AOT 编译器 jdk9 引入了 AOT 编译器（静态提前编译器，Ahead of Time Compiler）\nJava 9 引入了实验性 AOT 编译工具 jaotc。它借助了 Graal 编译器，将所输入的 Java 类文件转换为机器码，并存放至生成的动态共享库之中。\n所谓 AOT 编译，是与即时编译相对立的一个概念。我们知道，即时编译指的是在程序的运行过程中，将字节码转换为可在硬件上直接运行的机器码，并部署至托管环境中的过程。而AOT 编译指的则是，在程序运行之前，便将字节码转换为机器码的过程。\n最大的好处：Java 虚拟机加载已经预编译成二进制库，可以直接执行。不必等待及时编译器的预热，减少 Java 应用给人带来“第一次运行慢” 的不良体验\n缺点：\n破坏了 java “ 一次编译，到处运行”的理念，必须为每个不同的硬件，OS 编译对应的发行包 降低了 Java 链接过程的动态性，加载的代码在编译器就必须全部已知。 还需要继续优化中，最初只支持 Linux X64 java base ","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/95s11895/","title":"09-执行引擎"},{"content":" 11. 垃圾回收概述及算法 11.1. 垃圾回收概述 11.1.1. 什么是垃圾？ 垃圾收集，不是 Java 语言的伴生产物。早在 1960 年，第一门开始使用内存动态分配和垃圾收集技术的 Lisp 语言诞生。\n关于垃圾收集有三个经典问题：\n哪些内存需要回收？ 什么时候回收？ 如何回收？ 垃圾收集机制是 Java 的招牌能力，极大地提高了开发效率。如今，垃圾收集几乎成为现代语言的标配，即使经过如此长时间的发展，Java 的垃圾收集机制仍然在不断的演进中，不同大小的设备、不同特征的应用场景，对垃圾收集提出了新的挑战，这当然也是面试的热点。\n大厂面试题\n蚂蚁金服\n你知道哪几种垃圾回收器，各自的优缺点，重点讲一下 cms 和 G1？\nJVM GC 算法有哪些，目前的 JDK 版本采用什么回收算法？\nG1 回收器讲下回收过程 GC 是什么？为什么要有 GC？\nGC 的两种判定方法？CMS 收集器与 G1 收集器的特点\n百度\n说一下 GC 算法，分代回收说下\n垃圾收集策略和算法\n天猫\nJVM GC 原理，JVM 怎么回收内存\nCMS 特点，垃圾回收算法有哪些？各自的优缺点，他们共同的缺点是什么？\n滴滴\nJava 的垃圾回收器都有哪些，说下 g1 的应用场景，平时你是如何搭配使用垃圾回收器的 京东\n你知道哪几种垃圾收集器，各自的优缺点，重点讲下 cms 和 G1，\n包括原理，流程，优缺点。垃圾回收算法的实现原理\n阿里\n讲一讲垃圾回收算法。\n什么情况下触发垃圾回收？\n如何选择合适的垃圾收集算法？\nJVM 有哪三种垃圾回收器？\n字节跳动\n常见的垃圾回收器算法有哪些，各有什么优劣？ System.gc（）和 Runtime.gc（）会做什么事情？ Java GC 机制？GC Roots 有哪些？ Java 对象的回收方式，回收算法。 CMS 和 G1 了解么，CMS 解决什么问题，说一下回收的过程。 CMS 回收停顿了几次，为什么要停顿两次? 什么是垃圾？ An object is considered garbage when it can no longer be reached from any pointer in the running program\n垃圾是指在运行程序中没有任何指针指向的对象，这个对象就是需要被回收的垃圾。\n如果不及时对内存中的垃圾进行清理，那么，这些垃圾对象所占的内存空间会一直保留到应用程序的结束，被保留的空间无法被其它对象使用，甚至可能导致内存溢出。\n磁盘碎片整理的日子\n机械硬盘需要进行磁盘整理，同时还有坏道\n11.1.2. 为什么需要 GC 想要学习 GC，首先需要理解为什么需要 GC？\n对于高级语言来说，一个基本认知是如果不进行垃圾回收，内存迟早都会被消耗完，因为不断地分配内存空间而不进行回收，就好像不停地生产生活垃圾而从来不打扫一样。\n除了释放没用的对象，垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端，以便JVM 将整理出的内存分配给新的对象。\n随着应用程序所应付的业务越来越庞大、复杂，用户越来越多，没有 GC 就不能保证应用程序的正常进行。而经常造成 STW 的 GC 又跟不上实际的需求，所以才会不断地尝试对 GC 进行优化。\n11.1.3. 早期垃圾回收 在早期的 C/C++时代，垃圾回收基本上是手工进行的。开发人员可以使用 new 关键字进行内存申请，并使用 delete 关键字进行内存释放。比如以下代码：\n1 2 3 4 MibBridge *pBridge= new cmBaseGroupBridge(); //如果注册失败，使用Delete释放该对象所占内存区域 if (pBridge-\u0026gt;Register(kDestroy) != NO ERROR） delete pBridge; Copied! 这种方式可以灵活控制内存释放的时间，但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码的问题忘记被回收，那么就会产生内存泄漏，垃圾对象永远无法被清除，随着系统运行时间的不断增长，垃圾对象所耗内存可能持续上升，直到出现内存溢出并造成应用程序崩溃。\n在有了垃圾回收机制后，上述代码极有可能变成这样\n1 2 MibBridge *pBridge = new cmBaseGroupBridge(); pBridge-\u0026gt;Register(kDestroy); Copied! 现在，除了 Java 以外，C#、Python、Ruby 等语言都使用了自动垃圾回收的思想，也是未来发展趋势，可以说这种自动化的内存分配和来及回收方式已经成为了线代开发语言必备的标准。\n11.1.4. Java 垃圾回收机制 自动内存管理，无需开发人员手动参与内存的分配与回收，这样降低内存泄漏和内存溢出的风险\n没有垃圾回收器，java 也会和 cpp 一样，各种悬垂指针，野指针，泄露问题让你头疼不已。 自动内存管理机制，将程序员从繁重的内存管理中释放出来，可以更专心地专注于业务开发\noracle 官网关于垃圾回收的介绍 https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html 担忧 对于 Java 开发人员而言，自动内存管理就像是一个黑匣子，如果过度依赖于“自动”，那么这将会是一场灾难，最严重的就会弱化 Java 开发人员在程序出现内存溢出时定位问题和解决问题的能力。\n此时，了解 JVM 的自动内存分配和内存回收原理就显得非常重要，只有在真正了解 JVM 是如何管理内存后，我们才能够在遇见 outofMemoryError 时，快速地根据错误异常日志定位问题和解决问题。\n当需要排查各种内存溢出、内存泄漏问题时，当垃圾收集成为系统达到更高并发量的瓶颈时，我们就必须对这些“自动化”的技术实施必要的监控和调节。\nGC 主要关注的区域 GC 主要关注于 方法区 和堆中的垃圾收集\n垃圾收集器可以对年轻代回收，也可以对老年代回收，甚至是全栈和方法区的回收。其中，Java 堆是垃圾收集器的工作重点\n从次数上讲：\n频繁收集 Young 区 较少收集 Old 区 基本不收集 Perm 区（元空间） 11.2. 垃圾回收相关算法 对象存活判断\n在堆里存放着几乎所有的 Java 对象实例，在 GC 执行垃圾回收之前，首先需要区分出内存中哪些是存活对象，哪些是已经死亡的对象。只有被标记为己经死亡的对象，GC 才会在执行垃圾回收时，释放掉其所占用的内存空间，因此这个过程我们可以称为垃圾标记阶段。\n那么在 JVM 中究竟是如何标记一个死亡对象呢？简单来说，当一个对象已经不再被任何的存活对象继续引用时，就可以宣判为已经死亡。\n判断对象存活一般有两种方式：引用计数算法和可达性分析算法。\n11.2.1. 标记阶段：引用计数算法 方式一：引用计数算法 引用计数算法（Reference Counting）比较简单，对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。\n对于一个对象 A，只要有任何一个对象引用了 A，则 A 的引用计数器就加 1；当引用失效时，引用计数器就减 1。只要对象 A 的引用计数器的值为 0，即表示对象 A 不可能再被使用，可进行回收。\n优点：实现简单，垃圾对象便于辨识；判定效率高，回收没有延迟性。\n缺点：\n它需要单独的字段存储计数器，这样的做法增加了存储空间的开销。 每次赋值都需要更新计数器，伴随着加法和减法操作，这增加了时间开销。 引用计数器有一个严重的问题，即无法处理循环引用的情况。这是一条致命缺陷，导致在 Java 的垃圾回收器中没有使用这类算法。 循环引用 当 p 的指针断开的时候，内部的引用形成一个循环，这就是循环引用\n举例\n测试 Java 中是否采用的是引用计数算法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class RefCountGC { // 这个成员属性的唯一作用就是占用一点内存 private byte[] bigSize = new byte[5*1024*1024]; // 引用 Object reference = null; public static void main(String[] args) { RefCountGC obj1 = new RefCountGC(); RefCountGC obj2 = new RefCountGC(); obj1.reference = obj2; obj2.reference = obj1; obj1 = null; obj2 = null; // 显示的执行垃圾收集行为 // 这里发生GC，obj1和obj2是否被回收？ System.gc(); } } // 运行结果 PSYoungGen: 15490K-\u0026gt;808K(76288K)] 15490K-\u0026gt;816K(251392K) Copied! 上述进行了 GC 收集的行为，所以可以证明 JVM 中采用的不是引用计数器的算法\n小结 引用计数算法，是很多语言的资源回收选择，例如因人工智能而更加火热的 Python，它更是同时支持引用计数和垃圾收集机制。\n具体哪种最优是要看场景的，业界有大规模实践中仅保留引用计数机制，以提高吞吐量的尝试。\nJava 并没有选择引用计数，是因为其存在一个基本的难题，也就是很难处理循环引用关系。\nPython 如何解决循环引用？\n手动解除：很好理解，就是在合适的时机，解除引用关系。 使用弱引用 weakref，weakref 是 Python 提供的标准库，旨在解决循环引用。 11.2.2. 标记阶段：可达性分析算法 可达性分析算法（根搜索算法、追踪性垃圾收集） 相对于引用计数算法而言，可达性分析算法不仅同样具备实现简单和执行高效等特点，更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题，防止内存泄漏的发生。\n相较于引用计数算法，这里的可达性分析就是 Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集（Tracing Garbage Collection）\n所谓\u0026quot;GCRoots”根集合就是一组必须活跃的引用。\n基本思路 可达性分析算法是以根对象集合（GCRoots）为起始点，按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。 使用可达性分析算法后，内存中的存活对象都会被根对象集合直接或间接连接着，搜索所走过的路径称为引用链（Reference Chain） 如果目标对象没有任何引用链相连，则是不可达的，就意味着该对象己经死亡，可以标记为垃圾对象。 在可达性分析算法中，只有能够被根对象集合直接或者间接连接的对象才是存活对象。 在 Java 语言中，GC Roots 包括以下几类元素：\n虚拟机栈中引用的对象 比如：各个线程被调用的方法中使用到的参数、局部变量等。 本地方法栈内 JNI（通常说的本地方法）引用的对象 方法区中类静态属性引用的对象 比如：Java 类的引用类型静态变量 方法区中常量引用的对象 比如：字符串常量池（String Table）里的引用 所有被同步锁 synchronized 持有的对象 Java 虚拟机内部的引用。 基本数据类型对应的 Class 对象，一些常驻的异常对象（如：NullPointerException、OutOfMemoryError），系统类加载器。 反映 java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。 除了这些固定的 GC Roots 集合以外，根据用户所选用的垃圾收集器以及当前回收的内存区域不同，还可以有其他对象“临时性”地加入，共同构成完整 GC Roots 集合。比如：分代收集和局部回收（PartialGC）。\n如果只针对 Java 堆中的某一块区域进行垃圾回收（比如：典型的只针对新生代），必须考虑到内存区域是虚拟机自己的实现细节，更不是孤立封闭的，这个区域的对象完全有可能被其他区域的对象所引用，这时候就需要一并将关联的区域对象也加入 GCRoots 集合中去考虑，才能保证可达性分析的准确性。\n小技巧：由于 Root 采用栈方式存放变量和指针，所以如果一个指针，它保存了堆内存里面的对象，但是自己又不存放在堆内存里面，那它就是一个 Root。\n注意\n如果要使用可达性分析算法来判断内存是否可回收，那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。\n这点也是导致 GC 进行时必须“stop The World”的一个重要原因。\n即使是号称（几乎）不会发生停顿的 CMS 收集器中，枚举根节点时也是必须要停顿的。 11.2.3. 对象的 finalization 机制 Java 语言提供了对象终止（finalization）机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。\n当垃圾回收器发现没有引用指向一个对象，即：垃圾回收此对象之前，总会先调用这个对象的 finalize()方法。\nfinalize() 方法允许在子类中被重写，用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作，比如关闭文件、套接字和数据库连接等。\n永远不要主动调用某个对象的 finalize()方法 I 应该交给垃圾回收机制调用。理由包括下面三点：\n在 finalize()时可能会导致对象复活。 finalize()方法的执行时间是没有保障的，它完全由 GC 线程决定，极端情况下，若不发生 GC，则 finalize()方法将没有执行机会。 一个糟糕的 finalize()会严重影响 Gc 的性能。 从功能上来说，finalize()方法与 C++中的析构函数比较相似，但是 Java 采用的是基于垃圾回收器的自动内存管理机制，所以 finalize()方法在本质上不同于 C++中的析构函数。\n由于 finalize()方法的存在，虚拟机中的对象一般处于三种可能的状态。\n生存还是死亡？ 如果从所有的根节点都无法访问到某个对象，说明对象己经不再使用了。一般来说，此对象需要被回收。但事实上，也并非是“非死不可”的，这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己，如果这样，那么对它的回收就是不合理的，为此，定义虚拟机中的对象可能的三种状态。如下：\n可触及的：从根节点开始，可以到达这个对象。 可复活的：对象的所有引用都被释放，但是对象有可能在 finalize()中复活。 不可触及的：对象的 finalize()被调用，并且没有复活，那么就会进入不可触及状态。不可触及的对象不可能被复活，因为finalize()只会被调用一次。 以上 3 种状态中，是由于 inalize()方法的存在，进行的区分。只有在对象不可触及时才可以被回收。\n具体过程 判定一个对象 objA 是否可回收，至少要经历两次标记过程：\n如果对象 objA 到 GC Roots 没有引用链，则进行第一次标记。 进行筛选，判断此对象是否有必要执行 finalize()方法 如果对象 objA 没有重写 finalize()方法，或者 finalize()方法已经被虚拟机调用过，则虚拟机视为“没有必要执行”，objA 被判定为不可触及的。 如果对象 objA 重写了 finalize()方法，且还未执行过，那么 objA 会被插入到 F-Queue 队列中，由一个虚拟机自动创建的、低优先级的 Finalizer 线程触发其 finalize()方法执行。 finalize()方法是对象逃脱死亡的最后机会，稍后 GC 会对 F-Queue 队列中的对象进行第二次标记。如果 objA 在 finalize()方法中与引用链上的任何一个对象建立了联系，那么在第二次标记时，objA 会被移出“即将回收”集合。之后，对象会再次出现没有引用存在的情况。在这个情况下，finalize 方法不会被再次调用，对象会直接变成不可触及的状态，也就是说，一个对象的 finalize 方法只会被调用一次。 举例\n1 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 37 public class CanReliveObj { // 类变量，属于GC Roots的一部分 public static CanReliveObj canReliveObj; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println(\u0026#34;调用当前类重写的finalize()方法\u0026#34;); canReliveObj = this; } public static void main(String[] args) throws InterruptedException { canReliveObj = new CanReliveObj(); canReliveObj = null; System.gc(); System.out.println(\u0026#34;-----------------第一次gc操作------------\u0026#34;); // 因为Finalizer线程的优先级比较低，暂停2秒，以等待它 Thread.sleep(2000); if (canReliveObj == null) { System.out.println(\u0026#34;obj is dead\u0026#34;); } else { System.out.println(\u0026#34;obj is still alive\u0026#34;); } System.out.println(\u0026#34;-----------------第二次gc操作------------\u0026#34;); canReliveObj = null; System.gc(); // 下面代码和上面代码是一样的，但是 canReliveObj却自救失败了 Thread.sleep(2000); if (canReliveObj == null) { System.out.println(\u0026#34;obj is dead\u0026#34;); } else { System.out.println(\u0026#34;obj is still alive\u0026#34;); } } } Copied! 运行结果\n1 obj is dead Copied! 在第一次 GC 时，执行了 finalize 方法，但 finalize()方法只会被调用一次，所以第二次该对象被 GC 标记并清除了。\n11.2.4. MAT 与 JProfiler 的 GC Roots 溯源 MAT 是什么？ MAT 是 Memory Analyzer 的简称，它是一款功能强大的 Java 堆内存分析器。用于查找内存泄漏以及查看内存消耗情况。\nMAT 是基于 Eclipse 开发的，是一款免费的性能分析工具。\n大家可以在 http://www.eclipse.org/mat/ 下载并使用 MAT\n获取 dump 文件 方式一：命令行使用 jmap 方式二：使用 JVisualVM 导出 捕获的 heap dump 文件是一个临时文件，关闭 JVisualVM 后自动删除，若要保留，需要将其另存为文件。\n可通过以下方法捕获 heap dump：\n在左侧“Application\u0026quot;（应用程序）子窗口中右击相应的应用程序，选择 Heap Dump（堆 Dump）。\n在 Monitor（监视）子标签页中点击 Heap Dump（堆 Dump）按钮。\n本地应用程序的 Heap dumps 作为应用程序标签页的一个子标签页打开。同时，heap dump 在左侧的 Application（应用程序）栏中对应一个含有时间戳的节点。\n右击这个节点选择 save as（另存为）即可将 heap dump 保存到本地。\n方式三：使用 MAT 打开 Dump 文件 JProfiler 的 GC Roots 溯源 我们在实际的开发中，一般不会查找全部的 GC Roots，可能只是查找某个对象的整个链路，或者称为 GC Roots 溯源，这个时候，我们就可以使用 JProfiler\n11.2.5. 清除阶段：标记-清除算法 当成功区分出内存中存活对象和死亡对象后，GC 接下来的任务就是执行垃圾回收，释放掉无用对象所占用的内存空间，以便有足够的可用内存空间为新对象分配内存。\n目前在 JVM 中比较常见的三种垃圾收集算法是标记一清除算法（Mark-Sweep）、复制算法（copying）、标记-压缩算法（Mark-Compact）\n标记-清除算法（Mark-Sweep） 标记-清除算法（Mark-Sweep）是一种非常基础和常见的垃圾收集算法，该算法被 J.McCarthy 等人在 1960 年提出并并应用于 Lisp 语言。\n执行过程 当堆中的有效内存空间（available memory）被耗尽的时候，就会停止整个程序（也被称为 stop the world），然后进行两项工作，第一项则是标记，第二项则是清除\n标记：Collector 从引用根节点开始遍历，标记所有被引用的对象。一般是在对象的 Header(对象头) 中记录为可达对象。\n清除：Collector 对堆内存从头到尾进行线性的遍历，如果发现某个对象在其 Header 中没有标记为可达对象，则将其回收\n缺点 标记清除算法的效率不算高 在进行 GC 的时候，需要停止整个应用程序，用户体验较差 这种方式清理出来的空闲内存是不连续的，产生内碎片，需要维护一个空闲列表 何为清除？ 这里所谓的清除并不是真的置空，而是把需要清除的对象地址保存在空闲的地址列表里。下次有新对象需要加载时，判断垃圾的位置空间是否够，如果够，就存放覆盖原有的地址。\n11.2.6. 清除阶段：复制算法 复制（Copying）算法 为了解决标记-清除算法在垃圾收集效率方面的缺陷，M.L.Minsky 于 1963 年发表了著名的论文，“使用双存储区的 Lisp 语言垃圾收集器 CA LISP Garbage Collector Algorithm Using Serial Secondary Storage）”。M.L.Minsky 在该论文中描述的算法被人们称为复制（Copying）算法，它也被 M.L.Minsky 本人成功地引入到了 Lisp 语言的一个实现版本中。\n核心思想 将活着的内存空间分为两块，每次只使用其中一块，在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中，之后清除正在使用的内存块中的所有对象，交换两个内存的角色，最后完成垃圾回收\n优点 没有标记和清除过程，实现简单，运行高效 复制过去以后保证空间的连续性，不会出现“碎片”问题。 缺点 此算法的缺点也是很明显的，就是需要两倍的内存空间。 对于 G1 这种分拆成为大量 region 的 GC，复制而不是移动，意味着 GC 需要维护 region 之间对象引用关系，不管是内存占用或者时间开销也不小；句柄池和hotspot中用的直接指针的优缺点 特别的 如果系统中的存活的对象很多，复制算法就不太理想。复制算法需要复制的存活对象数量并不会太大，或者说非常低才行，所以基本用在新生代这种朝生夕死的垃圾清理，并不会用在老年代中\n应用场景 在新生代，对常规应用的垃圾回收，一次通常可以回收 70% - 99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。\n11.2.7. 清除阶段：标记-压缩（整理）算法 标记-压缩（或标记-整理、Mark-Compact）算法 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生，但是在老年代，更常见的情况是大部分对象都是存活对象。如果依然使用复制算法，由于存活对象较多，复制的成本也将很高。因此，基于老年代垃圾回收的特性，需要使用其他的算法。\n标记一清除算法的确可以应用在老年代中，但是该算法不仅执行效率低下，而且在执行完内存回收后还会产生内存碎片，所以 JVM 的设计者需要在此基础之上进行改进。标记-压缩（Mark-Compact）算法由此诞生。\n1970 年前后，G.L.Steele、C.J.Chene 和 D.s.Wise 等研究者发布标记-压缩算法。在许多现代的垃圾收集器中，人们都使用了标记-压缩算法或其改进版本。\n执行过程 第一阶段和标记清除算法一样，从根节点开始标记所有被引用对象\n第二阶段将所有的存活对象压缩到内存的一端，按顺序排放。\n之后，清理边界外所有的空间。\n标记-压缩算法的最终效果等同于标记-清除算法执行完成后，再进行一次内存碎片整理，因此，也可以把它称为标记-清除-压缩（Mark-Sweep-Compact）算法。\n二者的本质差异在于标记-清除算法是一种非移动式的回收算法，标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策。可以看到，标记的存活对象将会被整理，按照内存地址依次排列，而未被标记的内存会被清理掉。如此一来，当我们需要给新对象分配内存时，JVM 只需要持有一个内存的起始地址即可，这比维护一个空闲列表显然少了许多开销。\n指针碰撞（Bump the Pointer） 如果内存空间以规整和有序的方式分布，即已用和未用的内存都各自一边，彼此之间维系着一个记录下一次分配起始点的标记指针，当为新对象分配内存时，只需要通过修改指针的偏移量将新对象分配在第一个空闲内存位置上，这种分配方式就叫做指针碰撞（Bump tHe Pointer）。\n优点 消除了标记-清除算法当中，内存区域分散的缺点，我们需要给新对象分配内存时，JVM 只需要持有一个内存的起始地址即可。 消除了复制算法当中，内存减半的高额代价。 缺点 从效率上来说，标记-整理算法要低于复制算法。 移动对象的同时，如果对象被其他对象引用，则还需要调整引用的地址 移动过程中，需要全程暂停用户应用程序。即：STW 原理 Lisp2 算法 Lisp2算法包括两个阶段\n标记 压缩 标记阶段：选取gc根对象。从这些对象开始向下遍历其子对象，最终可能会形成一个又向有环图。在此图中的对象就是活动对象，也就是将要压缩的对象。 整理阶段：将对象向着一端移动，移动后对象的相对顺序不变，但是对象紧临。 比如：在垃圾回收前：有ABCDEF五个对象，可以看出A和E是非活动对象。其中根引用了B和D，B引用了C，D引用了F\n标记整理-初始状态 标记结束后： 标记整理-标记后 在垃圾回收后： 标记整理-整理后 标记阶段算法 选取gc roots，然后以深度优先或者广度优先的方式遍历。\n1 2 3 4 5 6 7 8 9 10 11 mark_phase(){ for(r : $roots) mark(*r) } mark(obj){ if(obj.mark == FALSE) obj.mark = TRUE for(child : children(obj)) mark(*child) } Copied! $roots为gc roots，mark是对象头的标记位\n标记的时候可以在对象头上标记，但是这样与写时复制不兼容。当复制了一个对象，还不需要写的时候开始gc，那么就必须进行对象复制操作。为了解决这个问题可以使用位图标记，不再操作对象头，这样与写时复制兼容，并且标记与清除效率更高。\n这种朴素的标记法，在标记阶段必须全局暂停，为了可以并发，可以使用三色标记法。\n压缩阶段具体实现 lisp2中的对象中的对象：在对象头中有一个forwarding指针，指向将来该对象的位置。\nlisp2中的对象 步骤：\n设置forwarding指针 更新指针 移动对象 1 2 3 4 5 compaction_phase(){ set_forwarding_ptr() adjust_ptr() move_obj() } Copied! 1.设置forwarding指针\n在步骤1中，会搜索整个堆，给活动对象设置forwarding的位置。\n1 2 3 4 5 6 7 8 set_forwarding_ptr(){ scan = new_address = $heap_start while(scan \u0026lt; $heap_end) if(scan.mark == TRUE) scan.forwarding = new_address new_address += scan.size scan += scan.size } Copied! scan是搜索堆中对象的指针，new_address是指向目标地点的指针。当scan扫描到活动对象，就会将new_address赋值给该对象的forwarding，并且移动new_address的位置。直到结束。\nlisp2设置forwarding指针 这样做的原因是整理前的堆和整理后的堆是同一个空间，不像复制算法中的from和to可以切换。因此有必要设置forwarding的指针。\n2.更新指针\n第2步需要让每一个对象知道移动后子对象将来的位置。因此需要修改指针.\n1 2 3 4 5 6 7 8 9 10 11 12 13 adjust_ptr(){ //更改根的指针 for(r : $roots) *r = (*r).forwarding scan = $heap_start while(scan \u0026lt; $heap_end) //如果标记过，就将子对象设置为子对象将来的位置（forwarding） if(scan.mark == TRUE) for(child : children(scan)) *child = (*child).forwarding scan += scan.size } Copied! 这里的scan与上面的scan作用相同。\n更改指针 3.移动对象\n第3步，移动对象，过程很简单，每个对象都知道要去的地址，只需要顺序移动便可，然后清除之前的标记。注意要顺序移动，不然会覆盖掉活动对象。\n1 2 3 4 5 6 7 8 9 10 11 move_obj(){ scan = $free = $heap_start while(scan \u0026lt; $heap_end) if(scan.mark == TRUE) new_address = scan.forwarding copy_data(new_address, scan, scan.size) new_address.forwarding = NULL new_address.mark = FALSE $free += new_address.size scan += scan.size } Copied! lisp2整理后 时间复杂度\n标记阶段：遍历所有的存活对象，与活动对象数成正比 设置forwarding指针阶段：扫描整个堆 更改子对象指针阶段：扫描整个堆 移动阶段：扫描整个堆 优点\n相比于标记清除与引用计数：没有内存碎片 相比于复制算法：有效利用堆 缺点\n一次遍历活动对象+三次扫描整个堆，吞吐量较小。\nhttps://gentlezuo.github.io/2019/08/10/gc- 标记整理算法的两种实现/#双指针算法\n11.2.8. 小结 Mark-Sweep标记清除 Mark-Compact标记整理 Copying复制算法 速率 中等 最慢 最快 空间开销 少（但会堆积碎片） 少（不堆积碎片） 通常需要活对象的 2 倍空间（不堆积碎片） 移动对象 否 是 是 对象申请内存 虚拟机维护空闲空间列表 指针碰撞 指针碰撞 STW 是 是 是 效率上来说，复制算法是当之无愧的老大，但是却浪费了太多内存。\n而为了尽量兼顾上面提到的三个指标，标记-整理算法相对来说更平滑一些，但是效率上不尽如人意，它比复制算法多了一个标记的阶段，比标记-清除多了一个整理内存的阶段\n难道就没有一种最优算法吗？\n回答：无，没有最好的算法，只有最合适的算法。\n11.2.9. 分代收集算法 前面所有这些算法中，并没有一种算法可以完全替代其他算法，它们都具有自己独特的优势和特点。分代收集算法应运而生。\n分代收集算法，是基于这样一个事实：不同的对象的生命周期是不一样的。因此，不同生命周期的对象可以采取不同的收集方式，以便提高回收效率。一般是把 Java 堆分为新生代和老年代，这样就可以根据各个年代的特点使用不同的回收算法，以提高垃圾回收的效率。\n在 Java 程序运行的过程中，会产生大量的对象，其中有些对象是与业务信息相关，比如Http 请求中的 Session 对象、线程、Socket 连接，这类对象跟业务直接挂钩，因此生命周期比较长。但是还有一些对象，主要是程序运行过程中生成的临时变量，这些对象生命周期会比较短，比如：String 对象，由于其不变类的特性，系统会产生大量的这些对象，有些对象甚至只用一次即可回收。\n目前几乎所有的 GC 都采用分代手机算法执行垃圾回收的。\n在 HotSpot 中，基于分代的概念，GC 所使用的内存回收算法必须结合年轻代和老年代各自的特点。\n年轻代（Young Gen） 年轻代特点：区域相对老年代较小，对象生命周期短、存活率低，回收频繁。\n这种情况复制算法的回收整理，速度是最快的。复制算法的效率只和当前存活对象大小有关，因此很适用于年轻代的回收。而复制算法内存利用率不高的问题，通过 hotspot 中的两个 survivor 的设计得到缓解。\n老年代（Tenured Gen） 老年代特点：区域较大，对象生命周期长、存活率高，回收不及年轻代频繁。\n这种情况存在大量存活率高的对象，复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。\nMark 阶段的开销与存活对象的数量成正比。 Sweep 阶段的开销与所管理区域的大小成正相关。 Compact 阶段的开销与存活对象的数据成正比。 以 HotSpot 中的 CMS 回收器为例，CMS 是基于 Mark-Sweep 实现的，对于对象的回收效率很高。而对于碎片问题，CMS 采用基于 Mark-Compact 算法的 Serial Old 回收器作为补偿措施：当内存回收不佳（碎片导致的 Concurrent Mode Failure 时），将采用 Serial Old 执行 Full GC 以达到对老年代内存的整理。\n分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代\n11.2.X. 增量收集算法、分区算法 增量收集算法 上述现有的算法，在垃圾回收过程中，应用软件将处于一种 Stop the World 的状态。在 Stop the World 状态下，应用程序所有的线程都会挂起，暂停一切正常的工作，等待垃圾回收的完成。如果垃圾回收时间过长，应用程序会被挂起很久，将严重影响用户体验或者系统的稳定性。为了解决这个问题，即对实时垃圾收集算法的研究直接导致了增量收集（Incremental Collecting）算法的诞生。\n基本思想 如果一次性将所有的垃圾进行处理，需要造成系统长时间的停顿，那么就可以让垃圾收集线程和应用程序线程交替执行。每次，垃圾收集线程只收集一小片区域的内存空间，接着切换到应用程序线程。依次反复，直到垃圾收集完成。\n总的来说，增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理，允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作\n缺点 使用这种方式，由于在垃圾回收过程中，间断性地还执行了应用程序代码，所以能减少系统的停顿时间。但是，因为线程切换和上下文转换的消耗，会使得垃圾回收的总体成本上升，造成系统吞吐量的下降。\n分区算法 一般来说，在相同条件下，堆空间越大，一次 Gc 时所需要的时间就越长，有关 GC 产生的停顿也越长。为了更好地控制 GC 产生的停顿时间，将一块大的内存区域分割成多个小块，根据目标的停顿时间，每次合理地回收若干个小区间，而不是整个堆空间，从而减少一次 GC 所产生的停顿。\n分代算法将按照对象的生命周期长短划分成两个部分，分区算法将整个堆空间划分成连续的不同小区间。\n每一个小区间都独立使用，独立回收。这种算法的好处是可以控制一次回收多少个小区间。\n写到最后 注意，这些只是基本的算法思路，实际 GC 实现过程要复杂的多，目前还在发展中的前沿 GC 都是复合算法，并且并行和并发兼备。\n","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/71491671/","title":"11-垃圾回收概述及算法"},{"content":" 12. 垃圾回收相关概念 12.1. System.gc()的理解 在默认情况下，通过 system.gc()或者 Runtime.getRuntime().gc() 的调用，会显式触发 Full GC，同时对老年代和新生代和方法区进行回收，尝试释放被丢弃对象占用的内存。\n然而 System.gc() 调用附带一个免责声明，无法保证对垃圾收集器的调用。(不能确保立即生效)\nJVM 实现者可以通过 System.gc() 调用来决定 JVM 的 GC 行为。而一般情况下，垃圾回收应该是自动进行的，无须手动触发，否则就太过于麻烦了。在一些特殊情况下，如我们正在编写一个性能基准，我们可以在运行之间调用 System.gc()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class SystemGCTest { public static void main(String[] args) { new SystemGCTest(); System.gc();// 提醒JVM的垃圾回收器执行gc，但是不确定是否马上执行gc // 与Runtime.getRuntime().gc();的作用一样 System.runFinalization();//强制执行失去引用的对象的finalize()方法 } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println(\u0026#34;SystemGCTest 重写了finalize()\u0026#34;); } } Copied! Full GC会让新生区存活的对象放在老年代 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 37 public class LocalVarGC { public void localvarGC1() { byte[] buffer = new byte[10 * 1024 * 1024];//10MB System.gc();//不会回收 } public void localvarGC2() { byte[] buffer = new byte[10 * 1024 * 1024]; buffer = null; System.gc();//会回收 } public void localvarGC3() { { byte[] buffer = new byte[10 * 1024 * 1024]; } System.gc();//不会回收，局部变量表中有buffer } public void localvarGC4() { { byte[] buffer = new byte[10 * 1024 * 1024]; } int value = 10; System.gc();//会回收，局部变量表中buffer的位置被value替换，导致对象没有被根结点引用 } public void localvarGC5() { localvarGC1(); System.gc();//会回收，localvarGC1方法出栈了，没有引用指向对象 } public static void main(String[] args) { LocalVarGC local = new LocalVarGC(); local.localvarGC1(); } } Copied! 12.2. 内存溢出与内存泄露 内存溢出（OOM） 内存溢出相对于内存泄漏来说，尽管更容易被理解，但是同样的，内存溢出也是引发程序崩溃的罪魁祸首之一。\n由于 GC 一直在发展，所有一般情况下，除非应用程序占用的内存增长速度非常快，造成垃圾回收已经跟不上内存消耗的速度，否则不太容易出现 ooM 的情况。\n大多数情况下，GC 会进行各种年龄段的垃圾回收，实在不行了就放大招，来一次独占式的 Full GC 操作，这时候会回收大量的内存，供应用程序继续使用。\njavadoc 中对 OutOfMemoryError 的解释是，没有空闲内存，并且垃圾收集器也无法提供更多内存。\n首先说没有空闲内存的情况：说明 Java 虚拟机的堆内存不够。原因有二：\nJava 虚拟机的堆内存设置不够。\n比如：可能存在内存泄漏问题；也很有可能就是堆的大小不合理，比如我们要处理比较可观的数据量，但是没有显式指定 JVM 堆大小或者指定数值偏小。我们可以通过参数-Xms 、-Xmx来调整。\n代码中创建了大量大对象，并且长时间不能被垃圾收集器收集（存在被引用）\n对于老版本的 Oracle JDK，因为永久代的大小是有限的，并且 JVM 对永久代垃圾回收（如，常量池回收、卸载不再需要的类型）非常不积极，所以当我们不断添加新类型的时候，永久代出现 OutOfMemoryError 也非常多见，尤其是在运行时存在大量动态类型生成的场合；类似 intern 字符串缓存占用太多空间，也会导致 OOM 问题。对应的异常信息，会标记出来和永久代相关：“java.lang.OutOfMemoryError: PermGen space\u0026quot;。\n随着元数据区的引入，方法区内存已经不再那么窘迫，所以相应的 ooM 有所改观，出现 OOM，异常信息则变成了：“java.lang.OutofMemoryError:Metaspace\u0026quot;。直接内存不足，也会导致 OOM。\n这里面隐含着一层意思是，在抛出 OutOfMemoryError 之前，通常垃圾收集器会被触发，尽其所能去清理出空间。\n例如：在引用机制分析中，涉及到 JVM 会去尝试回收软引用指向的对象等。 在java.nio.BIts.reserveMemory()方法中，我们能清楚的看到，System.gc()会被调用，以清理空间。 当然，也不是在任何情况下垃圾收集器都会被触发的\n比如，我们去分配一个超大对象，类似一个超大数组超过堆的最大值，JVM 可以判断出垃圾收集并不能解决这个问题，所以直接抛出 OutOfMemoryError。 内存泄漏（Memory Leak） 也称作“存储渗漏”。严格来说，只有对象不会再被程序用到了，但是 GC 又不能回收他们的情况，才叫内存泄漏。\n但实际情况很多时候一些不太好的实践（或疏忽）会导致对象的生命周期变得很长甚至导致 00M，也可以叫做宽泛意义上的“内存泄漏”。\n尽管内存泄漏并不会立刻引起程序崩溃，但是一旦发生内存泄漏，程序中的可用内存就会被逐步蚕食，直至耗尽所有内存，最终出现 OutOfMemory 异常，导致程序崩溃。\n注意，这里的存储空间并不是指物理内存，而是指虚拟内存大小，这个虚拟内存大小取决于磁盘交换区设定的大小。\n举例\n单例模式\n单例的生命周期和应用程序是一样长的，所以单例程序中，如果持有对外部对象的引用的话，那么这个外部对象是不能被回收的，则会导致内存泄漏的产生。\n一些提供 close 的资源未关闭导致内存泄漏\n数据库连接（dataSourse.getConnection() ），网络连接（socket）和 io 连接必须手动 close，否则是不能被回收的。\n12.3. Stop The World Stop-the-World，简称 STW，指的是 GC 事件发生过程中，会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停，没有任何响应，有点像卡死的感觉，这个停顿称为 STW。\n可达性分析算法中枚举根节点（GC Roots）会导致所有 Java 执行线程停顿。\n分析工作必须在一个能确保一致性的快照中进行 一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上 如果出现分析过程中对象引用关系还在不断变化，则分析结果的准确性无法保证 被 STW 中断的应用程序线程会在完成 GC 之后恢复，频繁中断会让用户感觉像是网速不快造成电影卡带一样，所以我们需要减少 STW 的发生。\nSTW 事件和采用哪款 GC 无关，所有的 GC 都有这个事件。\n哪怕是 G1 也不能完全避免 Stop-the-World 情况发生，只能说垃圾回收器越来越优秀，回收效率越来越高，尽可能地缩短了暂停时间。\nSTW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下，把用户正常的工作线程全部停掉。\n开发中不要用 System.gc() 会导致 Stop-the-World 的发生。\n1 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 37 38 39 40 41 42 43 44 45 46 47 public class StopTheWorldDemo { public static class WorkThread extends Thread { List\u0026lt;byte[]\u0026gt; list = new ArrayList\u0026lt;byte[]\u0026gt;(); public void run() { try { while (true) { for(int i = 0;i \u0026lt; 1000;i++){ byte[] buffer = new byte[1024]; list.add(buffer); } if(list.size() \u0026gt; 10000){ list.clear(); System.gc();//会触发full gc，进而会出现STW事件 } } } catch (Exception ex) { ex.printStackTrace(); } } } public static class PrintThread extends Thread { public final long startTime = System.currentTimeMillis(); public void run() { try { while (true) { // 每秒打印时间信息 long t = System.currentTimeMillis() - startTime; System.out.println(t / 1000 + \u0026#34;.\u0026#34; + t % 1000); Thread.sleep(1000); } } catch (Exception ex) { ex.printStackTrace(); } } } public static void main(String[] args) { WorkThread w = new WorkThread(); PrintThread p = new PrintThread(); w.start(); p.start(); } } Copied! 12.4. 垃圾回收的并行与并发 并发（Concurrent） 在操作系统中，是指一个时间段中有几个程序都处于已启动运行到运行完毕之间，且这几个程序都是在同一个处理器上运行。\n并发不是真正意义上的“同时进行”，只是 CPU 把一个时间段划分成几个时间片段（时间区间），然后在这几个时间区间之间来回切换，由于 CPU 处理的速度非常快，只要时间间隔处理得当，即可让用户感觉是多个应用程序同时在进行。\n并行（Parallel） 当系统有一个以上 CPU 时，当一个 CPU 执行一个进程时，另一个 CPU 可以执行另一个进程，两个进程互不抢占 CPU 资源，可以同时进行，我们称之为并行（Parallel）。\n其实决定并行的因素不是 CPU 的数量，而是 CPU 的核心数量，比如一个 CPU 多个核也可以并行。\n适合科学计算，后台处理等弱交互场景\n并发 vs 并行 并发，指的是多个事情，在同一时间段内同时发生了。\n并行，指的是多个事情，在同一时间点上同时发生了。\n并发的多个任务之间是互相抢占资源的。\n并行的多个任务之间是不互相抢占资源的。\n只有在多 CPU 或者一个 CPU 多核的情况中，才会发生并行。\n否则，看似同时发生的事情，其实都是并发执行的。\n垃圾回收的并发与并行 并发和并行，在谈论垃圾收集器的上下文语境中，它们可以解释如下：\n并行（Parallel） 指多条垃圾收集线程并行工作，但此时用户线程仍处于等待状态。如 ParNew、Parallel Scavenge、Parallel Old；\n串行（Serial） 相较于并行的概念，单线程执行。如果内存不够，则程序暂停，启动 JM 垃圾回收器进行垃圾回收。回收完，再启动程序的线程。\n并发（Concurrent） 指用户线程与垃圾收集线程同时执行（但不一定是并行的，可能会交替执行），垃圾回收线程在执行时不会停顿用户程序的运行。用户程序在继续运行，而垃圾收集程序线程运行于另一个 CPU 上；如：CMS、G1\n12.5. 安全点与安全区域 安全点 程序执行时并非在所有地方都能停顿下来开始 GC，只有在特定的位置才能停顿下来开始 GC，这些位置称为“安全点（Safepoint）”。\nSafe Point 的选择很重要，如果太少可能导致 GC 等待的时间太长，如果太频繁可能导致运行时的性能问题。大部分指令的执行时间都非常短暂，通常会根据“是否具有让程序长时间执行的特征”为标准。比如：选择一些执行时间较长的指令作为 Safe Point，如方法调用、循环跳转和异常跳转等。\n如何在 GC 发生时，检查所有线程都跑到最近的安全点停顿下来呢？\n抢先式中断：（目前没有虚拟机采用了） 首先中断所有线程。如果还有线程不在安全点，就恢复线程，让线程跑到安全点。\\ 主动式中断 设置一个中断标志，各个线程运行到 Safe Point 的时候主动轮询这个标志，如果中断标志为真，则将自己进行中断挂起。（有轮询的机制）\n安全区域（Safe Resion） Safepoint 机制保证了程序执行时，在不太长的时间内就会遇到可进入 GC 的 Safepoint。但是，程序“不执行”的时候呢？例如线程处于 Sleep 状态或 Blocked 状态，这时候线程无法响应 JVM 的中断请求，“走”到安全点去中断挂起，JVM 也不太可能等待线程被唤醒。对于这种情况，就需要安全区域（Safe Region）来解决。\n安全区域是指在一段代码片段中，对象的引用关系不会发生变化，在这个区域中的任何位置开始 Gc 都是安全的。我们也可以把 Safe Region 看做是被扩展了的 Safepoint。\n实际执行时： 当线程运行到 Safe Region 的代码时，首先标识已经进入了 Safe Relgion，如果这段时间内发生 GC，JVM 会忽略标识为 Safe Region 状态的线程 当线程即将离开 Safe Region 时，会检查 JVM 是否已经完成 GC，如果完成了，则继续运行，否则线程必须等待直到收到可以安全离开 Safe Region 的信号为止； 12.6. 再谈引用：强引用 我们希望能描述这样一类对象：当内存空间还足够时，则能保留在内存中；如果内存空间在进行垃圾收集后还是很紧张，则可以抛弃这些对象。\n【既偏门又非常高频的面试题】强引用、软引用、弱引用、虚引用有什么区别？具体使用场景是什么？\n在 JDK1.2 版之后，Java 对引用的概念进行了扩充，将引用分为：强引用（Strong Reference）、软引用（Soft Reference）、弱引用（Weak Reference）、虚引用（Phantom Reference）这 4 种引用强度依次逐渐减弱。\n除强引用外，其他 3 种引用均可以在 java.lang.ref 包中找到它们的身影。如下图，显示了这 3 种引用类型对应的类，开发人员可以在应用程序中直接使用它们。\n. Reference 子类中只有终结器引用是包内可见的，其他 3 种引用类型均为 public，可以在应用程序中直接使用\n强引用（StrongReference）：最传统的“引用”的定义，是指在程序代码之中普遍存在的引用赋值，即类似“Object obj = new Object()”这种引用关系。无论任何情况下，只要强引用关系还存在，垃圾收集器就永远不会回收掉被引用的对象。 软引用（SoftReference）：在系统将要发生内存溢出之前，将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存，才会抛出内存流出异常。 弱引用（WeakReference）：被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时，无论内存空间是否足够，都会回收掉被弱引用关联的对象。 虚引用（PhantomReference）：一个对象是否有虚引用的存在，完全不会对其生存时间构成影响，也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 强引用（Strong Reference）——不回收 在 Java 程序中，最常见的引用类型是强引用（普通系统 99%以上都是强引用），也就是我们最常见的普通对象引用，也是默认的引用类型。\n当在 Java 语言中使用 new 操作符创建一个新的对象，并将其赋值给一个变量的时候，这个变量就成为指向该对象的一个强引用。\n强引用的对象是可触及的，垃圾收集器就永远不会回收掉被引用的对象。\n对于一个普通的对象，如果没有其他的引用关系，只要超过了引用的作用域或者显式地将相应（强）引用赋值为 nu11，就是可以当做垃圾被收集了，当然具体回收时机还是要看垃圾收集策略。\n相对的，软引用、弱引用和虚引用的对象是软可触及、弱可触及和虚可触及的，在一定条件下，都是可以被回收的。所以，强引用是造成 Java 内存泄漏的主要原因之一。\n强引用例子\n1 StringBuffer str = new StringBuffer(\u0026#34;hello mogublog\u0026#34;); Copied! 局部变量 str 指向 StringBuffer 实例所在堆空间，通过 str 可以操作该实例，那么 str 就是 StringBuffer 实例的强引用\n对应内存结构\n此时，如果再运行一个赋值语句\n1 StringBuffer str1 = str; Copied! 对应的内存结构\n本例中的两个引用，都是强引用，强引用具备以下特点：\n强引用可以直接访问目标对象。 强引用所指向的对象在任何时候都不会被系统回收，虚拟机宁愿抛出 OOM 异常，也不会回收强引用所指向对象。 强引用可能导致内存泄漏。 12.8. 再谈引用： 软引用 软引用（Soft Reference）——内存不足即回收 软引用是用来描述一些还有用，但非必需的对象。只被软引用关联着的对象，在系统将要发生内存溢出异常前，会把这些对象列进回收范围之中进行第二次回收，如果这次回收还没有足够的内存，才会抛出内存溢出异常。\n软引用通常用来实现内存敏感的缓存。比如：高速缓存就有用到软引用。如果还有空闲内存，就可以暂时保留缓存，当内存不足时清理掉，这样就保证了使用缓存的同时，不会耗尽内存。\n垃圾回收器在某个时刻决定回收软可达的对象的时候，会清理软引用，并可选地把引用存放到一个引用队列（Reference Queue）。\n类似弱引用，只不过 Java 虚拟机会尽量让软引用的存活时间长一些，迫不得已才清理。\n在 JDK1.2 版之后提供了 java.lang.ref.SoftReference 类来实现软引用\n1 2 3 4 5 6 Object obj = new Object(); // 声明强引用 SoftReference\u0026lt;Object\u0026gt; sf = new SoftReference\u0026lt;\u0026gt;(obj); obj = null; //销毁强引用 //或者 SoftReference\u0026lt;Object\u0026gt; sf = new SoftReference\u0026lt;\u0026gt;(new Object()); Copied! 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 /** * 软引用的测试：内存不足即回收 * * @author shkstart shkstart@126.com * @create 2020 16:06 */ public class SoftReferenceTest { public static class User { public User(int id, String name) { this.id = id; this.name = name; } public int id; public String name; @Override public String toString() { return \u0026#34;[id=\u0026#34; + id + \u0026#34;, name=\u0026#34; + name + \u0026#34;] \u0026#34;; } } public static void main(String[] args) { //创建对象，建立软引用 // SoftReference\u0026lt;User\u0026gt; userSoftRef = new SoftReference\u0026lt;User\u0026gt;(new User(1, \u0026#34;songhk\u0026#34;)); //上面的一行代码，等价于如下的三行代码 User u1 = new User(1,\u0026#34;songhk\u0026#34;); SoftReference\u0026lt;User\u0026gt; userSoftRef = new SoftReference\u0026lt;User\u0026gt;(u1); u1 = null;//取消强引用 //从软引用中重新获得强引用对象 System.out.println(userSoftRef.get()); System.gc(); System.out.println(\u0026#34;After GC:\u0026#34;); // //垃圾回收之后获得软引用中的对象 System.out.println(userSoftRef.get());//由于堆空间内存足够，所有不会回收软引用的可达对象。 // try { //让系统认为内存资源紧张、不够 // byte[] b = new byte[1024 * 1024 * 7]; byte[] b = new byte[1024 * 7168 - 635 * 1024]; } catch (Throwable e) { e.printStackTrace(); } finally { //再次从软引用中获取数据 System.out.println(userSoftRef.get());//在报OOM之前，垃圾回收器会回收软引用的可达对象。 } } } Copied! 12.9. 再谈引用：弱引用 弱引用（Weak Reference）——发现即回收 弱引用也是用来描述那些非必需对象，只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统 GC 时，只要发现弱引用，不管系统堆空间使用是否充足，都会回收掉只被弱引用关联的对象。\n但是，由于垃圾回收器的线程通常优先级很低，因此，并不一定能很快地发现持有弱引用的对象。在这种情况下，弱引用对象可以存在较长的时间。\n弱引用和软引用一样，在构造弱引用时，也可以指定一个引用队列，当弱引用对象被回收时，就会加入指定的引用队列，通过这个队列可以跟踪对象的回收情况。\n软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做，当系统内存不足时，这些缓存数据会被回收，不会导致内存溢出。而当内存资源充足时，这些缓存数据又可以存在相当长的时间，从而起到加速系统的作用。\n在 JDK1.2 版之后提供了 WeakReference 类来实现弱引用\n1 2 3 4 5 Object obj = new Object(); // 声明强引用 WeakReference\u0026lt;Object\u0026gt; sf = new WeakReference\u0026lt;\u0026gt;(obj); obj = null; //销毁强引用 //或者 WeakReference\u0026lt;Object\u0026gt; sf = new WeakReference\u0026lt;\u0026gt;(new Object()); Copied! 弱引用对象与软引用对象的最大不同就在于，当 GC 在进行回收时，需要通过算法检查是否回收软引用对象，而对于弱引用对象，GC 总是进行回收。弱引用对象更容易、更快被 GC 回收。\n面试题：你开发中使用过 WeakHashMap 吗？\nWeakHashMap 用来存储图片信息，可以在内存不足的时候，及时回收，避免了 OOM\n1 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 /** * 弱引用的测试 * * @author shkstart shkstart@126.com * @create 2020 16:06 */ public class WeakReferenceTest { public static class User { public User(int id, String name) { this.id = id; this.name = name; } public int id; public String name; @Override public String toString() { return \u0026#34;[id=\u0026#34; + id + \u0026#34;, name=\u0026#34; + name + \u0026#34;] \u0026#34;; } } public static void main(String[] args) { //构造了弱引用 WeakReference\u0026lt;User\u0026gt; userWeakRef = new WeakReference\u0026lt;User\u0026gt;(new User(1, \u0026#34;songhk\u0026#34;)); //从弱引用中重新获取对象 System.out.println(userWeakRef.get()); System.gc(); // 不管当前内存空间足够与否，都会回收它的内存 System.out.println(\u0026#34;After GC:\u0026#34;); //重新尝试从弱引用中获取对象 System.out.println(userWeakRef.get()); } } Copied! 12.X. 再谈引用：虚引用 虚引用（Phantom Reference）——对象回收跟踪 也称为“幽灵引用”或者“幻影引用”，是所有引用类型中最弱的一个。\n一个对象是否有虚引用的存在，完全不会决定对象的生命周期。如果一个对象仅持有虚引用，那么它和没有引用几乎是一样的，随时都可能被垃圾回收器回收。\n它不能单独使用，也无法通过虚引用来获取被引用的对象。当试图通过虚引用的 get()方法取得对象时，总是 null\n为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如：能在这个对象被收集器回收时收到一个系统通知。\n虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时，如果发现它还有虚引用，就会在回收对象后，将这个虚引用加入引用队列，以通知应用程序对象的回收情况。\n由于虚引用可以跟踪对象的回收时间，因此，也可以将一些资源释放操作放置在虚引用中执行和记录。\n在 JDK1.2 版之后提供了 PhantomReference 类来实现虚引用。\n1 2 3 4 Object obj = new Object(); // 声明强引用 ReferenceQueue phantomQueue = new ReferenceQueue(); PhantomReference\u0026lt;Object\u0026gt; sf = new PhantomReference\u0026lt;\u0026gt;(obj, phantomQueue); obj = null; Copied! 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 /** * 虚引用的测试 * * @author shkstart shkstart@126.com * @create 2020 16:07 */ public class PhantomReferenceTest { public static PhantomReferenceTest obj;//当前类对象的声明 static ReferenceQueue\u0026lt;PhantomReferenceTest\u0026gt; phantomQueue = null;//引用队列 public static class CheckRefQueue extends Thread { @Override public void run() { while (true) { if (phantomQueue != null) { PhantomReference\u0026lt;PhantomReferenceTest\u0026gt; objt = null; try { objt = (PhantomReference\u0026lt;PhantomReferenceTest\u0026gt;) phantomQueue.remove(); } catch (InterruptedException e) { e.printStackTrace(); } if (objt != null) { System.out.println(\u0026#34;追踪垃圾回收过程：PhantomReferenceTest实例被GC了\u0026#34;); } } } } } @Override protected void finalize() throws Throwable { //finalize()方法只能被调用一次！ super.finalize(); System.out.println(\u0026#34;调用当前类的finalize()方法\u0026#34;); obj = this; } public static void main(String[] args) { Thread t = new CheckRefQueue(); t.setDaemon(true);//设置为守护线程：当程序中没有非守护线程时，守护线程也就执行结束。 t.start(); phantomQueue = new ReferenceQueue\u0026lt;PhantomReferenceTest\u0026gt;(); obj = new PhantomReferenceTest(); //构造了 PhantomReferenceTest 对象的虚引用，并指定了引用队列 PhantomReference\u0026lt;PhantomReferenceTest\u0026gt; phantomRef = new PhantomReference\u0026lt;PhantomReferenceTest\u0026gt;(obj, phantomQueue); try { //不可获取虚引用中的对象 System.out.println(phantomRef.get()); //将强引用去除 obj = null; //第一次进行GC,由于对象可复活，GC无法回收该对象 System.gc(); Thread.sleep(1000); if (obj == null) { System.out.println(\u0026#34;obj 是 null\u0026#34;); } else { System.out.println(\u0026#34;obj 可用\u0026#34;); } System.out.println(\u0026#34;第 2 次 gc\u0026#34;); obj = null; System.gc(); //一旦将obj对象回收，就会将此虚引用存放到引用队列中。 Thread.sleep(1000); if (obj == null) { System.out.println(\u0026#34;obj 是 null\u0026#34;); } else { System.out.println(\u0026#34;obj 可用\u0026#34;); } } catch (InterruptedException e) { e.printStackTrace(); } } } Copied! 12.11. 终结器引用 它用于实现对象的 finalize() 方法，也可以称为终结器引用。无需手动编码，其内部配合引用队列使用。\n在 GC 时，终结器引用入队。由 Finalizer 线程通过终结器引用找到被引用对象调用它的 finalize()方法，第二次 GC 时才回收被引用的对象\n","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/916f1491/","title":"12-垃圾回收相关概念"},{"content":" 13. 垃圾回收器 13.1. GC 分类与性能指标 13.1.1. 垃圾回收器概述 垃圾收集器没有在规范中进行过多的规定，可以由不同的厂商、不同版本的 JVM 来实现。\n由于 JDK 的版本处于高速迭代过程中，因此 Java 发展至今已经衍生了众多的 GC 版本。\n从不同角度分析垃圾收集器，可以将 GC 分为不同的类型。\n13.1.2. 垃圾收集器分类 按线程数分，可以分为串行垃圾回收器和并行垃圾回收器。\n串行回收指的是在同一时间段内只允许有一个 CPU 用于执行垃圾回收操作，此时工作线程被暂停，直至垃圾收集工作结束。\n在诸如单 CPU 处理器或者较小的应用内存等硬件平台不是特别优越的场合，串行回收器的性能表现可以超过并行回收器和并发回收器。所以，串行回收默认被应用在客户端的 Client 模式下的 JVM 中 在并发能力比较强的 CPU 上，并行回收器产生的停顿时间要短于串行回收器。 和串行回收相反，并行收集可以运用多个 CPU 同时执行垃圾回收，因此提升了应用的吞吐量，不过并行回收仍然与串行回收一样，采用独占式，使用了“Stop-the-World”机制。\n按照工作模式分，可以分为并发式垃圾回收器和独占式垃圾回收器。\n并发式垃圾回收器与应用程序线程交替工作，以尽可能减少应用程序的停顿时间。 独占式垃圾回收器（Stop the world）一旦运行，就停止应用程序中的所有用户线程，直到垃圾回收过程完全结束。 按碎片处理方式分，可分为压缩式垃圾回收器和非压缩式垃圾回收器。\n压缩式垃圾回收器会在回收完成后，对存活对象进行压缩整理，消除回收后的碎片。 非压缩式的垃圾回收器不进行这步操作。 按工作的内存区间分，又可分为年轻代垃圾回收器和老年代垃圾回收器。\n13.1.3. 评估 GC 的性能指标 吞吐量：运行用户代码的时间占总运行时间的比例（总运行时间 = 程序的运行时间 + 内存回收的时间） 垃圾收集开销：吞吐量的补数，垃圾收集所用时间与总运行时间的比例。 暂停时间：执行垃圾收集时，程序的工作线程被暂停的时间。 收集频率：相对于应用程序的执行，收集操作发生的频率。 内存占用：Java 堆区所占的内存大小。 快速：一个对象从诞生到被回收所经历的时间。 吞吐量、暂停时间、内存占用 这三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。\n这三项里，暂停时间的重要性日益凸显。因为随着硬件发展，内存占用多些越来越能容忍，硬件性能的提升也有助于降低收集器运行时对应用程序的影响，即提高了吞吐量。而内存的扩大，对延迟反而带来负面效果。\n简单来说，主要抓住两点：吞吐量、暂停时间\n吞吐量 吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值，即吞吐量 = 运行用户代码时间 /（运行用户代码时间+垃圾收集时间）。比如：虚拟机总共运行了 100 分钟，其中垃圾收集花掉 1 分钟，那吞吐量就是 99%。\n这种情况下，应用程序能容忍较高的暂停时间，因此，高吞吐量的应用程序有更长的时间基准，快速响应是不必考虑的\n吞吐量优先，意味着在单位时间内，STW 的时间最短：0.2 + 0.2 = 0.4\n暂停时间 “暂停时间”是指一个时间段内应用程序线程暂停，让 GC 线程执行的状态。\n例如，GC 期间 100 毫秒的暂停时间意味着在这 100 毫秒期间内没有应用程序线程是活动的。\n暂停时间优先，意味着尽可能让单次 STW 的时间最短：0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5\n吞吐量 vs 暂停时间 高吞吐量较好因为这会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作。直觉上，吞吐量越高程序运行越快。\n低暂停时间（低延迟）较好因为从最终用户的角度来看不管是 GC 还是其他原因导致一个应用被挂起始终是不好的。这取决于应用程序的类型，有时候甚至短暂的 200 毫秒暂停都可能打断终端用户体验。因此，具有低的较大暂停时间是非常重要的，特别是对于一个交互式应用程序。\n不幸的是”高吞吐量”和”低暂停时间”是一对相互竞争的目标（矛盾）。\n因为如果选择以吞吐量优先，那么必然需要降低内存回收的执行频率，但是这样会导致 GC 需要更长的暂停时间来执行内存回收。 相反的，如果选择以低延迟优先为原则，那么为了降低每次执行内存回收时的暂停时间，也只能频繁地执行内存回收，但这又引起了年轻代内存的缩减和导致程序吞吐量的下降。 在设计（或使用）GC 算法时，我们必须确定我们的目标：一个 GC 算法只可能针对两个目标之一（即只专注于较大吞吐量或最小暂停时间），或尝试找到一个二者的折衷。\n现在标准：在最大吞吐量优先的情况下，降低停顿时间\n13.2. 不同的垃圾回收器概述 垃圾收集机制是 Java 的招牌能力，极大地提高了开发效率。这当然也是面试的热点。\n13.2.1. 垃圾回收器发展史 有了虚拟机，就一定需要收集垃圾的机制，这就是 Garbage Collection，对应的产品我们称为 Garbage Collector。\n1999 年随 JDK1.3.1 一起来的是串行方式的 serialGc，它是第一款 GC。ParNew 垃圾收集器是 Serial 收集器的多线程版本 2002 年 2 月 26 日，Parallel GC 和 Concurrent Mark Sweep GC 跟随 JDK1.4.2 一起发布· Parallel GC 在 JDK6 之后成为 HotSpot 默认 GC。 2012 年，在 JDK1.7u4 版本中，G1 可用。 2017 年，JDK9 中 G1 变成默认的垃圾收集器，以替代 CMS。 2018 年 3 月，JDK10 中 G1 垃圾回收器的并行完整垃圾回收，实现并行性来改善最坏情况下的延迟。 2018 年 9 月，JDK11 发布。引入 Epsilon 垃圾回收器，又被称为 \u0026ldquo;No-Op(无操作)“ 回收器。同时，引入 ZGC：可伸缩的低延迟垃圾回收器（Experimental） 2019 年 3 月，JDK12 发布。增强 G1，自动返回未用堆内存给操作系统。同时，引入 Shenandoah GC：低停顿时间的 GC（Experimental）。· 2019 年 9 月，JDK13 发布。增强 ZGC，自动返回未用堆内存给操作系统。 2020 年 3 月，JDK14 发布。删除 CMS 垃圾回收器。扩展 ZGC 在 macos 和 Windows 上的应用 13.2.2. 7 种经典的垃圾收集器 串行回收器：Serial、Serial Old 并行回收器：ParNew、Parallel Scavenge、Parallel old 并发回收器：CMS、G1 官方手册：https://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf 13.2.3. 7 款经典收集器与垃圾分代之间的关系 新生代收集器：Serial、ParNew、Parallel Scavenge；\n老年代收集器：Serial Old、Parallel Old、CMS；\n整堆收集器：G1；\n13.2.4. 垃圾收集器的组合关系 两个收集器间有连线，表明它们可以搭配使用：Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1； 其中 Serial Old 作为 CMS 出现\u0026rdquo;Concurrent Mode Failure\u0026ldquo;失败的后备预案。 （红色虚线）由于维护和兼容性测试的成本，在 JDK 8 时将 Serial+CMS、ParNew+Serial Old 这两个组合声明为废弃（JEP173），并在 JDK9 中完全取消了这些组合的支持（JEP214），即：移除。 （绿色虚线）JDK14 中：弃用 Parallel Scavenge 和 Serialold GC 组合（JEP366） （绿色虚框）JDK14 中：删除 CMS 垃圾回收器（JEP363） 13.2.5. 不同的垃圾收集器概述 为什么要有很多收集器，一个不够吗？因为 Java 的使用场景很多，移动端，服务器等。所以就需要针对不同的场景，提供不同的垃圾收集器，提高垃圾收集的性能。\n虽然我们会对各个收集器进行比较，但并非为了挑选一个最好的收集器出来。没有一种放之四海皆准、任何场景下都适用的完美收集器存在，更加没有万能的收集器。所以我们选择的只是对具体应用最合适的收集器。\n13.2.6. 如何查看默认垃圾收集器 -XX:+PrintCommandLineFlags：查看命令行相关参数（包含使用的垃圾收集器）\n使用命令行指令：jinfo -flag 相关垃圾回收器参数 进程ID\n13.3. Serial 回收器：串行回收 Serial 收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3 之前回收新生代唯一的选择。\nSerial 收集器作为 HotSpot 中 client 模式下的默认新生代垃圾收集器。\nSerial 收集器采用复制算法、串行回收和\u0026quot;stop-the-World\u0026quot;机制的方式执行内存回收。\n除了年轻代之外，Serial 收集器还提供用于执行老年代垃圾收集的 Serial Old 收集器。Serial Old 收集器同样也采用了串行回收和\u0026quot;Stop the World\u0026quot;机制，只不过内存回收算法使用的是标记-压缩算法。\nSerial old 是运行在 Client 模式下默认的老年代的垃圾回收器 Serial 0ld 在 Server 模式下主要有两个用途：① 与新生代的 Parallel scavenge 配合使用 ② 作为老年代 CMS 收集器的后备垃圾收集方案 这个收集器是一个单线程的收集器，但它的“单线程”的意义并不仅仅说明它只会使用一个 CPU 或一条收集线程去完成垃圾收集工作，更重要的是在它进行垃圾收集时，必须暂停其他所有的工作线程，直到它收集结束（Stop The World）\n优势：简单而高效（与其他收集器的单线程比），对于限定单个 CPU 的环境来说，Serial 收集器由于没有线程交互的开销，专心做垃圾收集自然可以获得最高的单线程收集效率。运行在 Client 模式下的虚拟机是个不错的选择。\n在用户的桌面应用场景中，可用内存一般不大（几十 MB 至一两百 MB），可以在较短时间内完成垃圾收集（几十 ms 至一百多 ms），只要不频繁发生，使用串行回收器是可以接受的。\n在 HotSpot 虚拟机中，使用-XX:+UseSerialGC参数可以指定年轻代和老年代都使用串行收集器。等价于新生代用 Serial GC，且老年代用 Serial Old GC\n总结\n这种垃圾收集器大家了解，现在已经不用串行的了。而且在限定单核 cpu 才可以用。现在都不是单核的了。\n对于交互较强的应用而言，这种垃圾收集器是不能接受的。一般在 Java web 应用程序中是不会采用串行垃圾收集器的。\n13.4. ParNew 回收器：并行回收 如果说 Serial GC 是年轻代中的单线程垃圾收集器，那么 ParNew 收集器则是 Serial 收集器的多线程版本。Par 是 Parallel 的缩写，New：只能处理的是新生代\nParNew 收集器除了采用并行回收的方式执行内存回收外，两款垃圾收集器之间几乎没有任何区别。ParNew 收集器在年轻代中同样也是采用复制算法、\u0026ldquo;Stop-the-World\u0026quot;机制。\nParNew 是很多 JVM 运行在 Server 模式下新生代的默认垃圾收集器。\n对于新生代，回收次数频繁，使用并行方式高效。 对于老年代，回收次数少，使用串行方式节省资源。（CPU 并行需要切换线程，串行可以省去切换线程的资源） 由于 ParNew 收集器是基于并行回收，那么是否可以断定 ParNew 收集器的回收效率在任何场景下都会比 serial 收集器更高效？\nParNew 收集器运行在多 CPU 的环境下，由于可以充分利用多 CPU、多核心等物理硬件资源优势，可以更快速地完成垃圾收集，提升程序的吞吐量。 但是在单个 CPU 的环境下，ParNew 收集器不比 Serial 收集器更高效。虽然 Serial 收集器是基于串行回收，但是由于 CPU 不需要频繁地做任务切换，因此可以有效避免多线程交互过程中产生的一些额外开销。 因为除 Serial 外，目前只有 ParNew GC 能与 CMS 收集器配合工作\n在程序中，开发人员可以通过选项\u0026rdquo;-XX:+UseParNewGC\u0026ldquo;手动指定使用 ParNew 收集器执行内存回收任务。它表示年轻代使用并行收集器，不影响老年代。\n-XX:ParallelGCThreads限制线程数量，默认开启和 CPU 核心数相同的线程数。\n13.5. Parallel 回收器：吞吐量优先 HotSpot 的年轻代中除了拥有 ParNew 收集器是基于并行回收的以外，Parallel Scavenge 收集器同样也采用了复制算法、并行回收和\u0026quot;Stop the World\u0026quot;机制。\n那么 Parallel 收集器的出现是否多此一举？\n和 ParNew 收集器不同，ParallelScavenge 收集器的目标则是达到一个可控制的吞吐量（Throughput），它也被称为吞吐量优先的垃圾收集器。 自适应调节策略也是 Parallel Scavenge 与 ParNew 一个重要区别。 高吞吐量则可以高效率地利用 CPU 时间，尽快完成程序的运算任务，主要适合在后台运算而不需要太多交互的任务。因此，常见在服务器环境中使用。例如，那些执行批量处理、订单处理、工资支付、科学计算的应用程序。\nParallel 收集器在 JDK1.6 时提供了用于执行老年代垃圾收集的 Parallel Old 收集器，用来代替老年代的 Serial Old 收集器。\nParallel Old 收集器采用了标记-压缩算法，但同样也是基于并行回收和\u0026quot;Stop-the-World\u0026quot;机制。\n在程序吞吐量优先的应用场景中，Parallel 收集器和 Parallel Old 收集器的组合，在 Server 模式下的内存回收性能很不错。在 Java8 中，默认是此垃圾收集器。\n参数配置\n-XX:+UseParallelGC 手动指定年轻代使用 Parallel 并行收集器执行内存回收任务。\n-XX:+UseParallelOldGC 手动指定老年代都是使用并行回收收集器。\n分别适用于新生代和老年代。默认 jdk8 是开启的。 上面两个参数，默认开启一个，另一个也会被开启。（互相激活） -XX:ParallelGCThreads 设置年轻代并行收集器的线程数。一般地，最好与 CPU 数量相等，以避免过多的线程数影响垃圾收集性能。\n$$ ParallelGCThreads = \\begin{cases} CPU_Count \u0026amp; \\text (CPU_Count \u0026lt;= 8) \\ 3 + (5 * CPU＿Count / 8) \u0026amp; \\text (CPU_Count \u0026gt; 8) \\end{cases} $$\n-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间（即 STw 的时间）。单位是毫秒。\n为了尽可能地把停顿时间控制在 MaxGCPauseMills 以内，收集器在工作时会调整 Java 堆大小或者其他一些参数。 对于用户来讲，停顿时间越短体验越好。但是在服务器端，我们注重高并发，整体的吞吐量。所以服务器端适合 Parallel，进行控制。 该参数使用需谨慎。 -XX:GCTimeRatio 垃圾收集时间占总时间的比例（=1/（N+1））。用于衡量吞吐量的大小。\n取值范围（0, 100）。默认值 99，也就是垃圾回收时间不超过 1%。 与前一个-XX:MaxGCPauseMillis 参数有一定矛盾性。暂停时间越长，Radio 参数就容易超过设定的比例。 -XX:+UseAdaptivesizePolicy 设置 Parallel Scavenge 收集器具有自适应调节策略\n在这种模式下，年轻代的大小、Eden 和 Survivor 的比例、晋升老年代的对象年龄等参数会被自动调整，已达到在堆大小、吞吐量和停顿时间之间的平衡点。 在手动调优比较困难的场合，可以直接使用这种自适应的方式，仅指定虚拟机的最大堆、目标的吞吐量（GCTimeRatio）和停顿时间（MaxGCPauseMills），让虚拟机自己完成调优工作。 13.6. CMS 回收器：低延迟 在 JDK1.5 时期，Hotspot 推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器：CMS（Concurrent-Mark-Sweep）收集器，这款收集器是 HotSpot 虚拟机中第一款真正意义上的并发收集器，它第一次实现了让垃圾收集线程与用户线程同时工作。\nCMS 收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短（低延迟）就越适合与用户交互的程序，良好的响应速度能提升用户体验。\n目前很大一部分的 Java 应用集中在互联网站或者 B/S 系统的服务端上，这类应用尤其重视服务的响应速度，希望系统停顿时间最短，以给用户带来较好的体验。CMS 收集器就非常符合这类应用的需求。 CMS 的垃圾收集算法采用标记-清除算法，并且也会\u0026quot;Stop-the-World\u0026rdquo;\n不幸的是，CMS 作为老年代的收集器，却无法与 JDK1.4.0 中已经存在的新生代收集器 Parallel Scavenge 配合工作，所以在 JDK1.5 中使用 CMS 来收集老年代的时候，新生代只能选择 ParNew 或者 Serial 收集器中的一个。\n在 G1 出现之前，CMS 使用还是非常广泛的。一直到今天，仍然有很多系统使用 CMS GC。\nCMS 整个过程比之前的收集器要复杂，整个过程分为 4 个主要阶段，即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段\n初始标记（Initial-Mark）阶段：在这个阶段中，程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停，这个阶段的主要任务仅仅只是标记出 GCRoots 能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小，所以这里的速度非常快。 并发标记（Concurrent-Mark）阶段：从 GC Roots 的直接关联对象开始遍历整个对象图的过程，这个过程耗时较长但是不需要停顿用户线程，可以与垃圾收集线程一起并发运行。但是可能会导致已经标记过的对象状态发生改变。 重新标记（Remark）阶段：由于在并发标记阶段中，程序的工作线程会和垃圾收集线程同时运行或者交叉运行，因此为了修正并发标记期间，因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录，这个阶段的停顿时间通常会比初始标记阶段稍长一些，但也远比并发标记阶段的时间短。 并发清除（Concurrent-Sweep）阶段：GC线程开始对未标记的区域做清扫，释放内存空间，这个阶段如果有新增对象会被标记为黑色，不做任何处理。由于不需要移动存活对象，所以这个阶段也是可以与用户线程同时并发的 并发重置：重置本次GC过程中的标记数据 尽管 CMS 收集器采用的是并发回收（非独占式），但是在其初始化标记和再次标记这两个阶段中仍然需要执行“Stop-the-World”机制暂停程序中的工作线程，不过暂停时间并不会太长，因此可以说明目前所有的垃圾收集器都做不到完全不需要“stop-the-World”，只是尽可能地缩短暂停时间。\n由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作，所以整体的回收是低停顿的。\n另外，由于在垃圾收集阶段用户线程没有中断，所以在 CMS 回收过程中，还应该确保应用程序用户线程有足够的内存可用。因此，CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集，而是当堆内存使用率达到某一阈值时，便开始进行回收，以确保应用程序在 CMS 工作过程中依然有足够的空间支持应用程序运行。要是 CMS 运行期间预留的内存无法满足程序需要，就会出现一次“Concurrent Mode Failure” 失败，这时虚拟机将启动后备预案：临时启用 Serial Old 收集器来重新进行老年代的垃圾收集，这样停顿时间就很长了。\nCMS 收集器的垃圾收集算法采用的是标记清除算法，这意味着每次执行完内存回收后，由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块，不可避免地将会产生一些内存碎片。那么 CMS 在为新对象分配内存空间时，将无法使用指针碰撞（Bump the Pointer）技术，而只能够选择空闲列表（Free List）执行内存分配。\n有人会觉得既然 Mark Sweep 会造成内存碎片，那么为什么不把算法换成 Mark Compact？\n答案其实很简单，因为当并发清除的时候，用 Compact 整理内存的话，原来的用户线程使用的内存还怎么用呢？要保证用户线程能继续执行，前提的它运行的资源不受影响嘛。Mark Compact 更适合“Stop the World” 这种场景下使用\n通过参数 -XX:+UseCMSCompactAtFullCollection ，可以让jvm在执行标记清除后再做内存整理 13.6.1. CMS 的优点 并发收集 低延迟 13.6.2. CMS 的弊端 会产生内存碎片，导致并发清除后，用户线程可用的空间不足。在无法分配大对象的情况下，不得不提前触发 FullGC。 CMS 收集器对 CPU 资源非常敏感。在并发阶段，它虽然不会导致用户停顿，但是会因为占用了一部分线程而导致应用程序变慢，总吞吐量会降低。 CMS 收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure\u0026ldquo;失败而导致另一次 Full GC 的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的，那么在并发标记阶段如果产生新的垃圾对象，CMS 将无法对这些垃圾对象进行标记，最终会导致这些新产生的垃圾对象没有被及时回收，从而只能在下一次执行 GC 时释放这些之前未被回收的内存空间。 CMS重新标记阶段只能处理漏标问题，不能处理浮动垃圾 13.6 三色标记 三色标记法将对象的颜色分为了黑、灰、白，三种颜色。\n白色：该对象没有被标记过（对象垃圾）。可达性分析算法刚开始阶段，所有对象都是白色的\n灰色：该对象已经被标记过了，但该对象下的直接属性没有全被标记完。（GC需要从此对象中去寻找垃圾）\n黑色：该对象已经被标记过了，且该对象下的直接属性也全部都被标记过了。（程序所需要的对象）\n首先创建三个集合：白、灰、黑。 将所有对象放入白色集合中。 然后从根节点开始遍历所有直接对象（注意这里并不递归遍历），把遍历到的对象从白色集合放入灰色集合。CMS的初始标记 之后遍历灰色集合，将灰色对象引用的直接对象从白色集合放入灰色集合，之后将此灰色对象放入黑色集合。CMS的并发标记 重复 4 直到灰色中无任何对象 通过write-barrier检测对象有变化，重复以上操作 收集所有白色对象（垃圾） CMS中会产生浮动垃圾，在并发标记过程中，如果由于方法结束导致部分局部变量(GC Roots)被销毁，这个gcroot引用的对象之前又被扫描过(被标记为可达对象)，那么本轮GC不会回收这部分内存，这部分本应该回收但是没有回收到的内存，被称为浮动垃圾。浮动垃圾并不会影响垃圾回收的正确性，只是等到下一轮垃圾回收中才会被清除；\n另外，针对并发标记(并发清理)开始后产生的新对象，通常的做法是直接全部当成黑色，本轮不会进行清除。这部分对象期间可能也会变成垃圾，这也算是浮动垃圾的一部分。 CMS中并发标记会产生漏标问题，会导致引用的对象被当成垃圾误删除，这是严重bug，必须解决；有两种解决方案：\n1、增量更新(incremental update)：当黑色对象插入新的指向白色对象的引用关系时，就将这个新插入的引用记录下来，等到并发扫描结束之后，再将这些记录过的引用关系中的黑色对象为根，重新扫描一次，可以简化理解为，黑色对象一旦新插入了指向白色对象的引用之后，它就变回灰色对象了。\n​\t因为增量更新不记录删除的引用关系，当某个引用关系被标记后，用户线程将该引用关系断开，这个断开操作不会被记录，所以断开的对象就不会被清除，产生了浮动垃圾。\n2、原始快照(Snapshot At The Beginning,STAB)：当灰色对象要删除指向白色对象的引用关系时，就将这个要删除的引用记录下来，相当于保存当时引用链的快照。在并发扫描结束之后，再从这些记录过的引用关系中的对象当根结点扫描，这样就能把扫描到的对象标记为黑色，目的就是让这种对象在本轮gc清理中能存活下来，待下一次gc的时候重新扫描，这个对象也有可能是浮动垃圾。这也可以简化理解为，无论引用关系删除与否，都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。\n​\t对象可能会在 G1 收集期间死亡并且不会被收集。G1 使用一种称为开始快照 (SATB) 的技术来保证垃圾收集器找到所有活动对象。SATB 指出，出于收集的目的，在并发标记（整个堆上的标记）开始时处于活动状态的任何对象都被认为是活动的。SATB 允许以类似于 CMS 增量更新的方式浮动垃圾。\nFloating Garbage Objects can die during a G1 collection and not be collected. G1 uses a technique called snapshot-at-the-beginning (SATB) to guarantee that all live objects are found by the garbage collector. SATB states that any object that is live at the start of the concurrent marking (a marking over the entire heap) is considered live for the purpose of the collection. SATB allows floating garbage in a way analogous to that of a CMS incremental update.\n以上无论是对对象引用关系记录的插入还是删除，虚拟机的记录操作都是通过写屏障实现的 CMS重新标记阶段使用写屏障 + 增量更新的措施，不能处理浮动垃圾 G1最终标记阶段使用写屏障 +原始快照+RememberedSet的措施，能处理浮动垃圾，但是也可能会存在；处理浮动垃圾，配合Rset，去扫描哪些Region引用到当前的白色对象，若没有引用到当前对象，则回收 常见的漏标：\nSATB效率高于增量更新的原因？ 因为SATB在重新标记环节只需要去重新扫描那些被推到堆栈中的引用，并配合Rset 来判断当前对象是否被引用来进行回收；\n并且在最后G1 并不会选择回收所有垃圾对象，而是根据Region 的垃圾多少来判断与预估回收价值（指回收的垃圾与回收的STW 时间的一个预估值），将一个或者多个Region 放到CSet 中，最后将这些Region 中的存活对象压缩并复制到新的Region 中，清空原来的Region\n6.1 写屏障 这是JVM代码级别的屏障，相当于aop，和禁止指令重排的内存屏障没关系\n无论是增量更新还是原始快照，都是通过写屏障来实现的。\n增量更新和原始快照都是对引用的操作，一个是新增引用，一个是删除引用，不管是新增还是删除，最终都要把他们收集到集合里去。那么如何收集呢？其实就是在赋值操作之前或者赋值操作之后，把引用丢到集合中去。 在赋值操作的前面或者后面做一些事情，这个过程我们把它叫做代码的操作屏障。\n下面来看看赋值屏障的伪代码，以给某个对象的成员变量赋值为例，底层代码大概是这样的:：\n1 2 3 4 5 6 7 /** * @param field 某对象的成员变量，如 a.b.d * @param new_value 新值，如 null */ voidoop_field_store(oop*field,oopnew_value){ *field = new_value; // 赋值操作 } Copied! 所谓的写屏障，其实就是指在赋值操作前后，加入一些处理(可以参考AOP的概念):\n1 2 3 4 5 voidoop_field_store(oop*field,oopnew_value){ pre_write_barrier(field); // 写屏障‐写前操作 *field = new_value; post_write_barrier(field, value); // 写屏障‐写后操作 } Copied! 写屏障实现原始快照 原始快照是记录对引用的删除。比如在执行a.b.d=null的时候，利用写屏障，将原来B成员变量的引用 对象D记录下来:\n1 2 3 4 5 // 写屏障代码 void pre_write_barrier(oop*field){ oop old_value = *field; // 获取旧值 remark_set.add(old_value); // 记录原来的引用对象 } Copied! 写屏障实现增量更新 当对象A的成员变量的引用发生变化时，比如新增引用(a.d = d)，我们可以利用写屏障，将A新的成员变量引用对象D 记录下来:\n1 2 3 void post_write_barrier(oop*field,oopnew_value){ remark_set.add(new_value); // 记录新引用的对象 } Copied! 这两块都是屏障代码，一个是在写前执行，一个是在写后执行。 删除操作要在写前执行， 赋值操作要在写后执行。\n下面来看看hotspot源码是如何实现写屏障的，找到oop.inline.hpp文件\n1 2 3 4 5 6 7 8 9 /** * c++底层调用的赋值方法 */ template \u0026lt;class T\u0026gt; inline void oop_store(volatile T* p, oop v) { update_barrier_set_pre((T*)p, v); // cast away volatile // Used by release_obj_field_put, so use release_store_ptr. oopDesc::release_encode_store_heap_oop(p, v); update_barrier_set((void*)p, v); // cast away type } Copied! 这就是一个赋值操作。update_barrier_set_pre((T*)p, v);是一个写前屏障，update_barrier_set((void*)p, v);是一个写后屏障。也就是说在赋值之前和之后增加了一段操作代码。其实可以看出来这段代码和我们的伪代码差不多。名字虽不同，但是含义是一样的。\n再看看SATB在hotspot源码中是如何实现写屏障的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 void G1SATBCardTableModRefBS::enqueue(oop pre_val) { // Nulls should have been already filtered. assert(pre_val-\u0026gt;is_oop(true), \u0026#34;Error\u0026#34;); if (!JavaThread::satb_mark_queue_set().is_active()) return; Thread* thr = Thread::current(); if (thr-\u0026gt;is_Java_thread()) { JavaThread* jt = (JavaThread*)thr; jt-\u0026gt;satb_mark_queue().enqueue(pre_val); } else { MutexLockerEx x(Shared_SATB_Q_lock, Mutex::_no_safepoint_check_flag); JavaThread::satb_mark_queue_set().shared_satb_queue()-\u0026gt;enqueue(pre_val); } } Copied! 我们看到这句话satb_mark_queue_set().shared_satb_queue()-\u0026gt;enqueue(pre_val); 将旧值放到队列里。这时为什么会放到队列里面呢？为了提高效率。因为是写操作，在写操作之前和之后增加逻辑，是会影响原来代码的效率的，为了避免对源代码的影响，放入到队列中进行处理。\n6.2 读屏障 1 2 3 4 oopoop_field_load(oop*field){ pre_load_barrier(field); // 读屏障‐读取前操作 return *field; } Copied! 读屏障是直接针对第一步:D d = a.b.d，当读取成员变量时，一律记录下来:\n1 2 3 4 voidpre_load_barrier(oop*field){ oop old_value = *field; remark_set.add(old_value); // 记录读取到的对象 } Copied! 现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想，尽管实现的方式不尽相同:比如白色/黑色 集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可 以是广度/深度遍历等等。\n6.3 各种垃圾收集器对漏标的处理方案 对于读写屏障，以Java HotSpot VM为例，其并发标记时对漏标的处理方案如下:\nCMS：采用的是写屏障 + 增量更新 G1： 采用的是写屏障 + 原汁快照（SATB） ZGC：采用的是读屏障 工程实现中，读写屏障还有其他功能，比如写屏障可以用于记录跨代/区引用的变化，读屏障可以用于支持移动对象的并 发执行等。功能之外，还有性能的考虑，所以对于选择哪种，每款垃圾回收器都有自己的想法。\n6.4 记忆集和卡表 1.记忆集（Remember Set） 在新生代触发Minor GC进行GC Root可达性扫描的时候，可能会碰到跨代引用。比如：新生代的一个对象被老年代引用了，这个时候，在垃圾回收的时候，我们不应该把这块空间回收掉。那怎么办呢？要去扫描一遍老年代么？这显然不行，效率太低了。为了解决这个问题，GC在扫描的时候，会把老年代引用的对象放在一个叫做记忆集的集合中。\n这样在垃圾回收的时候，除了会扫描GC Root下的对象，还会扫描一遍记忆集中的引用。记忆集是存储在新生代的空间，保存着老年代对新生代内存的引用关系。记忆集就是为了解决对象的跨代引用问题。\n垃圾收集过程中， 收集器只需要通过记忆集来判断某一块非收集区域是否存在指向收集区域的指针即可，无需了解跨代引用指针的全部细节。\n2.卡表（Card Table） hotspot使用的是卡表（cardtable）来实现记忆集。卡表其实就是记忆集的一个实现，卡表和记忆集的关系就像HashMap和Map的关系。记忆集相当于一个概念，而jdk中是通过卡表来实现的。到底是如何实现的呢？\n卡表是使用字节数组实现的，卡表的每一个元素对应着其标志的内存区域里一块待定大小的内存块。这些待定的内存块就是“卡页”。堆空间分为新生代和老年代，卡表会把老年代划分为一块一块小的格子，这些小格子就是“卡页”。卡页划分是按照512字节大小进行划分的。如果有一个卡页引用了新生代的对象，那么就将这个卡页就会被标记为“dirty”。卡表是一个数组，里面记录了所有卡页的状态，用010101来标记卡页是否引用了新生代对象。如果是就标记为1，不是就保持原来的0. 数组里除了存放卡页的状态，还有卡页的地址。在垃圾收集器进行扫描的时候，除了扫描GC Root之外，还会扫描卡表里那些状态为1的卡页里的对象。 卡页是在老年代，维护卡页的卡表是在年轻代。\n13.6.3. 设置的参数 -XX:+UseConcMarkSweepGC 手动指定使用 CMS 收集器执行内存回收任务。\n开启该参数后会自动将-xx:+UseParNewGC打开。即：ParNew（Young 区用）+CMS（Old 区用）+ Serial Old 的组合。\n-XX:CMSInitiatingOccupancyFraction 设置堆内存使用率的阈值，一旦达到该阈值，便开始进行回收。\nJDK5 及以前版本的默认值为 68，即当老年代的空间使用率达到 68%时，会执行一次 CMS 回收。JDK6 及以上版本默认值为 92% 如果内存增长缓慢，则可以设置一个稍大的值，大的阀值可以有效降低 CMS 的触发频率，减少老年代回收的次数可以较为明显地改善应用程序性能。反之，如果应用程序内存使用率增长很快，则应该降低这个阈值，以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低 Ful1Gc 的执行次数。 -XX:+UseCMSInitiatingOccupanyOnly 只使用设定的阈值(CMSInitiatingOccupanyFraction),如果不指定，jvm仅在第一次使用设定值，后续会自动调整阈值 -XX:+UseCMSCompactAtFullCollection 用于指定在执行完 Full GC 后对内存空间进行压缩整理，以此避免内存碎片的产生。不过由于内存压缩整理过程无法并发执行，所带来的问题就是停顿时间变得更长了。默认开启\n-XX:CMSFullGCsBeforeCompaction 设置在执行多少次 Full GC 后对内存空间进行压缩整理，默认为0。\n-XX:CMSScavengeBeforeRemark,在CMS GC前启动一次minorGC，目的减少老年代对新生代的引用，降低CMS GC的标记时段的开销，一般CMS的GC耗时80%都在标记阶段，默认关闭\n-XX:+CMSParallelInitialMarkEnabled,表示在初始标记阶段多线程执行，缩短STW，默认开启\n-XX:+CMSParallelRemarkEnabled,CMSParallelRemarkEnabled表示在重新标记阶段多线程执行，缩短STW，默认开启\n-XX:ParallelcMSThreads 设置 CMS 的线程数量。\nCMS 默认启动的线程数是（ParallelGCThreads+3）/4，ParallelGCThreads 是年轻代并行收集器的线程数。当 CPU 资源比较紧张时，受到 CMS 收集器线程的影响，应用程序的性能在垃圾回收阶段可能会非常糟糕。 小结 HotSpot 有这么多的垃圾回收器，那么如果有人问，Serial GC、Parallel GC、Concurrent Mark Sweep GC 这三个 Gc 有什么不同呢？\n请记住以下口令：\n如果你想要最小化地使用内存和并行开销，请选 Serial GC； 如果你想要最大化应用程序的吞吐量，请选 Parallel GC； 如果你想要最小化 GC 的中断或停顿时间，请选 CMS GC。 13.6.4. JDK 后续版本中 CMS 的变化 JDK9 新特性：CMS 被标记为 Deprecate 了（JEP291）\n如果对 JDK9 及以上版本的 HotSpot 虚拟机使用参数-XX: +UseConcMarkSweepGC来开启 CMS 收集器的话，用户会收到一个警告信息，提示 CMS 未来将会被废弃。 JDK14 新特性：删除 CMS 垃圾回收器（JEP363）\n移除了 CMS 垃圾收集器，如果在 JDK14 中使用 -XX:+UseConcMarkSweepGC的话，JVM 不会报错，只是给出一个 warning 信息，但是不会 exit。JVM 会自动回退以默认 GC 方式启动 JVM 13.7. G1 回收器：区域化分代式 既然我们已经有了前面几个强大的 GC，为什么还要发布 Garbage First（G1）？\n原因就在于应用程序所应对的业务越来越庞大、复杂，用户越来越多，没有 GC 就不能保证应用程序正常进行，而经常造成 STW 的 GC 又跟不上实际的需求，所以才会不断地尝试对 GC 进行优化。G1（Garbage-First）垃圾回收器是在 Java7 update4 之后引入的一个新的垃圾回收器，是当今收集器技术发展的最前沿成果之一。\n与此同时，为了适应现在不断扩大的内存和不断增加的处理器数量，进一步降低暂停时间（pause time），同时兼顾良好的吞吐量。\n官方给 G1 设定的目标是在延迟可控的情况下获得尽可能高的吞吐量，所以才担当起“全功能收集器”的重任与期望。\n为什么名字叫 Garbage First(G1)呢？\n因为 G1 是一个并行回收器，它把堆内存分割为很多不相关的区域（Region）（物理上不连续的）。使用不同的 Region 来表示 Eden、幸存者 0 区，幸存者 1 区，老年代等。\nG1 GC 有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 跟踪各个 Region 里面的垃圾堆积的价值大小（回收所获得的空间大小以及回收所需时间的经验值），在后台维护一个优先列表，每次根据允许的收集时间，优先回收价值最大的 Region。\n由于这种方式的侧重点在于回收垃圾最大量的区间（Region），所以我们给 G1 一个名字：垃圾优先（Garbage First）。\nG1（Garbage-First）是一款面向服务端应用的垃圾收集器，主要针对配备多核 CPU 及大容量内存的机器，以极高概率满足 GC 停顿时间的同时，还兼具高吞吐量的性能特征。\n在 JDK1.7 版本正式启用，移除了 Experimenta1 的标识，是JDK9 以后的默认垃圾回收器，取代了 CMS 回收器以及 Parallel+Parallel Old 组合。被 Oracle 官方称为“全功能的垃圾收集器”。\n与此同时，CMS 已经在 JDK9 中被标记为废弃（deprecated）。在 jdk8 中还不是默认的垃圾回收器，需要使用-XX:+UseG1GC来启用。\n13.7.1. G1 回收器的特点（优势） 与其他 GC 收集器相比，G1 使用了全新的分区算法，其特点如下所示：\n并行与并发 并行性：G1 在回收期间，可以有多个 GC 线程同时工作，有效利用多核计算能力。此时用户线程 STW 并发性：G1 拥有与应用程序交替执行的能力，部分工作可以和应用程序同时执行，因此，一般来说，不会在整个回收阶段发生完全阻塞应用程序的情况 分代收集 从分代上看，G1 依然属于分代型垃圾回收器，它会区分年轻代和老年代，年轻代依然有 Eden 区和 Survivor 区。但从堆的结构上看，它不要求整个 Eden 区、年轻代或者老年代都是连续的，也不再坚持固定大小和固定数量。 将堆空间分为若干个区域（Region），这些区域中包含了逻辑上的年轻代和老年代。 和之前的各类回收器不同，它同时兼顾年轻代和老年代。对比其他回收器，或者工作在年轻代，或者工作在老年代； 空间整合 CMS：“标记-清除”算法、内存碎片、若干次 Gc 后进行一次碎片整理 G1 将内存划分为一个个的 region。内存的回收是以 region 作为基本单位的。Region 之间是复制算法，但整体上实际可看作是标记-压缩（Mark-Compact）算法，两种算法都可以避免内存碎片。这种特性有利于程序长时间运行，分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。尤其是当 Java 堆非常大的时候，G1 的优势更加明显。 可预测的停顿时间模型（即：软实时 soft real-time） 这是 G1 相对于 CMS 的另一大优势，G1 除了追求低停顿外，还能建立可预测的停顿时间模型，能让使用者明确指定在一个长度为 M 毫秒的时间片段内，消耗在垃圾收集上的时间不得超过 N 毫秒。\n由于分区的原因，G1 可以只选取部分区域进行内存回收，这样缩小了回收的范围，因此对于全局停顿情况的发生也能得到较好的控制。 G1 跟踪各个 Region 里面的垃圾堆积的价值大小（回收所获得的空间大小以及回收所需时间的经验值），在后台维护一个优先列表，每次根据允许的收集时间，优先回收价值最大的 Region。保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。 相比于 CMSGC，G1 未必能做到 CMS 在最好情况下的延时停顿，但是最差情况要好很多。 13.7.2. G1 垃圾收集器的缺点 相较于 CMS，G1 还不具备全方位、压倒性优势。比如在用户程序运行过程中，G1 无论是为了垃圾收集产生的内存占用（Footprint）还是程序运行时的额外执行负载（Overload）都要比 CMS 要高。\n从经验上来说，在小内存应用上 CMS 的表现大概率会优于 G1，而 G1 在大内存应用上则发挥其优势。平衡点在 6-8GB 之间。\n13.7.3. G1 回收器的参数设置 -XX:+UseG1GC：手动指定使用 G1 垃圾收集器执行内存回收任务 -XX:G1HeapRegionSize 设置每个 Region 的大小。值是 2 的幂，范围是 1MB 到 32MB 之间，目标是根据最小的 Java 堆大小划分出约 2048 个区域。默认是堆内存的 1/2000。 -XX:MaxGCPauseMillis 设置期望达到的最大 GC 停顿时间指标（JVM 会尽力实现，但不保证达到）。默认值是 200ms（人的平均反应速度） -XX:+ParallelGCThread 设置 STW 工作线程数的值。最多设置为 8（上面说过 Parallel 回收器的线程计算公式，当 CPU_Count \u0026gt; 8 时，ParallelGCThreads 也会大于 8） -XX:ConcGCThreads 设置并发标记的线程数。将 n 设置为并行垃圾回收线程数（ParallelGCThreads）的 1/4 左右。 -XX:InitiatingHeapOccupancyPercent 设置触发并发 GC 周期的 Java 堆占用率阈值。超过此值，就触发 GC。默认值是 45。 13.7.4. G1 收集器的常见操作步骤 G1 的设计原则就是简化 JVM 性能调优，开发人员只需要简单的三步即可完成调优：\n第一步：开启 G1 垃圾收集器 第二步：设置堆的最大内存 第三步：设置最大的停顿时间 G1 中提供了三种垃圾回收模式：Young GC、Mixed GC 和 Full GC，在不同的条件下被触发。\n13.7.5. G1 收集器的适用场景 面向服务端应用，针对具有大内存、多处理器的机器。（在普通大小的堆里表现并不惊喜）\n最主要的应用是需要低 GC 延迟，并具有大堆的应用程序提供解决方案；如：在堆大小约 6GB 或更大时，可预测的暂停时间可以低于 0.5 秒；（G1 通过每次只清理一部分而不是全部的 Region 的增量式清理来保证每次 GC 停顿时间不会过长）。\n用来替换掉 JDK1.5 中的 CMS 收集器；在下面的情况时，使用 G1 可能比 CMS 好：\n超过 50%的 Java 堆被活动数据占用； 对象分配频率或年代提升频率变化很大； GC 停顿时间过长（长于 0.5 至 1 秒） HotSpot 垃圾收集器里，除了 G1 以外，其他的垃圾收集器使用内置的 JVM 线程执行 GC 的多线程操作，而 G1 GC 可以采用应用线程承担后台运行的 GC 工作，即当 JVM 的 GC 线程处理速度慢时，系统会调用应用程序线程帮助加速垃圾回收过程。\n13.7.6. 分区 Region：化整为零 使用 G1 收集器时，它将整个 Java 堆划分成约 2048 个大小相同的独立 Region 块，每个 Region 块大小根据堆空间的实际大小而定，整体被控制在 1MB 到 32MB 之间，且为 2 的 N 次幂，即 1MB，2MB，4MB，8MB，16MB，32MB。可以通过-XX:G1HeapRegionSize设定。所有的 Region 大小相同，且在 JVM 生命周期内不会被改变。\n虽然还保留有新生代和老年代的概念，但新生代和老年代不再是物理隔离的了，它们都是一部分 Region（不需要连续）的集合。通过 Region 的动态分配方式实现逻辑上的连续。\n一个 region 有可能属于 Eden，Survivor 或者 Old/Tenured 内存区域。但是一个 region 只可能属于一个角色。图中的 E 表示该 region 属于 Eden 内存区域，S 表示属于 survivor 内存区域，O 表示属于 Old 内存区域。图中空白的表示未使用的内存空间。\nG1 垃圾收集器还增加了一种新的内存区域，叫做 Humongous 内存区域，如图中的 H 块。主要用于存储大对象，如果超过 1.5 个 region，就放到 H。\n设置 H 的原因：对于堆中的对象，默认直接会被分配到老年代，但是如果它是一个短期存在的大对象就会对垃圾收集器造成负面影响。为了解决这个问题，G1 划分了一个 Humongous 区，它用来专门存放大对象。如果一个 H 区装不下一个大对象，那么 G1 会寻找连续的 H 区来存储。为了能找到连续的 H 区，有时候不得不启动 Full GC。G1 的大多数行为都把 H 区作为老年代的一部分来看待。\n每个 Region 都是通过指针碰撞来分配空间\n13.7.7. G1 垃圾回收器的回收过程 G1GC 的垃圾回收过程主要包括如下三个环节：\n年轻代 GC（Young GC）\n老年代并发标记过程（Concurrent Marking）\n混合回收（Mixed GC）\n（如果需要，单线程、独占式、高强度的 Full GC 还是继续存在的。它针对 GC 的评估失败提供了一种失败保护机制，即强力回收。）\n顺时针，Young gc -\u0026gt; Young gc + Concurrent mark-\u0026gt;Mixed GC 顺序，进行垃圾回收。\n应用程序分配内存，当年轻代的 Eden 区用尽时开始年轻代回收过程；G1 的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期，G1GC 暂停所有应用程序线程，启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到 Survivor 区间或者老年区间，也有可能是两个区间都会涉及。\n当堆内存使用达到一定值（默认 45%）时，开始老年代并发标记过程。\n标记完成马上开始混合回收过程。对于一个混合回收期，G1 GC 从老年区间移动存活对象到空闲区间，这些空闲区间也就成为了老年代的一部分。和年轻代不同，老年代的 G1 回收器和其他 GC 不同，G1 的老年代回收器不需要整个老年代被回收，一次只需要扫描/回收一小部分老年代的 Region 就可以了。同时，这个老年代 Region 是和年轻代一起被回收的。\n举个例子：一个 Web 服务器，Java 进程最大堆内存为 4G，每分钟响应 1500 个请求，每 45 秒钟会新分配大约 2G 的内存。G1 会每 45 秒钟进行一次年轻代回收，每 31 个小时整个堆的使用率会达到 45%，会开始老年代并发标记过程，标记完成后开始四到五次的混合回收。\n13.7.8. Remembered Set 一个对象被不同区域引用的问题\n一个 Region 不可能是孤立的，一个 Region 中的对象可能被其他任意 Region 中对象引用，判断对象存活时，是否需要扫描整个 Java 堆才能保证准确？\n在其他的分代收集器，也存在这样的问题（而 G1 更突出）回收新生代也不得不同时扫描老年代？\n这样的话会降低 MinorGC 的效率；\n解决方法：\n无论 G1 还是其他分代收集器，JVM 都是使用 Remembered Set 来避免全局扫描：\n每个 Region 都有一个对应的 Remembered Set；\n每次 Reference 类型数据写操作时，都会产生一个 Write Barrier 暂时中断操作；\n然后检查将要写入的引用指向的对象是否和该 Reference 类型数据在不同的 Region（其他收集器：检查老年代对象是否引用了新生代对象）；\n如果不同，通过 CardTable 把相关引用信息记录到引用指向对象的所在 Region 对应的 Remembered Set 中；\n当进行垃圾收集时，在 GC 根节点的枚举范围加入 Remembered Set；就可以保证不进行全局扫描，也不会有遗漏。\n13.7.9. G1 回收过程一：年轻代 GC JVM 启动时，G1 先准备好 Eden 区，程序在运行过程中不断创建对象到 Eden 区，当 Eden 空间耗尽时，G1 会启动一次年轻代垃圾回收过程。\n年轻代垃圾回收只会回收 Eden 区和 Survivor 区。\n首先 G1 停止应用程序的执行（Stop-The-World），G1 创建回收集（Collection Set），回收集是指需要被回收的内存分段的集合，年轻代回收过程的回收集包含年轻代 Eden 区和 Survivor 区所有的内存分段。\n然后开始如下回收过程：\n第一阶段，扫描根。根是指 static 变量指向的对象，正在执行的方法调用链条上的局部变量等。根引用连同 RSet 记录的外部引用作为扫描存活对象的入口。 第二阶段，更新 RSet。处理 dirty card queue（见备注）中的 card，更新 RSet。此阶段完成后，RSet 可以准确的反映老年代对所在的内存分段中对象的引用。 第三阶段，处理 RSet。识别被老年代对象指向的 Eden 中的对象，这些被指向的 Eden 中的对象被认为是存活的对象。 第四阶段，复制对象。此阶段，对象树被遍历，Eden 区内存段中存活的对象会被复制到 Survivor 区中空的内存分段，Survivor 区内存段中存活的对象如果年龄未达阈值，年龄会加 1，达到阀值会被会被复制到 Old 区中空的内存分段。如果 Survivor 空间不够，Eden 空间的部分数据会直接晋升到老年代空间。 第五阶段，处理引用。处理 Soft，Weak，Phantom，Final，JNI Weak 等引用。最终 Eden 空间的数据为空，GC 停止工作，而目标内存中的对象都是连续存储的，没有碎片，所以复制过程可以达到内存整理的效果，减少碎片。 13.7.10. G1 回收过程二：并发标记过程 初始标记阶段：标记从根节点直接可达的对象。这个阶段是 STW 的，并且会触发一次年轻代 GC。 根区域扫描（Root Region Scanning）：G1 GC 扫描 Survivor 区直接可达的老年代区域对象，并标记被引用的对象。这一过程必须在 YoungGC 之前完成。 并发标记（Concurrent Marking）：在整个堆中进行并发标记（和应用程序并发执行），此过程可能被 YoungGC 中断。在并发标记阶段，若发现区域对象中的所有对象都是垃圾，那这个区域会被立即回收。同时，并发标记过程中，会计算每个区域的对象活性（区域中存活对象的比例）。 再次标记（Remark）：由于应用程序持续进行，需要修正上一次的标记结果。是 STW 的。G1 中采用了比 CMS 更快的初始快照算法：snapshot-at-the-beginning（SATB）。 独占清理（cleanup，STW）：计算各个区域的存活对象和 GC 回收比例，并进行排序，识别可以混合回收的区域。为下阶段做铺垫。是 STW 的。这个阶段并不会实际上去做垃圾的收集 并发清理阶段：识别并清理完全空闲的区域。 13.7.11. G1 回收过程三：混合回收 当越来越多的对象晋升到老年代 o1d region 时，为了避免堆内存被耗尽，虚拟机会触发一个混合的垃圾收集器，即 Mixed GC，该算法并不是一个 Old GC，除了回收整个 Young Region，还会回收一部分的 Old Region。这里需要注意：是一部分老年代，而不是全部老年代。可以选择哪些 Old Region 进行收集，从而可以对垃圾回收的耗时时间进行控制。也要注意的是 Mixed GC 并不是 Full GC。\n并发标记结束以后，老年代中百分百为垃圾的内存分段被回收了，部分为垃圾的内存分段被计算了出来。默认情况下，这些老年代的内存分段会分 8 次（可以通过-XX:G1MixedGCCountTarget设置）被回收\n混合回收的回收集（Collection Set）包括八分之一的老年代内存分段，Eden 区内存分段，Survivor 区内存分段。混合回收的算法和年轻代回收的算法完全一样，只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。\n由于老年代中的内存分段默认分 8 次回收，G1 会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的，越会被先回收。并且有一个阈值会决定内存分段是否被回收，-XX:G1MixedGCLiveThresholdPercent，默认为 65%，意思是垃圾占内存分段比例要达到 65%才会被回收。如果垃圾占比太低，意味着存活的对象占比高，在复制的时候会花费更多的时间。\n混合回收并不一定要进行 8 次。有一个阈值-XX:G1HeapWastePercent，默认值为 10%，意思是允许整个堆内存中有 10%的空间被浪费，意味着如果发现可以回收的垃圾占堆内存的比例低于 10%，则不再进行混合回收。因为 GC 会花费很多的时间但是回收到的内存却很少。\n13.7.12. G1 回收可选的过程四：Full GC G1 的初衷就是要避免 Full GC 的出现。但是如果上述方式不能正常工作，G1 会停止应用程序的执行（Stop-The-World），使用单线程的内存回收算法进行垃圾回收，性能会非常差，应用程序停顿时间会很长。\n要避免 Full GC 的发生，一旦发生需要进行调整。什么时候会发生 Full GC 呢？比如堆内存太小，当 G1 在复制存活对象的时候没有空的内存分段可用，则会回退到 Full GC，这种情况可以通过增大内存解决。\n导致 G1 Full GC 的原因可能有两个：\nEvacuation 的时候没有足够的 to-space 来存放晋升的对象； 并发处理过程完成之前空间耗尽。 13.7.13. 补充 从 Oracle 官方透露出来的信息可获知，回收阶段（Evacuation）其实本也有想过设计成与用户程序一起并发执行，但这件事情做起来比较复杂，考虑到 G1 只是回一部分 Region，停顿时间是用户可控制的，所以并不迫切去实现，而选择把这个特性放到了 G1 之后出现的低延迟垃圾收集器（即 ZGC）中。另外，还考虑到 G1 不是仅仅面向低延迟，停顿用户线程能够最大幅度提高垃圾收集效率，为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。\n13.7.14. G1 回收器优化建议 年轻代大小\n避免使用-Xmn或-XX:NewRatio等相关选项显式设置年轻代大小 固定年轻代的大小会覆盖暂停时间目标 暂停时间目标不要太过严苛\nG1 GC 的吞吐量目标是 90%的应用程序时间和 10%的垃圾回收时间 评估 G1 GC 的吞吐量时，暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更多的垃圾回收开销，而这些会直接影响到吞吐量。 13.8. 垃圾回收器总结 13.8.1. 7 种经典垃圾回收器总结 截止 JDK1.8，一共有 7 款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点，在具体使用的时候，需要根据具体的情况选用不同的垃圾收集器。\n垃圾收集器 分类 作用位置 使用算法 特点 适用场景 Serial 串行运行 作用于新生代 复制算法 响应速度优先 适用于单 CPU 环境下的 client 模式 ParNew 并行运行 作用于新生代 复制算法 响应速度优先 多 CPU 环境 Server 模式下与 CMS 配合使用 Parallel 并行运行 作用于新生代 复制算法 吞吐量优先 适用于后台运算而不需要太多交互的场景 Serial Old 串行运行 作用于老年代 标记-压缩算法 响应速度优先 适用于单 CPU 环境下的 Client 模式 Parallel Old 并行运行 作用于老年代 标记-压缩算法 吞吐量优先 适用于后台运算而不需要太多交互的场景 CMS 并发运行 作用于老年代 标记-清除算法 响应速度优先 适用于互联网或 B／S 业务 G1 并发、并行运行 作用于新生代、老年代 标记-压缩算法、复制算法 响应速度优先 面向服务端应用 GC 发展阶段：Serial =\u0026gt; Parallel（并行）=\u0026gt; CMS（并发）=\u0026gt; G1 =\u0026gt; ZGC\n13.8.2. 垃圾回收器组合 不同厂商、不同版本的虚拟机实现差距比较大。HotSpot 虚拟机在 JDK7/8 后所有收集器及组合如下图\n两个收集器间有连线，表明它们可以搭配使用：Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;\n其中 Serial Old 作为 CMS 出现＂Concurrent Mode Failure＂失败的后备预案。\n（红色虚线）由于维护和兼容性测试的成本，在 JDK 8 时将 Serial ＋ CMS、ParNew ＋ Serial old 这两个组合声明为 Deprecated（JEP 173），并在 JDK 9 中\n完全取消了这些组合的支持（JEP214），即：移除。\n（绿色虚线）JDK 14 中：弃用 ParallelScavenge 和 SeriaOold GC 组合(JEP 366)\n（绿色虚框）JDK 14 中：删除 CMS 垃圾回收器（JEP 363）\n13.8.3. 怎么选择垃圾回收器 内存角度：4G以下可以用parallel，4-8G可以用parNew+CMS，8G以上可以用G1，几百G以上可以用ZGC\n业务应用对吞吐量要求较高，对响应时间没有特别要求的，推荐使用并行收集器。如：科学计算和后台处理程序等等。 对响应时间要求较高的中大型应用程序，推荐使用并发收集器。如：web服务器等。 对应JDK版本1.8以上，多CPU处理器且内存资源不是瓶颈，建议优先考虑使用G1回收器。 单线程应用使用串行收集器。 堆内存：\n应用程序运行时，计算老年代存活对象的占用空间大小X。程序整个堆大小（Xmx和Xms）设置为X的34倍；永久代PermSize和MaxPermSize设置为X的1.21.5倍。年轻代Xmn的设置为X的11.5倍。老年代内存大小设置为X的23倍。 JDK官方建议年轻代占整个堆大小空间的3/8左右。 完成一次Full GC后，应该释放出70%的堆空间（30%的空间仍然占用）。 并发量比较大的情况应当适量增大年轻代的空间 Java 垃圾收集器的配置对于 JVM 优化来说是一个很重要的选择，选择合适的垃圾收集器可以让 JVM 的性能有一个很大的提升。\n怎么选择垃圾收集器？\n优先调整堆的大小让 JVM 自适应完成。\n如果内存小于 100M，使用串行收集器\n如果是单核、单机程序，并且没有停顿时间的要求，串行收集器\n如果是多 CPU、需要高吞吐量、允许停顿时间超过 1 秒，选择并行或者 JVM 自己选择\n如果是多 CPU、追求低停顿时间，需快速响应（比如延迟不能超过 1 秒，如互联网应用），使用并发收集器\n官方推荐 G1，性能高。现在互联网的项目，基本都是使用 G1。\n最后需要明确一个观点：\n没有最好的收集器，更没有万能的收集 调优永远是针对特定场景、特定需求，不存在一劳永逸的收集器 面试\n对于垃圾收集，面试官可以循序渐进从理论、实践各种角度深入，也未必是要求面试者什么都懂。但如果你懂得原理，一定会成为面试中的加分项。 这里较通用、基础性的部分如下：\n垃圾收集的算法有哪些？如何判断一个对象是否可以回收？\n垃圾收集器工作的基本流程。\n另外，大家需要多关注垃圾回收器这一章的各种常用的参数\n13.9. GC 日志分析 通过阅读 Gc 日志，我们可以了解 Java 虚拟机内存分配与回收策略。 内存分配与垃圾回收的参数列表\n-XX:+PrintGC 输出 GC 日志。类似：-verbose:gc -XX:+PrintGCDetails 输出 GC 的详细日志 -XX:+PrintGCTimestamps 输出 GC 的时间戳（以基准时间的形式） -XX:+PrintGCDatestamps 输出 GcC 的时间戳（以日期的形式，如 2013-05-04T21：53：59.234+0800） -XX:+PrintHeapAtGC 在进行 GC 的前后打印出堆的信息 -Xloggc:../logs/gc.log 日志文件的输出路径 打开 GC 日志\n1 -verbose:gc Copied! 这个只会显示总的 GC 堆的变化，如下：\n1 2 3 [GC (Allocation Failure) 80832K-\u0026gt;19298K(227840K),0.0084018 secs] [GC (Metadata GC Threshold) 109499K-\u0026gt;21465K(228352K),0.0184066 secs] [Full GC (Metadata GC Threshold) 21465K-\u0026gt;16716K(201728K),0.0619261 secs] Copied! 参数解析\n1 2 3 4 5 GC、Full GC：GC的类型，GC只在新生代上进行，Full GC包括永生代，新生代，老年代。 Allocation Failure：GC发生的原因。 80832K-\u0026gt;19298K：堆在GC前的大小和GC后的大小。 228840k：现在的堆大小。 0.0084018 secs：GC持续的时间。 Copied! 打开 GC 日志\n1 -verbose:gc -XX:+PrintGCDetails Copied! 输入信息如下\n1 2 3 4 5 [GC (Allocation Failure) [PSYoungGen:70640K-\u0026gt;10116K(141312K)] 80541K-\u0026gt;20017K(227328K),0.0172573 secs] [Times:user=0.03 sys=0.00,real=0.02 secs] [GC (Metadata GC Threshold) [PSYoungGen:98859K-\u0026gt;8154K(142336K)] 108760K-\u0026gt;21261K(228352K),0.0151573 secs] [Times:user=0.00 sys=0.01,real=0.02 secs] [Full GC (Metadata GC Threshold)[PSYoungGen:8154K-\u0026gt;0K(142336K)] [ParOldGen:13107K-\u0026gt;16809K(62464K)] 21261K-\u0026gt;16809K(204800K),[Metaspace:20599K-\u0026gt;20599K(1067008K)],0.0639732 secs] [Times:user=0.14 sys=0.00,real=0.06 secs] Copied! 参数解析\n1 2 3 4 5 6 7 GC，Full FC：同样是GC的类型 Allocation Failure：GC原因 PSYoungGen：使用了Parallel Scavenge并行垃圾收集器的新生代GC前后大小的变化 ParOldGen：使用了Parallel Old并行垃圾收集器的老年代GC前后大小的变化 Metaspace： 元数据区GC前后大小的变化，JDK1.8中引入了元数据区以替代永久代 xxx secs：指GC花费的时间 Times：user：指的是垃圾收集器花费的所有CPU时间，sys：花费在等待系统调用或系统事件的时间，real：GC从开始到结束的时间，包括其他进程占用时间片的实际时间。 Copied! 打开 GC 日志\n1 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimestamps -XX:+PrintGCDatestamps Copied! 输入信息如下\n1 2 3 4 5 2019-09-24T22:15:24.518+0800: 3.287: [GC (Allocation Failure) [PSYoungGen:136162K-\u0026gt;5113K(136192K)] 141425K-\u0026gt;17632K(222208K),0.0248249 secs] [Times:user=0.05 sys=0.00,real=0.03 secs] 2019-09-24T22:15:25.559+0800: 4.329: [GC (Metadata GC Threshold) [PSYoungGen:97578K-\u0026gt;10068K(274944K)] 110096K-\u0026gt;22658K(360960K),0.0094071 secs] [Times: user=0.00 sys=0.00,real=0.01 secs] 2019-09-24T22:15:25.569+0800: 4.338: [Full GC (Metadata GC Threshold) [PSYoungGen:10068K-\u0026gt;0K(274944K)][ParoldGen:12590K-\u0026gt;13564K(56320K)] 22658K-\u0026gt;13564K(331264K),[Metaspace:20590K-\u0026gt;20590K(1067008K)],0.0494875 secs] [Times: user=0.17 sys=0.02,real=0.05 secs] Copied! 说明：带上了日期和实践\n如果想把 GC 日志存到文件的话，是下面的参数：\n1 -Xloggc:/path/to/gc.log Copied! 日志补充说明\n\u0026ldquo;[GC\u0026ldquo;和\u0026rdquo;[Full GC\u0026ldquo;说明了这次垃圾收集的停顿类型，如果有\u0026quot;Full\u0026quot;则说明 GC 发生了\u0026quot;Stop The World\u0026rdquo;\n使用 Serial 收集器在新生代的名字是 Default New Generation，因此显示的是\u0026rdquo;[DefNew\u0026rdquo;\n使用 ParNew 收集器在新生代的名字会变成\u0026rdquo;[ParNew\u0026quot;，意思是\u0026quot;Parallel New Generation\u0026quot;\n使用 Parallel scavenge 收集器在新生代的名字是”[PSYoungGen\u0026quot;\n老年代的收集和新生代道理一样，名字也是收集器决定的\n使用 G1 收集器的话，会显示为\u0026quot;garbage-first heap\u0026quot;\nAllocation Failure\n表明本次引起 GC 的原因是因为在年轻代中没有足够的空间能够存储新的数据了。\n[PSYoungGen：5986K-\u0026gt;696K(8704K) ] 5986K-\u0026gt;704K(9216K)\n中括号内：GC 回收前年轻代大小，回收后大小，（年轻代总大小）\n括号外：GC 回收前年轻代和老年代大小，回收后大小，（年轻代和老年代总大小）\nuser 代表用户态回收耗时，sys 内核态回收耗时，rea 实际耗时。由于多核的原因，时间总和可能会超过 real 时间\n1 2 3 4 5 6 7 8 9 10 11 Heap（堆） PSYoungGen（Parallel Scavenge收集器新生代）total 9216K，used 6234K [0x00000000ff600000,0x0000000100000000,0x0000000100000000) eden space（堆中的Eden区默认占比是8）8192K，768 used [0x00000000ff600000,0x00000000ffc16b08,0x00000000ffe00000) from space（堆中的Survivor，这里是From Survivor区默认占比是1）1024K， 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) to space（堆中的Survivor，这里是to Survivor区默认占比是1，需要先了解一下堆的分配策略）1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000) ParOldGen（老年代总大小和使用大小）total 10240K， used 7001K ［0x00000000fec00000,0x00000000ff600000,0x00000000ff600000) object space（显示个使用百分比）10240K，688 used [0x00000000fec00000,0x00000000ff2d6630,0x00000000ff600000) PSPermGen（永久代总大小和使用大小）total 21504K， used 4949K [0x00000000f9a00000,0x00000000faf00000,0x00000000fec00000) object space（显示个使用百分比，自己能算出来）21504K， 238 used [0x00000000f9a00000,0x00000000f9ed55e0,0x00000000faf00000) Copied! Minor GC 日志 Full GC 日志 举例\n1 2 3 4 5 6 7 8 9 10 11 12 13 private static final int _1MB = 1024 * 1024; public static void testAllocation() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation4 = new byte[4 * _1MB]; } public static void main(String[] args) { testAllocation(); } Copied! 设置 JVM 参数\n1 -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseParallelGC Copied! 图示\n上图是JDK1.7情况：Eden区已经有三个2M对象，这时新创建的4M对象无法放入Eden，触发minorGC。因为from和to区都是1M的空间，Eden中的每个2M对象都放不进Survivor区，所以会放入老年代\nJDK1.8情况：Eden区已经有三个2M对象，这时新创建的4M对象无法放入Eden，这时会判断新增的对象是否大于Eden区总空间的二分之一，然后直接把4M对象放入老年代\n可以用一些工具去分析这些 GC 日志\n将GC日志保存到文件：\n1 -Xloggc:/path/to/gc.log Copied! 常用的日志分析工具有：GCViewer、GCEasy、GCHisto、GCLogViewer、Hpjmeter、garbagecat 等\n13.X. 垃圾回收器的新发展 GC 仍然处于飞速发展之中，目前的默认选项G1 GC 在不断的进行改进，很多我们原来认为的缺点，例如串行的 Fu11GC、Card Table 扫描的低效等，都已经被大幅改进，例如，JDK10 以后，Fu11GC 已经是并行运行，在很多场景下，其表现还略优于 ParallelGC 的并行 Ful1GC 实现。\n即使是 Serial GC，虽然比较古老，但是简单的设计和实现未必就是过时的，它本身的开销，不管是 GC 相关数据结构的开销，还是线程的开销，都是非常小的，所以随着云计算的兴起，在 Serverless 等新的应用场景下，Serial GC 找到了新的舞台。\n比较不幸的是 CMSGC，因为其算法的理论缺陷等原因，虽然现在还有非常大的用户群体，但在 JDK9 中已经被标记为废弃，并在 JDK14 版本中移除\n13.X.1. JDK11 新特性 Epsilon:A No-Op GarbageCollector（Epsilon 垃圾回收器，\u0026ldquo;No-Op（无操作）\u0026ldquo;回收器）http://openidk.iava.net/jeps/318 ZGC:A Scalable Low-Latency Garbage Collector（Experimental）（ZGC：可伸缩的低延迟垃圾回收器，处于实验性阶段）http://openidk.iava.net/jeps/333 现在 G1 回收器已成为默认回收器好几年了。\n我们还看到了引入了两个新的收集器：ZGC（JDK11 出现）和 Shenandoah（Open JDK12）。主打特点：低停顿时间\n13.X.2. Open JDK12 的 Shenandoash GC Open JDK12 的 Shenandoash GC：低停顿时间的 GC（实验性）\nShenandoah，无疑是众多 GC 中最孤独的一个。是第一款不由 oracle 公司团队领导开发的 Hotspot 垃圾收集器。不可避免的受到官方的排挤。比如号称 OpenJDK 和 OracleJDK 没有区别的 Oracle 公司仍拒绝在 OracleJDK12 中支持 Shenandoah。\nShenandoah 垃圾回收器最初由 RedHat 进行的一项垃圾收集器研究项目 Pauseless GC 的实现，旨在针对 JVM 上的内存回收实现低停顿的需求.。在 2014 年贡献给 OpenJDK。\nRed Hat 研发 Shenandoah 团队对外宣称，Shenandoah 垃圾回收器的暂停时间与堆大小无关，这意味着无论将堆设置为 200MB 还是 200GB，99.9%的目标都可以把垃圾收集的停顿时间限制在十毫秒以内。不过实际使用性能将取决于实际工作堆的大小和工作负载。\n这是 RedHat 在 2016 年发表的论文数据，测试内容是使用 Es 对 200GB 的维基百科数据进行索引。从结果看：\n停顿时间比其他几款收集器确实有了质的飞跃，但也未实现最大停顿时间控制在十毫秒以内的目标。 而吞吐量方面出现了明显的下降，总运行时间是所有测试收集器里最长的。 总结\nShenandoah GC 的弱项：高运行负担下的吞吐量下降。 Shenandoah GC 的强项：低延迟时间。 Shenandoah GC 的工作过程大致分为九个阶段，这里就不再赘述。在之前 Java12 新特性视频里有过介绍。 【Java12 新特性地址】\nhttp://www.atguigu.com/download_detail.shtml?v=222 或\nhttps://www.bilibili.com/video/BV1jJ411M7kQ?from=search\u0026amp;seid=12339069673726242866 13.X.3. 令人震惊、革命性的 ZGC 官方地址：https://docs.oracle.com/en/java/javase/12/gctuning/ ZGC 与 Shenandoah 目标高度相似，在尽可能对吞吐量影响不大的前提下，实现在任意堆内存大小下都可以把垃圾收集的停颇时间限制在十毫秒以内的低延迟。\n《深入理解 Java 虚拟机》一书中这样定义 ZGC：ZGC 收集器是一款基于 Region 内存布局的，（暂时）不设分代的，使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-压缩算法的，以低延迟为首要目标的一款垃圾收集器。\nZGC 的工作过程可以分为 4 个阶段：并发标记 - 并发预备重分配 - 并发重分配 - 并发重映射 等。\nZGC 几乎在所有地方并发执行的，除了初始标记的是 STw 的。所以停顿时间几乎就耗费在初始标记上，这部分的实际时间是非常少的。\n测试数据：\n在 ZGC 的强项停顿时间测试上，它毫不留情的将 Parallel、G1 拉开了两个数量级的差距。无论平均停顿、95％停顿、99％停顿、99.9％停顿，还是最大停顿时间，ZGC 都能毫不费劲控制在 10 毫秒以内。\n虽然 ZGC 还在试验状态，没有完成所有特性，但此时性能已经相当亮眼，用“令人震惊、革命性”来形容，不为过。 未来将在服务端、大内存、低延迟应用的首选垃圾收集器。\nJEP 364：ZGC 应用在 macos 上\nJEP 365：ZGC 应用在 Windows 上\nJDK14 之前，ZGC 仅 Linux 才支持。\n尽管许多使用 zGc 的用户都使用类 Linux 的环境，但在 Windows 和 macos 上，人们也需要 ZGC 进行开发部署和测试。许多桌面应用也可以从 ZGC 中受益。因此，ZGC 特性被移植到了 Windows 和 macos 上。\n现在 mac 或 Windows 上也能使用 zGC 了，示例如下：\n1 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC Copied! 颜色指针 有浮动垃圾 不能指针压缩 使用读屏障 Marked1和Marked0标示颜色\n13.X.4. 其他垃圾回收器：AliGC AliGC 是阿里巴巴 JVM 团队基于 G1 算法，面向大堆（LargeHeap）应用场景。指定场景下的对比：\n当然，其它厂商也提供了各种别具一格的 GC 实现，例如比较有名的低延迟 GC：Zing，有兴趣可以参考提供的链接 https://www.infoq.com/articles/azul_gc_in_detail ","date":"2023-05-03T03:17:32+08:00","permalink":"https://qh.1357810.xyz/p/2023/05/17845g17/","title":"13-垃圾回收器"},{"content":" SpringCloud01 1.认识微服务 随着互联网行业的发展，对服务的要求也越来越高，服务架构也从单体架构逐渐演变为现在流行的微服务架构。这些架构之间有怎样的差别呢？\n1.0.学习目标 了解微服务架构的优缺点\n1.1.单体架构 单体架构：将业务的所有功能集中在一个项目中开发，打成一个包部署。\n单体架构的优缺点如下：\n优点：\n架构简单 部署成本低 缺点：\n耦合度高（维护困难、升级困难） 1.2.分布式架构 分布式架构：根据业务功能对系统做拆分，每个业务功能模块作为独立项目开发，称为一个服务。\n分布式架构的优缺点：\n优点：\n降低服务耦合 有利于服务升级和拓展 缺点：\n服务调用关系错综复杂 分布式架构虽然降低了服务耦合，但是服务拆分时也有很多问题需要思考：\n服务拆分的粒度如何界定？ 服务之间如何调用？ 服务的调用关系如何管理？ 人们需要制定一套行之有效的标准来约束分布式架构。\n1.3.微服务 微服务的架构特征：\n单一职责：微服务拆分粒度更小，每一个服务都对应唯一的业务能力，做到单一职责 自治：团队独立、技术独立、数据独立，独立部署和交付 面向服务：服务提供统一标准的接口，与语言和技术无关 隔离性强：服务调用做好隔离、容错、降级，避免出现级联问题 微服务的上述特性其实是在给分布式架构制定一个标准，进一步降低服务之间的耦合度，提供服务的独立性和灵活性。做到高内聚，低耦合。\n因此，可以认为微服务是一种经过良好架构设计的分布式架构方案 。\n但方案该怎么落地？选用什么样的技术栈？全球的互联网公司都在积极尝试自己的微服务落地方案。\n其中在Java领域最引人注目的就是SpringCloud提供的方案了。\n1.4.SpringCloud SpringCloud是目前国内使用最广泛的微服务框架。官网地址：https://spring.io/projects/spring-cloud。\nSpringCloud集成了各种微服务功能组件，并基于SpringBoot实现了这些组件的自动装配，从而提供了良好的开箱即用体验。\n其中常见的组件包括：\n另外，SpringCloud底层是依赖于SpringBoot的，并且有版本的兼容关系，如下：\n我们课堂学习的版本是 Hoxton.SR10，因此对应的SpringBoot版本是2.3.x版本。\n1.5.总结 单体架构：简单方便，高度耦合，扩展性差，适合小型项目。例如：学生管理系统\n分布式架构：松耦合，扩展性好，但架构复杂，难度大。适合大型互联网项目，例如：京东、淘宝\n微服务：一种良好的分布式架构方案\n①优点：拆分粒度更小、服务更独立、耦合度更低\n②缺点：架构非常复杂，运维、监控、部署难度提高\nSpringCloud是微服务架构的一站式解决方案，集成了各种优秀微服务功能组件\n2.服务拆分和远程调用 任何分布式架构都离不开服务的拆分，微服务也是一样。\n2.1.服务拆分原则 这里我总结了微服务拆分时的几个原则：\n不同微服务，不要重复开发相同业务 微服务数据独立，不要访问其它微服务的数据库 微服务可以将自己的业务暴露为接口，供其它微服务调用 2.2.服务拆分示例 以课前资料中的微服务cloud-demo为例，其结构如下：\ncloud-demo：父工程，管理依赖\norder-service：订单微服务，负责订单相关业务 user-service：用户微服务，负责用户相关业务 要求：\n订单微服务和用户微服务都必须有各自的数据库，相互独立 订单服务和用户服务都对外暴露Restful的接口 订单服务如果需要查询用户信息，只能调用用户服务的Restful接口，不能查询用户数据库 2.2.1.导入Sql语句 首先，将课前资料提供的cloud-order.sql和cloud-user.sql导入到mysql中：\ncloud-user表中初始数据如下：\ncloud-order表中初始数据如下：\ncloud-order表中持有cloud-user表中的id字段。\n2.2.2.导入demo工程 用IDEA导入课前资料提供的Demo：\n项目结构如下：\n导入后，会在IDEA右下角出现弹窗：\n点击弹窗，然后按下图选择：\n会出现这样的菜单：\n配置下项目使用的JDK：\n2.3.实现远程调用案例 在order-service服务中，有一个根据id查询订单的接口：\n根据id查询订单，返回值是Order对象，如图：\n其中的user为null\n在user-service中有一个根据id查询用户的接口：\n查询的结果如图：\n2.3.1.案例需求： 修改order-service中的根据id查询订单业务，要求在查询订单的同时，根据订单中包含的userId查询出用户信息，一起返回。\n因此，我们需要在order-service中 向user-service发起一个http的请求，调用http://localhost:8081/user/{userId}这个接口。\n大概的步骤是这样的：\n注册一个RestTemplate的实例到Spring容器 修改order-service服务中的OrderService类中的queryOrderById方法，根据Order对象中的userId查询User 将查询的User填充到Order对象，一起返回 2.3.2.注册RestTemplate 首先，我们在order-service服务中的OrderApplication启动类中，注册RestTemplate实例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package cn.itcast.order; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @MapperScan(\u0026#34;cn.itcast.order.mapper\u0026#34;) @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } Copied! 2.3.3.实现远程调用 修改order-service服务中的cn.itcast.order.service包下的OrderService类中的queryOrderById方法：\n2.4.提供者与消费者 在服务调用关系中，会有两个不同的角色：\n服务提供者：一次业务中，被其它微服务调用的服务。（提供接口给其它微服务）\n服务消费者：一次业务中，调用其它微服务的服务。（调用其它微服务提供的接口）\n但是，服务提供者与服务消费者的角色并不是绝对的，而是相对于业务而言。\n如果服务A调用了服务B，而服务B又调用了服务C，服务B的角色是什么？\n对于A调用B的业务而言：A是服务消费者，B是服务提供者 对于B调用C的业务而言：B是服务消费者，C是服务提供者 因此，服务B既可以是服务提供者，也可以是服务消费者。\n3.Eureka注册中心 假如我们的服务提供者user-service部署了多个实例，如图：\n大家思考几个问题：\norder-service在发起远程调用的时候，该如何得知user-service实例的ip地址和端口？ 有多个user-service实例地址，order-service调用时该如何选择？ order-service如何得知某个user-service实例是否依然健康，是不是已经宕机？ 3.1.Eureka的结构和作用 这些问题都需要利用SpringCloud中的注册中心来解决，其中最广为人知的注册中心就是Eureka，其结构如下：\n回答之前的各个问题。\n问题1：order-service如何得知user-service实例地址？\n获取地址信息的流程如下：\nuser-service服务实例启动后，将自己的信息注册到eureka-server（Eureka服务端）。这个叫服务注册 eureka-server保存服务名称到服务实例地址列表的映射关系 order-service根据服务名称，拉取实例地址列表。这个叫服务发现或服务拉取 问题2：order-service如何从多个user-service实例中选择具体的实例？\norder-service从实例列表中利用负载均衡算法选中一个实例地址 向该实例地址发起远程调用 问题3：order-service如何得知某个user-service实例是否依然健康，是不是已经宕机？\nuser-service会每隔一段时间（默认30秒）向eureka-server发起请求，报告自己状态，称为心跳 当超过一定时间没有发送心跳时，eureka-server会认为微服务实例故障，将该实例从服务列表中剔除 order-service拉取服务时，就能将故障实例排除了 注意：一个微服务，既可以是服务提供者，又可以是服务消费者，因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端\n因此，接下来我们动手实践的步骤包括：\n3.2.搭建eureka-server 首先大家注册中心服务端：eureka-server，这必须是一个独立的微服务\n3.2.1.创建eureka-server服务 在cloud-demo父工程下，创建一个子模块：\n填写模块信息：\n然后填写服务信息：\n3.2.2.引入eureka依赖 引入SpringCloud为eureka提供的starter依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-eureka-server\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 3.2.3.编写启动类 给eureka-server服务编写一个启动类，一定要添加一个@EnableEurekaServer注解，开启eureka的注册中心功能：\n1 2 3 4 5 6 7 8 9 10 11 12 13 package cn.itcast.eureka; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } } Copied! 3.2.4.编写配置文件 编写一个application.yml文件，内容如下：\n1 2 3 4 5 6 7 8 9 server: port: 10086 spring: application: name: eureka-server eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka Copied! 3.2.5.启动服务 启动微服务，然后在浏览器访问：http://127.0.0.1:10086\n看到下面结果应该是成功了：\n3.3.服务注册 下面，我们将user-service注册到eureka-server中去。\n1）引入依赖 在user-service的pom文件中，引入下面的eureka-client依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-eureka-client\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 2）配置文件 在user-service中，修改application.yml文件，添加服务名称、eureka地址：\n1 2 3 4 5 6 7 spring: application: name: userservice eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka Copied! 3）启动多个user-service实例 为了演示一个服务有多个实例的场景，我们添加一个SpringBoot的启动配置，再启动一个user-service。\n首先，复制原来的user-service启动配置：\n然后，在弹出的窗口中，填写信息：\n现在，SpringBoot窗口会出现两个user-service启动配置：\n不过，第一个是8081端口，第二个是8082端口。\n启动两个user-service实例：\n查看eureka-server管理页面：\n3.4.服务发现 下面，我们将order-service的逻辑修改：向eureka-server拉取user-service的信息，实现服务发现。\n1）引入依赖 之前说过，服务发现、服务注册统一都封装在eureka-client依赖，因此这一步与服务注册时一致。\n在order-service的pom文件中，引入下面的eureka-client依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-eureka-client\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 2）配置文件 服务发现也需要知道eureka地址，因此第二步与服务注册一致，都是配置eureka信息：\n在order-service中，修改application.yml文件，添加服务名称、eureka地址：\n1 2 3 4 5 6 7 spring: application: name: orderservice eureka: client: service-url: defaultZone: http://127.0.0.1:10086/eureka Copied! 3）服务拉取和负载均衡 最后，我们要去eureka-server中拉取user-service服务的实例列表，并且实现负载均衡。\n不过这些动作不用我们去做，只需要添加一些注解即可。\n在order-service的OrderApplication中，给RestTemplate这个Bean添加一个@LoadBalanced注解：\n修改order-service服务中的cn.itcast.order.service包下的OrderService类中的queryOrderById方法。修改访问的url路径，用服务名代替ip、端口：\nspring会自动帮助我们从eureka-server端，根据userservice这个服务名称，获取实例列表，而后完成负载均衡。\n4.Ribbon负载均衡 上一节中，我们添加了@LoadBalanced注解，即可实现负载均衡功能，这是什么原理呢？\n4.1.负载均衡原理 SpringCloud底层其实是利用了一个名为Ribbon的组件，来实现负载均衡功能的。\n那么我们发出的请求明明是http://userservice/user/1，怎么变成了http://localhost:8081的呢？\n4.2.源码跟踪 为什么我们只输入了service名称就可以访问了呢？之前还要获取ip和端口。\n显然有人帮我们根据service名称，获取到了服务实例的ip和端口。它就是LoadBalancerInterceptor，这个类会在对RestTemplate的请求进行拦截，然后从Eureka根据服务id获取服务列表，随后利用负载均衡算法得到真实的服务地址信息，替换服务id。\n我们进行源码跟踪：\n1）LoadBalancerIntercepor 可以看到这里的intercept方法，拦截了用户的HttpRequest请求，然后做了几件事：\nrequest.getURI()：获取请求uri，本例中就是 http://user-service/user/8 originalUri.getHost()：获取uri路径的主机名，其实就是服务id，user-service this.loadBalancer.execute()：处理服务id，和用户请求。 这里的this.loadBalancer是LoadBalancerClient类型，我们继续跟入。\n2）LoadBalancerClient 继续跟入execute方法：\n代码是这样的：\ngetLoadBalancer(serviceId)：根据服务id获取ILoadBalancer，而ILoadBalancer会拿着服务id去eureka中获取服务列表并保存起来。 getServer(loadBalancer)：利用内置的负载均衡算法，从服务列表中选择一个。本例中，可以看到获取了8082端口的服务 放行后，再次访问并跟踪，发现获取的是8081：\n果然实现了负载均衡。\n3）负载均衡策略IRule 在刚才的代码中，可以看到获取服务使通过一个getServer方法来做负载均衡:\n我们继续跟入：\n继续跟踪源码chooseServer方法，发现这么一段代码：\n我们看看这个rule是谁：\n这里的rule默认值是一个RoundRobinRule，看类的介绍：\n这不就是轮询的意思嘛。\n到这里，整个负载均衡的流程我们就清楚了。\n4）总结 SpringCloudRibbon的底层采用了一个拦截器，拦截了RestTemplate发出的请求，对地址做了修改。用一幅图来总结一下：\n基本流程如下：\n拦截我们的RestTemplate请求http://userservice/user/1 RibbonLoadBalancerClient会从请求url中获取服务名称，也就是user-service DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表 eureka返回列表，localhost:8081、localhost:8082 IRule利用内置负载均衡规则，从列表中选择一个，例如localhost:8081 RibbonLoadBalancerClient修改请求地址，用localhost:8081替代userservice，得到http://localhost:8081/user/1，发起真实请求 4.3.负载均衡策略 4.3.1.负载均衡策略 负载均衡的规则都定义在IRule接口中，而IRule有很多不同的实现类：\n不同规则的含义如下：\n内置负载均衡规则类 规则描述 RoundRobinRule 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 AvailabilityFilteringRule 对以下两种服务器进行忽略： （1）在默认情况下，这台服务器如果3次连接失败，这台服务器就会被设置为“短路”状态。短路状态将持续30秒，如果再次连接失败，短路的持续时间就会几何级地增加。 （2）并发数过高的服务器。如果一个服务器的并发连接数过高，配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限，可以由客户端的..ActiveConnectionsLimit属性进行配置。 WeightedResponseTimeRule 为每一个服务器赋予一个权重值。服务器响应时间越长，这个服务器的权重就越小。这个规则会随机选择服务器，这个权重值会影响服务器的选择。 ZoneAvoidanceRule 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类，这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 BestAvailableRule 忽略那些短路的服务器，并选择并发数较低的服务器。 RandomRule 随机选择一个可用的服务器。 RetryRule 重试机制的选择逻辑 默认的实现就是ZoneAvoidanceRule，是一种轮询方案\n4.3.2.自定义负载均衡策略 通过定义IRule实现可以修改负载均衡规则，有两种方式：\n代码方式：在order-service中的OrderApplication类中，定义一个新的IRule： 1 2 3 4 @Bean public IRule randomRule(){ return new RandomRule(); } Copied! 配置文件方式：在order-service的application.yml文件中，添加新的配置也可以修改规则： 1 2 3 userservice: # 给某个微服务配置负载均衡规则，这里是userservice服务 ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则 Copied! 注意，一般用默认的负载均衡规则，不做修改。\n4.4.饥饿加载 Ribbon默认是采用懒加载，即第一次访问时才会去创建LoadBalanceClient，请求时间会很长。\n而饥饿加载则会在项目启动时创建，降低第一次访问的耗时，通过下面配置开启饥饿加载：\n1 2 3 4 ribbon: eager-load: enabled: true clients: userservice Copied! 5.Nacos注册中心 国内公司一般都推崇阿里巴巴的技术，比如注册中心，SpringCloudAlibaba也推出了一个名为Nacos的注册中心。\n5.1.认识和安装Nacos Nacos 是阿里巴巴的产品，现在是SpringCloud 中的一个组件。相比Eureka 功能更加丰富，在国内受欢迎程度较高。\n安装方式可以参考课前资料《Nacos安装指南.md》\n5.2.服务注册到nacos Nacos是SpringCloudAlibaba的组件，而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说，并没有太大区别。\n主要差异在于：\n依赖不同 服务地址不同 1）引入依赖 在cloud-demo父工程的pom文件中的\u0026lt;dependencyManagement\u0026gt;中引入SpringCloudAlibaba的依赖：\n1 2 3 4 5 6 7 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-alibaba-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.2.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 然后在user-service和order-service中的pom文件中引入nacos-discovery依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-nacos-discovery\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 注意：不要忘了注释掉eureka的依赖。\n2）配置nacos地址 在user-service和order-service的application.yml中添加nacos地址：\n1 2 3 4 spring: cloud: nacos: server-addr: localhost:8848 Copied! 注意：不要忘了注释掉eureka的地址\n3）重启 重启微服务后，登录nacos管理页面，可以看到微服务信息：\n5.3.服务分级存储模型 一个服务可以有多个实例，例如我们的user-service，可以有:\n127.0.0.1:8081 127.0.0.1:8082 127.0.0.1:8083 假如这些实例分布于全国各地的不同机房，例如：\n127.0.0.1:8081，在上海机房 127.0.0.1:8082，在上海机房 127.0.0.1:8083，在杭州机房 Nacos就将同一机房内的实例 划分为一个集群。\n也就是说，user-service是服务，一个服务可以包含多个集群，如杭州、上海，每个集群下可以有多个实例，形成分级模型，如图：\n微服务互相访问时，应该尽可能访问同集群实例，因为本地访问速度更快。当本集群内不可用时，才访问其它集群。例如：\n杭州机房内的order-service应该优先访问同机房的user-service。\n5.3.1.给user-service配置集群 修改user-service的application.yml文件，添加集群配置：\n1 2 3 4 5 6 spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ # 集群名称 Copied! 重启两个user-service实例后，我们可以在nacos控制台看到下面结果：\n我们再次复制一个user-service启动配置，添加属性：\n1 -Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH Copied! 配置如图所示：\n启动UserApplication3后再次查看nacos控制台：\n5.3.2.同集群优先的负载均衡 默认的ZoneAvoidanceRule并不能实现根据同集群优先来实现负载均衡。\n因此Nacos中提供了一个NacosRule的实现，可以优先从同集群中挑选实例。\n1）给order-service配置集群信息\n修改order-service的application.yml文件，添加集群配置：\n1 2 3 4 5 6 spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ # 集群名称 Copied! 2）修改负载均衡规则\n修改order-service的application.yml文件，修改负载均衡规则：\n1 2 3 userservice: ribbon: NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则 Copied! 5.4.权重配置 实际部署中会出现这样的场景：\n服务器设备性能有差异，部分实例所在机器性能较好，另一些较差，我们希望性能好的机器承担更多的用户请求。\n但默认情况下NacosRule是同集群内随机挑选，不会考虑机器的性能问题。\n因此，Nacos提供了权重配置来控制访问频率，权重越大则访问频率越高。\n在nacos控制台，找到user-service的实例列表，点击编辑，即可修改权重：\n在弹出的编辑窗口，修改权重：\n注意：如果权重修改为0，则该实例永远不会被访问\n5.5.环境隔离 Nacos提供了namespace来实现环境隔离功能。\nnacos中可以有多个namespace namespace下可以有group、service等 不同namespace之间相互隔离，例如不同namespace的服务互相不可见 5.5.1.创建namespace 默认情况下，所有service、data、group都在同一个namespace，名为public：\n我们可以点击页面新增按钮，添加一个namespace：\n然后，填写表单：\n就能在页面看到一个新的namespace：\n5.5.2.给微服务配置namespace 给微服务配置namespace只能通过修改配置来实现。\n例如，修改order-service的application.yml文件：\n1 2 3 4 5 6 7 spring: cloud: nacos: server-addr: localhost:8848 discovery: cluster-name: HZ namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间，填ID Copied! 重启order-service后，访问控制台，可以看到下面的结果：\n此时访问order-service，因为namespace不同，会导致找不到userservice，控制台会报错：\n5.6.Nacos与Eureka的区别 Nacos的服务实例分为两种l类型：\n临时实例：如果实例宕机超过一定时间，会从服务列表剔除，默认的类型。\n非临时实例：如果实例宕机，不会从服务列表剔除，也可以叫永久实例。\n配置一个服务实例为永久实例：\n1 2 3 4 5 spring: cloud: nacos: discovery: ephemeral: false # 设置为非临时实例 Copied! Nacos和Eureka整体结构类似，服务注册、服务拉取、心跳等待，但是也存在一些差异：\nNacos与eureka的共同点\n都支持服务注册和服务拉取 都支持服务提供者心跳方式做健康检测 Nacos与Eureka的区别\nNacos支持服务端主动检测提供者状态：临时实例采用心跳模式，非临时实例采用主动检测模式 临时实例心跳不正常会被剔除，非临时实例则不会被剔除 Nacos支持服务列表变更的消息推送模式，服务列表更新更及时 Nacos集群默认采用AP方式，当集群中存在非临时实例时，采用CP模式；Eureka采用AP方式 ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/89c61189/","title":"1.SpringCloud实用篇01"},{"content":" Redis集群 本章是基于CentOS7下的Redis集群教程，包括：\n单机安装Redis Redis主从 Redis分片集群 1.单机安装Redis 首先需要安装Redis所需要的依赖：\n1 yum install -y gcc tcl Copied! 然后将课前资料提供的Redis安装包上传到虚拟机的任意目录：\n例如，我放到了/tmp目录：\n解压缩：\n1 tar -xvf redis-6.2.4.tar.gz Copied! 解压后：\n进入redis目录：\n1 cd redis-6.2.4 Copied! 运行编译命令：\n1 make \u0026amp;\u0026amp; make install Copied! 如果没有出错，应该就安装成功了。\n然后修改redis.conf文件中的一些配置：\n1 2 3 4 # 绑定地址，默认是127.0.0.1，会导致只能在本地访问。修改为0.0.0.0则可以在任意IP访问 bind 0.0.0.0 # 数据库数量，设置为1 databases 1 Copied! 启动Redis：\n1 redis-server redis.conf Copied! 停止redis服务：\n1 redis-cli shutdown Copied! 2.Redis主从集群 2.1.集群结构 我们搭建的主从集群结构如图：\n共包含三个节点，一个主节点，两个从节点。\n这里我们会在同一台虚拟机中开启3个redis实例，模拟主从集群，信息如下：\nIP PORT 角色 192.168.150.101 7001 master 192.168.150.101 7002 slave 192.168.150.101 7003 slave 2.2.准备实例和配置 要在同一台虚拟机开启3个实例，必须准备三份不同的配置文件和目录，配置文件所在目录也就是工作目录。\n1）创建目录\n我们创建三个文件夹，名字分别叫7001、7002、7003：\n1 2 3 4 # 进入/tmp目录 cd /tmp # 创建目录 mkdir 7001 7002 7003 Copied! 如图：\n2）恢复原始配置\n修改redis-6.2.4/redis.conf文件，将其中的持久化模式改为默认的RDB模式，AOF保持关闭状态。\n1 2 3 4 5 6 7 8 # 开启RDB # save \u0026#34;\u0026#34; save 3600 1 save 300 100 save 60 10000 # 关闭AOF appendonly no Copied! 3）拷贝配置文件到每个实例目录\n然后将redis-6.2.4/redis.conf文件拷贝到三个目录中（在/tmp目录执行下列命令）：\n1 2 3 4 5 6 # 方式一：逐个拷贝 cp redis-6.2.4/redis.conf 7001 cp redis-6.2.4/redis.conf 7002 cp redis-6.2.4/redis.conf 7003 # 方式二：管道组合命令，一键拷贝 echo 7001 7002 7003 | xargs -t -n 1 cp redis-6.2.4/redis.conf Copied! 4）修改每个实例的端口、工作目录\n修改每个文件夹内的配置文件，将端口分别修改为7001、7002、7003，将rdb文件保存位置都修改为自己所在目录（在/tmp目录执行下列命令）：\n1 2 3 sed -i -e \u0026#39;s/6379/7001/g\u0026#39; -e \u0026#39;s/dir .\\//dir \\/tmp\\/7001\\//g\u0026#39; 7001/redis.conf sed -i -e \u0026#39;s/6379/7002/g\u0026#39; -e \u0026#39;s/dir .\\//dir \\/tmp\\/7002\\//g\u0026#39; 7002/redis.conf sed -i -e \u0026#39;s/6379/7003/g\u0026#39; -e \u0026#39;s/dir .\\//dir \\/tmp\\/7003\\//g\u0026#39; 7003/redis.conf Copied! 5）修改每个实例的声明IP\n虚拟机本身有多个IP，为了避免将来混乱，我们需要在redis.conf文件中指定每一个实例的绑定ip信息，格式如下：\n1 2 # redis实例的声明 IP replica-announce-ip 192.168.150.101 Copied! 每个目录都要改，我们一键完成修改（在/tmp目录执行下列命令）：\n1 2 3 4 5 6 7 # 逐一执行 sed -i \u0026#39;1a replica-announce-ip 192.168.150.101\u0026#39; 7001/redis.conf sed -i \u0026#39;1a replica-announce-ip 192.168.150.101\u0026#39; 7002/redis.conf sed -i \u0026#39;1a replica-announce-ip 192.168.150.101\u0026#39; 7003/redis.conf # 或者一键修改 printf \u0026#39;%s\\n\u0026#39; 7001 7002 7003 | xargs -I{} -t sed -i \u0026#39;1a replica-announce-ip 192.168.150.101\u0026#39; {}/redis.conf Copied! 2.3.启动 为了方便查看日志，我们打开3个ssh窗口，分别启动3个redis实例，启动命令：\n1 2 3 4 5 6 # 第1个 redis-server 7001/redis.conf # 第2个 redis-server 7002/redis.conf # 第3个 redis-server 7003/redis.conf Copied! 启动后：\n如果要一键停止，可以运行下面命令：\n1 printf \u0026#39;%s\\n\u0026#39; 7001 7002 7003 | xargs -I{} -t redis-cli -p {} shutdown Copied! 2.4.开启主从关系 现在三个实例还没有任何关系，要配置主从可以使用replicaof 或者slaveof（5.0以前）命令。\n有临时和永久两种模式：\n修改配置文件（永久生效）\n在redis.conf中添加一行配置：slaveof \u0026lt;masterip\u0026gt; \u0026lt;masterport\u0026gt; 使用redis-cli客户端连接到redis服务，执行slaveof命令（重启后失效）：\n1 slaveof \u0026lt;masterip\u0026gt; \u0026lt;masterport\u0026gt; Copied! 注意：在5.0以后新增命令replicaof，与salveof效果一致。\n这里我们为了演示方便，使用方式二。\n通过redis-cli命令连接7002，执行下面命令：\n1 2 3 4 # 连接 7002 redis-cli -p 7002 # 执行slaveof slaveof 192.168.150.101 7001 Copied! 通过redis-cli命令连接7003，执行下面命令：\n1 2 3 4 # 连接 7003 redis-cli -p 7003 # 执行slaveof slaveof 192.168.150.101 7001 Copied! 然后连接 7001节点，查看集群状态：\n1 2 3 4 # 连接 7001 redis-cli -p 7001 # 查看状态 info replication Copied! 结果：\n2.5.测试 执行下列操作以测试：\n利用redis-cli连接7001，执行set num 123\n利用redis-cli连接7002，执行get num，再执行set num 666\n利用redis-cli连接7003，执行get num，再执行set num 888\n可以发现，只有在7001这个master节点上可以执行写操作，7002和7003这两个slave节点只能执行读操作。\n3.搭建哨兵集群 3.1.集群结构 这里我们搭建一个三节点形成的Sentinel集群，来监管之前的Redis主从集群。如图：\n三个sentinel实例信息如下：\n节点 IP PORT s1 192.168.150.101 27001 s2 192.168.150.101 27002 s3 192.168.150.101 27003 3.2.准备实例和配置 要在同一台虚拟机开启3个实例，必须准备三份不同的配置文件和目录，配置文件所在目录也就是工作目录。\n我们创建三个文件夹，名字分别叫s1、s2、s3：\n1 2 3 4 # 进入/tmp目录 cd /tmp # 创建目录 mkdir s1 s2 s3 Copied! 如图：\n然后我们在s1目录创建一个sentinel.conf文件，添加下面的内容：\n1 2 3 4 5 6 port 27001 sentinel announce-ip 192.168.150.101 sentinel monitor mymaster 192.168.150.101 7001 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 60000 dir \u0026#34;/tmp/s1\u0026#34; Copied! 解读：\nport 27001：是当前sentinel实例的端口 sentinel monitor mymaster 192.168.150.101 7001 2：指定主节点信息 mymaster：主节点名称，自定义，任意写 192.168.150.101 7001：主节点的ip和端口 2：选举master时的quorum值 然后将s1/sentinel.conf文件拷贝到s2、s3两个目录中（在/tmp目录执行下列命令）：\n1 2 3 4 5 # 方式一：逐个拷贝 cp s1/sentinel.conf s2 cp s1/sentinel.conf s3 # 方式二：管道组合命令，一键拷贝 echo s2 s3 | xargs -t -n 1 cp s1/sentinel.conf Copied! 修改s2、s3两个文件夹内的配置文件，将端口分别修改为27002、27003：\n1 2 sed -i -e \u0026#39;s/27001/27002/g\u0026#39; -e \u0026#39;s/s1/s2/g\u0026#39; s2/sentinel.conf sed -i -e \u0026#39;s/27001/27003/g\u0026#39; -e \u0026#39;s/s1/s3/g\u0026#39; s3/sentinel.conf Copied! 3.3.启动 为了方便查看日志，我们打开3个ssh窗口，分别启动3个redis实例，启动命令：\n1 2 3 4 5 6 # 第1个 redis-sentinel s1/sentinel.conf # 第2个 redis-sentinel s2/sentinel.conf # 第3个 redis-sentinel s3/sentinel.conf Copied! 启动后：\n3.4.测试 尝试让master节点7001宕机，查看sentinel日志：\n查看7003的日志：\n查看7002的日志：\n4.搭建分片集群 4.1.集群结构 分片集群需要的节点数量较多，这里我们搭建一个最小的分片集群，包含3个master节点，每个master包含一个slave节点，结构如下：\n这里我们会在同一台虚拟机中开启6个redis实例，模拟分片集群，信息如下：\nIP PORT 角色 192.168.150.101 7001 master 192.168.150.101 7002 master 192.168.150.101 7003 master 192.168.150.101 8001 slave 192.168.150.101 8002 slave 192.168.150.101 8003 slave 4.2.准备实例和配置 删除之前的7001、7002、7003这几个目录，重新创建出7001、7002、7003、8001、8002、8003目录：\n1 2 3 4 5 6 # 进入/tmp目录 cd /tmp # 删除旧的，避免配置干扰 rm -rf 7001 7002 7003 # 创建目录 mkdir 7001 7002 7003 8001 8002 8003 Copied! 在/tmp下准备一个新的redis.conf文件，内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 port 6379 # 开启集群功能 cluster-enabled yes # 集群的配置文件名称，不需要我们创建，由redis自己维护 cluster-config-file /tmp/6379/nodes.conf # 节点心跳失败的超时时间 cluster-node-timeout 5000 # 持久化文件存放目录 dir /tmp/6379 # 绑定地址 bind 0.0.0.0 # 让redis后台运行 daemonize yes # 注册的实例ip replica-announce-ip 192.168.150.101 # 保护模式 protected-mode no # 数据库数量 databases 1 # 日志 logfile /tmp/6379/run.log Copied! 将这个文件拷贝到每个目录下：\n1 2 3 4 # 进入/tmp目录 cd /tmp # 执行拷贝 echo 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 cp redis.conf Copied! 修改每个目录下的redis.conf，将其中的6379修改为与所在目录一致：\n1 2 3 4 # 进入/tmp目录 cd /tmp # 修改配置文件 printf \u0026#39;%s\\n\u0026#39; 7001 7002 7003 8001 8002 8003 | xargs -I{} -t sed -i \u0026#39;s/6379/{}/g\u0026#39; {}/redis.conf Copied! 4.3.启动 因为已经配置了后台启动模式，所以可以直接启动服务：\n1 2 3 4 # 进入/tmp目录 cd /tmp # 一键启动所有服务 printf \u0026#39;%s\\n\u0026#39; 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-server {}/redis.conf Copied! 通过ps查看状态：\n1 ps -ef | grep redis Copied! 发现服务都已经正常启动：\n如果要关闭所有进程，可以执行命令：\n1 ps -ef | grep redis | awk \u0026#39;{print $2}\u0026#39; | xargs kill Copied! 或者（推荐这种方式）：\n1 printf \u0026#39;%s\\n\u0026#39; 7001 7002 7003 8001 8002 8003 | xargs -I{} -t redis-cli -p {} shutdown Copied! 4.4.创建集群 虽然服务启动了，但是目前每个服务之间都是独立的，没有任何关联。\n我们需要执行命令来创建集群，在Redis5.0之前创建集群比较麻烦，5.0之后集群管理命令都集成到了redis-cli中。\n1）Redis5.0之前\nRedis5.0之前集群命令都是用redis安装包下的src/redis-trib.rb来实现的。因为redis-trib.rb是有ruby语言编写的所以需要安装ruby环境。\n1 2 3 # 安装依赖 yum -y install zlib ruby rubygems gem install redis Copied! 然后通过命令来管理集群：\n1 2 3 4 # 进入redis的src目录 cd /tmp/redis-6.2.4/src # 创建集群 ./redis-trib.rb create --replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003 Copied! 2）Redis5.0以后\n我们使用的是Redis6.2.4版本，集群管理以及集成到了redis-cli中，格式如下：\n1 redis-cli --cluster create --cluster-replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003 Copied! 命令说明：\nredis-cli --cluster或者./redis-trib.rb：代表集群操作命令 create：代表是创建集群 --replicas 1或者--cluster-replicas 1 ：指定集群中每个master的副本个数为1，此时节点总数 ÷ (replicas + 1) 得到的就是master的数量。因此节点列表中的前n个就是master，其它节点都是slave节点，随机分配到不同master 运行后的样子：\n这里输入yes，则集群开始创建：\n通过命令可以查看集群状态：\n1 redis-cli -p 7001 cluster nodes Copied! 4.5.测试 尝试连接7001节点，存储一个数据：\n1 2 3 4 5 6 7 8 # 连接 redis-cli -p 7001 # 存储数据 set num 123 # 读取数据 get num # 再次存储 set a 1 Copied! 结果悲剧了：\n集群操作时，需要给redis-cli加上-c参数才可以：\n1 redis-cli -c -p 7001 Copied! 这次可以了：\n","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/91a71491/","title":"10.Redis集群"},{"content":" 分布式缓存 \u0026ndash; 基于Redis集群解决单机Redis存在的问题\n单机的Redis存在四大问题：\n0.学习目标 1.Redis持久化 Redis有两种持久化方案：\nRDB持久化 AOF持久化 1.1.RDB持久化 RDB全称Redis Database Backup file（Redis数据备份文件），也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后，从磁盘读取快照文件，恢复数据。快照文件称为RDB文件，默认是保存在当前运行目录。\n1.1.1.执行时机 RDB持久化在四种情况下会执行：\n执行save命令 执行bgsave命令 Redis停机时 触发RDB条件时 1）save命令\n执行下面的命令，可以立即执行一次RDB：\nsave命令会导致主进程执行RDB，这个过程中其它所有命令都会被阻塞。只有在数据迁移时可能用到。\n2）bgsave命令\n下面的命令可以异步执行RDB：\n这个命令执行后会开启独立进程完成RDB，主进程可以持续处理用户请求，不受影响。\n3）停机时\nRedis停机时会执行一次save命令，实现RDB持久化。\n4）触发RDB条件\nRedis内部有触发RDB的机制，可以在redis.conf文件中找到，格式如下：\n1 2 3 4 # 900秒内，如果至少有1个key被修改，则执行bgsave ， 如果是save \u0026#34;\u0026#34; 则表示禁用RDB save 900 1 save 300 10 save 60 10000 Copied! RDB的其它配置也可以在redis.conf文件中设置：\n1 2 3 4 5 6 7 8 # 是否压缩 ,建议不开启，压缩也会消耗cpu，磁盘的话不值钱 rdbcompression yes # RDB文件名称 dbfilename dump.rdb # 文件保存的路径目录 dir ./ Copied! 1.1.2.RDB原理 bgsave开始时会fork主进程得到子进程，子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。\nfork采用的是copy-on-write技术：\n当主进程执行读操作时，访问共享内存； 当主进程执行写操作时，则会拷贝一份数据，执行写操作。 1.1.3.小结 RDB方式bgsave的基本流程？\nfork主进程得到一个子进程，共享内存空间 子进程读取内存数据并写入新的RDB文件 用新RDB文件替换旧的RDB文件 RDB会在什么时候执行？save 60 1000代表什么含义？\n默认是服务停止时 代表60秒内至少执行1000次修改则触发RDB RDB的缺点？\nRDB执行间隔时间长，两次RDB之间写入数据有丢失的风险 fork子进程、压缩、写出RDB文件都比较耗时 1.2.AOF持久化 1.2.1.AOF原理 AOF全称为Append Only File（追加文件）。Redis处理的每一个写命令都会记录在AOF文件，可以看做是命令日志文件。\n1.2.2.AOF配置 AOF默认是关闭的，需要修改redis.conf配置文件来开启AOF：\n1 2 3 4 # 是否开启AOF功能，默认是no appendonly yes # AOF文件的名称 appendfilename \u0026#34;appendonly.aof\u0026#34; Copied! AOF的命令记录的频率也可以通过redis.conf文件来配：\n1 2 3 4 5 6 # 表示每执行一次写命令，立即记录到AOF文件 appendfsync always # 写命令执行完先放入AOF缓冲区，然后表示每隔1秒将缓冲区数据写到AOF文件，是默认方案 appendfsync everysec # 写命令执行完先放入AOF缓冲区，由操作系统决定何时将缓冲区内容写回磁盘 appendfsync no Copied! 三种策略对比：\n1.2.3.AOF文件重写 因为是记录命令，AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作，但只有最后一次写操作才有意义。通过执行bgrewriteaof命令，可以让AOF文件执行重写功能，用最少的命令达到相同效果。\n如图，AOF原本有三个命令，但是set num 123 和 set num 666都是对num的操作，第二次会覆盖第一次的值，因此第一个命令记录下来没有意义。\n所以重写命令后，AOF文件内容就是：mset name jack num 666\nRedis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置：\n1 2 3 4 # AOF文件比上次文件 增长超过多少百分比则触发重写 auto-aof-rewrite-percentage 100 # AOF文件体积最小多大以上才触发重写 auto-aof-rewrite-min-size 64mb Copied! 1.3.RDB与AOF对比 RDB和AOF各有自己的优缺点，如果对数据安全性要求较高，在实际开发中往往会结合两者来使用。\n2.Redis主从 2.1.搭建主从架构 单节点Redis的并发能力是有上限的，要进一步提高Redis的并发能力，就需要搭建主从集群，实现读写分离。\n具体搭建流程参考课前资料《Redis集群.md》：\n2.2.主从数据同步原理 2.2.1.全量同步 主从第一次建立连接时，会执行全量同步，将master节点的所有数据都拷贝给slave节点，流程：\n这里有一个问题，master如何得知salve是第一次来连接呢？？\n有几个概念，可以作为判断依据：\nReplication Id：简称replid，是数据集的标记，id一致则说明是同一数据集。每一个master都有唯一的replid，slave则会继承master节点的replid offset：偏移量，随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset，说明slave数据落后于master，需要更新。 因此slave做数据同步，必须向master声明自己的replication id 和offset，master才可以判断到底需要同步哪些数据。\n因为slave原本也是一个master，有自己的replid和offset，当第一次变成slave，与master建立连接时，发送的replid和offset是自己的replid和offset。\nmaster判断发现slave发送来的replid与自己的不一致，说明这是一个全新的slave，就知道要做全量同步了。\nmaster会将自己的replid和offset都发送给这个slave，slave保存这些信息。以后slave的replid就与master一致了。\n因此，master判断一个节点是否是第一次同步的依据，就是看replid是否一致。\n如图：\n完整流程描述：\nslave节点请求增量同步 master节点判断replid，发现不一致，拒绝增量同步 master将完整内存数据生成RDB，发送RDB到slave slave清空本地数据，加载master的RDB master将RDB期间的命令记录在repl_baklog，并持续将log中的命令发送给slave slave执行接收到的命令，保持与master之间的同步 2.2.2.增量同步 全量同步需要先做RDB，然后将RDB文件通过网络传输个slave，成本太高了。因此除了第一次做全量同步，其它大多数时候slave与master都是做增量同步。\n什么是增量同步？就是只更新slave与master存在差异的部分数据。如图：\n那么master怎么知道slave与自己的数据差异在哪里呢?\n2.2.3.repl_backlog原理 master怎么知道slave与自己的数据差异在哪里呢?\n这就要说到全量同步时的repl_baklog文件了。\n这个文件是一个固定大小的数组，只不过数组是环形，也就是说角标到达数组末尾后，会再次从0开始读写，这样数组头部的数据就会被覆盖。\nrepl_baklog中会记录Redis处理过的命令日志及offset，包括master当前的offset，和slave已经拷贝到的offset：\nslave与master的offset之间的差异，就是salve需要增量拷贝的数据了。\n随着不断有数据写入，master的offset逐渐变大，slave也不断的拷贝，追赶master的offset：\n直到数组被填满：\n此时，如果有新的数据写入，就会覆盖数组中的旧数据。不过，旧的数据只要是绿色的，说明是已经被同步到slave的数据，即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。\n但是，如果slave出现网络阻塞，导致master的offset远远超过了slave的offset：\n如果master继续写入新数据，其offset就会覆盖旧的数据，直到将slave现在的offset也覆盖：\n棕色框中的红色部分，就是尚未同步，但是却已经被覆盖的数据。此时如果slave恢复，需要同步，却发现自己的offset都没有了，无法完成增量同步了。只能做全量同步。\n2.3.主从同步优化 主从同步可以保证主从数据的一致性，非常重要。\n可以从以下几个方面来优化Redis主从就集群：\n在master中配置repl-diskless-sync yes启用无磁盘复制，避免全量同步时的磁盘IO。 Redis单节点上的内存占用不要太大，减少RDB导致的过多磁盘IO 适当提高repl_baklog的大小，发现slave宕机时尽快实现故障恢复，尽可能避免全量同步 限制一个master上的slave节点数量，如果实在是太多slave，则可以采用主-从-从链式结构，减少master压力 主从从架构图：\n2.4.小结 简述全量同步和增量同步区别？\n全量同步：master将完整内存数据生成RDB，发送RDB到slave。后续命令则记录在repl_baklog，逐个发送给slave。 增量同步：slave提交自己的offset到master，master获取repl_baklog中从offset之后的命令给slave 什么时候执行全量同步？\nslave节点第一次连接master节点时 slave节点断开时间太久，repl_baklog中的offset已经被覆盖时 什么时候执行增量同步？\nslave节点断开又恢复，并且在repl_baklog中能找到offset时 3.Redis哨兵 Redis提供了哨兵（Sentinel）机制来实现主从集群的自动故障恢复。\n3.1.哨兵原理 3.1.1.集群结构和作用 哨兵的结构如图：\n哨兵的作用如下：\n监控：Sentinel 会不断检查您的master和slave是否按预期工作 自动故障恢复：如果master故障，Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主 通知：Sentinel充当Redis客户端的服务发现来源，当集群发生故障转移时，会将最新信息推送给Redis的客户端 3.1.2.集群监控原理 Sentinel基于心跳机制监测服务状态，每隔1秒向集群的每个实例发送ping命令：\n•主观下线：如果某sentinel节点发现某实例未在规定时间响应，则认为该实例主观下线。\n•客观下线：若超过指定数量（quorum）的sentinel都认为该实例主观下线，则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。\n3.1.3.集群故障恢复原理 一旦发现master故障，sentinel需要在salve中选择一个作为新的master，选择依据是这样的：\n首先会判断slave节点与master节点断开时间长短，如果超过指定值（down-after-milliseconds * 10）则会排除该slave节点 然后判断slave节点的slave-priority值，越小优先级越高，如果是0则永不参与选举 如果slave-prority一样，则判断slave节点的offset值，越大说明数据越新，优先级越高 最后是判断slave节点的运行id大小，越小优先级越高。 当选出一个新的master后，该如何实现切换呢？\n流程如下：\nsentinel给备选的slave1节点发送slaveof no one命令，让该节点成为master sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令，让这些slave成为新master的从节点，开始从新的master上同步数据。 最后，sentinel将故障节点标记为slave，当故障节点恢复后会自动成为新的master的slave节点 3.1.4.小结 Sentinel的三个作用是什么？\n监控 故障转移 通知 Sentinel如何判断一个redis实例是否健康？\n每隔1秒发送一次ping命令，如果超过一定时间没有相向则认为是主观下线 如果大多数sentinel都认为实例主观下线，则判定服务下线 故障转移步骤有哪些？\n首先选定一个slave作为新的master，执行slaveof no one 然后让所有节点都执行slaveof 新master 修改故障节点配置，添加slaveof 新master 3.2.搭建哨兵集群 具体搭建流程参考课前资料《Redis集群.md》：\n3.3.RedisTemplate 在Sentinel集群监管下的Redis主从集群，其节点会因为自动故障转移而发生变化，Redis的客户端必须感知这种变化，及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。\n下面，我们通过一个测试来实现RedisTemplate集成哨兵机制。\n3.3.1.导入Demo工程 首先，我们引入课前资料提供的Demo工程：\n3.3.2.引入依赖 在项目的pom文件中引入依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-data-redis\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 3.3.3.配置Redis地址 然后在配置文件application.yml中指定redis的sentinel相关信息：\n1 2 3 4 5 6 7 8 spring: redis: sentinel: master: mymaster nodes: - 192.168.150.101:27001 - 192.168.150.101:27002 - 192.168.150.101:27003 Copied! 3.3.4.配置读写分离 在项目的启动类中，添加一个新的bean：\n1 2 3 4 @Bean public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){ return clientConfigurationBuilder -\u0026gt; clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED); } Copied! 这个bean中配置的就是读写策略，包括四种：\nMASTER：从主节点读取 MASTER_PREFERRED：优先从master节点读取，master不可用才读取replica REPLICA：从slave（replica）节点读取 REPLICA _PREFERRED：优先从slave（replica）节点读取，所有的slave都不可用才读取master 4.Redis分片集群 4.1.搭建分片集群 主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决：\n海量数据存储问题\n高并发写的问题\n使用分片集群可以解决上述问题，如图:\n分片集群特征：\n集群中有多个master，每个master保存不同数据\n每个master都可以有多个slave节点\nmaster之间通过ping监测彼此健康状态\n客户端请求可以访问集群任意节点，最终都会被转发到正确节点\n具体搭建流程参考课前资料《Redis集群.md》：\n4.2.散列插槽 4.2.1.插槽原理 Redis会把每一个master节点映射到0~16383共16384个插槽（hash slot）上，查看集群信息时就能看到：\n数据key不是与节点绑定，而是与插槽绑定。redis会根据key的有效部分计算插槽值，分两种情况：\nkey中包含\u0026quot;{}\u0026quot;，且“{}”中至少包含1个字符，“{}”中的部分是有效部分 key中不包含“{}”，整个key都是有效部分 例如：key是num，那么就根据num计算，如果是{itcast}num，则根据itcast计算。计算方式是利用CRC16算法得到一个hash值，然后对16384取余，得到的结果就是slot值。\n如图，在7001这个节点执行set a 1时，对a做hash运算，对16384取余，得到的结果是15495，因此要存储到103节点。\n到了7003后，执行get num时，对num做hash运算，对16384取余，得到的结果是2765，因此需要切换到7001节点\n4.2.1.小结 Redis如何判断某个key应该在哪个实例？\n将16384个插槽分配到不同的实例 根据key的有效部分计算哈希值，对16384取余 余数作为插槽，寻找插槽所在实例即可 如何将同一类数据固定的保存在同一个Redis实例？\n这一类数据使用相同的有效部分，例如key都以{typeId}为前缀 4.3.集群伸缩 redis-cli \u0026ndash;cluster提供了很多操作集群的命令，可以通过下面方式查看：\n比如，添加节点的命令：\n4.3.1.需求分析 需求：向集群中添加一个新的master节点，并向其中存储 num = 10\n启动一个新的redis实例，端口为7004 添加7004到之前的集群，并作为一个master节点 给7004节点分配插槽，使得num这个key可以存储到7004实例 这里需要两个新的功能：\n添加一个节点到集群中 将部分插槽分配到新插槽 4.3.2.创建新的redis实例 创建一个文件夹：\n1 mkdir 7004 Copied! 拷贝配置文件：\n1 cp redis.conf /7004 Copied! 修改配置文件：\n1 sed /s/6379/7004/g 7004/redis.conf Copied! 启动\n1 redis-server 7004/redis.conf Copied! 4.3.3.添加新节点到redis 添加节点的语法如下：\n执行命令：\n1 redis-cli --cluster add-node 192.168.150.101:7004 192.168.150.101:7001 Copied! 通过命令查看集群状态：\n1 redis-cli -p 7001 cluster nodes Copied! 如图，7004加入了集群，并且默认是一个master节点：\n但是，可以看到7004节点的插槽数量为0，因此没有任何数据可以存储到7004上\n4.3.4.转移插槽 我们要将num存储到7004节点，因此需要先看看num的插槽是多少：\n如上图所示，num的插槽为2765.\n我们可以将0~3000的插槽从7001转移到7004，命令格式如下：\n具体命令如下：\n建立连接：\n得到下面的反馈：\n询问要移动多少个插槽，我们计划是3000个：\n新的问题来了：\n那个node来接收这些插槽？？\n显然是7004，那么7004节点的id是多少呢？\n复制这个id，然后拷贝到刚才的控制台后：\n这里询问，你的插槽是从哪里移动过来的？\nall：代表全部，也就是三个节点各转移一部分 具体的id：目标节点的id done：没有了 这里我们要从7001获取，因此填写7001的id：\n填完后，点击done，这样插槽转移就准备好了：\n确认要转移吗？输入yes：\n然后，通过命令查看结果：\n可以看到：\n目的达成。\n4.4.故障转移 集群初识状态是这样的：\n其中7001、7002、7003都是master，我们计划让7002宕机。\n4.4.1.自动故障转移 当集群中有一个master宕机会发生什么呢？\n直接停止一个redis实例，例如7002：\n1 redis-cli -p 7002 shutdown Copied! 1）首先是该实例与其它实例失去连接\n2）然后是疑似宕机：\n3）最后是确定下线，自动提升一个slave为新的master：\n4）当7002再次启动，就会变为一个slave节点了：\n4.4.2.手动故障转移 利用cluster failover命令可以手动让集群中的某个master宕机，切换到执行cluster failover命令的这个slave节点，实现无感知的数据迁移。其流程如下：\n这种failover命令可以指定三种模式：\n缺省：默认的流程，如图1~6歩 force：省略了对offset的一致性校验 takeover：直接执行第5歩，忽略数据一致性、忽略master状态和其它master的意见 案例需求：在7002这个slave节点执行手动故障转移，重新夺回master地位\n步骤如下：\n1）利用redis-cli连接7002这个节点\n2）执行cluster failover命令\n如图：\n效果：\n4.5.RedisTemplate访问分片集群 RedisTemplate底层同样基于lettuce实现了分片集群的支持，而使用的步骤与哨兵模式基本一致：\n1）引入redis的starter依赖\n2）配置分片集群地址\n3）配置读写分离\n与哨兵模式相比，其中只有分片集群的配置方式略有差异，如下：\n1 2 3 4 5 6 7 8 9 10 spring: redis: cluster: nodes: - 192.168.150.101:7001 - 192.168.150.101:7002 - 192.168.150.101:7003 - 192.168.150.101:8001 - 192.168.150.101:8002 - 192.168.150.101:8003 Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/57138157/","title":"10.分布式缓存"},{"content":" 安装和配置Canal 下面我们就开启mysql的主从同步机制，让Canal来模拟salve\n1.开启MySQL主从 Canal是基于MySQL的主从同步功能，因此必须先开启MySQL的主从功能才可以。\n这里以之前用Docker运行的mysql为例：\n1.1.开启binlog 打开mysql容器挂载的日志文件，我的在/tmp/mysql/conf目录:\n修改文件：\n1 vi /tmp/mysql/conf/my.cnf Copied! 添加内容：\n1 2 log-bin=/var/lib/mysql/mysql-bin binlog-do-db=heima Copied! 配置解读：\nlog-bin=/var/lib/mysql/mysql-bin：设置binary log文件的存放地址和文件名，叫做mysql-bin binlog-do-db=heima：指定对哪个database记录binary log events，这里记录heima这个库 最终效果：\n1 2 3 4 5 6 7 [mysqld] skip-name-resolve character_set_server=utf8 datadir=/var/lib/mysql server-id=1000 log-bin=/var/lib/mysql/mysql-bin binlog-do-db=heima Copied! 1.2.设置用户权限 接下来添加一个仅用于数据同步的账户，出于安全考虑，这里仅提供对heima这个库的操作权限。\n1 2 3 create user canal@\u0026#39;%\u0026#39; IDENTIFIED by \u0026#39;canal\u0026#39;; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO \u0026#39;canal\u0026#39;@\u0026#39;%\u0026#39; identified by \u0026#39;canal\u0026#39;; FLUSH PRIVILEGES; Copied! 重启mysql容器即可\n1 docker restart mysql Copied! 测试设置是否成功：在mysql控制台，或者Navicat中，输入命令：\n1 show master status; Copied! 2.安装Canal 2.1.创建网络 我们需要创建一个网络，将MySQL、Canal、MQ放到同一个Docker网络中：\n1 docker network create heima Copied! 让mysql加入这个网络：\n1 docker network connect heima mysql Copied! 2.3.安装Canal 课前资料中提供了canal的镜像压缩包:\n大家可以上传到虚拟机，然后通过命令导入：\n1 docker load -i canal.tar Copied! 然后运行命令创建Canal容器：\n1 2 3 4 5 6 7 8 9 10 11 docker run -p 11111:11111 --name canal \\ -e canal.destinations=heima \\ -e canal.instance.master.address=mysql:3306 \\ -e canal.instance.dbUsername=canal \\ -e canal.instance.dbPassword=canal \\ -e canal.instance.connectionCharset=UTF-8 \\ -e canal.instance.tsdb.enable=true \\ -e canal.instance.gtidon=false \\ -e canal.instance.filter.regex=heima\\\\..* \\ --network heima \\ -d canal/canal-server:v1.1.5 Copied! 说明:\n-p 11111:11111：这是canal的默认监听端口 -e canal.instance.master.address=mysql:3306：数据库地址和端口，如果不知道mysql容器地址，可以通过docker inspect 容器id来查看 -e canal.instance.dbUsername=canal：数据库用户名 -e canal.instance.dbPassword=canal ：数据库密码 -e canal.instance.filter.regex=：要监听的表名称 表名称监听支持的语法：\n1 2 3 4 5 6 7 8 mysql 数据解析关注的表，Perl正则表达式. 多个正则之间以逗号(,)分隔，转义符需要双斜杠(\\\\) 常见例子： 1. 所有表：.* or .*\\\\..* 2. canal schema下所有表： canal\\\\..* 3. canal下的以canal打头的表：canal\\\\.canal.* 4. canal schema下的一张表：canal.test1 5. 多个规则组合使用然后以逗号隔开：canal\\\\..*,mysql.test1,mysql.test2 Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/49z67149/","title":"11.安装Canal"},{"content":" 安装OpenResty 1.安装 首先你的Linux虚拟机必须联网\n1）安装开发库 首先要安装OpenResty的依赖开发库，执行命令：\n1 yum install -y pcre-devel openssl-devel gcc --skip-broken Copied! 2）安装OpenResty仓库 你可以在你的 CentOS 系统中添加 openresty 仓库，这样就可以便于未来安装或更新我们的软件包（通过 yum check-update 命令）。运行下面的命令就可以添加我们的仓库：\n1 yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo Copied! 如果提示说命令不存在，则运行：\n1 yum install -y yum-utils Copied! 然后再重复上面的命令\n3）安装OpenResty 然后就可以像下面这样安装软件包，比如 openresty：\n1 yum install -y openresty Copied! 4）安装opm工具 opm是OpenResty的一个管理工具，可以帮助我们安装一个第三方的Lua模块。\n如果你想安装命令行工具 opm，那么可以像下面这样安装 openresty-opm 包：\n1 yum install -y openresty-opm Copied! 5）目录结构 默认情况下，OpenResty安装的目录是：/usr/local/openresty\n看到里面的nginx目录了吗，OpenResty就是在Nginx基础上集成了一些Lua模块。\n6）配置nginx的环境变量 打开配置文件：\n1 vi /etc/profile Copied! 在最下面加入两行：\n1 2 export NGINX_HOME=/usr/local/openresty/nginx export PATH=${NGINX_HOME}/sbin:$PATH Copied! NGINX_HOME：后面是OpenResty安装目录下的nginx的目录\n然后让配置生效：\n1 source /etc/profile Copied! 2.启动和运行 OpenResty底层是基于Nginx的，查看OpenResty目录的nginx目录，结构与windows中安装的nginx基本一致：\n所以运行方式与nginx基本一致：\n1 2 3 4 5 6 # 启动nginx nginx # 重新加载配置 nginx -s reload # 停止 nginx -s stop Copied! nginx的默认配置文件注释太多，影响后续我们的编辑，这里将nginx.conf中的注释部分删除，保留有效部分。\n修改/usr/local/openresty/nginx/conf/nginx.conf文件，内容如下：\n1 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 #user nobody; worker_processes 1; error_log logs/error.log; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 8081; server_name localhost; location / { root html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } } Copied! 在Linux的控制台输入命令以启动nginx：\n1 nginx Copied! 然后访问页面：http://192.168.150.101:8081，注意ip地址替换为你自己的虚拟机IP：\n3.备注 加载OpenResty的lua模块：\n1 2 3 4 #lua 模块 lua_package_path \u0026#34;/usr/local/openresty/lualib/?.lua;;\u0026#34;; #c模块 lua_package_cpath \u0026#34;/usr/local/openresty/lualib/?.so;;\u0026#34;; Copied! common.lua\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- 封装函数，发送http请求，并解析响应 local function read_http(path, params) local resp = ngx.location.capture(path,{ method = ngx.HTTP_GET, args = params, }) if not resp then -- 记录错误信息，返回404 ngx.log(ngx.ERR, \u0026#34;http not found, path: \u0026#34;, path , \u0026#34;, args: \u0026#34;, args) ngx.exit(404) end return resp.body end -- 将方法导出 local _M = { read_http = read_http } return _M Copied! 释放Redis连接API：\n1 2 3 4 5 6 7 8 9 -- 关闭redis连接的工具方法，其实是放入连接池 local function close_redis(red) local pool_max_idle_time = 10000 -- 连接的空闲时间，单位是毫秒 local pool_size = 100 --连接池大小 local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.log(ngx.ERR, \u0026#34;放入redis连接池失败: \u0026#34;, err) end end Copied! 读取Redis数据的API：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 -- 查询redis的方法 ip和port是redis地址，key是查询的key local function read_redis(ip, port, key) -- 获取一个连接 local ok, err = red:connect(ip, port) if not ok then ngx.log(ngx.ERR, \u0026#34;连接redis失败 : \u0026#34;, err) return nil end -- 查询redis local resp, err = red:get(key) -- 查询失败处理 if not resp then ngx.log(ngx.ERR, \u0026#34;查询Redis失败: \u0026#34;, err, \u0026#34;, key = \u0026#34; , key) end --得到的数据为空处理 if resp == ngx.null then resp = nil ngx.log(ngx.ERR, \u0026#34;查询Redis数据为空, key = \u0026#34;, key) end close_redis(red) return resp end Copied! 开启共享词典：\n1 2 # 共享字典，也就是本地缓存，名称叫做：item_cache，大小150m lua_shared_dict item_cache 150m; Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/451178u5/","title":"11.安装OpenResty"},{"content":" 案例导入说明 为了演示多级缓存，我们先导入一个商品管理的案例，其中包含商品的CRUD功能。我们将来会给查询商品添加多级缓存。\n1.安装MySQL 后期做数据同步需要用到MySQL的主从功能，所以需要大家在虚拟机中，利用Docker来运行一个MySQL容器。\n1.1.准备目录 为了方便后期配置MySQL，我们先准备两个目录，用于挂载容器的数据和配置文件目录：\n1 2 3 4 5 6 # 进入/tmp目录 cd /tmp # 创建文件夹 mkdir mysql # 进入mysql目录 cd mysql Copied! 1.2.运行命令 进入mysql目录后，执行下面的Docker命令：\n1 2 3 4 5 6 7 8 9 10 docker run \\ -p 3306:3306 \\ --name mysql \\ -v $PWD/conf:/etc/mysql/conf.d \\ -v $PWD/logs:/logs \\ -v $PWD/data:/var/lib/mysql \\ -e MYSQL_ROOT_PASSWORD=123 \\ --privileged \\ -d \\ mysql:5.7.25 Copied! 1.3.修改配置 在/tmp/mysql/conf目录添加一个my.cnf文件，作为mysql的配置文件：\n1 2 # 创建文件 touch /tmp/mysql/conf/my.cnf Copied! 文件的内容如下：\n1 2 3 4 5 [mysqld] skip-name-resolve character_set_server=utf8 datadir=/var/lib/mysql server-id=1000 Copied! 1.4.重启 配置修改后，必须重启容器：\n1 docker restart mysql Copied! 2.导入SQL 接下来，利用Navicat客户端连接MySQL，然后导入课前资料提供的sql文件：\n其中包含两张表：\ntb_item：商品表，包含商品的基本信息 tb_item_stock：商品库存表，包含商品的库存信息 之所以将库存分离出来，是因为库存是更新比较频繁的信息，写操作较多。而其他信息修改的频率非常低。\n3.导入Demo工程 下面导入课前资料提供的工程：\n项目结构如图所示：\n其中的业务包括：\n分页查询商品 新增商品 修改商品 修改库存 删除商品 根据id查询商品 根据id查询库存 业务全部使用mybatis-plus来实现，如有需要请自行修改业务逻辑。\n3.1.分页查询商品 在com.heima.item.web包的ItemController中可以看到接口定义：\n3.2.新增商品 在com.heima.item.web包的ItemController中可以看到接口定义：\n3.3.修改商品 在com.heima.item.web包的ItemController中可以看到接口定义：\n3.4.修改库存 在com.heima.item.web包的ItemController中可以看到接口定义：\n3.5.删除商品 在com.heima.item.web包的ItemController中可以看到接口定义：\n这里是采用了逻辑删除，将商品状态修改为3\n3.6.根据id查询商品 在com.heima.item.web包的ItemController中可以看到接口定义：\n这里只返回了商品信息，不包含库存\n3.7.根据id查询库存 在com.heima.item.web包的ItemController中可以看到接口定义：\n3.8.启动 注意修改application.yml文件中配置的mysql地址信息：\n需要修改为自己的虚拟机地址信息、还有账号和密码。\n修改后，启动服务，访问：http://localhost:8081/item/10001即可查询数据\n4.导入商品查询页面 商品查询是购物页面，与商品管理的页面是分离的。\n部署方式如图：\n我们需要准备一个反向代理的nginx服务器，如上图红框所示，将静态的商品页面放到nginx目录中。\n页面需要的数据通过ajax向服务端（nginx业务集群）查询。\n4.1.运行nginx服务 这里我已经给大家准备好了nginx反向代理服务器和静态资源。\n我们找到课前资料的nginx目录：\n将其拷贝到一个非中文目录下，运行这个nginx服务。\n运行命令：\n1 start nginx.exe Copied! 然后访问 http://localhost/item.html?id=10001即可：\n4.2.反向代理 现在，页面是假数据展示的。我们需要向服务器发送ajax请求，查询商品数据。\n打开控制台，可以看到页面有发起ajax查询数据：\n而这个请求地址同样是80端口，所以被当前的nginx反向代理了。\n查看nginx的conf目录下的nginx.conf文件：\n其中的关键配置如下：\n其中的192.168.150.101是我的虚拟机IP，也就是我的Nginx业务集群要部署的地方：\n完整内容如下：\n1 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 37 38 #user nobody; worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; #tcp_nopush on; keepalive_timeout 65; upstream nginx-cluster{ server 192.168.150.101:8081; } server { listen 80; server_name localhost; location /api { proxy_pass http://nginx-cluster; } location / { root html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } } Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/34916834/","title":"11.案例导入说明"},{"content":" 多级缓存 0.学习目标 1.什么是多级缓存 传统的缓存策略一般是请求到达Tomcat后，先查询Redis，如果未命中则查询数据库，如图：\n存在下面的问题：\n•请求要经过Tomcat处理，Tomcat的性能成为整个系统的瓶颈\n•Redis缓存失效时，会对数据库产生冲击\n多级缓存就是充分利用请求处理的每个环节，分别添加缓存，减轻Tomcat压力，提升服务性能：\n浏览器访问静态资源时，优先读取浏览器本地缓存 访问非静态资源（ajax查询数据）时，访问服务端 请求到达Nginx后，优先读取Nginx本地缓存 如果Nginx本地缓存未命中，则去直接查询Redis（不经过Tomcat） 如果Redis查询未命中，则查询Tomcat 请求进入Tomcat后，优先查询JVM进程缓存 如果JVM进程缓存未命中，则查询数据库 在多级缓存架构中，Nginx内部需要编写本地缓存查询、Redis查询、Tomcat查询的业务逻辑，因此这样的nginx服务不再是一个反向代理服务器，而是一个编写业务的Web服务器了。\n因此这样的业务Nginx服务也需要搭建集群来提高并发，再有专门的nginx服务来做反向代理，如图：\n另外，我们的Tomcat服务将来也会部署为集群模式：\n可见，多级缓存的关键有两个：\n一个是在nginx中编写业务，实现nginx本地缓存、Redis、Tomcat的查询\n另一个就是在Tomcat中实现JVM进程缓存\n其中Nginx编程则会用到OpenResty框架结合Lua这样的语言。\n这也是今天课程的难点和重点。\n2.JVM进程缓存 为了演示多级缓存的案例，我们先准备一个商品查询的业务。\n2.1.导入案例 参考课前资料的：《案例导入说明.md》\n2.2.初识Caffeine 缓存在日常开发中启动至关重要的作用，由于是存储在内存中，数据的读取速度是非常快的，能大量减少对数据库的访问，减少数据库的压力。我们把缓存分为两类：\n分布式缓存，例如Redis： 优点：存储容量更大、可靠性更好、可以在集群间共享 缺点：访问缓存有网络开销 场景：缓存数据量较大、可靠性要求较高、需要在集群间共享 进程本地缓存，例如HashMap、GuavaCache： 优点：读取本地内存，没有网络开销，速度更快 缺点：存储容量有限、可靠性较低、无法共享 场景：性能要求较高，缓存数据量较小 我们今天会利用Caffeine框架来实现JVM进程缓存。\nCaffeine是一个基于Java8开发的，提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址：https://github.com/ben-manes/caffeine\nCaffeine的性能非常好，下图是官方给出的性能对比：\n可以看到Caffeine的性能遥遥领先！\n缓存使用的基本API：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test void testBasicOps() { // 构建cache对象 Cache\u0026lt;String, String\u0026gt; cache = Caffeine.newBuilder().build(); // 存数据 cache.put(\u0026#34;gf\u0026#34;, \u0026#34;迪丽热巴\u0026#34;); // 取数据 String gf = cache.getIfPresent(\u0026#34;gf\u0026#34;); System.out.println(\u0026#34;gf = \u0026#34; + gf); // 取数据，包含两个参数： // 参数一：缓存的key // 参数二：Lambda表达式，表达式参数就是缓存的key，方法体是查询数据库的逻辑 // 优先根据key查询JVM缓存，如果未命中，则执行参数二的Lambda表达式 String defaultGF = cache.get(\u0026#34;defaultGF\u0026#34;, key -\u0026gt; { // 根据key去数据库查询数据 return \u0026#34;柳岩\u0026#34;; }); System.out.println(\u0026#34;defaultGF = \u0026#34; + defaultGF); } Copied! Caffeine既然是缓存的一种，肯定需要有缓存的清除策略，不然的话内存总会有耗尽的时候。\nCaffeine提供了三种缓存驱逐策略：\n基于容量：设置缓存的数量上限\n1 2 3 4 // 创建缓存对象 Cache\u0026lt;String, String\u0026gt; cache = Caffeine.newBuilder() .maximumSize(1) // 设置缓存大小上限为 1 .build(); Copied! 基于时间：设置缓存的有效时间\n1 2 3 4 5 // 创建缓存对象 Cache\u0026lt;String, String\u0026gt; cache = Caffeine.newBuilder() // 设置缓存有效期为 10 秒，从最后一次写入开始计时 .expireAfterWrite(Duration.ofSeconds(10)) .build(); Copied! 基于引用：设置缓存为软引用或弱引用，利用GC来回收缓存数据。性能较差，不建议使用。\n注意：在默认情况下，当一个缓存元素过期的时候，Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后，或者在空闲时间完成对失效数据的驱逐。\n2.3.实现JVM进程缓存 2.3.1.需求 利用Caffeine实现下列需求：\n给根据id查询商品的业务添加缓存，缓存未命中时查询数据库 给根据id查询商品库存的业务添加缓存，缓存未命中时查询数据库 缓存初始大小为100 缓存上限为10000 2.3.2.实现 首先，我们需要定义两个Caffeine的缓存对象，分别保存商品、库存的缓存数据。\n在item-service的com.heima.item.config包下定义CaffeineConfig类：\n1 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 package com.heima.item.config; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.heima.item.pojo.Item; import com.heima.item.pojo.ItemStock; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class CaffeineConfig { @Bean public Cache\u0026lt;Long, Item\u0026gt; itemCache(){ return Caffeine.newBuilder() .initialCapacity(100) .maximumSize(10_000) .build(); } @Bean public Cache\u0026lt;Long, ItemStock\u0026gt; stockCache(){ return Caffeine.newBuilder() .initialCapacity(100) .maximumSize(10_000) .build(); } } Copied! 然后，修改item-service中的com.heima.item.web包下的ItemController类，添加缓存逻辑：\n1 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 @RestController @RequestMapping(\u0026#34;item\u0026#34;) public class ItemController { @Autowired private IItemService itemService; @Autowired private IItemStockService stockService; @Autowired private Cache\u0026lt;Long, Item\u0026gt; itemCache; @Autowired private Cache\u0026lt;Long, ItemStock\u0026gt; stockCache; // ...其它略 @GetMapping(\u0026#34;/{id}\u0026#34;) public Item findById(@PathVariable(\u0026#34;id\u0026#34;) Long id) { return itemCache.get(id, key -\u0026gt; itemService.query() .ne(\u0026#34;status\u0026#34;, 3).eq(\u0026#34;id\u0026#34;, key) .one() ); } @GetMapping(\u0026#34;/stock/{id}\u0026#34;) public ItemStock findStockById(@PathVariable(\u0026#34;id\u0026#34;) Long id) { return stockCache.get(id, key -\u0026gt; stockService.getById(key)); } } Copied! 3.Lua语法入门 Nginx编程需要用到Lua语言，因此我们必须先入门Lua的基本语法。\n3.1.初识Lua Lua 是一种轻量小巧的脚本语言，用标准C语言编写并以源代码形式开放， 其设计目的是为了嵌入应用程序中，从而为应用程序提供灵活的扩展和定制功能。官网：https://www.lua.org/\nLua经常嵌入到C语言开发的程序中，例如游戏开发、游戏插件等。\nNginx本身也是C语言开发，因此也允许基于Lua做拓展。\n3.1.HelloWorld CentOS7默认已经安装了Lua语言环境，所以可以直接运行Lua代码。\n1）在Linux虚拟机的任意目录下，新建一个hello.lua文件\n2）添加下面的内容\n1 print(\u0026#34;Hello World!\u0026#34;) Copied! 3）运行\n3.2.变量和循环 学习任何语言必然离不开变量，而变量的声明必须先知道数据的类型。\n3.2.1.Lua的数据类型 Lua中支持的常见数据类型包括：\n另外，Lua提供了type()函数来判断一个变量的数据类型：\n3.2.2.声明变量 Lua声明变量的时候无需指定数据类型，而是用local来声明变量为局部变量：\n1 2 3 4 5 6 7 8 -- 声明字符串，可以用单引号或双引号， local str = \u0026#39;hello\u0026#39; -- 字符串拼接可以使用 .. local str2 = \u0026#39;hello\u0026#39; .. \u0026#39;world\u0026#39; -- 声明数字 local num = 21 -- 声明布尔类型 local flag = true Copied! Lua中的table类型既可以作为数组，又可以作为Java中的map来使用。数组就是特殊的table，key是数组角标而已：\n1 2 3 4 -- 声明数组 ，key为角标的 table local arr = {\u0026#39;java\u0026#39;, \u0026#39;python\u0026#39;, \u0026#39;lua\u0026#39;} -- 声明table，类似java的map local map = {name=\u0026#39;Jack\u0026#39;, age=21} Copied! Lua中的数组角标是从1开始，访问的时候与Java中类似：\n1 2 -- 访问数组，lua数组的角标从1开始 print(arr[1]) Copied! Lua中的table可以用key来访问：\n1 2 3 -- 访问table print(map[\u0026#39;name\u0026#39;]) print(map.name) Copied! 3.2.3.循环 对于table，我们可以利用for循环来遍历。不过数组和普通table遍历略有差异。\n遍历数组：\n1 2 3 4 5 6 -- 声明数组 key为索引的 table local arr = {\u0026#39;java\u0026#39;, \u0026#39;python\u0026#39;, \u0026#39;lua\u0026#39;} -- 遍历数组 for index,value in ipairs(arr) do print(index, value) end Copied! 遍历普通table\n1 2 3 4 5 6 -- 声明map，也就是table local map = {name=\u0026#39;Jack\u0026#39;, age=21} -- 遍历table for key,value in pairs(map) do print(key, value) end Copied! 3.3.条件控制、函数 Lua中的条件控制和函数声明与Java类似。\n3.3.1.函数 定义函数的语法：\n1 2 3 4 function 函数名( argument1, argument2..., argumentn) -- 函数体 return 返回值 end Copied! 例如，定义一个函数，用来打印数组：\n1 2 3 4 5 function printArr(arr) for index, value in ipairs(arr) do print(value) end end Copied! 3.3.2.条件控制 类似Java的条件控制，例如if、else语法：\n1 2 3 4 5 6 if(布尔表达式) then --[ 布尔表达式为 true 时执行该语句块 --] else --[ 布尔表达式为 false 时执行该语句块 --] end Copied! 与java不同，布尔表达式中的逻辑运算是基于英文单词：\n3.3.3.案例 需求：自定义一个函数，可以打印table，当参数为nil时，打印错误信息\n1 2 3 4 5 6 7 8 function printArr(arr) if not arr then print(\u0026#39;数组不能为空！\u0026#39;) end for index, value in ipairs(arr) do print(value) end end Copied! 4.实现多级缓存 多级缓存的实现离不开Nginx编程，而Nginx编程又离不开OpenResty。\n4.1.安装OpenResty OpenResty® 是一个基于 Nginx的高性能 Web 平台，用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点：\n具备Nginx的完整功能 基于Lua语言进行扩展，集成了大量精良的 Lua 库、第三方模块 允许使用Lua自定义业务逻辑、自定义库 官方网站： https://openresty.org/cn/ 安装Lua可以参考课前资料提供的《安装OpenResty.md》：\n4.2.OpenResty快速入门 我们希望达到的多级缓存架构如图：\n其中：\nwindows上的nginx用来做反向代理服务，将前端的查询商品的ajax请求代理到OpenResty集群\nOpenResty集群用来编写多级缓存业务\n4.2.1.反向代理流程 现在，商品详情页使用的是假的商品数据。不过在浏览器中，可以看到页面有发起ajax请求查询真实商品数据。\n这个请求如下：\n请求地址是localhost，端口是80，就被windows上安装的Nginx服务给接收到了。然后代理给了OpenResty集群：\n我们需要在OpenResty中编写业务，查询商品数据并返回到浏览器。\n但是这次，我们先在OpenResty接收请求，返回假的商品数据。\n4.2.2.OpenResty监听请求 OpenResty的很多功能都依赖于其目录下的Lua库，需要在nginx.conf中指定依赖库的目录，并导入依赖：\n1）添加对OpenResty的Lua模块的加载\n修改/usr/local/openresty/nginx/conf/nginx.conf文件，在其中的http下面，添加下面代码：\n1 2 3 4 #lua 模块 lua_package_path \u0026#34;/usr/local/openresty/lualib/?.lua;;\u0026#34;; #c模块 lua_package_cpath \u0026#34;/usr/local/openresty/lualib/?.so;;\u0026#34;; Copied! 2）监听/api/item路径\n修改/usr/local/openresty/nginx/conf/nginx.conf文件，在nginx.conf的server下面，添加对/api/item这个路径的监听：\n1 2 3 4 5 6 location /api/item { # 默认的响应类型 default_type application/json; # 响应结果由lua/item.lua文件来决定 content_by_lua_file lua/item.lua; } Copied! 这个监听，就类似于SpringMVC中的@GetMapping(\u0026quot;/api/item\u0026quot;)做路径映射。\n而content_by_lua_file lua/item.lua则相当于调用item.lua这个文件，执行其中的业务，把结果返回给用户。相当于java中调用service。\n4.2.3.编写item.lua 1）在/usr/loca/openresty/nginx目录创建文件夹：lua\n2）在/usr/loca/openresty/nginx/lua文件夹下，新建文件：item.lua\n3）编写item.lua，返回假数据\nitem.lua中，利用ngx.say()函数返回数据到Response中\n1 ngx.say(\u0026#39;{\u0026#34;id\u0026#34;:10001,\u0026#34;name\u0026#34;:\u0026#34;SALSA AIR\u0026#34;,\u0026#34;title\u0026#34;:\u0026#34;RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4\u0026#34;,\u0026#34;price\u0026#34;:17900,\u0026#34;image\u0026#34;:\u0026#34;https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp\u0026#34;,\u0026#34;category\u0026#34;:\u0026#34;拉杆箱\u0026#34;,\u0026#34;brand\u0026#34;:\u0026#34;RIMOWA\u0026#34;,\u0026#34;spec\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;status\u0026#34;:1,\u0026#34;createTime\u0026#34;:\u0026#34;2019-04-30T16:00:00.000+00:00\u0026#34;,\u0026#34;updateTime\u0026#34;:\u0026#34;2019-04-30T16:00:00.000+00:00\u0026#34;,\u0026#34;stock\u0026#34;:2999,\u0026#34;sold\u0026#34;:31290}\u0026#39;) Copied! 4）重新加载配置\n1 nginx -s reload Copied! 刷新商品页面：http://localhost/item.html?id=1001，即可看到效果：\n4.3.请求参数处理 上一节中，我们在OpenResty接收前端请求，但是返回的是假数据。\n要返回真实数据，必须根据前端传递来的商品id，查询商品信息才可以。\n那么如何获取前端传递的商品参数呢？\n4.3.1.获取参数的API OpenResty中提供了一些API用来获取不同类型的前端请求参数：\n4.3.2.获取参数并返回 在前端发起的ajax请求如图：\n可以看到商品id是以路径占位符方式传递的，因此可以利用正则表达式匹配的方式来获取ID\n1）获取商品id\n修改/usr/loca/openresty/nginx/nginx.conf文件中监听/api/item的代码，利用正则表达式获取ID：\n1 2 3 4 5 6 location ~ /api/item/(\\d+) { # 默认的响应类型 default_type application/json; # 响应结果由lua/item.lua文件来决定 content_by_lua_file lua/item.lua; } Copied! 2）拼接ID并返回\n修改/usr/loca/openresty/nginx/lua/item.lua文件，获取id并拼接到结果中返回：\n1 2 3 4 -- 获取商品id local id = ngx.var[1] -- 拼接并返回 ngx.say(\u0026#39;{\u0026#34;id\u0026#34;:\u0026#39; .. id .. \u0026#39;,\u0026#34;name\u0026#34;:\u0026#34;SALSA AIR\u0026#34;,\u0026#34;title\u0026#34;:\u0026#34;RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4\u0026#34;,\u0026#34;price\u0026#34;:17900,\u0026#34;image\u0026#34;:\u0026#34;https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp\u0026#34;,\u0026#34;category\u0026#34;:\u0026#34;拉杆箱\u0026#34;,\u0026#34;brand\u0026#34;:\u0026#34;RIMOWA\u0026#34;,\u0026#34;spec\u0026#34;:\u0026#34;\u0026#34;,\u0026#34;status\u0026#34;:1,\u0026#34;createTime\u0026#34;:\u0026#34;2019-04-30T16:00:00.000+00:00\u0026#34;,\u0026#34;updateTime\u0026#34;:\u0026#34;2019-04-30T16:00:00.000+00:00\u0026#34;,\u0026#34;stock\u0026#34;:2999,\u0026#34;sold\u0026#34;:31290}\u0026#39;) Copied! 3）重新加载并测试\n运行命令以重新加载OpenResty配置：\n1 nginx -s reload Copied! 刷新页面可以看到结果中已经带上了ID：\n4.4.查询Tomcat 拿到商品ID后，本应去缓存中查询商品信息，不过目前我们还未建立nginx、redis缓存。因此，这里我们先根据商品id去tomcat查询商品信息。我们实现如图部分：\n需要注意的是，我们的OpenResty是在虚拟机，Tomcat是在Windows电脑上。两者IP一定不要搞错了。\n4.4.1.发送http请求的API nginx提供了内部API用以发送http请求：\n1 2 3 4 local resp = ngx.location.capture(\u0026#34;/path\u0026#34;,{ method = ngx.HTTP_GET, -- 请求方式 args = {a=1,b=2}, -- get方式传参数 }) Copied! 返回的响应内容包括：\nresp.status：响应状态码 resp.header：响应头，是一个table resp.body：响应体，就是响应数据 注意：这里的path是路径，并不包含IP和端口。这个请求会被nginx内部的server监听并处理。\n但是我们希望这个请求发送到Tomcat服务器，所以还需要编写一个server来对这个路径做反向代理：\n1 2 3 4 location /path { # 这里是windows电脑的ip和Java服务端口，需要确保windows防火墙处于关闭状态 proxy_pass http://192.168.150.1:8081; } Copied! 原理如图：\n4.4.2.封装http工具 下面，我们封装一个发送Http请求的工具，基于ngx.location.capture来实现查询tomcat。\n1）添加反向代理，到windows的Java服务\n因为item-service中的接口都是/item开头，所以我们监听/item路径，代理到windows上的tomcat服务。\n修改 /usr/local/openresty/nginx/conf/nginx.conf文件，添加一个location：\n1 2 3 location /item { proxy_pass http://192.168.150.1:8081; } Copied! 以后，只要我们调用ngx.location.capture(\u0026quot;/item\u0026quot;)，就一定能发送请求到windows的tomcat服务。\n2）封装工具类\n之前我们说过，OpenResty启动时会加载以下两个目录中的工具文件：\n所以，自定义的http工具也需要放到这个目录下。\n在/usr/local/openresty/lualib目录下，新建一个common.lua文件：\n1 vi /usr/local/openresty/lualib/common.lua Copied! 内容如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- 封装函数，发送http请求，并解析响应 local function read_http(path, params) local resp = ngx.location.capture(path,{ method = ngx.HTTP_GET, args = params, }) if not resp then -- 记录错误信息，返回404 ngx.log(ngx.ERR, \u0026#34;http请求查询失败, path: \u0026#34;, path , \u0026#34;, args: \u0026#34;, args) ngx.exit(404) end return resp.body end -- 将方法导出 local _M = { read_http = read_http } return _M Copied! 这个工具将read_http函数封装到_M这个table类型的变量中，并且返回，这类似于导出。\n使用的时候，可以利用require('common')来导入该函数库，这里的common是函数库的文件名。\n3）实现商品查询\n最后，我们修改/usr/local/openresty/lua/item.lua文件，利用刚刚封装的函数库实现对tomcat的查询：\n1 2 3 4 5 6 7 8 9 10 -- 引入自定义common工具模块，返回值是common中返回的 _M local common = require(\u0026#34;common\u0026#34;) -- 从 common中获取read_http这个函数 local read_http = common.read_http -- 获取路径参数 local id = ngx.var[1] -- 根据id查询商品 local itemJSON = read_http(\u0026#34;/item/\u0026#34;.. id, nil) -- 根据id查询商品库存 local itemStockJSON = read_http(\u0026#34;/item/stock/\u0026#34;.. id, nil) Copied! 这里查询到的结果是json字符串，并且包含商品、库存两个json字符串，页面最终需要的是把两个json拼接为一个json：\n这就需要我们先把JSON变为lua的table，完成数据整合后，再转为JSON。\n4.4.3.CJSON工具类 OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。\n官方地址： https://github.com/openresty/lua-cjson/ 1）引入cjson模块：\n1 local cjson = require \u0026#34;cjson\u0026#34; Copied! 2）序列化：\n1 2 3 4 5 6 local obj = { name = \u0026#39;jack\u0026#39;, age = 21 } -- 把 table 序列化为 json local json = cjson.encode(obj) Copied! 3）反序列化：\n1 2 3 4 local json = \u0026#39;{\u0026#34;name\u0026#34;: \u0026#34;jack\u0026#34;, \u0026#34;age\u0026#34;: 21}\u0026#39; -- 反序列化 json为 table local obj = cjson.decode(json); print(obj.name) Copied! 4.4.4.实现Tomcat查询 下面，我们修改之前的item.lua中的业务，添加json处理功能：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 -- 导入common函数库 local common = require(\u0026#39;common\u0026#39;) local read_http = common.read_http -- 导入cjson库 local cjson = require(\u0026#39;cjson\u0026#39;) -- 获取路径参数 local id = ngx.var[1] -- 根据id查询商品 local itemJSON = read_http(\u0026#34;/item/\u0026#34;.. id, nil) -- 根据id查询商品库存 local itemStockJSON = read_http(\u0026#34;/item/stock/\u0026#34;.. id, nil) -- JSON转化为lua的table local item = cjson.decode(itemJSON) local stock = cjson.decode(stockJSON) -- 组合数据 item.stock = stock.stock item.sold = stock.sold -- 把item序列化为json 返回结果 ngx.say(cjson.encode(item)) Copied! 4.4.5.基于ID负载均衡 刚才的代码中，我们的tomcat是单机部署。而实际开发中，tomcat一定是集群模式：\n因此，OpenResty需要对tomcat集群做负载均衡。\n而默认的负载均衡规则是轮询模式，当我们查询/item/10001时：\n第一次会访问8081端口的tomcat服务，在该服务内部就形成了JVM进程缓存 第二次会访问8082端口的tomcat服务，该服务内部没有JVM缓存（因为JVM缓存无法共享），会查询数据库 \u0026hellip; 你看，因为轮询的原因，第一次查询8081形成的JVM缓存并未生效，直到下一次再次访问到8081时才可以生效，缓存命中率太低了。\n怎么办？\n如果能让同一个商品，每次查询时都访问同一个tomcat服务，那么JVM缓存就一定能生效了。\n也就是说，我们需要根据商品id做负载均衡，而不是轮询。\n1）原理 nginx提供了基于请求路径做负载均衡的算法：\nnginx根据请求路径做hash运算，把得到的数值对tomcat服务的数量取余，余数是几，就访问第几个服务，实现负载均衡。\n例如：\n我们的请求路径是 /item/10001 tomcat总数为2台（8081、8082） 对请求路径/item/1001做hash运算求余的结果为1 则访问第一个tomcat服务，也就是8081 只要id不变，每次hash运算结果也不会变，那就可以保证同一个商品，一直访问同一个tomcat服务，确保JVM缓存生效。\n2）实现 修改/usr/local/openresty/nginx/conf/nginx.conf文件，实现基于ID做负载均衡。\n首先，定义tomcat集群，并设置基于路径做负载均衡：\n1 2 3 4 5 upstream tomcat-cluster { hash $request_uri; server 192.168.150.1:8081; server 192.168.150.1:8082; } Copied! 然后，修改对tomcat服务的反向代理，目标指向tomcat集群：\n1 2 3 location /item { proxy_pass http://tomcat-cluster; } Copied! 重新加载OpenResty\n1 nginx -s reload Copied! 3）测试 启动两台tomcat服务：\n同时启动：\n清空日志后，再次访问页面，可以看到不同id的商品，访问到了不同的tomcat服务：\n4.5.Redis缓存预热 Redis缓存会面临冷启动问题：\n冷启动：服务刚刚启动时，Redis中并没有缓存，如果所有商品数据都在第一次查询时添加缓存，可能会给数据库带来较大压力。\n缓存预热：在实际开发中，我们可以利用大数据统计用户访问的热点数据，在项目启动时将这些热点数据提前查询并保存到Redis中。\n我们数据量较少，并且没有数据统计相关功能，目前可以在启动时将所有数据都放入缓存中。\n1）利用Docker安装Redis\n1 docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes Copied! 2）在item-service服务中引入Redis依赖\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-data-redis\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 3）配置Redis地址\n1 2 3 spring: redis: host: 192.168.150.101 Copied! 4）编写初始化类\n缓存预热需要在项目启动时完成，并且必须是拿到RedisTemplate之后。\n这里我们利用InitializingBean接口来实现，因为InitializingBean可以在对象被Spring创建并且成员变量全部注入后执行。\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package com.heima.item.config; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.heima.item.pojo.Item; import com.heima.item.pojo.ItemStock; import com.heima.item.service.IItemService; import com.heima.item.service.IItemStockService; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.List; @Component public class RedisHandler implements InitializingBean { @Autowired private StringRedisTemplate redisTemplate; @Autowired private IItemService itemService; @Autowired private IItemStockService stockService; private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public void afterPropertiesSet() throws Exception { // 初始化缓存 // 1.查询商品信息 List\u0026lt;Item\u0026gt; itemList = itemService.list(); // 2.放入缓存 for (Item item : itemList) { // 2.1.item序列化为JSON String json = MAPPER.writeValueAsString(item); // 2.2.存入redis redisTemplate.opsForValue().set(\u0026#34;item:id:\u0026#34; + item.getId(), json); } // 3.查询商品库存信息 List\u0026lt;ItemStock\u0026gt; stockList = stockService.list(); // 4.放入缓存 for (ItemStock stock : stockList) { // 2.1.item序列化为JSON String json = MAPPER.writeValueAsString(stock); // 2.2.存入redis redisTemplate.opsForValue().set(\u0026#34;item:stock:id:\u0026#34; + stock.getId(), json); } } } Copied! 4.6.查询Redis缓存 现在，Redis缓存已经准备就绪，我们可以再OpenResty中实现查询Redis的逻辑了。如下图红框所示：\n当请求进入OpenResty之后：\n优先查询Redis缓存 如果Redis缓存未命中，再查询Tomcat 4.6.1.封装Redis工具 OpenResty提供了操作Redis的模块，我们只要引入该模块就能直接使用。但是为了方便，我们将Redis操作封装到之前的common.lua工具库中。\n修改/usr/local/openresty/lualib/common.lua文件：\n1）引入Redis模块，并初始化Redis对象\n1 2 3 4 5 -- 导入redis local redis = require(\u0026#39;resty.redis\u0026#39;) -- 初始化redis local red = redis:new() red:set_timeouts(1000, 1000, 1000) Copied! 2）封装函数，用来释放Redis连接，其实是放入连接池\n1 2 3 4 5 6 7 8 9 -- 关闭redis连接的工具方法，其实是放入连接池 local function close_redis(red) local pool_max_idle_time = 10000 -- 连接的空闲时间，单位是毫秒 local pool_size = 100 --连接池大小 local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.log(ngx.ERR, \u0026#34;放入redis连接池失败: \u0026#34;, err) end end Copied! 3）封装函数，根据key查询Redis数据\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 -- 查询redis的方法 ip和port是redis地址，key是查询的key local function read_redis(ip, port, key) -- 获取一个连接 local ok, err = red:connect(ip, port) if not ok then ngx.log(ngx.ERR, \u0026#34;连接redis失败 : \u0026#34;, err) return nil end -- 查询redis local resp, err = red:get(key) -- 查询失败处理 if not resp then ngx.log(ngx.ERR, \u0026#34;查询Redis失败: \u0026#34;, err, \u0026#34;, key = \u0026#34; , key) end --得到的数据为空处理 if resp == ngx.null then resp = nil ngx.log(ngx.ERR, \u0026#34;查询Redis数据为空, key = \u0026#34;, key) end close_redis(red) return resp end Copied! 4）导出\n1 2 3 4 5 6 -- 将方法导出 local _M = { read_http = read_http, read_redis = read_redis } return _M Copied! 完整的common.lua：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 -- 导入redis local redis = require(\u0026#39;resty.redis\u0026#39;) -- 初始化redis local red = redis:new() red:set_timeouts(1000, 1000, 1000) -- 关闭redis连接的工具方法，其实是放入连接池 local function close_redis(red) local pool_max_idle_time = 10000 -- 连接的空闲时间，单位是毫秒 local pool_size = 100 --连接池大小 local ok, err = red:set_keepalive(pool_max_idle_time, pool_size) if not ok then ngx.log(ngx.ERR, \u0026#34;放入redis连接池失败: \u0026#34;, err) end end -- 查询redis的方法 ip和port是redis地址，key是查询的key local function read_redis(ip, port, key) -- 获取一个连接 local ok, err = red:connect(ip, port) if not ok then ngx.log(ngx.ERR, \u0026#34;连接redis失败 : \u0026#34;, err) return nil end -- 查询redis local resp, err = red:get(key) -- 查询失败处理 if not resp then ngx.log(ngx.ERR, \u0026#34;查询Redis失败: \u0026#34;, err, \u0026#34;, key = \u0026#34; , key) end --得到的数据为空处理 if resp == ngx.null then resp = nil ngx.log(ngx.ERR, \u0026#34;查询Redis数据为空, key = \u0026#34;, key) end close_redis(red) return resp end -- 封装函数，发送http请求，并解析响应 local function read_http(path, params) local resp = ngx.location.capture(path,{ method = ngx.HTTP_GET, args = params, }) if not resp then -- 记录错误信息，返回404 ngx.log(ngx.ERR, \u0026#34;http查询失败, path: \u0026#34;, path , \u0026#34;, args: \u0026#34;, args) ngx.exit(404) end return resp.body end -- 将方法导出 local _M = { read_http = read_http, read_redis = read_redis } return _M Copied! 4.6.2.实现Redis查询 接下来，我们就可以去修改item.lua文件，实现对Redis的查询了。\n查询逻辑是：\n根据id查询Redis 如果查询失败则继续查询Tomcat 将查询结果返回 1）修改/usr/local/openresty/lua/item.lua文件，添加一个查询函数：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -- 导入common函数库 local common = require(\u0026#39;common\u0026#39;) local read_http = common.read_http local read_redis = common.read_redis -- 封装查询函数 function read_data(key, path, params) -- 查询本地缓存 local val = read_redis(\u0026#34;127.0.0.1\u0026#34;, 6379, key) -- 判断查询结果 if not val then ngx.log(ngx.ERR, \u0026#34;redis查询失败，尝试查询http， key: \u0026#34;, key) -- redis查询失败，去查询http val = read_http(path, params) end -- 返回数据 return val end Copied! 2）而后修改商品查询、库存查询的业务：\n3）完整的item.lua代码：\n1 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 37 38 -- 导入common函数库 local common = require(\u0026#39;common\u0026#39;) local read_http = common.read_http local read_redis = common.read_redis -- 导入cjson库 local cjson = require(\u0026#39;cjson\u0026#39;) -- 封装查询函数 function read_data(key, path, params) -- 查询本地缓存 local val = read_redis(\u0026#34;127.0.0.1\u0026#34;, 6379, key) -- 判断查询结果 if not val then ngx.log(ngx.ERR, \u0026#34;redis查询失败，尝试查询http， key: \u0026#34;, key) -- redis查询失败，去查询http val = read_http(path, params) end -- 返回数据 return val end -- 获取路径参数 local id = ngx.var[1] -- 查询商品信息 local itemJSON = read_data(\u0026#34;item:id:\u0026#34; .. id, \u0026#34;/item/\u0026#34; .. id, nil) -- 查询库存信息 local stockJSON = read_data(\u0026#34;item:stock:id:\u0026#34; .. id, \u0026#34;/item/stock/\u0026#34; .. id, nil) -- JSON转化为lua的table local item = cjson.decode(itemJSON) local stock = cjson.decode(stockJSON) -- 组合数据 item.stock = stock.stock item.sold = stock.sold -- 把item序列化为json 返回结果 ngx.say(cjson.encode(item)) Copied! 4.7.Nginx本地缓存 现在，整个多级缓存中只差最后一环，也就是nginx的本地缓存了。如图：\n4.7.1.本地缓存API OpenResty为Nginx提供了shard dict的功能，可以在nginx的多个worker之间共享数据，实现缓存功能。\n1）开启共享字典，在nginx.conf的http下添加配置：\n1 2 # 共享字典，也就是本地缓存，名称叫做：item_cache，大小150m lua_shared_dict item_cache 150m; Copied! 2）操作共享字典：\n1 2 3 4 5 6 -- 获取本地缓存对象 local item_cache = ngx.shared.item_cache -- 存储, 指定key、value、过期时间，单位s，默认为0代表永不过期 item_cache:set(\u0026#39;key\u0026#39;, \u0026#39;value\u0026#39;, 1000) -- 读取 local val = item_cache:get(\u0026#39;key\u0026#39;) Copied! 4.7.2.实现本地缓存查询 1）修改/usr/local/openresty/lua/item.lua文件，修改read_data查询函数，添加本地缓存逻辑：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 -- 导入共享词典，本地缓存 local item_cache = ngx.shared.item_cache -- 封装查询函数 function read_data(key, expire, path, params) -- 查询本地缓存 local val = item_cache:get(key) if not val then ngx.log(ngx.ERR, \u0026#34;本地缓存查询失败，尝试查询Redis， key: \u0026#34;, key) -- 查询redis val = read_redis(\u0026#34;127.0.0.1\u0026#34;, 6379, key) -- 判断查询结果 if not val then ngx.log(ngx.ERR, \u0026#34;redis查询失败，尝试查询http， key: \u0026#34;, key) -- redis查询失败，去查询http val = read_http(path, params) end end -- 查询成功，把数据写入本地缓存 item_cache:set(key, val, expire) -- 返回数据 return val end Copied! 2）修改item.lua中查询商品和库存的业务，实现最新的read_data函数：\n其实就是多了缓存时间参数，过期后nginx缓存会自动删除，下次访问即可更新缓存。\n这里给商品基本信息设置超时时间为30分钟，库存为1分钟。\n因为库存更新频率较高，如果缓存时间过长，可能与数据库差异较大。\n3）完整的item.lua文件：\n1 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 37 38 39 40 41 42 43 44 45 46 47 -- 导入common函数库 local common = require(\u0026#39;common\u0026#39;) local read_http = common.read_http local read_redis = common.read_redis -- 导入cjson库 local cjson = require(\u0026#39;cjson\u0026#39;) -- 导入共享词典，本地缓存 local item_cache = ngx.shared.item_cache -- 封装查询函数 function read_data(key, expire, path, params) -- 查询本地缓存 local val = item_cache:get(key) if not val then ngx.log(ngx.ERR, \u0026#34;本地缓存查询失败，尝试查询Redis， key: \u0026#34;, key) -- 查询redis val = read_redis(\u0026#34;127.0.0.1\u0026#34;, 6379, key) -- 判断查询结果 if not val then ngx.log(ngx.ERR, \u0026#34;redis查询失败，尝试查询http， key: \u0026#34;, key) -- redis查询失败，去查询http val = read_http(path, params) end end -- 查询成功，把数据写入本地缓存 item_cache:set(key, val, expire) -- 返回数据 return val end -- 获取路径参数 local id = ngx.var[1] -- 查询商品信息 local itemJSON = read_data(\u0026#34;item:id:\u0026#34; .. id, 1800, \u0026#34;/item/\u0026#34; .. id, nil) -- 查询库存信息 local stockJSON = read_data(\u0026#34;item:stock:id:\u0026#34; .. id, 60, \u0026#34;/item/stock/\u0026#34; .. id, nil) -- JSON转化为lua的table local item = cjson.decode(itemJSON) local stock = cjson.decode(stockJSON) -- 组合数据 item.stock = stock.stock item.sold = stock.sold -- 把item序列化为json 返回结果 ngx.say(cjson.encode(item)) Copied! 5.缓存同步 大多数情况下，浏览器查询到的都是缓存数据，如果缓存数据与数据库数据存在较大差异，可能会产生比较严重的后果。\n所以我们必须保证数据库数据、缓存数据的一致性，这就是缓存与数据库的同步。\n5.1.数据同步策略 缓存数据同步的常见方式有三种：\n设置有效期：给缓存设置有效期，到期后自动删除。再次查询时更新\n优势：简单、方便 缺点：时效性差，缓存过期之前可能不一致 场景：更新频率较低，时效性要求低的业务 同步双写：在修改数据库的同时，直接修改缓存\n优势：时效性强，缓存与数据库强一致 缺点：有代码侵入，耦合度高； 场景：对一致性、时效性要求较高的缓存数据 **异步通知：**修改数据库时发送事件通知，相关服务监听到通知后修改缓存数据\n优势：低耦合，可以同时通知多个缓存服务 缺点：时效性一般，可能存在中间不一致状态 场景：时效性要求一般，有多个服务需要同步 而异步实现又可以基于MQ或者Canal来实现：\n1）基于MQ的异步通知：\n解读：\n商品服务完成对数据的修改后，只需要发送一条消息到MQ中。 缓存服务监听MQ消息，然后完成对缓存的更新 依然有少量的代码侵入。\n2）基于Canal的通知\n解读：\n商品服务完成商品修改后，业务直接结束，没有任何代码侵入 Canal监听MySQL变化，当发现变化后，立即通知缓存服务 缓存服务接收到canal通知，更新缓存 代码零侵入\n5.2.安装Canal 5.2.1.认识Canal Canal [kə\u0026rsquo;næl]，译意为水道/管道/沟渠，canal是阿里巴巴旗下的一款开源项目，基于Java开发。基于数据库增量日志解析，提供增量数据订阅\u0026amp;消费。GitHub的地址：https://github.com/alibaba/canal\nCanal是基于mysql的主从同步来实现的，MySQL主从同步的原理如下：\n1）MySQL master 将数据变更写入二进制日志( binary log），其中记录的数据叫做binary log events 2）MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log) 3）MySQL slave 重放 relay log 中事件，将数据变更反映它自己的数据 而Canal就是把自己伪装成MySQL的一个slave节点，从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端，进而完成对其它数据库的同步。\n5.2.2.安装Canal 安装和配置Canal参考课前资料文档：\n5.3.监听Canal Canal提供了各种语言的客户端，当Canal监听到binlog变化时，会通知Canal的客户端。\n我们可以利用Canal提供的Java客户端，监听Canal通知消息。当收到变化的消息时，完成对缓存的更新。\n不过这里我们会使用GitHub上的第三方开源的canal-starter客户端。地址：https://github.com/NormanGyllenhaal/canal-client\n与SpringBoot完美整合，自动装配，比官方客户端要简单好用很多。\n5.3.1.引入依赖： 1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;top.javatool\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;canal-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.1-RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 5.3.2.编写配置： 1 2 3 canal: destination: heima # canal的集群名字，要与安装canal时设置的名称一致 server: 192.168.150.101:11111 # canal服务地址 Copied! 5.3.3.修改Item实体类 通过@Id、@Column、等注解完成Item与数据库表字段的映射：\n1 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 37 package com.heima.item.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Transient; import javax.persistence.Column; import java.util.Date; @Data @TableName(\u0026#34;tb_item\u0026#34;) public class Item { @TableId(type = IdType.AUTO) @Id private Long id;//商品id @Column(name = \u0026#34;name\u0026#34;) private String name;//商品名称 private String title;//商品标题 private Long price;//价格（分） private String image;//商品图片 private String category;//分类名称 private String brand;//品牌名称 private String spec;//规格 private Integer status;//商品状态 1-正常，2-下架 private Date createTime;//创建时间 private Date updateTime;//更新时间 @TableField(exist = false) @Transient private Integer stock; @TableField(exist = false) @Transient private Integer sold; } Copied! 5.3.4.编写监听器 通过实现EntryHandler\u0026lt;T\u0026gt;接口编写监听器，监听Canal消息。注意两点：\n实现类通过@CanalTable(\u0026quot;tb_item\u0026quot;)指定监听的表信息 EntryHandler的泛型是与表对应的实体类 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 37 38 39 40 41 42 43 package com.heima.item.canal; import com.github.benmanes.caffeine.cache.Cache; import com.heima.item.config.RedisHandler; import com.heima.item.pojo.Item; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import top.javatool.canal.client.annotation.CanalTable; import top.javatool.canal.client.handler.EntryHandler; @CanalTable(\u0026#34;tb_item\u0026#34;) @Component public class ItemHandler implements EntryHandler\u0026lt;Item\u0026gt; { @Autowired private RedisHandler redisHandler; @Autowired private Cache\u0026lt;Long, Item\u0026gt; itemCache; @Override public void insert(Item item) { // 写数据到JVM进程缓存 itemCache.put(item.getId(), item); // 写数据到redis redisHandler.saveItem(item); } @Override public void update(Item before, Item after) { // 写数据到JVM进程缓存 itemCache.put(after.getId(), after); // 写数据到redis redisHandler.saveItem(after); } @Override public void delete(Item item) { // 删除数据到JVM进程缓存 itemCache.invalidate(item.getId()); // 删除数据到redis redisHandler.deleteItemById(item.getId()); } } Copied! 在这里对Redis的操作都封装到了RedisHandler这个对象中，是我们之前做缓存预热时编写的一个类，内容如下：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 package com.heima.item.config; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.heima.item.pojo.Item; import com.heima.item.pojo.ItemStock; import com.heima.item.service.IItemService; import com.heima.item.service.IItemStockService; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import java.util.List; @Component public class RedisHandler implements InitializingBean { @Autowired private StringRedisTemplate redisTemplate; @Autowired private IItemService itemService; @Autowired private IItemStockService stockService; private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public void afterPropertiesSet() throws Exception { // 初始化缓存 // 1.查询商品信息 List\u0026lt;Item\u0026gt; itemList = itemService.list(); // 2.放入缓存 for (Item item : itemList) { // 2.1.item序列化为JSON String json = MAPPER.writeValueAsString(item); // 2.2.存入redis redisTemplate.opsForValue().set(\u0026#34;item:id:\u0026#34; + item.getId(), json); } // 3.查询商品库存信息 List\u0026lt;ItemStock\u0026gt; stockList = stockService.list(); // 4.放入缓存 for (ItemStock stock : stockList) { // 2.1.item序列化为JSON String json = MAPPER.writeValueAsString(stock); // 2.2.存入redis redisTemplate.opsForValue().set(\u0026#34;item:stock:id:\u0026#34; + stock.getId(), json); } } public void saveItem(Item item) { try { String json = MAPPER.writeValueAsString(item); redisTemplate.opsForValue().set(\u0026#34;item:id:\u0026#34; + item.getId(), json); } catch (JsonProcessingException e) { throw new RuntimeException(e); } } public void deleteItemById(Long id) { redisTemplate.delete(\u0026#34;item:id:\u0026#34; + id); } } Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/38u56138/","title":"11.多级缓存"},{"content":" 服务异步通信-高级篇 消息队列在使用过程中，面临着很多实际问题需要思考：\n1.消息可靠性 消息从发送，到消费者接收，会经理多个过程：\n其中的每一步都可能导致消息丢失，常见的丢失原因包括：\n发送时丢失： 生产者发送的消息未送达exchange 消息到达exchange后未到达queue MQ宕机，queue将消息丢失 consumer接收到消息后未消费就宕机 针对这些问题，RabbitMQ分别给出了解决方案：\n生产者确认机制 mq持久化 消费者确认机制 失败重试机制 下面我们就通过案例来演示每一个步骤。\n首先，导入课前资料提供的demo工程：\n项目结构如下：\n1.1.生产者消息确认 RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。这种机制必须给每个消息指定一个唯一ID。消息发送到MQ以后，会返回一个结果给发送者，表示消息是否处理成功。\n返回结果有两种方式：\npublisher-confirm，发送者确认 消息成功投递到交换机，返回ack 消息未投递到交换机，返回nack publisher-return，发送者回执 消息投递到交换机了，但是没有路由到队列。返回ACK，及路由失败原因。 注意：\n1.1.1.修改配置 首先，修改publisher服务中的application.yml文件，添加下面的内容：\n1 2 3 4 5 6 7 spring: rabbitmq: publisher-confirm-type: correlated publisher-returns: true template: mandatory: true Copied! 说明：\npublish-confirm-type：开启publisher-confirm，这里支持两种类型： simple：同步等待confirm结果，直到超时 correlated：异步回调，定义ConfirmCallback，MQ返回结果时会回调这个ConfirmCallback publish-returns：开启publish-return功能，同样是基于callback机制，不过是定义ReturnCallback template.mandatory：定义消息路由失败时的策略。true，则调用ReturnCallback；false：则直接丢弃消息 1.1.2.定义Return回调 每个RabbitTemplate只能配置一个ReturnCallback，因此需要在项目加载时配置：\n修改publisher服务，添加一个：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package cn.itcast.mq.config; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Configuration; @Slf4j @Configuration public class CommonConfig implements ApplicationContextAware { @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { // 获取RabbitTemplate RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class); // 设置ReturnCallback rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -\u0026gt; { // 投递失败，记录日志 log.info(\u0026#34;消息发送失败，应答码{}，原因{}，交换机{}，路由键{},消息{}\u0026#34;, replyCode, replyText, exchange, routingKey, message.toString()); // 如果有业务需要，可以重发消息 }); } } Copied! 1.1.3.定义ConfirmCallback ConfirmCallback可以在发送消息时指定，因为每个业务处理confirm成功或失败的逻辑不一定相同。\n在publisher服务的cn.itcast.mq.spring.SpringAmqpTest类中，定义一个单元测试方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public void testSendMessage2SimpleQueue() throws InterruptedException { // 1.消息体 String message = \u0026#34;hello, spring amqp!\u0026#34;; // 2.全局唯一的消息ID，需要封装到CorrelationData中 CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); // 3.添加callback correlationData.getFuture().addCallback( result -\u0026gt; { if(result.isAck()){ // 3.1.ack，消息成功 log.debug(\u0026#34;消息发送成功, ID:{}\u0026#34;, correlationData.getId()); }else{ // 3.2.nack，消息失败 log.error(\u0026#34;消息发送失败, ID:{}, 原因{}\u0026#34;,correlationData.getId(), result.getReason()); } }, ex -\u0026gt; log.error(\u0026#34;消息发送异常, ID:{}, 原因{}\u0026#34;,correlationData.getId(),ex.getMessage()) ); // 4.发送消息 rabbitTemplate.convertAndSend(\u0026#34;task.direct\u0026#34;, \u0026#34;task\u0026#34;, message, correlationData); // 休眠一会儿，等待ack回执 Thread.sleep(2000); } Copied! 1.2.消息持久化 生产者确认可以确保消息投递到RabbitMQ的队列中，但是消息发送到RabbitMQ以后，如果突然宕机，也可能导致消息丢失。\n要想确保消息在RabbitMQ中安全保存，必须开启消息持久化机制。\n交换机持久化 队列持久化 消息持久化 1.2.1.交换机持久化 RabbitMQ中交换机默认是非持久化的，mq重启后就丢失。\nSpringAMQP中可以通过代码指定交换机持久化：\n1 2 3 4 5 @Bean public DirectExchange simpleExchange(){ // 三个参数：交换机名称、是否持久化、当没有queue与其绑定时是否自动删除 return new DirectExchange(\u0026#34;simple.direct\u0026#34;, true, false); } Copied! 事实上，默认情况下，由SpringAMQP声明的交换机都是持久化的。\n可以在RabbitMQ控制台看到持久化的交换机都会带上D的标示：\n1.2.2.队列持久化 RabbitMQ中队列默认是非持久化的，mq重启后就丢失。\nSpringAMQP中可以通过代码指定交换机持久化：\n1 2 3 4 5 @Bean public Queue simpleQueue(){ // 使用QueueBuilder构建队列，durable就是持久化的 return QueueBuilder.durable(\u0026#34;simple.queue\u0026#34;).build(); } Copied! 事实上，默认情况下，由SpringAMQP声明的队列都是持久化的。\n可以在RabbitMQ控制台看到持久化的队列都会带上D的标示：\n1.2.3.消息持久化 利用SpringAMQP发送消息时，可以设置消息的属性（MessageProperties），指定delivery-mode：\n1：非持久化 2：持久化 用java代码指定：\n默认情况下，SpringAMQP发出的任何消息都是持久化的，不用特意指定。\n1.3.消费者消息确认 RabbitMQ是阅后即焚机制，RabbitMQ确认消息被消费者消费后会立刻删除。\n而RabbitMQ是通过消费者回执来确认消费者是否成功处理消息的：消费者获取消息后，应该向RabbitMQ发送ACK回执，表明自己已经处理消息。\n设想这样的场景：\n1）RabbitMQ投递消息给消费者 2）消费者获取消息后，返回ACK给RabbitMQ 3）RabbitMQ删除消息 4）消费者宕机，消息尚未处理 这样，消息就丢失了。因此消费者返回ACK的时机非常重要。\n而SpringAMQP则允许配置三种确认模式：\n•manual：手动ack，需要在业务代码结束后，调用api发送ack。\n•auto：自动ack，由spring监测listener代码是否出现异常，没有异常则返回ack；抛出异常则返回nack\n•none：关闭ack，MQ假定消费者获取消息后会成功处理，因此消息投递后立即被删除\n由此可知：\nnone模式下，消息投递是不可靠的，可能丢失 auto模式类似事务机制，出现异常时返回nack，消息回滚到mq；没有异常，返回ack manual：自己根据业务情况，判断什么时候该ack 一般，我们都是使用默认的auto即可。\n1.3.1.演示none模式 修改consumer服务的application.yml文件，添加下面内容：\n1 2 3 4 5 spring: rabbitmq: listener: simple: acknowledge-mode: none # 关闭ack Copied! 修改consumer服务的SpringRabbitListener类中的方法，模拟一个消息处理异常：\n1 2 3 4 5 6 7 @RabbitListener(queues = \u0026#34;simple.queue\u0026#34;) public void listenSimpleQueue(String msg) { log.info(\u0026#34;消费者接收到simple.queue的消息：【{}】\u0026#34;, msg); // 模拟异常 System.out.println(1 / 0); log.debug(\u0026#34;消息处理完成！\u0026#34;); } Copied! 测试可以发现，当消息处理抛异常时，消息依然被RabbitMQ删除了。\n1.3.2.演示auto模式 再次把确认机制修改为auto:\n1 2 3 4 5 spring: rabbitmq: listener: simple: acknowledge-mode: auto # 关闭ack Copied! 在异常位置打断点，再次发送消息，程序卡在断点时，可以发现此时消息状态为unack（未确定状态）：\n抛出异常后，因为Spring会自动返回nack，所以消息恢复至Ready状态，并且没有被RabbitMQ删除：\n1.4.消费失败重试机制 当消费者出现异常后，消息会不断requeue（重入队）到队列，再重新发送给消费者，然后再次异常，再次requeue，无限循环，导致mq的消息处理飙升，带来不必要的压力：\n怎么办呢？\n1.4.1.本地重试 我们可以利用Spring的retry机制，在消费者出现异常时利用本地重试，而不是无限制的requeue到mq队列。\n修改consumer服务的application.yml文件，添加内容：\n1 2 3 4 5 6 7 8 9 10 spring: rabbitmq: listener: simple: retry: enabled: true # 开启消费者失败重试 initial-interval: 1000 # 初识的失败等待时长为1秒 multiplier: 1 # 失败的等待时长倍数，下次等待时长 = multiplier * last-interval max-attempts: 3 # 最大重试次数 stateless: true # true无状态；false有状态。如果业务中包含事务，这里改为false Copied! 重启consumer服务，重复之前的测试。可以发现：\n在重试3次后，SpringAMQP会抛出异常AmqpRejectAndDontRequeueException，说明本地重试触发了 查看RabbitMQ控制台，发现消息被删除了，说明最后SpringAMQP返回的是ack，mq删除消息了 结论：\n开启本地重试时，消息处理过程中抛出异常，不会requeue到队列，而是在消费者本地重试 重试达到最大次数后，Spring会返回ack，消息会被丢弃 1.4.2.失败策略 在之前的测试中，达到最大重试次数后，消息会被丢弃，这是由Spring内部机制决定的。\n在开启重试模式后，重试次数耗尽，如果消息依然失败，则需要有MessageRecovery接口来处理，它包含三种不同的实现：\nRejectAndDontRequeueRecoverer：重试耗尽后，直接reject，丢弃消息。默认就是这种方式\nImmediateRequeueMessageRecoverer：重试耗尽后，返回nack，消息重新入队\nRepublishMessageRecoverer：重试耗尽后，将失败消息投递到指定的交换机\n比较优雅的一种处理方案是RepublishMessageRecoverer，失败后将消息投递到一个指定的，专门存放异常消息的队列，后续由人工集中处理。\n1）在consumer服务中定义处理失败消息的交换机和队列\n1 2 3 4 5 6 7 8 9 10 11 12 @Bean public DirectExchange errorMessageExchange(){ return new DirectExchange(\u0026#34;error.direct\u0026#34;); } @Bean public Queue errorQueue(){ return new Queue(\u0026#34;error.queue\u0026#34;, true); } @Bean public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){ return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with(\u0026#34;error\u0026#34;); } Copied! 2）定义一个RepublishMessageRecoverer，关联队列和交换机\n1 2 3 4 @Bean public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){ return new RepublishMessageRecoverer(rabbitTemplate, \u0026#34;error.direct\u0026#34;, \u0026#34;error\u0026#34;); } Copied! 完整代码：\n1 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 package cn.itcast.mq.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.retry.MessageRecoverer; import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer; import org.springframework.context.annotation.Bean; @Configuration public class ErrorMessageConfig { @Bean public DirectExchange errorMessageExchange(){ return new DirectExchange(\u0026#34;error.direct\u0026#34;); } @Bean public Queue errorQueue(){ return new Queue(\u0026#34;error.queue\u0026#34;, true); } @Bean public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){ return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with(\u0026#34;error\u0026#34;); } @Bean public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){ return new RepublishMessageRecoverer(rabbitTemplate, \u0026#34;error.direct\u0026#34;, \u0026#34;error\u0026#34;); } } Copied! 1.5.总结 如何确保RabbitMQ消息的可靠性？\n开启生产者确认机制，确保生产者的消息能到达队列 开启持久化功能，确保消息未消费前在队列中不会丢失 开启消费者确认机制为auto，由spring确认消息处理成功后完成ack 开启消费者失败重试机制，并设置MessageRecoverer，多次重试失败后将消息投递到异常交换机，交由人工处理 2.死信交换机 2.1.初识死信交换机 2.1.1.什么是死信交换机 什么是死信？\n当一个队列中的消息满足下列情况之一时，可以成为死信（dead letter）：\n消费者使用basic.reject或 basic.nack声明消费失败，并且消息的requeue参数设置为false 消息是一个过期消息，超时无人消费 要投递的队列消息满了，无法投递 如果这个包含死信的队列配置了dead-letter-exchange属性，指定了一个交换机，那么队列中的死信就会投递到这个交换机中，而这个交换机称为死信交换机（Dead Letter Exchange，检查DLX）。\n如图，一个消息被消费者拒绝了，变成了死信：\n因为simple.queue绑定了死信交换机 dl.direct，因此死信会投递给这个交换机：\n如果这个死信交换机也绑定了一个队列，则消息最终会进入这个存放死信的队列：\n另外，队列将死信投递给死信交换机时，必须知道两个信息：\n死信交换机名称 死信交换机与死信队列绑定的RoutingKey 这样才能确保投递的消息能到达死信交换机，并且正确的路由到死信队列。\n2.1.2.利用死信交换机接收死信（拓展） 在失败重试策略中，默认的RejectAndDontRequeueRecoverer会在本地重试次数耗尽后，发送reject给RabbitMQ，消息变成死信，被丢弃。\n我们可以给simple.queue添加一个死信交换机，给死信交换机绑定一个队列。这样消息变成死信后也不会丢弃，而是最终投递到死信交换机，路由到与死信交换机绑定的队列。\n我们在consumer服务中，定义一组死信交换机、死信队列：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 声明普通的 simple.queue队列，并且为其指定死信交换机：dl.direct @Bean public Queue simpleQueue2(){ return QueueBuilder.durable(\u0026#34;simple.queue\u0026#34;) // 指定队列名称，并持久化 .deadLetterExchange(\u0026#34;dl.direct\u0026#34;) // 指定死信交换机 .build(); } // 声明死信交换机 dl.direct @Bean public DirectExchange dlExchange(){ return new DirectExchange(\u0026#34;dl.direct\u0026#34;, true, false); } // 声明存储死信的队列 dl.queue @Bean public Queue dlQueue(){ return new Queue(\u0026#34;dl.queue\u0026#34;, true); } // 将死信队列 与 死信交换机绑定 @Bean public Binding dlBinding(){ return BindingBuilder.bind(dlQueue()).to(dlExchange()).with(\u0026#34;simple\u0026#34;); } Copied! 2.1.3.总结 什么样的消息会成为死信？\n消息被消费者reject或者返回nack 消息超时未消费 队列满了 死信交换机的使用场景是什么？\n如果队列绑定了死信交换机，死信会投递到死信交换机； 可以利用死信交换机收集所有消费者处理失败的消息（死信），交由人工处理，进一步提高消息队列的可靠性。 2.2.TTL 一个队列中的消息如果超时未消费，则会变为死信，超时分为两种情况：\n消息所在的队列设置了超时时间 消息本身设置了超时时间 2.2.1.接收超时死信的死信交换机 在consumer服务的SpringRabbitListener中，定义一个新的消费者，并且声明 死信交换机、死信队列：\n1 2 3 4 5 6 7 8 @RabbitListener(bindings = @QueueBinding( value = @Queue(name = \u0026#34;dl.ttl.queue\u0026#34;, durable = \u0026#34;true\u0026#34;), exchange = @Exchange(name = \u0026#34;dl.ttl.direct\u0026#34;), key = \u0026#34;ttl\u0026#34; )) public void listenDlQueue(String msg){ log.info(\u0026#34;接收到 dl.ttl.queue的延迟消息：{}\u0026#34;, msg); } Copied! 2.2.2.声明一个队列，并且指定TTL 要给队列设置超时时间，需要在声明队列时配置x-message-ttl属性：\n1 2 3 4 5 6 7 @Bean public Queue ttlQueue(){ return QueueBuilder.durable(\u0026#34;ttl.queue\u0026#34;) // 指定队列名称，并持久化 .ttl(10000) // 设置队列的超时时间，10秒 .deadLetterExchange(\u0026#34;dl.ttl.direct\u0026#34;) // 指定死信交换机 .build(); } Copied! 注意，这个队列设定了死信交换机为dl.ttl.direct\n声明交换机，将ttl与交换机绑定：\n1 2 3 4 5 6 7 8 @Bean public DirectExchange ttlExchange(){ return new DirectExchange(\u0026#34;ttl.direct\u0026#34;); } @Bean public Binding ttlBinding(){ return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with(\u0026#34;ttl\u0026#34;); } Copied! 发送消息，但是不要指定TTL：\n1 2 3 4 5 6 7 8 9 10 11 @Test public void testTTLQueue() { // 创建消息 String message = \u0026#34;hello, ttl queue\u0026#34;; // 消息ID，需要封装到CorrelationData中 CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); // 发送消息 rabbitTemplate.convertAndSend(\u0026#34;ttl.direct\u0026#34;, \u0026#34;ttl\u0026#34;, message, correlationData); // 记录日志 log.debug(\u0026#34;发送消息成功\u0026#34;); } Copied! 发送消息的日志：\n查看下接收消息的日志：\n因为队列的TTL值是10000ms，也就是10秒。可以看到消息发送与接收之间的时差刚好是10秒。\n2.2.3.发送消息时，设定TTL 在发送消息时，也可以指定TTL：\n1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testTTLMsg() { // 创建消息 Message message = MessageBuilder .withBody(\u0026#34;hello, ttl message\u0026#34;.getBytes(StandardCharsets.UTF_8)) .setExpiration(\u0026#34;5000\u0026#34;) .build(); // 消息ID，需要封装到CorrelationData中 CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); // 发送消息 rabbitTemplate.convertAndSend(\u0026#34;ttl.direct\u0026#34;, \u0026#34;ttl\u0026#34;, message, correlationData); log.debug(\u0026#34;发送消息成功\u0026#34;); } Copied! 查看发送消息日志：\n接收消息日志：\n这次，发送与接收的延迟只有5秒。说明当队列、消息都设置了TTL时，任意一个到期就会成为死信。\n2.2.4.总结 消息超时的两种方式是？\n给队列设置ttl属性，进入队列后超过ttl时间的消息变为死信 给消息设置ttl属性，队列接收到消息超过ttl时间后变为死信 如何实现发送一个消息20秒后消费者才收到消息？\n给消息的目标队列指定死信交换机 将消费者监听的队列绑定到死信交换机 发送消息时给消息设置超时时间为20秒 2.3.延迟队列 利用TTL结合死信交换机，我们实现了消息发出后，消费者延迟收到消息的效果。这种消息模式就称为延迟队列（Delay Queue）模式。\n延迟队列的使用场景包括：\n延迟发送短信 用户下单，如果用户在15 分钟内未支付，则自动取消 预约工作会议，20分钟后自动通知所有参会人员 因为延迟队列的需求非常多，所以RabbitMQ的官方也推出了一个插件，原生支持延迟队列效果。\n这个插件就是DelayExchange插件。参考RabbitMQ的插件列表页面：https://www.rabbitmq.com/community-plugins.html\n使用方式可以参考官网地址：https://blog.rabbitmq.com/posts/2015/04/scheduling-messages-with-rabbitmq\n2.3.1.安装DelayExchange插件 参考课前资料：\n2.3.2.DelayExchange原理 DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时，流程如下：\n接收消息 判断消息是否具备x-delay属性 如果有x-delay属性，说明是延迟消息，持久化到硬盘，读取x-delay值，作为延迟时间 返回routing not found结果给消息发送者 x-delay时间到期后，重新投递消息到指定队列 2.3.3.使用DelayExchange 插件的使用也非常简单：声明一个交换机，交换机的类型可以是任意类型，只需要设定delayed属性为true即可，然后声明队列与其绑定即可。\n1）声明DelayExchange交换机 基于注解方式（推荐）：\n也可以基于@Bean的方式：\n2）发送消息 发送消息时，一定要携带x-delay属性，指定延迟的时间：\n2.3.4.总结 延迟队列插件的使用步骤包括哪些？\n•声明一个交换机，添加delayed属性为true\n•发送消息时，添加x-delay头，值为超时时间\n3.惰性队列 3.1.消息堆积问题 当生产者发送消息的速度超过了消费者处理消息的速度，就会导致队列中的消息堆积，直到队列存储消息达到上限。之后发送的消息就会成为死信，可能会被丢弃，这就是消息堆积问题。\n解决消息堆积有两种思路：\n增加更多消费者，提高消费速度。也就是我们之前说的work queue模式 扩大队列容积，提高堆积上限 要提升队列容积，把消息保存在内存中显然是不行的。\n3.2.惰性队列 从RabbitMQ的3.6.0版本开始，就增加了Lazy Queues的概念，也就是惰性队列。惰性队列的特征如下：\n接收到消息后直接存入磁盘而非内存 消费者要消费消息时才会从磁盘中读取并加载到内存 支持数百万条的消息存储 3.2.1.基于命令行设置lazy-queue 而要设置一个队列为惰性队列，只需要在声明队列时，指定x-queue-mode属性为lazy即可。可以通过命令行将一个运行中的队列修改为惰性队列：\n1 rabbitmqctl set_policy Lazy \u0026#34;^lazy-queue$\u0026#34; \u0026#39;{\u0026#34;queue-mode\u0026#34;:\u0026#34;lazy\u0026#34;}\u0026#39; --apply-to queues Copied! 命令解读：\nrabbitmqctl ：RabbitMQ的命令行工具 set_policy ：添加一个策略 Lazy ：策略名称，可以自定义 \u0026quot;^lazy-queue$\u0026quot; ：用正则表达式匹配队列的名字 '{\u0026quot;queue-mode\u0026quot;:\u0026quot;lazy\u0026quot;}' ：设置队列模式为lazy模式 --apply-to queues ：策略的作用对象，是所有的队列 3.2.2.基于@Bean声明lazy-queue 3.2.3.基于@RabbitListener声明LazyQueue 3.3.总结 消息堆积问题的解决方案？\n队列上绑定多个消费者，提高消费速度 使用惰性队列，可以再mq中保存更多消息 惰性队列的优点有哪些？\n基于磁盘存储，消息上限高 没有间歇性的page-out，性能比较稳定 惰性队列的缺点有哪些？\n基于磁盘存储，消息时效性会降低 性能受限于磁盘的IO 4.MQ集群 4.1.集群分类 RabbitMQ的是基于Erlang语言编写，而Erlang又是一个面向并发的语言，天然支持集群模式。RabbitMQ的集群有两种模式：\n•普通集群：是一种分布式集群，将队列分散到集群的各个节点，从而提高整个集群的并发能力。\n•镜像集群：是一种主从集群，普通集群的基础上，添加了主从备份功能，提高集群的数据可用性。\n镜像集群虽然支持主从，但主从同步并不是强一致的，某些情况下可能有数据丢失的风险。因此在RabbitMQ的3.8版本以后，推出了新的功能：仲裁队列来代替镜像集群，底层采用Raft协议确保主从的数据一致性。\n4.2.普通集群 4.2.1.集群结构和特征 普通集群，或者叫标准集群（classic cluster），具备下列特征：\n会在集群的各个节点间共享部分数据，包括：交换机、队列元信息。不包含队列中的消息。 当访问集群某节点时，如果队列不在该节点，会从数据所在节点传递到当前节点并返回 队列所在节点宕机，队列中的消息就会丢失 结构如图：\n4.2.2.部署 参考课前资料：《RabbitMQ部署指南.md》\n4.3.镜像集群 4.3.1.集群结构和特征 镜像集群：本质是主从模式，具备下面的特征：\n交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份。 创建队列的节点被称为该队列的主节点，备份到的其它节点叫做该队列的镜像节点。 一个队列的主节点可能是另一个队列的镜像节点 所有操作都是主节点完成，然后同步给镜像节点 主宕机后，镜像节点会替代成新的主 结构如图：\n4.3.2.部署 参考课前资料：《RabbitMQ部署指南.md》\n4.4.仲裁队列 4.4.1.集群特征 仲裁队列：仲裁队列是3.8版本以后才有的新功能，用来替代镜像队列，具备下列特征：\n与镜像队列一样，都是主从模式，支持主从数据同步 使用非常简单，没有复杂的配置 主从同步基于Raft协议，强一致 4.4.2.部署 参考课前资料：《RabbitMQ部署指南.md》\n4.4.3.Java代码创建仲裁队列 1 2 3 4 5 6 7 @Bean public Queue quorumQueue() { return QueueBuilder .durable(\u0026#34;quorum.queue\u0026#34;) // 持久化 .quorum() // 仲裁队列 .build(); } Copied! 4.4.4.SpringAMQP连接MQ集群 注意，这里用address来代替host、port方式\n1 2 3 4 5 6 spring: rabbitmq: addresses: 192.168.150.105:8071, 192.168.150.105:8072, 192.168.150.105:8073 username: itcast password: 123321 virtual-host: / Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/89461189/","title":"12.RabbitMQ-高级篇"},{"content":" Nacos源码分析 1.下载Nacos源码并运行 要研究Nacos源码自然不能用打包好的Nacos服务端jar包来运行，需要下载源码自己编译来运行。\n1.1.下载Nacos源码 Nacos的GitHub地址：https://github.com/alibaba/nacos\n课前资料中已经提供了下载好的1.4.2版本的Nacos源码：\n如果需要研究其他版本的同学，也可以自行下载：\n大家找到其release页面：https://github.com/alibaba/nacos/tags，找到其中的1.4.2.版本：\n点击进入后，下载Source code(zip)：\n1.2.导入Demo工程 我们的课前资料提供了一个微服务Demo，包含了服务注册、发现等业务。\n导入该项目后，查看其项目结构：\n结构说明：\ncloud-source-demo：项目父目录 cloud-demo：微服务的父工程，管理微服务依赖 order-service：订单微服务，业务中需要访问user-service，是一个服务消费者 user-service：用户微服务，对外暴露根据id查询用户的接口，是一个服务提供者 1.3.导入Nacos源码 将之前下载好的Nacos源码解压到cloud-source-demo项目目录中：\n然后，使用IDEA将其作为一个module来导入：\n1）选择项目结构选项：\n然后点击导入module：\n在弹出窗口中，选择nacos源码目录：\n然后选择maven模块，finish：\n最后，点击OK即可：\n导入后的项目结构：\n1.4.proto编译 Nacos底层的数据通信会基于protobuf对数据做序列化和反序列化。并将对应的proto文件定义在了consistency这个子模块中：\n我们需要先将proto文件编译为对应的Java代码。\n1.4.1.什么是protobuf protobuf的全称是Protocol Buffer，是Google提供的一种数据序列化协议，这是Google官方的定义：\nProtocol Buffers 是一种轻便高效的结构化数据存储格式，可以用于结构化数据序列化，很适合做数据存储或 RPC 数据交换格式。它可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。\n可以简单理解为，是一种跨语言、跨平台的数据传输格式。与json的功能类似，但是无论是性能，还是数据大小都比json要好很多。\nprotobuf的之所以可以跨语言，就是因为数据定义的格式为.proto格式，需要基于protoc编译为对应的语言。\n1.4.2.安装protoc Protobuf的GitHub地址：https://github.com/protocolbuffers/protobuf/releases\n我们可以下载windows版本的来使用：\n另外，课前资料也提供了下载好的安装包：\n解压到任意非中文目录下，其中的bin目录中的protoc.exe可以帮助我们编译：\n然后将这个bin目录配置到你的环境变量path中，可以参考JDK的配置方式：\n1.4.3.编译proto 进入nacos-1.4.2的consistency模块下的src/main目录下：\n然后打开cmd窗口，运行下面的两个命令：\n1 2 protoc --java_out=./java ./proto/consistency.proto protoc --java_out=./java ./proto/Data.proto Copied! 如图：\n会在nacos的consistency模块中编译出这些java代码：\n1.5.运行 nacos服务端的入口是在console模块中的Nacos类：\n我们需要让它单机启动：\n然后新建一个SpringBootApplication：\n然后填写应用信息：\n然后运行Nacos这个main函数：\n将order-service和user-service服务启动后，可以查看nacos控制台：\n2.服务注册 服务注册到Nacos以后，会保存在一个本地注册表中，其结构如下：\n首先最外层是一个Map，结构为：Map\u0026lt;String, Map\u0026lt;String, Service\u0026gt;\u0026gt;：\nkey：是namespace_id，起到环境隔离的作用。namespace下可以有多个group value：又是一个Map\u0026lt;String, Service\u0026gt;，代表分组及组内的服务。一个组内可以有多个服务 key：代表group分组，不过作为key时格式是group_name:service_name value：分组下的某一个服务，例如userservice，用户服务。类型为Service，内部也包含一个Map\u0026lt;String,Cluster\u0026gt;，一个服务下可以有多个集群 key：集群名称 value：Cluster类型，包含集群的具体信息。一个集群中可能包含多个实例，也就是具体的节点信息，其中包含一个Set\u0026lt;Instance\u0026gt;，就是该集群下的实例的集合 Instance：实例信息，包含实例的IP、Port、健康状态、权重等等信息 每一个服务去注册到Nacos时，就会把信息组织并存入这个Map中。\n2.1.服务注册接口 Nacos提供了服务注册的API接口，客户端只需要向该接口发送请求，即可实现服务注册。\n**接口说明：**注册一个实例到Nacos服务。\n请求类型：POST\n请求路径：/nacos/v1/ns/instance\n请求参数：\n名称 类型 是否必选 描述 ip 字符串 是 服务实例IP port int 是 服务实例port namespaceId 字符串 否 命名空间ID weight double 否 权重 enabled boolean 否 是否上线 healthy boolean 否 是否健康 metadata 字符串 否 扩展信息 clusterName 字符串 否 集群名 serviceName 字符串 是 服务名 groupName 字符串 否 分组名 ephemeral boolean 否 是否临时实例 错误编码：\n错误代码 描述 语义 400 Bad Request 客户端请求中的语法错误 403 Forbidden 没有权限 404 Not Found 无法找到资源 500 Internal Server Error 服务器内部错误 200 OK 正常 2.2.客户端 首先，我们需要找到服务注册的入口。\n2.2.1.NacosServiceRegistryAutoConfiguration 因为Nacos的客户端是基于SpringBoot的自动装配实现的，我们可以在nacos-discovery依赖：\nspring-cloud-starter-alibaba-nacos-discovery-2.2.6.RELEASE.jar\n这个包中找到Nacos自动装配信息：\n可以看到，有很多个自动配置类被加载了，其中跟服务注册有关的就是NacosServiceRegistryAutoConfiguration这个类，我们跟入其中。\n可以看到，在NacosServiceRegistryAutoConfiguration这个类中，包含一个跟自动注册有关的Bean：\n2.2.2.NacosAutoServiceRegistration NacosAutoServiceRegistration源码如图：\n可以看到在初始化时，其父类AbstractAutoServiceRegistration也被初始化了。\nAbstractAutoServiceRegistration如图：\n可以看到它实现了ApplicationListener接口，监听Spring容器启动过程中的事件。\n在监听到WebServerInitializedEvent（web服务初始化完成）的事件后，执行了bind 方法。\n其中的bind方法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void bind(WebServerInitializedEvent event) { // 获取 ApplicationContext ApplicationContext context = event.getApplicationContext(); // 判断服务的 namespace,一般都是null if (context instanceof ConfigurableWebServerApplicationContext) { if (\u0026#34;management\u0026#34;.equals(((ConfigurableWebServerApplicationContext) context) .getServerNamespace())) { return; } } // 记录当前 web 服务的端口 this.port.compareAndSet(0, event.getWebServer().getPort()); // 启动当前服务注册流程 this.start(); } Copied! 其中的start方法流程：\n1 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 void start() { if (!isEnabled()) { if (logger.isDebugEnabled()) { logger.debug(\u0026#34;Discovery Lifecycle disabled. Not starting\u0026#34;); } return; } // 当前服务处于未运行状态时，才进行初始化 if (!this.running.get()) { // 发布服务开始注册的事件 this.context.publishEvent( new InstancePreRegisteredEvent(this, getRegistration())); // ☆☆☆☆开始注册☆☆☆☆ register(); if (shouldRegisterManagement()) { registerManagement(); } // 发布注册完成事件 this.context.publishEvent( new InstanceRegisteredEvent\u0026lt;\u0026gt;(this, getConfiguration())); // 服务状态设置为运行状态，基于AtomicBoolean this.running.compareAndSet(false, true); } } Copied! 其中最关键的register()方法就是完成服务注册的关键，代码如下：\n1 2 3 protected void register() { this.serviceRegistry.register(getRegistration()); } Copied! 此处的this.serviceRegistry就是NacosServiceRegistry：\n2.2.3.NacosServiceRegistry NacosServiceRegistry是Spring的ServiceRegistry接口的实现类，而ServiceRegistry接口是服务注册、发现的规约接口，定义了register、deregister等方法的声明。\n而NacosServiceRegistry对register的实现如下：\n1 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 @Override public void register(Registration registration) { // 判断serviceId是否为空，也就是spring.application.name不能为空 if (StringUtils.isEmpty(registration.getServiceId())) { log.warn(\u0026#34;No service to register for nacos client...\u0026#34;); return; } // 获取Nacos的命名服务，其实就是注册中心服务 NamingService namingService = namingService(); // 获取 serviceId 和 Group String serviceId = registration.getServiceId(); String group = nacosDiscoveryProperties.getGroup(); // 封装服务实例的基本信息，如 cluster-name、是否为临时实例、权重、IP、端口等 Instance instance = getNacosInstanceFromRegistration(registration); try { // 开始注册服务 namingService.registerInstance(serviceId, group, instance); log.info(\u0026#34;nacos registry, {} {} {}:{} register finished\u0026#34;, group, serviceId, instance.getIp(), instance.getPort()); } catch (Exception e) { if (nacosDiscoveryProperties.isFailFast()) { log.error(\u0026#34;nacos registry, {} register failed...{},\u0026#34;, serviceId, registration.toString(), e); rethrowRuntimeException(e); } else { log.warn(\u0026#34;Failfast is false. {} register failed...{},\u0026#34;, serviceId, registration.toString(), e); } } } Copied! 可以看到方法中最终是调用NamingService的registerInstance方法实现注册的。\n而NamingService接口的默认实现就是NacosNamingService。\n2.2.4.NacosNamingService NacosNamingService提供了服务注册、订阅等功能。\n其中registerInstance就是注册服务实例，源码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException { // 检查超时参数是否异常。心跳超时时间(默认15秒)必须大于心跳周期(默认5秒) NamingUtils.checkInstanceIsLegal(instance); // 拼接得到新的服务名，格式为：groupName@@serviceId String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName); // 判断是否为临时实例，默认为 true。 if (instance.isEphemeral()) { // 如果是临时实例，需要定时向 Nacos 服务发送心跳 BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance); beatReactor.addBeatInfo(groupedServiceName, beatInfo); } // 发送注册服务实例的请求 serverProxy.registerService(groupedServiceName, groupName, instance); } Copied! 最终，由NacosProxy的registerService方法，完成服务注册。\n代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void registerService(String serviceName, String groupName, Instance instance) throws NacosException { NAMING_LOGGER.info(\u0026#34;[REGISTER-SERVICE] {} registering service {} with instance: {}\u0026#34;, namespaceId, serviceName, instance); // 组织请求参数 final Map\u0026lt;String, String\u0026gt; params = new HashMap\u0026lt;String, String\u0026gt;(16); params.put(CommonParams.NAMESPACE_ID, namespaceId); params.put(CommonParams.SERVICE_NAME, serviceName); params.put(CommonParams.GROUP_NAME, groupName); params.put(CommonParams.CLUSTER_NAME, instance.getClusterName()); params.put(\u0026#34;ip\u0026#34;, instance.getIp()); params.put(\u0026#34;port\u0026#34;, String.valueOf(instance.getPort())); params.put(\u0026#34;weight\u0026#34;, String.valueOf(instance.getWeight())); params.put(\u0026#34;enable\u0026#34;, String.valueOf(instance.isEnabled())); params.put(\u0026#34;healthy\u0026#34;, String.valueOf(instance.isHealthy())); params.put(\u0026#34;ephemeral\u0026#34;, String.valueOf(instance.isEphemeral())); params.put(\u0026#34;metadata\u0026#34;, JacksonUtils.toJson(instance.getMetadata())); // 通过POST请求将上述参数，发送到 /nacos/v1/ns/instance reqApi(UtilAndComs.nacosUrlInstance, params, HttpMethod.POST); } Copied! 这里提交的信息就是Nacos服务注册接口需要的完整参数，核心参数有：\nnamespace_id：环境 service_name：服务名称 group_name：组名称 cluster_name：集群名称 ip: 当前实例的ip地址 port: 当前实例的端口 而在NacosNamingService的registerInstance方法中，有一段是与服务心跳有关的代码，我们在后续会继续学习。\n2.2.5.客户端注册的流程图 如图：\n2.3.服务端 在nacos-console的模块中，会引入nacos-naming这个模块：\n模块结构如下：\n其中的com.alibaba.nacos.naming.controllers包下就有服务注册、发现等相关的各种接口，其中的服务注册是在InstanceController类中：\n2.3.1.InstanceController 进入InstanceController类，可以看到一个register方法，就是服务注册的方法了：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @CanDistro @PostMapping @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE) public String register(HttpServletRequest request) throws Exception { // 尝试获取namespaceId final String namespaceId = WebUtils .optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); // 尝试获取serviceName，其格式为 group_name@@service_name final String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); NamingUtils.checkServiceNameFormat(serviceName); // 解析出实例信息，封装为Instance对象 final Instance instance = parseInstance(request); // 注册实例 serviceManager.registerInstance(namespaceId, serviceName, instance); return \u0026#34;ok\u0026#34;; } Copied! 这里，进入到了serviceManager.registerInstance()方法中。\n2.3.2.ServiceManager ServiceManager就是Nacos中管理服务、实例信息的核心API，其中就包含Nacos的服务注册表：\n而其中的registerInstance方法就是注册服务实例的方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 /** * Register an instance to a service in AP mode. * * \u0026lt;p\u0026gt;This method creates service or cluster silently if they don\u0026#39;t exist. * * @param namespaceId id of namespace * @param serviceName service name * @param instance instance to register * @throws Exception any error occurred in the process */ public void registerInstance(String namespaceId, String serviceName, Instance instance) throws NacosException { // 创建一个空的service（如果是第一次来注册实例，要先创建一个空service出来，放入注册表） // 此时不包含实例信息 createEmptyService(namespaceId, serviceName, instance.isEphemeral()); // 拿到创建好的service Service service = getService(namespaceId, serviceName); // 拿不到则抛异常 if (service == null) { throw new NacosException(NacosException.INVALID_PARAM, \u0026#34;service not found, namespace: \u0026#34; + namespaceId + \u0026#34;, service: \u0026#34; + serviceName); } // 添加要注册的实例到service中 addInstance(namespaceId, serviceName, instance.isEphemeral(), instance); } Copied! 创建好了服务，接下来就要添加实例到服务中：\n1 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 /** * Add instance to service. * * @param namespaceId namespace * @param serviceName service name * @param ephemeral whether instance is ephemeral * @param ips instances * @throws NacosException nacos exception */ public void addInstance(String namespaceId, String serviceName, boolean ephemeral, Instance... ips) throws NacosException { // 监听服务列表用到的key，服务唯一标识，例如：com.alibaba.nacos.naming.iplist.ephemeral.public##DEFAULT_GROUP@@order-service String key = KeyBuilder.buildInstanceListKey(namespaceId, serviceName, ephemeral); // 获取服务 Service service = getService(namespaceId, serviceName); // 同步锁，避免并发修改的安全问题 synchronized (service) { // 1）获取要更新的实例列表 List\u0026lt;Instance\u0026gt; instanceList = addIpAddresses(service, ephemeral, ips); // 2）封装实例列表到Instances对象 Instances instances = new Instances(); instances.setInstanceList(instanceList); // 3）完成 注册表更新 以及 Nacos集群的数据同步 consistencyService.put(key, instances); } } Copied! 该方法中对修改服务列表的动作加锁处理，确保线程安全。而在同步代码块中，包含下面几步：\n1）先获取要更新的实例列表，addIpAddresses(service, ephemeral, ips); 2）然后将更新后的数据封装到Instances对象中，后面更新注册表时使用 3）最后，调用consistencyService.put()方法完成Nacos集群的数据同步，保证集群一致性。 注意：在第1步的addIPAddress中，会拷贝旧的实例列表，添加新实例到列表中。在第3步中，完成对实例状态更新后，则会用新列表直接覆盖旧实例列表。而在更新过程中，旧实例列表不受影响，用户依然可以读取。\n这样在更新列表状态过程中，无需阻塞用户的读操作，也不会导致用户读取到脏数据，性能比较好。这种方案称为CopyOnWrite方案。\n1）更服务列表 我们来看看实例列表的更新，对应的方法是addIpAddresses(service, ephemeral, ips);：\n1 2 3 private List\u0026lt;Instance\u0026gt; addIpAddresses(Service service, boolean ephemeral, Instance... ips) throws NacosException { return updateIpAddresses(service, UtilsAndCommons.UPDATE_INSTANCE_ACTION_ADD, ephemeral, ips); } Copied! 继续进入updateIpAddresses方法：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public List\u0026lt;Instance\u0026gt; updateIpAddresses(Service service, String action, boolean ephemeral, Instance... ips) throws NacosException { // 根据namespaceId、serviceName获取当前服务的实例列表，返回值是Datum // 第一次来，肯定是null Datum datum = consistencyService .get(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), ephemeral)); // 得到服务中现有的实例列表 List\u0026lt;Instance\u0026gt; currentIPs = service.allIPs(ephemeral); // 创建map，保存实例列表，key为ip地址，value是Instance对象 Map\u0026lt;String, Instance\u0026gt; currentInstances = new HashMap\u0026lt;\u0026gt;(currentIPs.size()); // 创建Set集合，保存实例的instanceId Set\u0026lt;String\u0026gt; currentInstanceIds = Sets.newHashSet(); // 遍历要现有的实例列表 for (Instance instance : currentIPs) { // 添加到map中 currentInstances.put(instance.toIpAddr(), instance); // 添加instanceId到set中 currentInstanceIds.add(instance.getInstanceId()); } // 创建map，用来保存更新后的实例列表 Map\u0026lt;String, Instance\u0026gt; instanceMap; if (datum != null \u0026amp;\u0026amp; null != datum.value) { // 如果服务中已经有旧的数据，则先保存旧的实例列表 instanceMap = setValid(((Instances) datum.value).getInstanceList(), currentInstances); } else { // 如果没有旧数据，则直接创建新的map instanceMap = new HashMap\u0026lt;\u0026gt;(ips.length); } // 遍历实例列表 for (Instance instance : ips) { // 判断服务中是否包含要注册的实例的cluster信息 if (!service.getClusterMap().containsKey(instance.getClusterName())) { // 如果不包含，创建新的cluster Cluster cluster = new Cluster(instance.getClusterName(), service); cluster.init(); // 将集群放入service的注册表 service.getClusterMap().put(instance.getClusterName(), cluster); Loggers.SRV_LOG .warn(\u0026#34;cluster: {} not found, ip: {}, will create new cluster with default configuration.\u0026#34;, instance.getClusterName(), instance.toJson()); } // 删除实例 or 新增实例 ？ if (UtilsAndCommons.UPDATE_INSTANCE_ACTION_REMOVE.equals(action)) { instanceMap.remove(instance.getDatumKey()); } else { // 新增实例，instance生成全新的instanceId Instance oldInstance = instanceMap.get(instance.getDatumKey()); if (oldInstance != null) { instance.setInstanceId(oldInstance.getInstanceId()); } else { instance.setInstanceId(instance.generateInstanceId(currentInstanceIds)); } // 放入instance列表 instanceMap.put(instance.getDatumKey(), instance); } } if (instanceMap.size() \u0026lt;= 0 \u0026amp;\u0026amp; UtilsAndCommons.UPDATE_INSTANCE_ACTION_ADD.equals(action)) { throw new IllegalArgumentException( \u0026#34;ip list can not be empty, service: \u0026#34; + service.getName() + \u0026#34;, ip list: \u0026#34; + JacksonUtils .toJson(instanceMap.values())); } // 将instanceMap中的所有实例转为List返回 return new ArrayList\u0026lt;\u0026gt;(instanceMap.values()); } Copied! 简单来讲，就是先获取旧的实例列表，然后把新的实例信息与旧的做对比，新的实例就添加，老的实例同步ID。然后返回最新的实例列表。\n2）Nacos集群一致性 在完成本地服务列表更新后，Nacos又实现了集群一致性更新，调用的是:\nconsistencyService.put(key, instances);\n这里的ConsistencyService接口，代表集群一致性的接口，有很多中不同实现：\n我们进入DelegateConsistencyServiceImpl来看：\n1 2 3 4 5 @Override public void put(String key, Record value) throws NacosException { // 根据实例是否是临时实例，判断委托对象 mapConsistencyService(key).put(key, value); } Copied! 其中的mapConsistencyService(key)方法就是选择委托方式的：\n1 2 3 4 5 6 private ConsistencyService mapConsistencyService(String key) { // 判断是否是临时实例： // 是，选择 ephemeralConsistencyService，也就是 DistroConsistencyServiceImpl类 // 否，选择 persistentConsistencyService，也就是PersistentConsistencyServiceDelegateImpl return KeyBuilder.matchEphemeralKey(key) ? ephemeralConsistencyService : persistentConsistencyService; } Copied! 默认情况下，所有实例都是临时实例，我们关注DistroConsistencyServiceImpl即可。\n2.3.4.DistroConsistencyServiceImpl 我们来看临时实例的一致性实现：DistroConsistencyServiceImpl类的put方法：\n1 2 3 4 5 6 7 public void put(String key, Record value) throws NacosException { // 先将要更新的实例信息写入本地实例列表 onPut(key, value); // 开始集群同步 distroProtocol.sync(new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX), DataOperation.CHANGE, globalConfig.getTaskDispatchPeriod() / 2); } Copied! 这里方法只有两行：\nonPut(key, value)：其中value就是Instances，要更新的服务信息。这行主要是基于线程池方式，异步的将Service信息写入注册表中(就是那个多重Map) distroProtocol.sync()：就是通过Distro协议将数据同步给集群中的其它Nacos节点 我们先看onPut方法\n2.3.4.1.更新本地实例列表 1）放入阻塞队列 onPut方法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void onPut(String key, Record value) { // 判断是否是临时实例 if (KeyBuilder.matchEphemeralInstanceListKey(key)) { // 封装 Instances 信息到 数据集：Datum Datum\u0026lt;Instances\u0026gt; datum = new Datum\u0026lt;\u0026gt;(); datum.value = (Instances) value; datum.key = key; datum.timestamp.incrementAndGet(); // 放入DataStore dataStore.put(key, datum); } if (!listeners.containsKey(key)) { return; } // 放入阻塞队列，这里的 notifier维护了一个阻塞队列，并且基于线程池异步执行队列中的任务 notifier.addTask(key, DataOperation.CHANGE); } Copied! notifier的类型就是DistroConsistencyServiceImpl.Notifier，内部维护了一个阻塞队列，存放服务列表变更的事件：\naddTask时，将任务加入该阻塞队列：\n1 2 3 4 5 6 7 8 9 10 11 12 // DistroConsistencyServiceImpl.Notifier类的 addTask 方法： public void addTask(String datumKey, DataOperation action) { if (services.containsKey(datumKey) \u0026amp;\u0026amp; action == DataOperation.CHANGE) { return; } if (action == DataOperation.CHANGE) { services.put(datumKey, StringUtils.EMPTY); } // 任务放入阻塞队列 tasks.offer(Pair.with(datumKey, action)); } Copied! 2）Notifier异步更新 同时，notifier还是一个Runnable，通过一个单线程的线程池来不断从阻塞队列中获取任务，执行服务列表的更新。来看下其中的run方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // DistroConsistencyServiceImpl.Notifier类的run方法： @Override public void run() { Loggers.DISTRO.info(\u0026#34;distro notifier started\u0026#34;); // 死循环，不断执行任务。因为是阻塞队列，不会导致CPU负载过高 for (; ; ) { try { // 从阻塞队列中获取任务 Pair\u0026lt;String, DataOperation\u0026gt; pair = tasks.take(); // 处理任务，更新服务列表 handle(pair); } catch (Throwable e) { Loggers.DISTRO.error(\u0026#34;[NACOS-DISTRO] Error while handling notifying task\u0026#34;, e); } } } Copied! 来看看handle方法：\n1 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 37 38 39 40 41 42 43 44 // DistroConsistencyServiceImpl.Notifier类的 handle 方法： private void handle(Pair\u0026lt;String, DataOperation\u0026gt; pair) { try { String datumKey = pair.getValue0(); DataOperation action = pair.getValue1(); services.remove(datumKey); int count = 0; if (!listeners.containsKey(datumKey)) { return; } // 遍历，找到变化的service，这里的 RecordListener就是 Service for (RecordListener listener : listeners.get(datumKey)) { count++; try { // 服务的实例列表CHANGE事件 if (action == DataOperation.CHANGE) { // 更新服务列表 listener.onChange(datumKey, dataStore.get(datumKey).value); continue; } // 服务的实例列表 DELETE 事件 if (action == DataOperation.DELETE) { listener.onDelete(datumKey); continue; } } catch (Throwable e) { Loggers.DISTRO.error(\u0026#34;[NACOS-DISTRO] error while notifying listener of key: {}\u0026#34;, datumKey, e); } } if (Loggers.DISTRO.isDebugEnabled()) { Loggers.DISTRO .debug(\u0026#34;[NACOS-DISTRO] datum change notified, key: {}, listener count: {}, action: {}\u0026#34;, datumKey, count, action.name()); } } catch (Throwable e) { Loggers.DISTRO.error(\u0026#34;[NACOS-DISTRO] Error while handling notifying task\u0026#34;, e); } } Copied! 3）覆盖实例列表 而在Service的onChange方法中，就可以看到更新实例列表的逻辑了：\n1 2 3 4 5 6 7 8 9 10 @Override public void onChange(String key, Instances value) throws Exception { Loggers.SRV_LOG.info(\u0026#34;[NACOS-RAFT] datum is changed, key: {}, value: {}\u0026#34;, key, value); // 更新实例列表 updateIPs(value.getInstanceList(), KeyBuilder.matchEphemeralInstanceListKey(key)); recalculateChecksum(); } Copied! updateIPs方法：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public void updateIPs(Collection\u0026lt;Instance\u0026gt; instances, boolean ephemeral) { // 准备一个Map，key是cluster，值是集群下的Instance集合 Map\u0026lt;String, List\u0026lt;Instance\u0026gt;\u0026gt; ipMap = new HashMap\u0026lt;\u0026gt;(clusterMap.size()); // 获取服务的所有cluster名称 for (String clusterName : clusterMap.keySet()) { ipMap.put(clusterName, new ArrayList\u0026lt;\u0026gt;()); } // 遍历要更新的实例 for (Instance instance : instances) { try { if (instance == null) { Loggers.SRV_LOG.error(\u0026#34;[NACOS-DOM] received malformed ip: null\u0026#34;); continue; } // 判断实例是否包含clusterName，没有的话用默认cluster if (StringUtils.isEmpty(instance.getClusterName())) { instance.setClusterName(UtilsAndCommons.DEFAULT_CLUSTER_NAME); } // 判断cluster是否存在，不存在则创建新的cluster if (!clusterMap.containsKey(instance.getClusterName())) { Loggers.SRV_LOG .warn(\u0026#34;cluster: {} not found, ip: {}, will create new cluster with default configuration.\u0026#34;, instance.getClusterName(), instance.toJson()); Cluster cluster = new Cluster(instance.getClusterName(), this); cluster.init(); getClusterMap().put(instance.getClusterName(), cluster); } // 获取当前cluster实例的集合，不存在则创建新的 List\u0026lt;Instance\u0026gt; clusterIPs = ipMap.get(instance.getClusterName()); if (clusterIPs == null) { clusterIPs = new LinkedList\u0026lt;\u0026gt;(); ipMap.put(instance.getClusterName(), clusterIPs); } // 添加新的实例到 Instance 集合 clusterIPs.add(instance); } catch (Exception e) { Loggers.SRV_LOG.error(\u0026#34;[NACOS-DOM] failed to process ip: \u0026#34; + instance, e); } } for (Map.Entry\u0026lt;String, List\u0026lt;Instance\u0026gt;\u0026gt; entry : ipMap.entrySet()) { //make every ip mine List\u0026lt;Instance\u0026gt; entryIPs = entry.getValue(); // 将实例集合更新到 clusterMap（注册表） clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral); } setLastModifiedMillis(System.currentTimeMillis()); // 发布服务变更的通知消息 getPushService().serviceChanged(this); StringBuilder stringBuilder = new StringBuilder(); for (Instance instance : allIPs()) { stringBuilder.append(instance.toIpAddr()).append(\u0026#34;_\u0026#34;).append(instance.isHealthy()).append(\u0026#34;,\u0026#34;); } Loggers.EVT_LOG.info(\u0026#34;[IP-UPDATED] namespace: {}, service: {}, ips: {}\u0026#34;, getNamespaceId(), getName(), stringBuilder.toString()); } Copied! 在第45行的代码中：clusterMap.get(entry.getKey()).updateIps(entryIPs, ephemeral);\n就是在更新注册表：\n1 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 37 38 39 40 41 42 public void updateIps(List\u0026lt;Instance\u0026gt; ips, boolean ephemeral) { // 获取旧实例列表 Set\u0026lt;Instance\u0026gt; toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances; HashMap\u0026lt;String, Instance\u0026gt; oldIpMap = new HashMap\u0026lt;\u0026gt;(toUpdateInstances.size()); for (Instance ip : toUpdateInstances) { oldIpMap.put(ip.getDatumKey(), ip); } // 检查新加入实例的状态 List\u0026lt;Instance\u0026gt; newIPs = subtract(ips, oldIpMap.values()); if (newIPs.size() \u0026gt; 0) { Loggers.EVT_LOG .info(\u0026#34;{} {SYNC} {IP-NEW} cluster: {}, new ips size: {}, content: {}\u0026#34;, getService().getName(), getName(), newIPs.size(), newIPs.toString()); for (Instance ip : newIPs) { HealthCheckStatus.reset(ip); } } // 移除要删除的实例 List\u0026lt;Instance\u0026gt; deadIPs = subtract(oldIpMap.values(), ips); if (deadIPs.size() \u0026gt; 0) { Loggers.EVT_LOG .info(\u0026#34;{} {SYNC} {IP-DEAD} cluster: {}, dead ips size: {}, content: {}\u0026#34;, getService().getName(), getName(), deadIPs.size(), deadIPs.toString()); for (Instance ip : deadIPs) { HealthCheckStatus.remv(ip); } } toUpdateInstances = new HashSet\u0026lt;\u0026gt;(ips); // 直接覆盖旧实例列表 if (ephemeral) { ephemeralInstances = toUpdateInstances; } else { persistentInstances = toUpdateInstances; } } Copied! 2.3.4.2.集群数据同步 在DistroConsistencyServiceImpl的put方法中分为两步：\n其中的onPut方法已经分析过了。\n下面的distroProtocol.sync()就是集群同步的逻辑了。\nDistroProtocol类的sync方法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void sync(DistroKey distroKey, DataOperation action, long delay) { // 遍历 Nacos 集群中除自己以外的其它节点 for (Member each : memberManager.allMembersWithoutSelf()) { DistroKey distroKeyWithTarget = new DistroKey(distroKey.getResourceKey(), distroKey.getResourceType(), each.getAddress()); // 定义一个Distro的同步任务 DistroDelayTask distroDelayTask = new DistroDelayTask(distroKeyWithTarget, action, delay); // 交给线程池去执行 distroTaskEngineHolder.getDelayTaskExecuteEngine().addTask(distroKeyWithTarget, distroDelayTask); if (Loggers.DISTRO.isDebugEnabled()) { Loggers.DISTRO.debug(\u0026#34;[DISTRO-SCHEDULE] {} to {}\u0026#34;, distroKey, each.getAddress()); } } } Copied! 其中同步的任务封装为一个DistroDelayTask对象。\n交给了distroTaskEngineHolder.getDelayTaskExecuteEngine()执行，这行代码的返回值是：\nNacosDelayTaskExecuteEngine，这个类维护了一个线程池，并且接收任务，执行任务。\n执行任务的方法为processTasks()方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 protected void processTasks() { Collection\u0026lt;Object\u0026gt; keys = getAllTaskKeys(); for (Object taskKey : keys) { AbstractDelayTask task = removeTask(taskKey); if (null == task) { continue; } NacosTaskProcessor processor = getProcessor(taskKey); if (null == processor) { getEngineLog().error(\u0026#34;processor not found for task, so discarded. \u0026#34; + task); continue; } try { // 尝试执行同步任务，如果失败会重试 if (!processor.process(task)) { retryFailedTask(taskKey, task); } } catch (Throwable e) { getEngineLog().error(\u0026#34;Nacos task execute error : \u0026#34; + e.toString(), e); retryFailedTask(taskKey, task); } } } Copied! 可以看出来基于Distro模式的同步是异步进行的，并且失败时会将任务重新入队并充实，因此不保证同步结果的强一致性，属于AP模式的一致性策略。\n2.3.5.服务端流程图 2.4.总结 Nacos的注册表结构是什么样的？\n答：Nacos是多级存储模型，最外层通过namespace来实现环境隔离，然后是group分组，分组下就是服务，一个服务有可以分为不同的集群，集群中包含多个实例。因此其注册表结构为一个Map，类型是：\nMap\u0026lt;String, Map\u0026lt;String, Service\u0026gt;\u0026gt;，\n外层key是namespace_id，内层key是group+serviceName.\nService内部维护一个Map，结构是：Map\u0026lt;String,Cluster\u0026gt;，key是clusterName，值是集群信息\nCluster内部维护一个Set集合，元素是Instance类型，代表集群中的多个实例。\nNacos如何保证并发写的安全性？\n答：首先，在注册实例时，会对service加锁，不同service之间本身就不存在并发写问题，互不影响。相同service时通过锁来互斥。并且，在更新实例列表时，是基于异步的线程池来完成，而线程池的线程数量为1. Nacos如何避免并发读写的冲突？\n答：Nacos在更新实例列表时，会采用CopyOnWrite技术，首先将Old实例列表拷贝一份，然后更新拷贝的实例列表，再用更新后的实例列表来覆盖旧的实例列表。 Nacos如何应对阿里内部数十万服务的并发写请求？\n答：Nacos内部会将服务注册的任务放入阻塞队列，采用线程池异步来完成实例更新，从而提高并发写能力。 3.服务心跳 Nacos的实例分为临时实例和永久实例两种，可以通过在yaml 文件配置：\n1 2 3 4 5 6 7 8 spring: application: name: order-service cloud: nacos: discovery: ephemeral: false # 设置实例为永久实例。true：临时; false：永久 server-addr: 192.168.150.1:8845 Copied! 临时实例基于心跳方式做健康检测，而永久实例则是由Nacos主动探测实例状态。\n其中Nacos提供的心跳的API接口为：\n接口描述：发送某个实例的心跳\n请求类型：PUT\n请求路径：\n1 /nacos/v1/ns/instance/beat Copied! 请求参数：\n名称 类型 是否必选 描述 serviceName 字符串 是 服务名 groupName 字符串 否 分组名 ephemeral boolean 否 是否临时实例 beat JSON格式字符串 是 实例心跳内容 错误编码：\n错误代码 描述 语义 400 Bad Request 客户端请求中的语法错误 403 Forbidden 没有权限 404 Not Found 无法找到资源 500 Internal Server Error 服务器内部错误 200 OK 正常 3.1.客户端 在2.2.4.服务注册这一节中，我们说过NacosNamingService这个类实现了服务的注册，同时也实现了服务心跳：\n1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException { NamingUtils.checkInstanceIsLegal(instance); String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName); // 判断是否是临时实例。 if (instance.isEphemeral()) { // 如果是临时实例，则构建心跳信息BeatInfo BeatInfo beatInfo = beatReactor.buildBeatInfo(groupedServiceName, instance); // 添加心跳任务 beatReactor.addBeatInfo(groupedServiceName, beatInfo); } serverProxy.registerService(groupedServiceName, groupName, instance); } Copied! 3.1.1.BeatInfo 这里的BeanInfo就包含心跳需要的各种信息：\n3.1.2.BeatReactor 而BeatReactor这个类则维护了一个线程池：\n当调用BeatReactor的.addBeatInfo(groupedServiceName, beatInfo)方法时，就会执行心跳：\n1 2 3 4 5 6 7 8 9 10 11 12 13 public void addBeatInfo(String serviceName, BeatInfo beatInfo) { NAMING_LOGGER.info(\u0026#34;[BEAT] adding beat: {} to beat map.\u0026#34;, beatInfo); String key = buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort()); BeatInfo existBeat = null; //fix #1733 if ((existBeat = dom2Beat.remove(key)) != null) { existBeat.setStopped(true); } dom2Beat.put(key, beatInfo); // 利用线程池，定期执行心跳任务，周期为 beatInfo.getPeriod() executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS); MetricsMonitor.getDom2BeatSizeMonitor().set(dom2Beat.size()); } Copied! 心跳周期的默认值在com.alibaba.nacos.api.common.Constants类中：\n可以看到是5秒，默认5秒一次心跳。\n3.1.3.BeatTask 心跳的任务封装在BeatTask这个类中，是一个Runnable，其run方法如下：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @Override public void run() { if (beatInfo.isStopped()) { return; } // 获取心跳周期 long nextTime = beatInfo.getPeriod(); try { // 发送心跳 JsonNode result = serverProxy.sendBeat(beatInfo, BeatReactor.this.lightBeatEnabled); long interval = result.get(\u0026#34;clientBeatInterval\u0026#34;).asLong(); boolean lightBeatEnabled = false; if (result.has(CommonParams.LIGHT_BEAT_ENABLED)) { lightBeatEnabled = result.get(CommonParams.LIGHT_BEAT_ENABLED).asBoolean(); } BeatReactor.this.lightBeatEnabled = lightBeatEnabled; if (interval \u0026gt; 0) { nextTime = interval; } // 判断心跳结果 int code = NamingResponseCode.OK; if (result.has(CommonParams.CODE)) { code = result.get(CommonParams.CODE).asInt(); } if (code == NamingResponseCode.RESOURCE_NOT_FOUND) { // 如果失败，则需要 重新注册实例 Instance instance = new Instance(); instance.setPort(beatInfo.getPort()); instance.setIp(beatInfo.getIp()); instance.setWeight(beatInfo.getWeight()); instance.setMetadata(beatInfo.getMetadata()); instance.setClusterName(beatInfo.getCluster()); instance.setServiceName(beatInfo.getServiceName()); instance.setInstanceId(instance.getInstanceId()); instance.setEphemeral(true); try { serverProxy.registerService(beatInfo.getServiceName(), NamingUtils.getGroupName(beatInfo.getServiceName()), instance); } catch (Exception ignore) { } } } catch (NacosException ex) { NAMING_LOGGER.error(\u0026#34;[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}\u0026#34;, JacksonUtils.toJson(beatInfo), ex.getErrCode(), ex.getErrMsg()); } catch (Exception unknownEx) { NAMING_LOGGER.error(\u0026#34;[CLIENT-BEAT] failed to send beat: {}, unknown exception msg: {}\u0026#34;, JacksonUtils.toJson(beatInfo), unknownEx.getMessage(), unknownEx); } finally { executorService.schedule(new BeatTask(beatInfo), nextTime, TimeUnit.MILLISECONDS); } } Copied! 3.1.5.发送心跳 最终心跳的发送还是通过NamingProxy的sendBeat方法来实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public JsonNode sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws NacosException { if (NAMING_LOGGER.isDebugEnabled()) { NAMING_LOGGER.debug(\u0026#34;[BEAT] {} sending beat to server: {}\u0026#34;, namespaceId, beatInfo.toString()); } // 组织请求参数 Map\u0026lt;String, String\u0026gt; params = new HashMap\u0026lt;String, String\u0026gt;(8); Map\u0026lt;String, String\u0026gt; bodyMap = new HashMap\u0026lt;String, String\u0026gt;(2); if (!lightBeatEnabled) { bodyMap.put(\u0026#34;beat\u0026#34;, JacksonUtils.toJson(beatInfo)); } params.put(CommonParams.NAMESPACE_ID, namespaceId); params.put(CommonParams.SERVICE_NAME, beatInfo.getServiceName()); params.put(CommonParams.CLUSTER_NAME, beatInfo.getCluster()); params.put(\u0026#34;ip\u0026#34;, beatInfo.getIp()); params.put(\u0026#34;port\u0026#34;, String.valueOf(beatInfo.getPort())); // 发送请求，这个地址就是：/v1/ns/instance/beat String result = reqApi(UtilAndComs.nacosUrlBase + \u0026#34;/instance/beat\u0026#34;, params, bodyMap, HttpMethod.PUT); return JacksonUtils.toObj(result); } Copied! 3.2.服务端 对于临时实例，服务端代码分两部分：\n1）InstanceController提供了一个接口，处理客户端的心跳请求 2）定时检测实例心跳是否按期执行 3.2.1.InstanceController 与服务注册时一样，在nacos-naming模块中的InstanceController类中，定义了一个方法用来处理心跳请求：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 @CanDistro @PutMapping(\u0026#34;/beat\u0026#34;) @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE) public ObjectNode beat(HttpServletRequest request) throws Exception { // 解析心跳的请求参数 ObjectNode result = JacksonUtils.createEmptyJsonNode(); result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, switchDomain.getClientBeatInterval()); String beat = WebUtils.optional(request, \u0026#34;beat\u0026#34;, StringUtils.EMPTY); RsInfo clientBeat = null; if (StringUtils.isNotBlank(beat)) { clientBeat = JacksonUtils.toObj(beat, RsInfo.class); } String clusterName = WebUtils .optional(request, CommonParams.CLUSTER_NAME, UtilsAndCommons.DEFAULT_CLUSTER_NAME); String ip = WebUtils.optional(request, \u0026#34;ip\u0026#34;, StringUtils.EMPTY); int port = Integer.parseInt(WebUtils.optional(request, \u0026#34;port\u0026#34;, \u0026#34;0\u0026#34;)); if (clientBeat != null) { if (StringUtils.isNotBlank(clientBeat.getCluster())) { clusterName = clientBeat.getCluster(); } else { // fix #2533 clientBeat.setCluster(clusterName); } ip = clientBeat.getIp(); port = clientBeat.getPort(); } String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); NamingUtils.checkServiceNameFormat(serviceName); Loggers.SRV_LOG.debug(\u0026#34;[CLIENT-BEAT] full arguments: beat: {}, serviceName: {}\u0026#34;, clientBeat, serviceName); // 尝试根据参数中的namespaceId、serviceName、clusterName、ip、port等信息 // 从Nacos的注册表中 获取实例 Instance instance = serviceManager.getInstance(namespaceId, serviceName, clusterName, ip, port); // 如果获取失败，说明心跳失败，实例尚未注册 if (instance == null) { if (clientBeat == null) { result.put(CommonParams.CODE, NamingResponseCode.RESOURCE_NOT_FOUND); return result; } Loggers.SRV_LOG.warn(\u0026#34;[CLIENT-BEAT] The instance has been removed for health mechanism, \u0026#34; + \u0026#34;perform data compensation operations, beat: {}, serviceName: {}\u0026#34;, clientBeat, serviceName); // 这里重新注册一个实例 instance = new Instance(); instance.setPort(clientBeat.getPort()); instance.setIp(clientBeat.getIp()); instance.setWeight(clientBeat.getWeight()); instance.setMetadata(clientBeat.getMetadata()); instance.setClusterName(clusterName); instance.setServiceName(serviceName); instance.setInstanceId(instance.getInstanceId()); instance.setEphemeral(clientBeat.isEphemeral()); serviceManager.registerInstance(namespaceId, serviceName, instance); } // 尝试基于namespaceId和serviceName从 注册表中获取Service服务 Service service = serviceManager.getService(namespaceId, serviceName); // 如果不存在，说明服务不存在，返回404 if (service == null) { throw new NacosException(NacosException.SERVER_ERROR, \u0026#34;service not found: \u0026#34; + serviceName + \u0026#34;@\u0026#34; + namespaceId); } if (clientBeat == null) { clientBeat = new RsInfo(); clientBeat.setIp(ip); clientBeat.setPort(port); clientBeat.setCluster(clusterName); } // 如果心跳没问题，开始处理心跳结果 service.processClientBeat(clientBeat); result.put(CommonParams.CODE, NamingResponseCode.OK); if (instance.containsMetadata(PreservedMetadataKeys.HEART_BEAT_INTERVAL)) { result.put(SwitchEntry.CLIENT_BEAT_INTERVAL, instance.getInstanceHeartBeatInterval()); } result.put(SwitchEntry.LIGHT_BEAT_ENABLED, switchDomain.isLightBeatEnabled()); return result; } Copied! 最终，在确认心跳请求对应的服务、实例都在的情况下，开始交给Service类处理这次心跳请求。调用了Service的processClientBeat方法\n3.2.2.处理心跳请求 查看Service的service.processClientBeat(clientBeat);方法：\n1 2 3 4 5 6 public void processClientBeat(final RsInfo rsInfo) { ClientBeatProcessor clientBeatProcessor = new ClientBeatProcessor(); clientBeatProcessor.setService(this); clientBeatProcessor.setRsInfo(rsInfo); HealthCheckReactor.scheduleNow(clientBeatProcessor); } Copied! 可以看到心跳信息被封装到了 ClientBeatProcessor类中，交给了HealthCheckReactor处理，HealthCheckReactor就是对线程池的封装，不用过多查看。\n关键的业务逻辑都在ClientBeatProcessor这个类中，它是一个Runnable，其中的run方法如下：\n1 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 @Override public void run() { Service service = this.service; if (Loggers.EVT_LOG.isDebugEnabled()) { Loggers.EVT_LOG.debug(\u0026#34;[CLIENT-BEAT] processing beat: {}\u0026#34;, rsInfo.toString()); } String ip = rsInfo.getIp(); String clusterName = rsInfo.getCluster(); int port = rsInfo.getPort(); // 获取集群信息 Cluster cluster = service.getClusterMap().get(clusterName); // 获取集群中的所有实例信息 List\u0026lt;Instance\u0026gt; instances = cluster.allIPs(true); for (Instance instance : instances) { // 找到心跳的这个实例 if (instance.getIp().equals(ip) \u0026amp;\u0026amp; instance.getPort() == port) { if (Loggers.EVT_LOG.isDebugEnabled()) { Loggers.EVT_LOG.debug(\u0026#34;[CLIENT-BEAT] refresh beat: {}\u0026#34;, rsInfo.toString()); } // 更新实例的最后一次心跳时间 lastBeat instance.setLastBeat(System.currentTimeMillis()); if (!instance.isMarked()) { if (!instance.isHealthy()) { instance.setHealthy(true); Loggers.EVT_LOG .info(\u0026#34;service: {} {POS} {IP-ENABLED} valid: {}:{}@{}, region: {}, msg: client beat ok\u0026#34;, cluster.getService().getName(), ip, port, cluster.getName(), UtilsAndCommons.LOCALHOST_SITE); getPushService().serviceChanged(service); } } } } } Copied! 处理心跳请求的核心就是更新心跳实例的最后一次心跳时间，lastBeat，这个会成为判断实例心跳是否过期的关键指标！\n3.3.3.心跳异常检测 在服务注册时，一定会创建一个Service对象，而Service中有一个init方法，会在注册时被调用：\n1 2 3 4 5 6 7 8 public void init() { // 开启心跳检测的任务 HealthCheckReactor.scheduleCheck(clientBeatCheckTask); for (Map.Entry\u0026lt;String, Cluster\u0026gt; entry : clusterMap.entrySet()) { entry.getValue().setService(this); entry.getValue().init(); } } Copied! 其中HealthCheckReactor.scheduleCheck就是执行心跳检测的定时任务：\n可以看到，该任务是5000ms执行一次，也就是5秒对实例的心跳状态做一次检测。\n此处的ClientBeatCheckTask同样是一个Runnable，其中的run方法为：\n1 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 37 38 39 40 41 42 43 44 45 46 47 @Override public void run() { try { // 找到所有临时实例的列表 List\u0026lt;Instance\u0026gt; instances = service.allIPs(true); // first set health status of instances: for (Instance instance : instances) { // 判断 心跳间隔（当前时间 - 最后一次心跳时间） 是否大于 心跳超时时间，默认15秒 if (System.currentTimeMillis() - instance.getLastBeat() \u0026gt; instance.getInstanceHeartBeatTimeOut()) { if (!instance.isMarked()) { if (instance.isHealthy()) { // 如果超时，标记实例为不健康 healthy = false instance.setHealthy(false); // 发布实例状态变更的事件 getPushService().serviceChanged(service); ApplicationUtils.publishEvent(new InstanceHeartbeatTimeoutEvent(this, instance)); } } } } if (!getGlobalConfig().isExpireInstance()) { return; } // then remove obsolete instances: for (Instance instance : instances) { if (instance.isMarked()) { continue; } // 判断心跳间隔（当前时间 - 最后一次心跳时间）是否大于 实例被删除的最长超时时间，默认30秒 if (System.currentTimeMillis() - instance.getLastBeat() \u0026gt; instance.getIpDeleteTimeout()) { // 如果是超过了30秒，则删除实例 Loggers.SRV_LOG.info(\u0026#34;[AUTO-DELETE-IP] service: {}, ip: {}\u0026#34;, service.getName(), JacksonUtils.toJson(instance)); deleteIp(instance); } } } catch (Exception e) { Loggers.SRV_LOG.warn(\u0026#34;Exception while processing client beat time out.\u0026#34;, e); } } Copied! 其中的超时时间同样是在com.alibaba.nacos.api.common.Constants这个类中：\n3.3.4.主动健康检测 对于非临时实例（ephemeral=false)，Nacos会采用主动的健康检测，定时向实例发送请求，根据响应来判断实例健康状态。\n入口在2.3.2小节的ServiceManager类中的registerInstance方法：\n创建空服务时：\n1 2 3 4 public void createEmptyService(String namespaceId, String serviceName, boolean local) throws NacosException { // 如果服务不存在，创建新的服务 createServiceIfAbsent(namespaceId, serviceName, local, null); } Copied! 创建服务流程：\n1 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 void createServiceIfAbsent(String namespaceId, String serviceName, boolean local, Cluster cluster) throws NacosException { // 尝试获取服务 Service service = getService(namespaceId, serviceName); if (service == null) { // 发现服务不存在，开始创建新服务 Loggers.SRV_LOG.info(\u0026#34;creating empty service {}:{}\u0026#34;, namespaceId, serviceName); service = new Service(); service.setName(serviceName); service.setNamespaceId(namespaceId); service.setGroupName(NamingUtils.getGroupName(serviceName)); // now validate the service. if failed, exception will be thrown service.setLastModifiedMillis(System.currentTimeMillis()); service.recalculateChecksum(); if (cluster != null) { cluster.setService(service); service.getClusterMap().put(cluster.getName(), cluster); } service.validate(); // ** 写入注册表并初始化 ** putServiceAndInit(service); if (!local) { addOrReplaceService(service); } } } Copied! 关键在putServiceAndInit(service)方法中：\n1 2 3 4 5 6 7 8 9 10 11 12 private void putServiceAndInit(Service service) throws NacosException { // 将服务写入注册表 putService(service); service = getService(service.getNamespaceId(), service.getName()); // 完成服务的初始化 service.init(); consistencyService .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), true), service); consistencyService .listen(KeyBuilder.buildInstanceListKey(service.getNamespaceId(), service.getName(), false), service); Loggers.SRV_LOG.info(\u0026#34;[NEW-SERVICE] {}\u0026#34;, service.toJson()); } Copied! 进入初始化逻辑：service.init()，这个会进入Service类中：\n1 2 3 4 5 6 7 8 9 10 11 12 13 /** * Init service. */ public void init() { // 开启临时实例的心跳监测任务 HealthCheckReactor.scheduleCheck(clientBeatCheckTask); // 遍历注册表中的集群 for (Map.Entry\u0026lt;String, Cluster\u0026gt; entry : clusterMap.entrySet()) { entry.getValue().setService(this); // 完成集群初识化 entry.getValue().init(); } } Copied! 这里集群的初始化 entry.getValue().init();会进入Cluster类型的init()方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 /** * Init cluster. */ public void init() { if (inited) { return; } // 创建健康检测的任务 checkTask = new HealthCheckTask(this); // 这里会开启对 非临时实例的 定时健康检测 HealthCheckReactor.scheduleCheck(checkTask); inited = true; } Copied! 这里的HealthCheckReactor.scheduleCheck(checkTask);会开启定时任务，对非临时实例做健康检测。检测逻辑定义在HealthCheckTask这个类中，是一个Runnable，其中的run方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void run() { try { if (distroMapper.responsible(cluster.getService().getName()) \u0026amp;\u0026amp; switchDomain .isHealthCheckEnabled(cluster.getService().getName())) { // 开始健康检测 healthCheckProcessor.process(this); // 记录日志 。。。 } } catch (Throwable e) { // 记录日志 。。。 } finally { if (!cancelled) { // 结束后，再次进行任务调度，一定延迟后执行 HealthCheckReactor.scheduleCheck(this); // 。。。 } } } Copied! 健康检测逻辑定义在healthCheckProcessor.process(this);方法中，在HealthCheckProcessor接口中，这个接口也有很多实现，默认是TcpSuperSenseProcessor：\n进入TcpSuperSenseProcessor的process方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public void process(HealthCheckTask task) { // 获取所有 非临时实例的 集合 List\u0026lt;Instance\u0026gt; ips = task.getCluster().allIPs(false); if (CollectionUtils.isEmpty(ips)) { return; } for (Instance ip : ips) { // 封装健康检测信息到 Beat Beat beat = new Beat(ip, task); // 放入一个阻塞队列中 taskQueue.add(beat); MetricsMonitor.getTcpHealthCheckMonitor().incrementAndGet(); } } Copied! 可以看到，所有的健康检测任务都被放入一个阻塞队列，而不是立即执行了。这里又采用了异步执行的策略，可以看到Nacos中大量这样的设计。\n而TcpSuperSenseProcessor本身就是一个Runnable，在它的构造函数中会把自己放入线程池中去执行，其run方法如下：\n1 2 3 4 5 6 7 8 9 10 11 public void run() { while (true) { try { // 处理任务 processTask(); // ... } catch (Throwable e) { SRV_LOG.error(\u0026#34;[HEALTH-CHECK] error while processing NIO task\u0026#34;, e); } } } Copied! 通过processTask来处理健康检测的任务：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void processTask() throws Exception { // 将任务封装为一个 TaskProcessor，并放入集合 Collection\u0026lt;Callable\u0026lt;Void\u0026gt;\u0026gt; tasks = new LinkedList\u0026lt;\u0026gt;(); do { Beat beat = taskQueue.poll(CONNECT_TIMEOUT_MS / 2, TimeUnit.MILLISECONDS); if (beat == null) { return; } tasks.add(new TaskProcessor(beat)); } while (taskQueue.size() \u0026gt; 0 \u0026amp;\u0026amp; tasks.size() \u0026lt; NIO_THREAD_COUNT * 64); // 批量处理集合中的任务 for (Future\u0026lt;?\u0026gt; f : GlobalExecutor.invokeAllTcpSuperSenseTask(tasks)) { f.get(); } } Copied! 任务被封装到了TaskProcessor中去执行了，TaskProcessor是一个Callable，其中的call方法：\n1 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 37 38 39 40 41 42 43 44 45 46 47 @Override public Void call() { // 获取检测任务已经等待的时长 long waited = System.currentTimeMillis() - beat.getStartTime(); if (waited \u0026gt; MAX_WAIT_TIME_MILLISECONDS) { Loggers.SRV_LOG.warn(\u0026#34;beat task waited too long: \u0026#34; + waited + \u0026#34;ms\u0026#34;); } SocketChannel channel = null; try { // 获取实例信息 Instance instance = beat.getIp(); // 通过NIO建立TCP连接 channel = SocketChannel.open(); channel.configureBlocking(false); // only by setting this can we make the socket close event asynchronous channel.socket().setSoLinger(false, -1); channel.socket().setReuseAddress(true); channel.socket().setKeepAlive(true); channel.socket().setTcpNoDelay(true); Cluster cluster = beat.getTask().getCluster(); int port = cluster.isUseIPPort4Check() ? instance.getPort() : cluster.getDefCkport(); channel.connect(new InetSocketAddress(instance.getIp(), port)); // 注册连接、读取事件 SelectionKey key = channel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ); key.attach(beat); keyMap.put(beat.toString(), new BeatKey(key)); beat.setStartTime(System.currentTimeMillis()); GlobalExecutor .scheduleTcpSuperSenseTask(new TimeOutTask(key), CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (Exception e) { beat.finishCheck(false, false, switchDomain.getTcpHealthParams().getMax(), \u0026#34;tcp:error:\u0026#34; + e.getMessage()); if (channel != null) { try { channel.close(); } catch (Exception ignore) { } } } return null; } Copied! 3.3.总结 Nacos的健康检测有两种模式：\n临时实例： 采用客户端心跳检测模式，心跳周期5秒 心跳间隔超过15秒则标记为不健康 心跳间隔超过30秒则从服务列表删除 永久实例： 采用服务端主动健康检测方式 周期为2000 + 5000毫秒内的随机数 检测异常只会标记为不健康，不会删除 那么为什么Nacos有临时和永久两种实例呢？\n以淘宝为例，双十一大促期间，流量会比平常高出很多，此时服务肯定需要增加更多实例来应对高并发，而这些实例在双十一之后就无需继续使用了，采用临时实例比较合适。而对于服务的一些常备实例，则使用永久实例更合适。\n与eureka相比，Nacos与Eureka在临时实例上都是基于心跳模式实现，差别不大，主要是心跳周期不同，eureka是30秒，Nacos是5秒。\n另外，Nacos支持永久实例，而Eureka不支持，Eureka只提供了心跳模式的健康监测，而没有主动检测功能。\n4.服务发现 Nacos提供了一个根据serviceId查询实例列表的接口：\n接口描述：查询服务下的实例列表\n请求类型：GET\n请求路径：\n1 /nacos/v1/ns/instance/list Copied! 请求参数：\n名称 类型 是否必选 描述 serviceName 字符串 是 服务名 groupName 字符串 否 分组名 namespaceId 字符串 否 命名空间ID clusters 字符串，多个集群用逗号分隔 否 集群名称 healthyOnly boolean 否，默认为false 是否只返回健康实例 错误编码：\n错误代码 描述 语义 400 Bad Request 客户端请求中的语法错误 403 Forbidden 没有权限 404 Not Found 无法找到资源 500 Internal Server Error 服务器内部错误 200 OK 正常 4.1.客户端 4.1.1.定时更新服务列表 4.1.1.1.NacosNamingService 在2.2.4小节中，我们讲到一个类NacosNamingService，这个类不仅仅提供了服务注册功能，同样提供了服务发现的功能。\n多个重载的方法最终都会进入一个方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Override public List\u0026lt;Instance\u0026gt; getAllInstances(String serviceName, String groupName, List\u0026lt;String\u0026gt; clusters, boolean subscribe) throws NacosException { ServiceInfo serviceInfo; // 1.判断是否需要订阅服务信息（默认为 true） if (subscribe) { // 1.1.订阅服务信息 serviceInfo = hostReactor.getServiceInfo(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, \u0026#34;,\u0026#34;)); } else { // 1.2.直接去nacos拉取服务信息 serviceInfo = hostReactor .getServiceInfoDirectlyFromServer(NamingUtils.getGroupedName(serviceName, groupName), StringUtils.join(clusters, \u0026#34;,\u0026#34;)); } // 2.从服务信息中获取实例列表并返回 List\u0026lt;Instance\u0026gt; list; if (serviceInfo == null || CollectionUtils.isEmpty(list = serviceInfo.getHosts())) { return new ArrayList\u0026lt;Instance\u0026gt;(); } return list; } Copied! 4.1.1.2.HostReactor 进入1.1.订阅服务消息，这里是由HostReactor类的getServiceInfo()方法来实现的：\n1 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 37 38 39 40 41 42 public ServiceInfo getServiceInfo(final String serviceName, final String clusters) { NAMING_LOGGER.debug(\u0026#34;failover-mode: \u0026#34; + failoverReactor.isFailoverSwitch()); // 由 服务名@@集群名拼接 key String key = ServiceInfo.getKey(serviceName, clusters); if (failoverReactor.isFailoverSwitch()) { return failoverReactor.getService(key); } // 读取本地服务列表的缓存，缓存是一个Map，格式：Map\u0026lt;String, ServiceInfo\u0026gt; ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters); // 判断缓存是否存在 if (null == serviceObj) { // 不存在，创建空ServiceInfo serviceObj = new ServiceInfo(serviceName, clusters); // 放入缓存 serviceInfoMap.put(serviceObj.getKey(), serviceObj); // 放入待更新的服务列表（updatingMap）中 updatingMap.put(serviceName, new Object()); // 立即更新服务列表 updateServiceNow(serviceName, clusters); // 从待更新列表中移除 updatingMap.remove(serviceName); } else if (updatingMap.containsKey(serviceName)) { // 缓存中有，但是需要更新 if (UPDATE_HOLD_INTERVAL \u0026gt; 0) { // hold a moment waiting for update finish 等待5秒中，待更新完成 synchronized (serviceObj) { try { serviceObj.wait(UPDATE_HOLD_INTERVAL); } catch (InterruptedException e) { NAMING_LOGGER .error(\u0026#34;[getServiceInfo] serviceName:\u0026#34; + serviceName + \u0026#34;, clusters:\u0026#34; + clusters, e); } } } } // 开启定时更新服务列表的功能 scheduleUpdateIfAbsent(serviceName, clusters); // 返回缓存中的服务信息 return serviceInfoMap.get(serviceObj.getKey()); } Copied! 基本逻辑就是先从本地缓存读，根据结果来选择：\n如果本地缓存没有，立即去nacos读取，updateServiceNow(serviceName, clusters)\n如果本地缓存有，则开启定时更新功能，并返回缓存结果：\nscheduleUpdateIfAbsent(serviceName, clusters) 在UpdateTask中，最终还是调用updateService方法：\n不管是立即更新服务列表，还是定时更新服务列表，最终都会执行HostReactor中的updateService()方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void updateService(String serviceName, String clusters) throws NacosException { ServiceInfo oldService = getServiceInfo0(serviceName, clusters); try { // 基于ServerProxy发起远程调用，查询服务列表 String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUdpPort(), false); if (StringUtils.isNotEmpty(result)) { // 处理查询结果 processServiceJson(result); } } finally { if (oldService != null) { synchronized (oldService) { oldService.notifyAll(); } } } } Copied! 4.1.1.3.ServerProxy 而ServerProxy的queryList方法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 public String queryList(String serviceName, String clusters, int udpPort, boolean healthyOnly) throws NacosException { // 准备请求参数 final Map\u0026lt;String, String\u0026gt; params = new HashMap\u0026lt;String, String\u0026gt;(8); params.put(CommonParams.NAMESPACE_ID, namespaceId); params.put(CommonParams.SERVICE_NAME, serviceName); params.put(\u0026#34;clusters\u0026#34;, clusters); params.put(\u0026#34;udpPort\u0026#34;, String.valueOf(udpPort)); params.put(\u0026#34;clientIP\u0026#34;, NetUtils.localIP()); params.put(\u0026#34;healthyOnly\u0026#34;, String.valueOf(healthyOnly)); // 发起请求，地址与API接口一致 return reqApi(UtilAndComs.nacosUrlBase + \u0026#34;/instance/list\u0026#34;, params, HttpMethod.GET); } Copied! 4.1.2.处理服务变更通知 除了定时更新服务列表的功能外，Nacos还支持服务列表变更时的主动推送功能。\n在HostReactor类的构造函数中，有非常重要的几个步骤：\n基本思路是：\n通过PushReceiver监听服务端推送的变更数据 解析数据后，通过NotifyCenter发布服务变更的事件 InstanceChangeNotifier监听变更事件，完成对服务列表的更新 4.1.2.1.PushReceiver 我们先看PushReceiver，这个类会以UDP方式接收Nacos服务端推送的服务变更数据。\n先看构造函数：\n1 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 PushReceiver(HostReactor hostReactor) { try { this.hostReactor = hostReactor; // 创建 UDP客户端 String udpPort = getPushReceiverUdpPort(); if (StringUtils.isEmpty(udpPort)) { this.udpSocket = new DatagramSocket(); } else { this.udpSocket = new DatagramSocket(new InetSocketAddress(Integer.parseInt(udpPort))); } // 准备线程池 this.executorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setDaemon(true); thread.setName(\u0026#34;com.alibaba.nacos.naming.push.receiver\u0026#34;); return thread; } }); // 开启线程任务，准备接收变更数据 this.executorService.execute(this); } catch (Exception e) { NAMING_LOGGER.error(\u0026#34;[NA] init udp socket failed\u0026#34;, e); } } Copied! PushReceiver构造函数中基于线程池来运行任务。这是因为PushReceiver本身也是一个Runnable，其中的run方法业务逻辑如下：\n1 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 @Override public void run() { while (!closed) { try { // byte[] is initialized with 0 full filled by default byte[] buffer = new byte[UDP_MSS]; DatagramPacket packet = new DatagramPacket(buffer, buffer.length); // 接收推送数据 udpSocket.receive(packet); // 解析为json字符串 String json = new String(IoUtils.tryDecompress(packet.getData()), UTF_8).trim(); NAMING_LOGGER.info(\u0026#34;received push data: \u0026#34; + json + \u0026#34; from \u0026#34; + packet.getAddress().toString()); // 反序列化为对象 PushPacket pushPacket = JacksonUtils.toObj(json, PushPacket.class); String ack; if (\u0026#34;dom\u0026#34;.equals(pushPacket.type) || \u0026#34;service\u0026#34;.equals(pushPacket.type)) { // 交给 HostReactor去处理 hostReactor.processServiceJson(pushPacket.data); // send ack to server 发送ACK回执，略。。 } catch (Exception e) { if (closed) { return; } NAMING_LOGGER.error(\u0026#34;[NA] error while receiving push data\u0026#34;, e); } } } Copied! 4.1.2.2.HostReactor 通知数据的处理由交给了HostReactor的processServiceJson方法：\n1 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 37 38 39 40 41 42 43 44 45 46 public ServiceInfo processServiceJson(String json) { // 解析出ServiceInfo信息 ServiceInfo serviceInfo = JacksonUtils.toObj(json, ServiceInfo.class); String serviceKey = serviceInfo.getKey(); if (serviceKey == null) { return null; } // 查询缓存中的 ServiceInfo ServiceInfo oldService = serviceInfoMap.get(serviceKey); // 如果缓存存在，则需要校验哪些数据要更新 boolean changed = false; if (oldService != null) { // 拉取的数据是否已经过期 if (oldService.getLastRefTime() \u0026gt; serviceInfo.getLastRefTime()) { NAMING_LOGGER.warn(\u0026#34;out of date data received, old-t: \u0026#34; + oldService.getLastRefTime() + \u0026#34;, new-t: \u0026#34; + serviceInfo.getLastRefTime()); } // 放入缓存 serviceInfoMap.put(serviceInfo.getKey(), serviceInfo); // 中间是缓存与新数据的对比，得到newHosts：新增的实例；remvHosts：待移除的实例; // modHosts：需要修改的实例 if (newHosts.size() \u0026gt; 0 || remvHosts.size() \u0026gt; 0 || modHosts.size() \u0026gt; 0) { // 发布实例变更的事件 NotifyCenter.publishEvent(new InstancesChangeEvent( serviceInfo.getName(), serviceInfo.getGroupName(), serviceInfo.getClusters(), serviceInfo.getHosts())); DiskCache.write(serviceInfo, cacheDir); } } else { // 本地缓存不存在 changed = true; // 放入缓存 serviceInfoMap.put(serviceInfo.getKey(), serviceInfo); // 直接发布实例变更的事件 NotifyCenter.publishEvent(new InstancesChangeEvent( serviceInfo.getName(), serviceInfo.getGroupName(), serviceInfo.getClusters(), serviceInfo.getHosts())); serviceInfo.setJsonFromServer(json); DiskCache.write(serviceInfo, cacheDir); } // 。。。 return serviceInfo; } Copied! 4.2.服务端 4.2.1.拉取服务列表接口 在2.3.1小节介绍的InstanceController中，提供了拉取服务列表的接口：\n1 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 /** * Get all instance of input service. * * @param request http request * @return list of instance * @throws Exception any error during list */ @GetMapping(\u0026#34;/list\u0026#34;) @Secured(parser = NamingResourceParser.class, action = ActionTypes.READ) public ObjectNode list(HttpServletRequest request) throws Exception { // 从request中获取namespaceId和serviceName String namespaceId = WebUtils.optional(request, CommonParams.NAMESPACE_ID, Constants.DEFAULT_NAMESPACE_ID); String serviceName = WebUtils.required(request, CommonParams.SERVICE_NAME); NamingUtils.checkServiceNameFormat(serviceName); String agent = WebUtils.getUserAgent(request); String clusters = WebUtils.optional(request, \u0026#34;clusters\u0026#34;, StringUtils.EMPTY); String clientIP = WebUtils.optional(request, \u0026#34;clientIP\u0026#34;, StringUtils.EMPTY); // 获取客户端的 UDP端口 int udpPort = Integer.parseInt(WebUtils.optional(request, \u0026#34;udpPort\u0026#34;, \u0026#34;0\u0026#34;)); String env = WebUtils.optional(request, \u0026#34;env\u0026#34;, StringUtils.EMPTY); boolean isCheck = Boolean.parseBoolean(WebUtils.optional(request, \u0026#34;isCheck\u0026#34;, \u0026#34;false\u0026#34;)); String app = WebUtils.optional(request, \u0026#34;app\u0026#34;, StringUtils.EMPTY); String tenant = WebUtils.optional(request, \u0026#34;tid\u0026#34;, StringUtils.EMPTY); boolean healthyOnly = Boolean.parseBoolean(WebUtils.optional(request, \u0026#34;healthyOnly\u0026#34;, \u0026#34;false\u0026#34;)); // 获取服务列表 return doSrvIpxt(namespaceId, serviceName, agent, clusters, clientIP, udpPort, env, isCheck, app, tenant, healthyOnly); } Copied! 进入doSrvIpxt()方法来获取服务列表：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public ObjectNode doSrvIpxt(String namespaceId, String serviceName, String agent, String clusters, String clientIP, int udpPort, String env, boolean isCheck, String app, String tid, boolean healthyOnly) throws Exception { ClientInfo clientInfo = new ClientInfo(agent); ObjectNode result = JacksonUtils.createEmptyJsonNode(); // 获取服务列表信息 Service service = serviceManager.getService(namespaceId, serviceName); long cacheMillis = switchDomain.getDefaultCacheMillis(); // now try to enable the push try { if (udpPort \u0026gt; 0 \u0026amp;\u0026amp; pushService.canEnablePush(agent)) { // 添加当前客户端 IP、UDP端口到 PushService 中 pushService .addClient(namespaceId, serviceName, clusters, agent, new InetSocketAddress(clientIP, udpPort), pushDataSource, tid, app); cacheMillis = switchDomain.getPushCacheMillis(serviceName); } } catch (Exception e) { Loggers.SRV_LOG .error(\u0026#34;[NACOS-API] failed to added push client {}, {}:{}\u0026#34;, clientInfo, clientIP, udpPort, e); cacheMillis = switchDomain.getDefaultCacheMillis(); } if (service == null) { // 如果没找到，返回空 if (Loggers.SRV_LOG.isDebugEnabled()) { Loggers.SRV_LOG.debug(\u0026#34;no instance to serve for service: {}\u0026#34;, serviceName); } result.put(\u0026#34;name\u0026#34;, serviceName); result.put(\u0026#34;clusters\u0026#34;, clusters); result.put(\u0026#34;cacheMillis\u0026#34;, cacheMillis); result.replace(\u0026#34;hosts\u0026#34;, JacksonUtils.createEmptyArrayNode()); return result; } // 结果的检测，异常实例的剔除等逻辑省略 // 最终封装结果并返回 。。。 result.replace(\u0026#34;hosts\u0026#34;, hosts); if (clientInfo.type == ClientInfo.ClientType.JAVA \u0026amp;\u0026amp; clientInfo.version.compareTo(VersionUtil.parseVersion(\u0026#34;1.0.0\u0026#34;)) \u0026gt;= 0) { result.put(\u0026#34;dom\u0026#34;, serviceName); } else { result.put(\u0026#34;dom\u0026#34;, NamingUtils.getServiceName(serviceName)); } result.put(\u0026#34;name\u0026#34;, serviceName); result.put(\u0026#34;cacheMillis\u0026#34;, cacheMillis); result.put(\u0026#34;lastRefTime\u0026#34;, System.currentTimeMillis()); result.put(\u0026#34;checksum\u0026#34;, service.getChecksum()); result.put(\u0026#34;useSpecifiedURL\u0026#34;, false); result.put(\u0026#34;clusters\u0026#34;, clusters); result.put(\u0026#34;env\u0026#34;, env); result.replace(\u0026#34;metadata\u0026#34;, JacksonUtils.transferToJsonNode(service.getMetadata())); return result; } Copied! 4.2.2.发布服务变更的UDP通知 在上一节中，InstanceController中的doSrvIpxt()方法中，有这样一行代码：\n1 2 3 pushService.addClient(namespaceId, serviceName, clusters, agent, new InetSocketAddress(clientIP, udpPort), pushDataSource, tid, app); Copied! 其实是把消费者的UDP端口、IP等信息封装为一个PushClient对象，存储PushService中。方便以后服务变更后推送消息。\nPushService类本身实现了ApplicationListener接口：\n这个是事件监听器接口，监听的是ServiceChangeEvent（服务变更事件）。\n当服务列表变化时，就会通知我们：\n4.3.总结 Nacos的服务发现分为两种模式：\n模式一：主动拉取模式，消费者定期主动从Nacos拉取服务列表并缓存起来，再服务调用时优先读取本地缓存中的服务列表。 模式二：订阅模式，消费者订阅Nacos中的服务列表，并基于UDP协议来接收服务变更通知。当Nacos中的服务列表更新时，会发送UDP广播给所有订阅者。 与Eureka相比，Nacos的订阅模式服务状态更新更及时，消费者更容易及时发现服务列表的变化，剔除故障服务。\n","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/9167149t/","title":"13.Nacos源码分析"},{"content":" Sentinel源码分析 1.Sentinel的基本概念 Sentinel实现限流、隔离、降级、熔断等功能，本质要做的就是两件事情：\n统计数据：统计某个资源的访问数据（QPS、RT等信息） 规则判断：判断限流规则、隔离规则、降级规则、熔断规则是否满足 这里的资源就是希望被Sentinel保护的业务，例如项目中定义的controller方法就是默认被Sentinel保护的资源。\n1.1.ProcessorSlotChain 实现上述功能的核心骨架是一个叫做ProcessorSlotChain的类。这个类基于责任链模式来设计，将不同的功能（限流、降级、系统保护）封装为一个个的Slot，请求进入后逐个执行即可。\n其工作流如图：\n责任链中的Slot也分为两大类：\n统计数据构建部分（statistic） NodeSelectorSlot：负责构建簇点链路中的节点（DefaultNode），将这些节点形成链路树 ClusterBuilderSlot：负责构建某个资源的ClusterNode，ClusterNode可以保存资源的运行信息（响应时间、QPS、block 数目、线程数、异常数等）以及来源信息（origin名称） StatisticSlot：负责统计实时调用数据，包括运行信息、来源信息等 规则判断部分（rule checking） AuthoritySlot：负责授权规则（来源控制） SystemSlot：负责系统保护规则 ParamFlowSlot：负责热点参数限流规则 FlowSlot：负责限流规则 DegradeSlot：负责降级规则 1.2.Node Sentinel中的簇点链路是由一个个的Node组成的，Node是一个接口，包括下面的实现：\n所有的节点都可以记录对资源的访问统计数据，所以都是StatisticNode的子类。\n按照作用分为两类Node：\nDefaultNode：代表链路树中的每一个资源，一个资源出现在不同链路中时，会创建不同的DefaultNode节点。而树的入口节点叫EntranceNode，是一种特殊的DefaultNode ClusterNode：代表资源，一个资源不管出现在多少链路中，只会有一个ClusterNode。记录的是当前资源被访问的所有统计数据之和。 DefaultNode记录的是资源在当前链路中的访问数据，用来实现基于链路模式的限流规则。ClusterNode记录的是资源在所有链路中的访问数据，实现默认模式、关联模式的限流规则。\n例如：我们在一个SpringMVC项目中，有两个业务：\n业务1：controller中的资源/order/query访问了service中的资源/goods 业务2：controller中的资源/order/save访问了service中的资源/goods 创建的链路图如下：\n1.3.Entry 默认情况下，Sentinel会将controller中的方法作为被保护资源，那么问题来了，我们该如何将自己的一段代码标记为一个Sentinel的资源呢？\nSentinel中的资源用Entry来表示。声明Entry的API示例：\n1 2 3 4 5 6 7 8 // 资源名可使用任意有业务语义的字符串，比如方法名、接口名或其它可唯一标识的字符串。 try (Entry entry = SphU.entry(\u0026#34;resourceName\u0026#34;)) { // 被保护的业务逻辑 // do something here... } catch (BlockException ex) { // 资源访问阻止，被限流或被降级 // 在此处进行相应的处理操作 } Copied! 1.3.1.自定义资源 例如，我们在order-service服务中，将OrderService的queryOrderById()方法标记为一个资源。\n1）首先在order-service中引入sentinel依赖\n1 2 3 4 5 \u0026lt;!--sentinel--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-sentinel\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 2）然后配置Sentinel地址\n1 2 3 4 5 spring: cloud: sentinel: transport: dashboard: localhost:8089 # 这里我的sentinel用了8089的端口 Copied! 3）修改OrderService类的queryOrderById方法\n代码这样来实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public Order queryOrderById(Long orderId) { // 创建Entry，标记资源，资源名为resource1 try (Entry entry = SphU.entry(\u0026#34;resource1\u0026#34;)) { // 1.查询订单，这里是假数据 Order order = Order.build(101L, 4999L, \u0026#34;小米 MIX4\u0026#34;, 1, 1L, null); // 2.查询用户，基于Feign的远程调用 User user = userClient.findById(order.getUserId()); // 3.设置 order.setUser(user); // 4.返回 return order; }catch (BlockException e){ log.error(\u0026#34;被限流或降级\u0026#34;, e); return null; } } Copied! 4）访问\n打开浏览器，访问order服务：http://localhost:8080/order/101\n然后打开sentinel控制台，查看簇点链路：\n1.3.2.基于注解标记资源 在之前学习Sentinel的时候，我们知道可以通过给方法添加@SentinelResource注解的形式来标记资源。\n这个是怎么实现的呢？\n来看下我们引入的Sentinel依赖包：\n其中的spring.factories声明需要就是自动装配的配置类，内容如下：\n我们来看下SentinelAutoConfiguration这个类：\n可以看到，在这里声明了一个Bean，SentinelResourceAspect：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 /** * Aspect for methods with {@link SentinelResource} annotation. * * @author Eric Zhao */ @Aspect public class SentinelResourceAspect extends AbstractSentinelAspectSupport { // 切点是添加了 @SentinelResource注解的类 @Pointcut(\u0026#34;@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)\u0026#34;) public void sentinelResourceAnnotationPointcut() { } // 环绕增强 @Around(\u0026#34;sentinelResourceAnnotationPointcut()\u0026#34;) public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable { // 获取受保护的方法 Method originMethod = resolveMethod(pjp); // 获取 @SentinelResource注解 SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class); if (annotation == null) { // Should not go through here. throw new IllegalStateException(\u0026#34;Wrong state for SentinelResource annotation\u0026#34;); } // 获取注解上的资源名称 String resourceName = getResourceName(annotation.value(), originMethod); EntryType entryType = annotation.entryType(); int resourceType = annotation.resourceType(); Entry entry = null; try { // 创建资源 Entry entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs()); // 执行受保护的方法 Object result = pjp.proceed(); return result; } catch (BlockException ex) { return handleBlockException(pjp, annotation, ex); } catch (Throwable ex) { Class\u0026lt;? extends Throwable\u0026gt;[] exceptionsToIgnore = annotation.exceptionsToIgnore(); // The ignore list will be checked first. if (exceptionsToIgnore.length \u0026gt; 0 \u0026amp;\u0026amp; exceptionBelongsTo(ex, exceptionsToIgnore)) { throw ex; } if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) { traceException(ex); return handleFallback(pjp, annotation, ex); } // No fallback function can handle the exception, so throw it out. throw ex; } finally { if (entry != null) { entry.exit(1, pjp.getArgs()); } } } } Copied! 简单来说，@SentinelResource注解就是一个标记，而Sentinel基于AOP思想，对被标记的方法做环绕增强，完成资源（Entry）的创建。\n1.4.Context 上一节，我们发现簇点链路中除了controller方法、service方法两个资源外，还多了一个默认的入口节点：\nsentinel_spring_web_context，是一个EntranceNode类型的节点\n这个节点是在初始化Context的时候由Sentinel帮我们创建的。\n1.4.1.什么是Context 那么，什么是Context呢？\nContext 代表调用链路上下文，贯穿一次调用链路中的所有资源（ Entry），基于ThreadLocal。 Context 维持着入口节点（entranceNode）、本次调用链路的 curNode（当前资源节点）、调用来源（origin）等信息。 后续的Slot都可以通过Context拿到DefaultNode或者ClusterNode，从而获取统计数据，完成规则判断 Context初始化的过程中，会创建EntranceNode，contextName就是EntranceNode的名称 对应的API如下：\n1 2 // 创建context，包含两个参数：context名称、 来源名称 ContextUtil.enter(\u0026#34;contextName\u0026#34;, \u0026#34;originName\u0026#34;); Copied! 1.4.2.Context的初始化 那么这个Context又是在何时完成初始化的呢？\n1.4.2.1.自动装配 来看下我们引入的Sentinel依赖包：\n其中的spring.factories声明需要就是自动装配的配置类，内容如下：\n我们先看SentinelWebAutoConfiguration这个类：\n这个类实现了WebMvcConfigurer，我们知道这个是SpringMVC自定义配置用到的类，可以配置HandlerInterceptor：\n可以看到这里配置了一个SentinelWebInterceptor的拦截器。\nSentinelWebInterceptor的声明如下：\n发现它继承了AbstractSentinelInterceptor这个类。\nHandlerInterceptor拦截器会拦截一切进入controller的方法，执行preHandle前置拦截方法，而Context的初始化就是在这里完成的。\n1.4.2.2.AbstractSentinelInterceptor HandlerInterceptor拦截器会拦截一切进入controller的方法，执行preHandle前置拦截方法，而Context的初始化就是在这里完成的。\n我们来看看这个类的preHandle实现：\n1 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 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { // 获取资源名称，一般是controller方法的@RequestMapping路径，例如/order/{orderId} String resourceName = getResourceName(request); if (StringUtil.isEmpty(resourceName)) { return true; } // 从request中获取请求来源，将来做 授权规则 判断时会用 String origin = parseOrigin(request); // 获取 contextName，默认是sentinel_spring_web_context String contextName = getContextName(request); // 创建 Context ContextUtil.enter(contextName, origin); // 创建资源，名称就是当前请求的controller方法的映射路径 Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN); request.setAttribute(baseWebMvcConfig.getRequestAttributeName(), entry); return true; } catch (BlockException e) { try { handleBlockException(request, response, e); } finally { ContextUtil.exit(); } return false; } } Copied! 1.4.2.3.ContextUtil 创建Context的方法就是 ContextUtil.enter(contextName, origin);\n我们进入该方法：\n1 2 3 4 5 6 7 public static Context enter(String name, String origin) { if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) { throw new ContextNameDefineException( \u0026#34;The \u0026#34; + Constants.CONTEXT_DEFAULT_NAME + \u0026#34; can\u0026#39;t be permit to defined!\u0026#34;); } return trueEnter(name, origin); } Copied! 进入trueEnter方法：\n1 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 37 38 protected static Context trueEnter(String name, String origin) { // 尝试获取context Context context = contextHolder.get(); // 判空 if (context == null) { // 如果为空，开始初始化 Map\u0026lt;String, DefaultNode\u0026gt; localCacheNameMap = contextNameNodeMap; // 尝试获取入口节点 DefaultNode node = localCacheNameMap.get(name); if (node == null) { LOCK.lock(); try { node = contextNameNodeMap.get(name); if (node == null) { // 入口节点为空，初始化入口节点 EntranceNode node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null); // 添加入口节点到 ROOT Constants.ROOT.addChild(node); // 将入口节点放入缓存 Map\u0026lt;String, DefaultNode\u0026gt; newMap = new HashMap\u0026lt;\u0026gt;(contextNameNodeMap.size() + 1); newMap.putAll(contextNameNodeMap); newMap.put(name, node); contextNameNodeMap = newMap; } } finally { LOCK.unlock(); } } // 创建Context，参数为：入口节点 和 contextName context = new Context(node, name); // 设置请求来源 origin context.setOrigin(origin); // 放入ThreadLocal contextHolder.set(context); } // 返回 return context; } Copied! 2.ProcessorSlotChain执行流程 接下来我们跟踪源码，验证下ProcessorSlotChain的执行流程。\n2.1.入口 首先，回到一切的入口，AbstractSentinelInterceptor类的preHandle方法：\n还有，SentinelResourceAspect的环绕增强方法：\n可以看到，任何一个资源必定要执行SphU.entry()这个方法:\n1 2 3 4 public static Entry entry(String name, int resourceType, EntryType trafficType, Object[] args) throws BlockException { return Env.sph.entryWithType(name, resourceType, trafficType, 1, args); } Copied! 继续进入Env.sph.entryWithType(name, resourceType, trafficType, 1, args);：\n1 2 3 4 5 6 7 8 @Override public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized, Object[] args) throws BlockException { // 将 资源名称等基本信息 封装为一个 StringResourceWrapper对象 StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType); // 继续 return entryWithPriority(resource, count, prioritized, args); } Copied! 进入entryWithPriority方法：\n1 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 private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException { // 获取 Context Context context = ContextUtil.getContext(); if (context == null) { // Using default context. context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME); } 、\t// 获取 Slot执行链，同一个资源，会创建一个执行链，放入缓存 ProcessorSlot\u0026lt;Object\u0026gt; chain = lookProcessChain(resourceWrapper); // 创建 Entry，并将 resource、chain、context 记录在 Entry中 Entry e = new CtEntry(resourceWrapper, chain, context); try { // 执行 slotChain chain.entry(context, resourceWrapper, null, count, prioritized, args); } catch (BlockException e1) { e.exit(count, args); throw e1; } catch (Throwable e1) { // This should not happen, unless there are errors existing in Sentinel internal. RecordLog.info(\u0026#34;Sentinel unexpected exception\u0026#34;, e1); } return e; } Copied! 在这段代码中，会获取ProcessorSlotChain对象，然后基于chain.entry()开始执行slotChain中的每一个Slot. 而这里创建的是其实现类：DefaultProcessorSlotChain.\n获取ProcessorSlotChain以后会保存到一个Map中，key是ResourceWrapper，值是ProcessorSlotChain.\n所以，一个资源只会有一个ProcessorSlotChain.\n2.2.DefaultProcessorSlotChain 我们进入DefaultProcessorSlotChain的entry方法：\n1 2 3 4 5 6 @Override public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args) throws Throwable { // first，就是责任链中的第一个 slot first.transformEntry(context, resourceWrapper, t, count, prioritized, args); } Copied! 这里的first，类型是AbstractLinkedProcessorSlot：\n看下继承关系：\n因此，first一定是这些实现类中的一个，按照最早讲的责任链顺序，first应该就是 NodeSelectorSlot。\n不过，既然是基于责任链模式，所以这里只要记住下一个slot就可以了，也就是next：\nnext确实是NodeSelectSlot类型。\n而NodeSelectSlot的next一定是ClusterBuilderSlot，依次类推：\n责任链就建立起来了。\n2.3.NodeSelectorSlot NodeSelectorSlot负责构建簇点链路中的节点（DefaultNode），将这些节点形成链路树。\n核心代码：\n1 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 @Override public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args) throws Throwable { // 尝试获取 当前资源的 DefaultNode DefaultNode node = map.get(context.getName()); if (node == null) { synchronized (this) { node = map.get(context.getName()); if (node == null) { // 如果为空，为当前资源创建一个新的 DefaultNode node = new DefaultNode(resourceWrapper, null); HashMap\u0026lt;String, DefaultNode\u0026gt; cacheMap = new HashMap\u0026lt;String, DefaultNode\u0026gt;(map.size()); cacheMap.putAll(map); // 放入缓存中，注意这里的 key是contextName， // 这样不同链路进入相同资源，就会创建多个 DefaultNode cacheMap.put(context.getName(), node); map = cacheMap; // 当前节点加入上一节点的 child中，这样就构成了调用链路树 ((DefaultNode) context.getLastNode()).addChild(node); } } } // context中的curNode（当前节点）设置为新的 node context.setCurNode(node); // 执行下一个 slot fireEntry(context, resourceWrapper, node, count, prioritized, args); } Copied! 这个Slot完成了这么几件事情：\n为当前资源创建 DefaultNode 将DefaultNode放入缓存中，key是contextName，这样不同链路入口的请求，将会创建多个DefaultNode，相同链路则只有一个DefaultNode 将当前资源的DefaultNode设置为上一个资源的childNode 将当前资源的DefaultNode设置为Context中的curNode（当前节点） 下一个slot，就是ClusterBuilderSlot\n2.4.ClusterBuilderSlot ClusterBuilderSlot负责构建某个资源的ClusterNode，核心代码：\n1 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 @Override public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable { // 判空，注意ClusterNode是共享的成员变量，也就是说一个资源只有一个ClusterNode，与链路无关 if (clusterNode == null) { synchronized (lock) { if (clusterNode == null) { // 创建 cluster node. clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType()); HashMap\u0026lt;ResourceWrapper, ClusterNode\u0026gt; newMap = new HashMap\u0026lt;\u0026gt;(Math.max(clusterNodeMap.size(), 16)); newMap.putAll(clusterNodeMap); // 放入缓存，可以是nodeId，也就是resource名称 newMap.put(node.getId(), clusterNode); clusterNodeMap = newMap; } } } // 将资源的 DefaultNode与 ClusterNode关联 node.setClusterNode(clusterNode); // 记录请求来源 origin 将 origin放入 entry if (!\u0026#34;\u0026#34;.equals(context.getOrigin())) { Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin()); context.getCurEntry().setOriginNode(originNode); } // 继续下一个slot fireEntry(context, resourceWrapper, node, count, prioritized, args); } Copied! 2.5.StatisticSlot StatisticSlot负责统计实时调用数据，包括运行信息（访问次数、线程数）、来源信息等。\nStatisticSlot是实现限流的关键，其中基于滑动时间窗口算法维护了计数器，统计进入某个资源的请求次数。\n核心代码：\n1 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 @Override public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable { try { // 放行到下一个 slot，做限流、降级等判断 fireEntry(context, resourceWrapper, node, count, prioritized, args); // 请求通过了, 线程计数器 +1 ，用作线程隔离 node.increaseThreadNum(); // 请求计数器 +1 用作限流 node.addPassRequest(count); if (context.getCurEntry().getOriginNode() != null) { // 如果有 origin，来源计数器也都要 +1 context.getCurEntry().getOriginNode().increaseThreadNum(); context.getCurEntry().getOriginNode().addPassRequest(count); } if (resourceWrapper.getEntryType() == EntryType.IN) { // 如果是入口资源，还要给全局计数器 +1. Constants.ENTRY_NODE.increaseThreadNum(); Constants.ENTRY_NODE.addPassRequest(count); } // 请求通过后的回调. for (ProcessorSlotEntryCallback\u0026lt;DefaultNode\u0026gt; handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) { handler.onPass(context, resourceWrapper, node, count, args); } } catch (Throwable e) { // 各种异常处理就省略了。。。 context.getCurEntry().setError(e); throw e; } } Copied! 另外，需要注意的是，所有的计数+1动作都包括两部分，以 node.addPassRequest(count);为例：\n1 2 3 4 5 6 7 @Override public void addPassRequest(int count) { // DefaultNode的计数器，代表当前链路的 计数器 super.addPassRequest(count); // ClusterNode计数器，代表当前资源的 总计数器 this.clusterNode.addPassRequest(count); } Copied! 具体计数方式，我们后续再看。\n接下来，进入规则校验的相关slot了，依次是：\nAuthoritySlot：负责授权规则（来源控制） SystemSlot：负责系统保护规则 ParamFlowSlot：负责热点参数限流规则 FlowSlot：负责限流规则 DegradeSlot：负责降级规则 2.6.AuthoritySlot 负责请求来源origin的授权规则判断，如图：\n核心API：\n1 2 3 4 5 6 7 8 @Override public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable { // 校验黑白名单 checkBlackWhiteAuthority(resourceWrapper, context); // 进入下一个 slot fireEntry(context, resourceWrapper, node, count, prioritized, args); } Copied! 黑白名单校验的逻辑：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException { // 获取授权规则 Map\u0026lt;String, Set\u0026lt;AuthorityRule\u0026gt;\u0026gt; authorityRules = AuthorityRuleManager.getAuthorityRules(); if (authorityRules == null) { return; } Set\u0026lt;AuthorityRule\u0026gt; rules = authorityRules.get(resource.getName()); if (rules == null) { return; } // 遍历规则并判断 for (AuthorityRule rule : rules) { if (!AuthorityRuleChecker.passCheck(rule, context)) { // 规则不通过，直接抛出异常 throw new AuthorityException(context.getOrigin(), rule); } } } Copied! 再看下AuthorityRuleChecker.passCheck(rule, context)方法：\n1 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 37 static boolean passCheck(AuthorityRule rule, Context context) { // 得到请求来源 origin String requester = context.getOrigin(); // 来源为空，或者规则为空，都直接放行 if (StringUtil.isEmpty(requester) || StringUtil.isEmpty(rule.getLimitApp())) { return true; } // rule.getLimitApp()得到的就是 白名单 或 黑名单 的字符串，这里先用 indexOf方法判断 int pos = rule.getLimitApp().indexOf(requester); boolean contain = pos \u0026gt; -1; if (contain) { // 如果包含 origin，还要进一步做精确判断，把名单列表以\u0026#34;,\u0026#34;分割，逐个判断 boolean exactlyMatch = false; String[] appArray = rule.getLimitApp().split(\u0026#34;,\u0026#34;); for (String app : appArray) { if (requester.equals(app)) { exactlyMatch = true; break; } } contain = exactlyMatch; } // 如果是黑名单，并且包含origin，则返回false int strategy = rule.getStrategy(); if (strategy == RuleConstant.AUTHORITY_BLACK \u0026amp;\u0026amp; contain) { return false; } // 如果是白名单，并且不包含origin，则返回false if (strategy == RuleConstant.AUTHORITY_WHITE \u0026amp;\u0026amp; !contain) { return false; } // 其它情况返回true return true; } Copied! 2.7.SystemSlot SystemSlot是对系统保护的规则校验：\n核心API：\n1 2 3 4 5 6 7 8 @Override public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,boolean prioritized, Object... args) throws Throwable { // 系统规则校验 SystemRuleManager.checkSystem(resourceWrapper); // 进入下一个 slot fireEntry(context, resourceWrapper, node, count, prioritized, args); } Copied! 来看下SystemRuleManager.checkSystem(resourceWrapper);的代码：\n1 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 37 38 39 40 41 42 43 public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException { if (resourceWrapper == null) { return; } // Ensure the checking switch is on. if (!checkSystemStatus.get()) { return; } // 只针对入口资源做校验，其它直接返回 if (resourceWrapper.getEntryType() != EntryType.IN) { return; } // 全局 QPS校验 double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps(); if (currentQps \u0026gt; qps) { throw new SystemBlockException(resourceWrapper.getName(), \u0026#34;qps\u0026#34;); } // 全局 线程数 校验 int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum(); if (currentThread \u0026gt; maxThread) { throw new SystemBlockException(resourceWrapper.getName(), \u0026#34;thread\u0026#34;); } // 全局平均 RT校验 double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt(); if (rt \u0026gt; maxRt) { throw new SystemBlockException(resourceWrapper.getName(), \u0026#34;rt\u0026#34;); } // 全局 系统负载 校验 if (highestSystemLoadIsSet \u0026amp;\u0026amp; getCurrentSystemAvgLoad() \u0026gt; highestSystemLoad) { if (!checkBbr(currentThread)) { throw new SystemBlockException(resourceWrapper.getName(), \u0026#34;load\u0026#34;); } } // 全局 CPU使用率 校验 if (highestCpuUsageIsSet \u0026amp;\u0026amp; getCurrentCpuUsage() \u0026gt; highestCpuUsage) { throw new SystemBlockException(resourceWrapper.getName(), \u0026#34;cpu\u0026#34;); } } Copied! 2.8.ParamFlowSlot ParamFlowSlot就是热点参数限流，如图：\n是针对进入资源的请求，针对不同的请求参数值分别统计QPS的限流方式。\n这里的单机阈值，就是最大令牌数量：maxCount\n这里的统计窗口时长，就是统计时长：duration\n含义是每隔duration时间长度内，最多生产maxCount个令牌，上图配置的含义是每1秒钟生产2个令牌。\n核心API：\n1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable { // 如果没有设置热点规则，直接放行 if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) { fireEntry(context, resourceWrapper, node, count, prioritized, args); return; } // 热点规则判断 checkFlow(resourceWrapper, count, args); // 进入下一个 slot fireEntry(context, resourceWrapper, node, count, prioritized, args); } Copied! 2.8.1.令牌桶 热点规则判断采用了令牌桶算法来实现参数限流，为每一个不同参数值设置令牌桶，Sentinel的令牌桶有两部分组成：\n这两个Map的key都是请求的参数值，value却不同，其中：\ntokenCounters：用来记录剩余令牌数量 timeCounters：用来记录上一个请求的时间 当一个携带参数的请求到来后，基本判断流程是这样的：\n2.9.FlowSlot FlowSlot是负责限流规则的判断，如图：\n包括：\n三种流控模式：直接模式、关联模式、链路模式 三种流控效果：快速失败、warm up、排队等待 三种流控模式，从底层数据统计角度，分为两类：\n对进入资源的所有请求（ClusterNode）做限流统计：直接模式、关联模式 对进入资源的部分链路（DefaultNode）做限流统计：链路模式 三种流控效果，从限流算法来看，分为两类：\n滑动时间窗口算法：快速失败、warm up 漏桶算法：排队等待效果 2.9.1.核心流程 核心API如下：\n1 2 3 4 5 6 7 8 @Override public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable { // 限流规则检测 checkFlow(resourceWrapper, context, node, count, prioritized); // 放行 fireEntry(context, resourceWrapper, node, count, prioritized, args); } Copied! checkFlow方法：\n1 2 3 4 5 void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized) throws BlockException { // checker是 FlowRuleChecker 类的一个对象 checker.checkFlow(ruleProvider, resource, context, node, count, prioritized); } Copied! 跟入FlowRuleChecker：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void checkFlow(Function\u0026lt;String, Collection\u0026lt;FlowRule\u0026gt;\u0026gt; ruleProvider, ResourceWrapper resource,Context context, DefaultNode node, int count, boolean prioritized) throws BlockException { if (ruleProvider == null || resource == null) { return; } // 获取当前资源的所有限流规则 Collection\u0026lt;FlowRule\u0026gt; rules = ruleProvider.apply(resource.getName()); if (rules != null) { for (FlowRule rule : rules) { // 遍历，逐个规则做校验 if (!canPassCheck(rule, context, node, count, prioritized)) { throw new FlowException(rule.getLimitApp(), rule); } } } } Copied! 这里的FlowRule就是限流规则接口，其中的几个成员变量，刚好对应表单参数：\n1 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 class FlowRule extends AbstractRule { /** * 阈值类型 (0: 线程, 1: QPS). */ private int grade = RuleConstant.FLOW_GRADE_QPS; /** * 阈值. */ private double count; /** * 三种限流模式. * * {@link RuleConstant#STRATEGY_DIRECT} 直连模式; * {@link RuleConstant#STRATEGY_RELATE} 关联模式; * {@link RuleConstant#STRATEGY_CHAIN} 链路模式. */ private int strategy = RuleConstant.STRATEGY_DIRECT; /** * 关联模式关联的资源名称. */ private String refResource; /** * 3种流控效果. * 0. 快速失败, 1. warm up, 2. 排队等待, 3. warm up + 排队等待 */ private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT; // 预热时长 private int warmUpPeriodSec = 10; /** * 队列最大等待时间. */ private int maxQueueingTimeMs = 500; // 。。。 略 } Copied! 校验的逻辑定义在FlowRuleChecker的canPassCheck方法中：\n1 2 3 4 5 6 7 8 9 10 public boolean canPassCheck(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node, int acquireCount, boolean prioritized) { // 获取限流资源名称 String limitApp = rule.getLimitApp(); if (limitApp == null) { return true; } // 校验规则 return passLocalCheck(rule, context, node, acquireCount, prioritized); } Copied! 进入passLocalCheck()：\n1 2 3 4 5 6 7 8 9 10 11 private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount, boolean prioritized) { // 基于限流模式判断要统计的节点， // 如果是直连模式，关联模式，对ClusterNode统计，如果是链路模式，则对DefaultNode统计 Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node); if (selectedNode == null) { return true; } // 判断规则 return rule.getRater().canPass(selectedNode, acquireCount, prioritized); } Copied! 这里对规则的判断先要通过FlowRule#getRater()获取流量控制器TrafficShapingController，然后再做限流。\n而TrafficShapingController有3种实现：\nDefaultController：快速失败，默认的方式，基于滑动时间窗口算法 WarmUpController：预热模式，基于滑动时间窗口算法，只不过阈值是动态的 RateLimiterController：排队等待模式，基于漏桶算法 最终的限流判断都在TrafficShapingController的canPass方法中。\n2.9.2.滑动时间窗口 滑动时间窗口的功能分两部分来看：\n一是时间区间窗口的QPS计数功能，这个是在StatisticSlot中调用的 二是对滑动窗口内的时间区间窗口QPS累加，这个是在FlowRule中调用的 先来看时间区间窗口的QPS计数功能。\n2.9.2.1.时间窗口请求量统计 回顾2.5章节中的StatisticSlot部分，有这样一段代码：\n就是在统计通过该节点的QPS，我们跟入看看，这里进入了DefaultNode内部：\n发现同时对DefaultNode和ClusterNode在做QPS统计，我们知道DefaultNode和ClusterNode都是StatisticNode的子类，这里调用addPassRequest()方法，最终都会进入StatisticNode中。\n随便跟入一个：\n这里有秒、分两种纬度的统计，对应两个计数器。找到对应的成员变量，可以看到：\n两个计数器都是ArrayMetric类型，并且传入了两个参数：\n1 2 3 4 5 // intervalInMs：是滑动窗口的时间间隔，默认为 1 秒 // sampleCount: 时间窗口的分隔数量，默认为 2，就是把 1秒分为 2个小时间窗 public ArrayMetric(int sampleCount, int intervalInMs) { this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs); } Copied! 如图：\n接下来，我们进入ArrayMetric类的addPass方法：\n1 2 3 4 5 6 7 @Override public void addPass(int count) { // 获取当前时间所在的时间窗 WindowWrap\u0026lt;MetricBucket\u0026gt; wrap = data.currentWindow(); // 计数器 +1 wrap.value().addPass(count); } Copied! 那么，计数器如何知道当前所在的窗口是哪个呢？\n这里的data是一个LeapArray：\nLeapArray的四个属性：\n1 2 3 4 5 6 7 8 9 10 public abstract class LeapArray\u0026lt;T\u0026gt; { // 小窗口的时间长度，默认是500ms ，值 = intervalInMs / sampleCount protected int windowLengthInMs; // 滑动窗口内的 小窗口 数量，默认为 2 protected int sampleCount; // 滑动窗口的时间间隔，默认为 1000ms protected int intervalInMs; // 滑动窗口的时间间隔，单位为秒，默认为 1 private double intervalInSecond; } Copied! LeapArray是一个环形数组，因为时间是无限的，数组长度不可能无限，因此数组中每一个格子放入一个时间窗（window），当数组放满后，角标归0，覆盖最初的window。\n因为滑动窗口最多分成sampleCount数量的小窗口，因此数组长度只要大于sampleCount，那么最近的一个滑动窗口内的2个小窗口就永远不会被覆盖，就不用担心旧数据被覆盖的问题了。\n我们跟入 data.currentWindow();方法：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public WindowWrap\u0026lt;T\u0026gt; currentWindow(long timeMillis) { if (timeMillis \u0026lt; 0) { return null; } // 计算当前时间对应的数组角标 int idx = calculateTimeIdx(timeMillis); // 计算当前时间所在窗口的开始时间. long windowStart = calculateWindowStart(timeMillis); /* * 先根据角标获取数组中保存的 oldWindow 对象，可能是旧数据，需要判断. * * (1) oldWindow 不存在, 说明是第一次，创建新 window并存入，然后返回即可 * (2) oldWindow的 starTime = 本次请求的 windowStar, 说明正是要找的窗口，直接返回. * (3) oldWindow的 starTime \u0026lt; 本次请求的 windowStar, 说明是旧数据，需要被覆盖，创建 * 新窗口，覆盖旧窗口 */ while (true) { WindowWrap\u0026lt;T\u0026gt; old = array.get(idx); if (old == null) { // 创建新 window WindowWrap\u0026lt;T\u0026gt; window = new WindowWrap\u0026lt;T\u0026gt;(windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); // 基于CAS写入数组，避免线程安全问题 if (array.compareAndSet(idx, null, window)) { // 写入成功，返回新的 window return window; } else { // 写入失败，说明有并发更新，等待其它人更新完成即可 Thread.yield(); } } else if (windowStart == old.windowStart()) { return old; } else if (windowStart \u0026gt; old.windowStart()) { if (updateLock.tryLock()) { try { // 获取并发锁，覆盖旧窗口并返回 return resetWindowTo(old, windowStart); } finally { updateLock.unlock(); } } else { // 获取锁失败，等待其它线程处理就可以了 Thread.yield(); } } else if (windowStart \u0026lt; old.windowStart()) { // 这种情况不应该存在，写这里只是以防万一。 return new WindowWrap\u0026lt;T\u0026gt;(windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); } } } Copied! 找到当前时间所在窗口（WindowWrap）后，只要调用WindowWrap对象中的add方法，计数器+1即可。\n这里只负责统计每个窗口的请求量，不负责拦截。限流拦截要看FlowSlot中的逻辑。\n2.9.2.2.滑动窗口QPS计算 在2.9.1小节我们讲过，FlowSlot的限流判断最终都由TrafficShapingController接口中的canPass方法来实现。该接口有三个实现类：\nDefaultController：快速失败，默认的方式，基于滑动时间窗口算法 WarmUpController：预热模式，基于滑动时间窗口算法，只不过阈值是动态的 RateLimiterController：排队等待模式，基于漏桶算法 因此，我们跟入默认的DefaultController中的canPass方法来分析：\n1 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 @Override public boolean canPass(Node node, int acquireCount, boolean prioritized) { // 计算目前为止滑动窗口内已经存在的请求量 int curCount = avgUsedTokens(node); // 判断：已使用请求量 + 需要的请求量（1） 是否大于 窗口的请求阈值 if (curCount + acquireCount \u0026gt; count) { // 大于，说明超出阈值，返回false if (prioritized \u0026amp;\u0026amp; grade == RuleConstant.FLOW_GRADE_QPS) { long currentTime; long waitInMs; currentTime = TimeUtil.currentTimeMillis(); waitInMs = node.tryOccupyNext(currentTime, acquireCount, count); if (waitInMs \u0026lt; OccupyTimeoutProperty.getOccupyTimeout()) { node.addWaitingRequest(currentTime + waitInMs, acquireCount); node.addOccupiedPass(acquireCount); sleep(waitInMs); // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}. throw new PriorityWaitException(waitInMs); } } return false; } // 小于等于，说明在阈值范围内，返回true return true; } Copied! 因此，判断的关键就是int curCount = avgUsedTokens(node);\n1 2 3 4 5 6 private int avgUsedTokens(Node node) { if (node == null) { return DEFAULT_AVG_USED_TOKENS; } return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps()); } Copied! 因为我们采用的是限流，走node.passQps()逻辑：\n1 2 3 4 5 6 // 这里又进入了 StatisticNode类 @Override public double passQps() { // 请求量 ÷ 滑动窗口时间间隔 ，得到的就是QPS return rollingCounterInSecond.pass() / rollingCounterInSecond.getWindowIntervalInSec(); } Copied! 那么rollingCounterInSecond.pass()是如何得到请求量的呢？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // rollingCounterInSecond 本质是ArrayMetric，之前说过 @Override public long pass() { // 获取当前窗口 data.currentWindow(); long pass = 0; // 获取 当前时间的 滑动窗口范围内 的所有小窗口 List\u0026lt;MetricBucket\u0026gt; list = data.values(); // 遍历 for (MetricBucket window : list) { // 累加求和 pass += window.pass(); } // 返回 return pass; } Copied! 来看看data.values()如何获取 滑动窗口范围内 的所有小窗口：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // 此处进入LeapArray类中： public List\u0026lt;T\u0026gt; values(long timeMillis) { if (timeMillis \u0026lt; 0) { return new ArrayList\u0026lt;T\u0026gt;(); } // 创建空集合，大小等于 LeapArray长度 int size = array.length(); List\u0026lt;T\u0026gt; result = new ArrayList\u0026lt;T\u0026gt;(size); // 遍历 LeapArray for (int i = 0; i \u0026lt; size; i++) { // 获取每一个小窗口 WindowWrap\u0026lt;T\u0026gt; windowWrap = array.get(i); // 判断这个小窗口是否在 滑动窗口时间范围内（1秒内） if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) { // 不在范围内，则跳过 continue; } // 在范围内，则添加到集合中 result.add(windowWrap.value()); } // 返回集合 return result; } Copied! 那么，isWindowDeprecated(timeMillis, windowWrap)又是如何判断窗口是否符合要求呢？\n1 2 3 4 5 public boolean isWindowDeprecated(long time, WindowWrap\u0026lt;T\u0026gt; windowWrap) { // 当前时间 - 窗口开始时间 是否大于 滑动窗口的最大间隔（1秒） // 也就是说，我们要统计的时 距离当前时间1秒内的 小窗口的 count之和 return time - windowWrap.windowStart() \u0026gt; intervalInMs; } Copied! 2.9.3.漏桶 上一节我们讲过，FlowSlot的限流判断最终都由TrafficShapingController接口中的canPass方法来实现。该接口有三个实现类：\nDefaultController：快速失败，默认的方式，基于滑动时间窗口算法 WarmUpController：预热模式，基于滑动时间窗口算法，只不过阈值是动态的 RateLimiterController：排队等待模式，基于漏桶算法 因此，我们跟入默认的RateLimiterController中的canPass方法来分析：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @Override public boolean canPass(Node node, int acquireCount, boolean prioritized) { // Pass when acquire count is less or equal than 0. if (acquireCount \u0026lt;= 0) { return true; } // 阈值小于等于 0 ，阻止请求 if (count \u0026lt;= 0) { return false; } // 获取当前时间 long currentTime = TimeUtil.currentTimeMillis(); // 计算两次请求之间允许的最小时间间隔 long costTime = Math.round(1.0 * (acquireCount) / count * 1000); // 计算本次请求 允许执行的时间点 = 最近一次请求的可执行时间 + 两次请求的最小间隔 long expectedTime = costTime + latestPassedTime.get(); // 如果允许执行的时间点小于当前时间，说明可以立即执行 if (expectedTime \u0026lt;= currentTime) { // 更新上一次的请求的执行时间 latestPassedTime.set(currentTime); return true; } else { // 不能立即执行，需要计算 预期等待时长 // 预期等待时长 = 两次请求的最小间隔 +最近一次请求的可执行时间 - 当前时间 long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis(); // 如果预期等待时间超出阈值，则拒绝请求 if (waitTime \u0026gt; maxQueueingTimeMs) { return false; } else { // 预期等待时间小于阈值，更新最近一次请求的可执行时间，加上costTime long oldTime = latestPassedTime.addAndGet(costTime); try { // 保险起见，再判断一次预期等待时间，是否超过阈值 waitTime = oldTime - TimeUtil.currentTimeMillis(); if (waitTime \u0026gt; maxQueueingTimeMs) { // 如果超过，则把刚才 加 的时间再 减回来 latestPassedTime.addAndGet(-costTime); // 拒绝 return false; } // in race condition waitTime may \u0026lt;= 0 if (waitTime \u0026gt; 0) { // 预期等待时间在阈值范围内，休眠要等待的时间，醒来后继续执行 Thread.sleep(waitTime); } return true; } catch (InterruptedException e) { } } } return false; } Copied! 与我们之前分析的漏桶算法基本一致：\n2.10.DegradeSlot 最后一关，就是降级规则判断了。\nSentinel的降级是基于状态机来实现的：\n对应的实现在DegradeSlot类中，核心API：\n1 2 3 4 5 6 7 8 @Override public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable { // 熔断降级规则判断 performChecking(context, resourceWrapper); // 继续下一个slot fireEntry(context, resourceWrapper, node, count, prioritized, args); } Copied! 继续进入performChecking方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 void performChecking(Context context, ResourceWrapper r) throws BlockException { // 获取当前资源上的所有的断路器 CircuitBreaker List\u0026lt;CircuitBreaker\u0026gt; circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName()); if (circuitBreakers == null || circuitBreakers.isEmpty()) { return; } for (CircuitBreaker cb : circuitBreakers) { // 遍历断路器，逐个判断 if (!cb.tryPass(context)) { throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule()); } } } Copied! 2.10.1.CircuitBreaker 我们进入CircuitBreaker的tryPass方法中：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public boolean tryPass(Context context) { // 判断状态机状态 if (currentState.get() == State.CLOSED) { // 如果是closed状态，直接放行 return true; } if (currentState.get() == State.OPEN) { // 如果是OPEN状态，断路器打开 // 继续判断OPEN时间窗是否结束，如果是则把状态从OPEN切换到 HALF_OPEN，返回true return retryTimeoutArrived() \u0026amp;\u0026amp; fromOpenToHalfOpen(context); } // OPEN状态，并且时间窗未到，返回false return false; } Copied! 有关时间窗的判断在retryTimeoutArrived()方法：\n1 2 3 4 protected boolean retryTimeoutArrived() { // 当前时间 大于 下一次 HalfOpen的重试时间 return TimeUtil.currentTimeMillis() \u0026gt;= nextRetryTimestamp; } Copied! OPEN到HALF_OPEN切换在fromOpenToHalfOpen(context)方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 protected boolean fromOpenToHalfOpen(Context context) { // 基于CAS修改状态，从 OPEN到 HALF_OPEN if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) { // 状态变更的事件通知 notifyObservers(State.OPEN, State.HALF_OPEN, null); // 得到当前资源 Entry entry = context.getCurEntry(); // 给资源设置监听器，在资源Entry销毁时（资源业务执行完毕时）触发 entry.whenTerminate(new BiConsumer\u0026lt;Context, Entry\u0026gt;() { @Override public void accept(Context context, Entry entry) { // 判断 资源业务是否异常 if (entry.getBlockError() != null) { // 如果异常，则再次进入OPEN状态 currentState.compareAndSet(State.HALF_OPEN, State.OPEN); notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d); } } }); return true; } return false; } Copied! 这里出现了从OPEN到HALF_OPEN、从HALF_OPEN到OPEN的变化，但是还有几个没有：\n从CLOSED到OPEN 从HALF_OPEN到CLOSED 2.10.2.触发断路器 请求经过所有插槽 后，一定会执行exit方法，而在DegradeSlot的exit方法中：\n会调用CircuitBreaker的onRequestComplete方法。而CircuitBreaker有两个实现：\n我们这里以异常比例熔断为例来看，进入ExceptionCircuitBreaker的onRequestComplete方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override public void onRequestComplete(Context context) { // 获取资源 Entry Entry entry = context.getCurEntry(); if (entry == null) { return; } // 尝试获取 资源中的 异常 Throwable error = entry.getError(); // 获取计数器，同样采用了滑动窗口来计数 SimpleErrorCounter counter = stat.currentWindow().value(); if (error != null) { // 如果出现异常，则 error计数器 +1 counter.getErrorCount().add(1); } // 不管是否出现异常，total计数器 +1 counter.getTotalCount().add(1); // 判断异常比例是否超出阈值 handleStateChangeWhenThresholdExceeded(error); } Copied! 来看阈值判断的方法：\n1 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 37 38 39 private void handleStateChangeWhenThresholdExceeded(Throwable error) { // 如果当前已经是OPEN状态，不做处理 if (currentState.get() == State.OPEN) { return; } // 如果已经是 HALF_OPEN 状态，判断是否需求切换状态 if (currentState.get() == State.HALF_OPEN) { if (error == null) { // 没有异常，则从 HALF_OPEN 到 CLOSED fromHalfOpenToClose(); } else { // 有一次，再次进入OPEN fromHalfOpenToOpen(1.0d); } return; } // 说明当前是CLOSE状态，需要判断是否触发阈值 List\u0026lt;SimpleErrorCounter\u0026gt; counters = stat.values(); long errCount = 0; long totalCount = 0; // 累加计算 异常请求数量、总请求数量 for (SimpleErrorCounter counter : counters) { errCount += counter.errorCount.sum(); totalCount += counter.totalCount.sum(); } // 如果总请求数量未达到阈值，什么都不做 if (totalCount \u0026lt; minRequestAmount) { return; } double curCount = errCount; if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) { // 计算请求的异常比例 curCount = errCount * 1.0d / totalCount; } // 如果比例超过阈值，切换到 OPEN if (curCount \u0026gt; threshold) { transformToOpen(curCount); } } Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/15713815/","title":"14.Sentinel源码分析"},{"content":" 常见面试题 1.微服务篇 1.1.SpringCloud常见组件有哪些？ 问题说明：这个题目主要考察对SpringCloud的组件基本了解\n难易程度：简单\n参考话术：\nSpringCloud包含的组件很多，有很多功能是重复的。其中最常用组件包括：\n•注册中心组件：Eureka、Nacos等\n•负载均衡组件：Ribbon\n•远程调用组件：OpenFeign\n•网关组件：Zuul、Gateway\n•服务保护组件：Hystrix、Sentinel\n•服务配置管理组件：SpringCloudConfig、Nacos\n1.2.Nacos的服务注册表结构是怎样的？ 问题说明：考察对Nacos数据分级结构的了解，以及Nacos源码的掌握情况\n难易程度：一般\n参考话术：\nNacos采用了数据的分级存储模型，最外层是Namespace，用来隔离环境。然后是Group，用来对服务分组。接下来就是服务（Service）了，一个服务包含多个实例，但是可能处于不同机房，因此Service下有多个集群（Cluster），Cluster下是不同的实例（Instance）。\n对应到Java代码中，Nacos采用了一个多层的Map来表示。结构为Map\u0026lt;String, Map\u0026lt;String, Service\u0026raquo;，其中最外层Map的key就是namespaceId，值是一个Map。内层Map的key是group拼接serviceName，值是Service对象。Service对象内部又是一个Map，key是集群名称，值是Cluster对象。而Cluster对象内部维护了Instance的集合。\n如图：\n1.3.Nacos如何支撑阿里内部数十万服务注册压力？ 问题说明：考察对Nacos源码的掌握情况\n难易程度：难\n参考话术：\nNacos内部接收到注册的请求时，不会立即写数据，而是将服务注册的任务放入一个阻塞队列就立即响应给客户端。然后利用线程池读取阻塞队列中的任务，异步来完成实例更新，从而提高并发写能力。\n1.4.Nacos如何避免并发读写冲突问题？ 问题说明：考察对Nacos源码的掌握情况\n难易程度：难\n参考话术：\nNacos在更新实例列表时，会采用CopyOnWrite技术，首先将旧的实例列表拷贝一份，然后更新拷贝的实例列表，再用更新后的实例列表来覆盖旧的实例列表。\n这样在更新的过程中，就不会对读实例列表的请求产生影响，也不会出现脏读问题了。\n1.5.Nacos与Eureka的区别有哪些？ 问题说明：考察对Nacos、Eureka的底层实现的掌握情况\n难易程度：难\n参考话术：\nNacos与Eureka有相同点，也有不同之处，可以从以下几点来描述：\n接口方式：Nacos与Eureka都对外暴露了Rest风格的API接口，用来实现服务注册、发现等功能 实例类型：Nacos的实例有永久和临时实例之分；而Eureka只支持临时实例 健康检测：Nacos对临时实例采用心跳模式检测，对永久实例采用主动请求来检测；Eureka只支持心跳模式 服务发现：Nacos支持定时拉取和订阅推送两种模式；Eureka只支持定时拉取模式 1.6.Sentinel的限流与Gateway的限流有什么差别？ 问题说明：考察对限流算法的掌握情况\n难易程度：难\n参考话术：\n限流算法常见的有三种实现：滑动时间窗口、令牌桶算法、漏桶算法。Gateway则采用了基于Redis实现的令牌桶算法。\n而Sentinel内部却比较复杂：\n默认限流模式是基于滑动时间窗口算法 排队等待的限流模式则基于漏桶算法 而热点参数限流则是基于令牌桶算法 1.7.Sentinel的线程隔离与Hystix的线程隔离有什么差别? 问题说明：考察对线程隔离方案的掌握情况\n难易程度：一般\n参考话术：\nHystix默认是基于线程池实现的线程隔离，每一个被隔离的业务都要创建一个独立的线程池，线程过多会带来额外的CPU开销，性能一般，但是隔离性更强。\nSentinel是基于信号量（计数器）实现的线程隔离，不用创建线程池，性能较好，但是隔离性一般。\n2.MQ篇 2.1.你们为什么选择了RabbitMQ而不是其它的MQ？ 如图：\n话术：\nkafka是以吞吐量高而闻名，不过其数据稳定性一般，而且无法保证消息有序性。我们公司的日志收集也有使用，业务模块中则使用的RabbitMQ。\n阿里巴巴的RocketMQ基于Kafka的原理，弥补了Kafka的缺点，继承了其高吞吐的优势，其客户端目前以Java为主。但是我们担心阿里巴巴开源产品的稳定性，所以就没有使用。\nRabbitMQ基于面向并发的语言Erlang开发，吞吐量不如Kafka，但是对我们公司来讲够用了。而且消息可靠性较好，并且消息延迟极低，集群搭建比较方便。支持多种协议，并且有各种语言的客户端，比较灵活。Spring对RabbitMQ的支持也比较好，使用起来比较方便，比较符合我们公司的需求。\n综合考虑我们公司的并发需求以及稳定性需求，我们选择了RabbitMQ。\n2.2.RabbitMQ如何确保消息的不丢失？ 话术：\nRabbitMQ针对消息传递过程中可能发生问题的各个地方，给出了针对性的解决方案：\n生产者发送消息时可能因为网络问题导致消息没有到达交换机： RabbitMQ提供了publisher confirm机制 生产者发送消息后，可以编写ConfirmCallback函数 消息成功到达交换机后，RabbitMQ会调用ConfirmCallback通知消息的发送者，返回ACK 消息如果未到达交换机，RabbitMQ也会调用ConfirmCallback通知消息的发送者，返回NACK 消息超时未发送成功也会抛出异常 消息到达交换机后，如果未能到达队列，也会导致消息丢失： RabbitMQ提供了publisher return机制 生产者可以定义ReturnCallback函数 消息到达交换机，未到达队列，RabbitMQ会调用ReturnCallback通知发送者，告知失败原因 消息到达队列后，MQ宕机也可能导致丢失消息： RabbitMQ提供了持久化功能，集群的主从备份功能 消息持久化，RabbitMQ会将交换机、队列、消息持久化到磁盘，宕机重启可以恢复消息 镜像集群，仲裁队列，都可以提供主从备份功能，主节点宕机，从节点会自动切换为主，数据依然在 消息投递给消费者后，如果消费者处理不当，也可能导致消息丢失 SpringAMQP基于RabbitMQ提供了消费者确认机制、消费者重试机制，消费者失败处理策略： 消费者的确认机制： 消费者处理消息成功，未出现异常时，Spring返回ACK给RabbitMQ，消息才被移除 消费者处理消息失败，抛出异常，宕机，Spring返回NACK或者不返回结果，消息不被异常 消费者重试机制： 默认情况下，消费者处理失败时，消息会再次回到MQ队列，然后投递给其它消费者。Spring提供的消费者重试机制，则是在处理失败后不返回NACK，而是直接在消费者本地重试。多次重试都失败后，则按照消费者失败处理策略来处理消息。避免了消息频繁入队带来的额外压力。 消费者失败策略： 当消费者多次本地重试失败时，消息默认会丢弃。 Spring提供了Republish策略，在多次重试都失败，耗尽重试次数后，将消息重新投递给指定的异常交换机，并且会携带上异常栈信息，帮助定位问题。 2.3.RabbitMQ如何避免消息堆积？ 话术：\n消息堆积问题产生的原因往往是因为消息发送的速度超过了消费者消息处理的速度。因此解决方案无外乎以下三点：\n提高消费者处理速度 增加更多消费者 增加队列消息存储上限 1）提高消费者处理速度\n消费者处理速度是由业务代码决定的，所以我们能做的事情包括：\n尽可能优化业务代码，提高业务性能 接收到消息后，开启线程池，并发处理多个消息 优点：成本低，改改代码即可\n缺点：开启线程池会带来额外的性能开销，对于高频、低时延的任务不合适。推荐任务执行周期较长的业务。\n2）增加更多消费者\n一个队列绑定多个消费者，共同争抢任务，自然可以提供消息处理的速度。\n优点：能用钱解决的问题都不是问题。实现简单粗暴\n缺点：问题是没有钱。成本太高\n3）增加队列消息存储上限\n在RabbitMQ的1.8版本后，加入了新的队列模式：Lazy Queue\n这种队列不会将消息保存在内存中，而是在收到消息后直接写入磁盘中，理论上没有存储上限。可以解决消息堆积问题。\n优点：磁盘存储更安全；存储无上限；避免内存存储带来的Page Out问题，性能更稳定；\n缺点：磁盘存储受到IO性能的限制，消息时效性不如内存模式，但影响不大。\n2.4.RabbitMQ如何保证消息的有序性？ 话术：\n其实RabbitMQ是队列存储，天然具备先进先出的特点，只要消息的发送是有序的，那么理论上接收也是有序的。不过当一个队列绑定了多个消费者时，可能出现消息轮询投递给消费者的情况，而消费者的处理顺序就无法保证了。\n因此，要保证消息的有序性，需要做的下面几点：\n保证消息发送的有序性 保证一组有序的消息都发送到同一个队列 保证一个队列只包含一个消费者 2.5.如何防止MQ消息被重复消费？ 话术：\n消息重复消费的原因多种多样，不可避免。所以只能从消费者端入手，只要能保证消息处理的幂等性就可以确保消息不被重复消费。\n而幂等性的保证又有很多方案：\n给每一条消息都添加一个唯一id，在本地记录消息表及消息状态，处理消息时基于数据库表的id唯一性做判断 同样是记录消息表，利用消息状态字段实现基于乐观锁的判断，保证幂等 基于业务本身的幂等性。比如根据id的删除、查询业务天生幂等；新增、修改等业务可以考虑基于数据库id唯一性、或者乐观锁机制确保幂等。本质与消息表方案类似。 2.6.如何保证RabbitMQ的高可用？ 话术：\n要实现RabbitMQ的高可用无外乎下面两点：\n做好交换机、队列、消息的持久化 搭建RabbitMQ的镜像集群，做好主从备份。当然也可以使用仲裁队列代替镜像集群。 2.7.使用MQ可以解决那些问题？ 话术：\nRabbitMQ能解决的问题很多，例如：\n解耦合：将几个业务关联的微服务调用修改为基于MQ的异步通知，可以解除微服务之间的业务耦合。同时还提高了业务性能。 流量削峰：将突发的业务请求放入MQ中，作为缓冲区。后端的业务根据自己的处理能力从MQ中获取消息，逐个处理任务。流量曲线变的平滑很多 延迟队列：基于RabbitMQ的死信队列或者DelayExchange插件，可以实现消息发送后，延迟接收的效果。 3.Redis篇 3.1.Redis与Memcache的区别？ redis支持更丰富的数据类型（支持更复杂的应用场景）：Redis不仅仅支持简单的k/v类型的数据，同时还提供list，set，zset，hash等数据结构的存储。memcache支持简单的数据类型，String。 Redis支持数据的持久化，可以将内存中的数据保持在磁盘中，重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。 集群模式：memcached没有原生的集群模式，需要依靠客户端来实现往集群中分片写入数据；但是 redis 目前是原生支持 cluster 模式的. Redis使用单线程：Memcached是多线程，非阻塞IO复用的网络模型；Redis使用单线程的多路 IO 复用模型。 3.2.Redis的单线程问题 面试官：Redis采用单线程，如何保证高并发？\n面试话术：\nRedis快的主要原因是：\n完全基于内存 数据结构简单，对数据操作也简单 使用多路 I/O 复用模型，充分利用CPU资源 面试官：这样做的好处是什么？\n面试话术：\n单线程优势有下面几点：\n代码更清晰，处理逻辑更简单 不用去考虑各种锁的问题，不存在加锁释放锁操作，没有因为锁而导致的性能消耗 不存在多进程或者多线程导致的CPU切换，充分利用CPU资源 3.2.Redis的持久化方案由哪些？ 相关资料：\n1）RDB 持久化\nRDB持久化可以使用save或bgsave，为了不阻塞主进程业务，一般都使用bgsave，流程：\nRedis 进程会 fork 出一个子进程（与父进程内存数据一致）。 父进程继续处理客户端请求命令 由子进程将内存中的所有数据写入到一个临时的 RDB 文件中。 完成写入操作之后，旧的 RDB 文件会被新的 RDB 文件替换掉。 下面是一些和 RDB 持久化相关的配置：\nsave 60 10000：如果在 60 秒内有 10000 个 key 发生改变，那就执行 RDB 持久化。 stop-writes-on-bgsave-error yes：如果 Redis 执行 RDB 持久化失败（常见于操作系统内存不足），那么 Redis 将不再接受 client 写入数据的请求。 rdbcompression yes：当生成 RDB 文件时，同时进行压缩。 dbfilename dump.rdb：将 RDB 文件命名为 dump.rdb。 dir /var/lib/redis：将 RDB 文件保存在/var/lib/redis目录下。 当然在实践中，我们通常会将stop-writes-on-bgsave-error设置为false，同时让监控系统在 Redis 执行 RDB 持久化失败时发送告警，以便人工介入解决，而不是粗暴地拒绝 client 的写入请求。\nRDB持久化的优点：\nRDB持久化文件小，Redis数据恢复时速度快 子进程不影响父进程，父进程可以持续处理客户端命令 子进程fork时采用copy-on-write方式，大多数情况下，没有太多的内存消耗，效率比较好。 RDB 持久化的缺点：\n子进程fork时采用copy-on-write方式，如果Redis此时写操作较多，可能导致额外的内存占用，甚至内存溢出 RDB文件压缩会减小文件体积，但通过时会对CPU有额外的消耗 如果业务场景很看重数据的持久性 (durability)，那么不应该采用 RDB 持久化。譬如说，如果 Redis 每 5 分钟执行一次 RDB 持久化，要是 Redis 意外奔溃了，那么最多会丢失 5 分钟的数据。 2）AOF 持久化\n可以使用appendonly yes配置项来开启 AOF 持久化。Redis 执行 AOF 持久化时，会将接收到的写命令追加到 AOF 文件的末尾，因此 Redis 只要对 AOF 文件中的命令进行回放，就可以将数据库还原到原先的状态。 与 RDB 持久化相比，AOF 持久化的一个明显优势就是，它可以提高数据的持久性 (durability)。因为在 AOF 模式下，Redis 每次接收到 client 的写命令，就会将命令write()到 AOF 文件末尾。 然而，在 Linux 中，将数据write()到文件后，数据并不会立即刷新到磁盘，而会先暂存在 OS 的文件系统缓冲区。在合适的时机，OS 才会将缓冲区的数据刷新到磁盘（如果需要将文件内容刷新到磁盘，可以调用fsync()或fdatasync()）。 通过appendfsync配置项，可以控制 Redis 将命令同步到磁盘的频率：\nalways：每次 Redis 将命令write()到 AOF 文件时，都会调用fsync()，将命令刷新到磁盘。这可以保证最好的数据持久性，但却会给系统带来极大的开销。 no：Redis 只将命令write()到 AOF 文件。这会让 OS 决定何时将命令刷新到磁盘。 everysec：除了将命令write()到 AOF 文件，Redis 还会每秒执行一次fsync()。在实践中，推荐使用这种设置，一定程度上可以保证数据持久性，又不会明显降低 Redis 性能。 然而，AOF 持久化并不是没有缺点的：Redis 会不断将接收到的写命令追加到 AOF 文件中，导致 AOF 文件越来越大。过大的 AOF 文件会消耗磁盘空间，并且导致 Redis 重启时更加缓慢。为了解决这个问题，在适当情况下，Redis 会对 AOF 文件进行重写，去除文件中冗余的命令，以减小 AOF 文件的体积。在重写 AOF 文件期间， Redis 会启动一个子进程，由子进程负责对 AOF 文件进行重写。 可以通过下面两个配置项，控制 Redis 重写 AOF 文件的频率：\nauto-aof-rewrite-min-size 64mb auto-aof-rewrite-percentage 100 上面两个配置的作用：当 AOF 文件的体积大于 64MB，并且 AOF 文件的体积比上一次重写之后的体积大了至少一倍，那么 Redis 就会执行 AOF 重写。\n优点：\n持久化频率高，数据可靠性高 没有额外的内存或CPU消耗 缺点：\n文件体积大 文件大导致服务数据恢复时效率较低 面试话术：\nRedis 提供了两种数据持久化的方式，一种是 RDB，另一种是 AOF。默认情况下，Redis 使用的是 RDB 持久化。\nRDB持久化文件体积较小，但是保存数据的频率一般较低，可靠性差，容易丢失数据。另外RDB写数据时会采用Fork函数拷贝主进程，可能有额外的内存消耗，文件压缩也会有额外的CPU消耗。\nROF持久化可以做到每秒钟持久化一次，可靠性高。但是持久化文件体积较大，导致数据恢复时读取文件时间较长，效率略低\n3.3.Redis的集群方式有哪些？ 面试话术：\nRedis集群可以分为主从集群和分片集群两类。\n主从集群一般一主多从，主库用来写数据，从库用来读数据。结合哨兵，可以再主库宕机时从新选主，目的是保证Redis的高可用。\n分片集群是数据分片，我们会让多个Redis节点组成集群，并将16383个插槽分到不同的节点上。存储数据时利用对key做hash运算，得到插槽值后存储到对应的节点即可。因为存储数据面向的是插槽而非节点本身，因此可以做到集群动态伸缩。目的是让Redis能存储更多数据。\n1）主从集群\n主从集群，也是读写分离集群。一般都是一主多从方式。\nRedis 的复制（replication）功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品，其中被复制的服务器为主服务器（master），而通过复制创建出来的服务器复制品则为从服务器（slave）。\n只要主从服务器之间的网络连接正常，主从服务器两者会具有相同的数据，主服务器就会一直将发生在自己身上的数据更新同步 给从服务器，从而一直保证主从服务器的数据相同。\n写数据时只能通过主节点完成 读数据可以从任何节点完成 如果配置了哨兵节点，当master宕机时，哨兵会从salve节点选出一个新的主。 主从集群分两种：\n带有哨兵的集群：\n2）分片集群\n主从集群中，每个节点都要保存所有信息，容易形成木桶效应。并且当数据量较大时，单个机器无法满足需求。此时我们就要使用分片集群了。\n集群特征：\n每个节点都保存不同数据\n所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.\n节点的fail是通过集群中超过半数的节点检测失效时才生效.\n客户端与redis节点直连,不需要中间proxy层连接集群中任何一个可用节点都可以访问到数据\nredis-cluster把所有的物理节点映射到[0-16383]slot（插槽）上，实现动态伸缩\n为了保证Redis中每个节点的高可用，我们还可以给每个节点创建replication（slave节点），如图：\n出现故障时，主从可以及时切换：\n3.4.Redis的常用数据类型有哪些？ 支持多种类型的数据结构，主要区别是value存储的数据格式不同：\nstring：最基本的数据类型，二进制安全的字符串，最大512M。\nlist：按照添加顺序保持顺序的字符串列表。\nset：无序的字符串集合，不存在重复的元素。\nsorted set：已排序的字符串集合。\nhash：key-value对格式\n3.5.聊一下Redis事务机制 相关资料：\n参考：http://redisdoc.com/topic/transaction.html\nRedis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的。Redis会将一个事务中的所有命令序列化，然后按顺序执行。但是Redis事务不支持回滚操作，命令运行出错后，正确的命令会继续执行。\nMULTI: 用于开启一个事务，它总是返回OK。 MULTI执行之后，客户端可以继续向服务器发送任意多条命令，这些命令不会立即被执行，而是被放到一个待执行命令队列中 EXEC：按顺序执行命令队列内的所有命令。返回所有命令的返回值。事务执行过程中，Redis不会执行其它事务的命令。 DISCARD：清空命令队列，并放弃执行事务， 并且客户端会从事务状态中退出 WATCH：Redis的乐观锁机制，利用compare-and-set（CAS）原理，可以监控一个或多个键，一旦其中有一个键被修改，之后的事务就不会执行 使用事务时可能会遇上以下两种错误：\n执行 EXEC 之前，入队的命令可能会出错。比如说，命令可能会产生语法错误（参数数量错误，参数名错误，等等），或者其他更严重的错误，比如内存不足（如果服务器使用 maxmemory 设置了最大内存限制的话）。 Redis 2.6.5 开始，服务器会对命令入队失败的情况进行记录，并在客户端调用 EXEC 命令时，拒绝执行并自动放弃这个事务。 命令可能在 EXEC 调用之后失败。举个例子，事务中的命令可能处理了错误类型的键，比如将列表命令用在了字符串键上面，诸如此类。 即使事务中有某个/某些命令在执行时产生了错误， 事务中的其他命令仍然会继续执行，不会回滚。 为什么 Redis 不支持回滚（roll back）？\n以下是这种做法的优点：\nRedis 命令只会因为错误的语法而失败（并且这些问题不能在入队时发现），或是命令用在了错误类型的键上面：这也就是说，从实用性的角度来说，失败的命令是由编程错误造成的，而这些错误应该在开发的过程中被发现，而不应该出现在生产环境中。 因为不需要对回滚进行支持，所以 Redis 的内部可以保持简单且快速。 鉴于没有任何机制能避免程序员自己造成的错误， 并且这类错误通常不会在生产环境中出现， 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。\n面试话术：\nRedis事务其实是把一系列Redis命令放入队列，然后批量执行，执行过程中不会有其它事务来打断。不过与关系型数据库的事务不同，Redis事务不支持回滚操作，事务中某个命令执行失败，其它命令依然会执行。\n为了弥补不能回滚的问题，Redis会在事务入队时就检查命令，如果命令异常则会放弃整个事务。\n因此，只要程序员编程是正确的，理论上说Redis会正确执行所有事务，无需回滚。\n面试官：如果事务执行一半的时候Redis宕机怎么办？\nRedis有持久化机制，因为可靠性问题，我们一般使用AOF持久化。事务的所有命令也会写入AOF文件，但是如果在执行EXEC命令之前，Redis已经宕机，则AOF文件中事务不完整。使用 redis-check-aof 程序可以移除 AOF 文件中不完整事务的信息，确保服务器可以顺利启动。\n3.6.Redis的Key过期策略 参考资料： 为什么需要内存回收？ 1、在Redis中，set指令可以指定key的过期时间，当过期时间到达以后，key就失效了； 2、Redis是基于内存操作的，所有的数据都是保存在内存中，一台机器的内存是有限且很宝贵的。 基于以上两点，为了保证Redis能继续提供可靠的服务，Redis需要一种机制清理掉不常用的、无效的、多余的数据，失效后的数据需要及时清理，这就需要内存回收了。\nRedis的内存回收主要分为过期删除策略和内存淘汰策略两部分。\n过期删除策略 删除达到过期时间的key。\n1）定时删除 对于每一个设置了过期时间的key都会创建一个定时器，一旦到达过期时间就立即删除。该策略可以立即清除过期的数据，对内存较友好，但是缺点是占用了大量的CPU资源去处理过期的数据，会影响Redis的吞吐量和响应时间。\n2）惰性删除 当访问一个key时，才判断该key是否过期，过期则删除。该策略能最大限度地节省CPU资源，但是对内存却十分不友好。有一种极端的情况是可能出现大量的过期key没有被再次访问，因此不会被清除，导致占用了大量的内存。\n在计算机科学中，懒惰删除（英文：lazy deletion）指的是从一个散列表（也称哈希表）中删除元素的一种方法。在这个方法中，删除仅仅是指标记一个元素被删除，而不是整个清除它。被删除的位点在插入时被当作空元素，在搜索之时被当作已占据。\n3）定期删除 每隔一段时间，扫描Redis中过期key字典，并清除部分过期的key。该策略是前两者的一个折中方案，还可以通过调整定时扫描的时间间隔和每次扫描的限定耗时，在不同情况下使得CPU和内存资源达到最优的平衡效果。\n在Redis中，同时使用了定期删除和惰性删除。不过Redis定期删除采用的是随机抽取的方式删除部分Key，因此不能保证过期key 100%的删除。\nRedis结合了定期删除和惰性删除，基本上能很好的处理过期数据的清理，但是实际上还是有点问题的，如果过期key较多，定期删除漏掉了一部分，而且也没有及时去查，即没有走惰性删除，那么就会有大量的过期key堆积在内存中，导致redis内存耗尽，当内存耗尽之后，有新的key到来会发生什么事呢？是直接抛弃还是其他措施呢？有什么办法可以接受更多的key？\n内存淘汰策略 Redis的内存淘汰策略，是指内存达到maxmemory极限时，使用某种算法来决定清理掉哪些数据，以保证新数据的存入。\nRedis的内存淘汰机制包括：\nnoeviction: 当内存不足以容纳新写入数据时，新写入操作会报错。 allkeys-lru：当内存不足以容纳新写入数据时，在键空间（server.db[i].dict）中，移除最近最少使用的 key（这个是最常用的）。 allkeys-random：当内存不足以容纳新写入数据时，在键空间（server.db[i].dict）中，随机移除某个 key。 volatile-lru：当内存不足以容纳新写入数据时，在设置了过期时间的键空间（server.db[i].expires）中，移除最近最少使用的 key。 volatile-random：当内存不足以容纳新写入数据时，在设置了过期时间的键空间（server.db[i].expires）中，随机移除某个 key。 volatile-ttl：当内存不足以容纳新写入数据时，在设置了过期时间的键空间（server.db[i].expires）中，有更早过期时间的 key 优先移除。 在配置文件中，通过maxmemory-policy可以配置要使用哪一个淘汰机制。\n什么时候会进行淘汰？\nRedis会在每一次处理命令的时候（processCommand函数调用freeMemoryIfNeeded）判断当前redis是否达到了内存的最大限制，如果达到限制，则使用对应的算法去处理需要删除的key。\n在淘汰key时，Redis默认最常用的是LRU算法（Latest Recently Used）。Redis通过在每一个redisObject保存lru属性来保存key最近的访问时间，在实现LRU算法时直接读取key的lru属性。\n具体实现时，Redis遍历每一个db，从每一个db中随机抽取一批样本key，默认是3个key，再从这3个key中，删除最近最少使用的key。\n面试话术： Redis过期策略包含定期删除和惰性删除两部分。定期删除是在Redis内部有一个定时任务，会定期删除一些过期的key。惰性删除是当用户查询某个Key时，会检查这个Key是否已经过期，如果没过期则返回用户，如果过期则删除。\n但是这两个策略都无法保证过期key一定删除，漏网之鱼越来越多，还可能导致内存溢出。当发生内存不足问题时，Redis还会做内存回收。内存回收采用LRU策略，就是最近最少使用。其原理就是记录每个Key的最近使用时间，内存回收时，随机抽取一些Key，比较其使用时间，把最老的几个删除。\nRedis的逻辑是：最近使用过的，很可能再次被使用\n3.7.Redis在项目中的哪些地方有用到? （1）共享session\n在分布式系统下，服务会部署在不同的tomcat，因此多个tomcat的session无法共享，以前存储在session中的数据无法实现共享，可以用redis代替session，解决分布式系统间数据共享问题。\n（2）数据缓存\nRedis采用内存存储，读写效率较高。我们可以把数据库的访问频率高的热点数据存储到redis中，这样用户请求时优先从redis中读取，减少数据库压力，提高并发能力。\n（3）异步队列\nReids在内存存储引擎领域的一大优点是提供 list 和 set 操作，这使得Redis能作为一个很好的消息队列平台来使用。而且Redis中还有pub/sub这样的专用结构，用于1对N的消息通信模式。\n（4）分布式锁\nRedis中的乐观锁机制，可以帮助我们实现分布式锁的效果，用于解决分布式系统下的多线程安全问题\n3.8.Redis的缓存击穿、缓存雪崩、缓存穿透 1）缓存穿透 参考资料：\n什么是缓存穿透\n正常情况下，我们去查询数据都是存在。那么请求去查询一条压根儿数据库中根本就不存在的数据，也就是缓存和数据库都查询不到这条数据，但是请求每次都会打到数据库上面去。这种查询不存在数据的现象我们称为缓存穿透。 穿透带来的问题\n试想一下，如果有黑客会对你的系统进行攻击，拿一个不存在的id 去查询数据，会产生大量的请求到数据库去查询。可能会导致你的数据库由于压力过大而宕掉。 解决办法\n缓存空值：之所以会发生穿透，就是因为缓存中没有存储这些空数据的key。从而导致每次查询都到数据库去了。那么我们就可以为这些key对应的值设置为null 丢到缓存里面去。后面再出现查询这个key 的请求的时候，直接返回null 。这样，就不用在到数据库中去走一圈了，但是别忘了设置过期时间。 BloomFilter（布隆过滤）：将所有可能存在的数据哈希到一个足够大的bitmap中，一个一定不存在的数据会被 这个bitmap拦截掉，从而避免了对底层存储系统的查询压力。在缓存之前在加一层 BloomFilter ，在查询的时候先去 BloomFilter 去查询 key 是否存在，如果不存在就直接返回，存在再走查缓存 -\u0026gt; 查 DB。 话术：\n缓存穿透有两种解决方案：其一是把不存在的key设置null值到缓存中。其二是使用布隆过滤器，在查询缓存前先通过布隆过滤器判断key是否存在，存在再去查询缓存。\n设置null值可能被恶意针对，攻击者使用大量不存在的不重复key ，那么方案一就会缓存大量不存在key数据。此时我们还可以对Key规定格式模板，然后对不存在的key做正则规范匹配，如果完全不符合就不用存null值到redis，而是直接返回错误。\n2）缓存击穿 相关资料：\n什么是缓存击穿？ key可能会在某些时间点被超高并发地访问，是一种非常“热点”的数据。这个时候，需要考虑一个问题：缓存被“击穿”的问题。\n当这个key在失效的瞬间，redis查询失败，持续的大并发就穿破缓存，直接请求数据库，就像在一个屏障上凿开了一个洞。\n解决方案： 使用互斥锁(mutex key)：mutex，就是互斥。简单地来说，就是在缓存失效的时候（判断拿出来的值为空），不是立即去load db，而是先使用Redis的SETNX去set一个互斥key，当操作返回成功时，再进行load db的操作并回设缓存；否则，就重试整个get缓存的方法。SETNX，是「SET if Not eXists」的缩写，也就是只有不存在的时候才设置，可以利用它来实现互斥的效果。 软过期：也就是逻辑过期，不使用redis提供的过期时间，而是业务层在数据中存储过期时间信息。查询时由业务程序判断是否过期，如果数据即将过期时，将缓存的时效延长，程序可以派遣一个线程去数据库中获取最新的数据，其他线程这时看到延长了的过期时间，就会继续使用旧数据，等派遣的线程获取最新数据后再更新缓存。 推荐使用互斥锁，因为软过期会有业务逻辑侵入和额外的判断。\n面试话术：\n缓存击穿主要担心的是某个Key过期，更新缓存时引起对数据库的突发高并发访问。因此我们可以在更新缓存时采用互斥锁控制，只允许一个线程去更新缓存，其它线程等待并重新读取缓存。例如Redis的setnx命令就能实现互斥效果。\n3）缓存雪崩 相关资料：\n缓存雪崩，是指在某一个时间段，缓存集中过期失效。对这批数据的访问查询，都落到了数据库上，对于数据库而言，就会产生周期性的压力波峰。\n解决方案：\n数据分类分批处理：采取不同分类数据，缓存不同周期 相同分类数据：采用固定时长加随机数方式设置缓存 热点数据缓存时间长一些，冷门数据缓存时间短一些 避免redis节点宕机引起雪崩，搭建主从集群，保证高可用 面试话术：\n解决缓存雪崩问题的关键是让缓存Key的过期时间分散。因此我们可以把数据按照业务分类，然后设置不同过期时间。相同业务类型的key，设置固定时长加随机数。尽可能保证每个Key的过期时间都不相同。\n另外，Redis宕机也可能导致缓存雪崩，因此我们还要搭建Redis主从集群及哨兵监控，保证Redis的高可用。\n3.9.缓存冷热数据分离 背景资料：\nRedis使用的是内存存储，当需要海量数据存储时，成本非常高。\n经过调研发现，当前主流DDR3内存和主流SATA SSD的单位成本价格差距大概在20倍左右，为了优化redis机器综合成本，我们考虑实现基于热度统计 的数据分级存储及数据在RAM/FLASH之间的动态交换，从而大幅度降低成本，达到性能与成本的高平衡。\n基本思路：基于key访问次数(LFU)的热度统计算法识别出热点数据，并将热点数据保留在redis中，对于无访问/访问次数少的数据则转存到SSD上，如果SSD上的key再次变热，则重新将其加载到redis内存中。\n目前流行的高性能磁盘存储，并且遵循Redis协议的方案包括：\nSSDB：http://ssdb.io/zh_cn/ RocksDB：https://rocksdb.org.cn/ 因此，我们就需要在应用程序与缓存服务之间引入代理，实现Redis和SSD之间的切换，如图：\n这样的代理方案阿里云提供的就有。当然也有一些开源方案，例如：https://github.com/JingchengLi/swapdb\n3.10.Redis实现分布式锁 分布式锁要满足的条件：\n多进程互斥：同一时刻，只有一个进程可以获取锁 保证锁可以释放：任务结束或出现异常，锁一定要释放，避免死锁 阻塞锁（可选）：获取锁失败时可否重试 重入锁（可选）：获取锁的代码递归调用时，依然可以获取锁 1）最基本的分布式锁： 利用Redis的setnx命令，这个命令的特征时如果多次执行，只有第一次执行会成功，可以实现互斥的效果。但是为了保证服务宕机时也可以释放锁，需要利用expire命令给锁设置一个有效期\n1 2 setnx lock thread-01 # 尝试获取锁 expire lock 10 # 设置有效期 Copied! 面试官问题1：如果expire之前服务宕机怎么办？\n要保证setnx和expire命令的原子性。redis的set命令可以满足：\n1 set key value [NX] [EX time] Copied! 需要添加nx和ex的选项：\nNX：与setnx一致，第一次执行成功 EX：设置过期时间 面试官问题2：释放锁的时候，如果自己的锁已经过期了，此时会出现安全漏洞，如何解决？\n在锁中存储当前进程和线程标识，释放锁时对锁的标识判断，如果是自己的则删除，不是则放弃操作。\n但是这两步操作要保证原子性，需要通过Lua脚本来实现。\n1 2 3 if redis.call(\u0026#34;get\u0026#34;,KEYS[1]) == ARGV[1] then redis.call(\u0026#34;del\u0026#34;,KEYS[1]) end Copied! 2）可重入分布式锁 如果有重入的需求，则除了在锁中记录进程标识，还要记录重试次数，流程如下：\n下面我们假设锁的key为“lock”，hashKey是当前线程的id：“threadId”，锁自动释放时间假设为20\n获取锁的步骤：\n1、判断lock是否存在 EXISTS lock 存在，说明有人获取锁了，下面判断是不是自己的锁 判断当前线程id作为hashKey是否存在：HEXISTS lock threadId 不存在，说明锁已经有了，且不是自己获取的，锁获取失败，end 存在，说明是自己获取的锁，重入次数+1：HINCRBY lock threadId 1，去到步骤3 2、不存在，说明可以获取锁，HSET key threadId 1 3、设置锁自动释放时间，EXPIRE lock 20 释放锁的步骤：\n1、判断当前线程id作为hashKey是否存在：HEXISTS lock threadId 不存在，说明锁已经失效，不用管了 存在，说明锁还在，重入次数减1：HINCRBY lock threadId -1，获取新的重入次数 2、判断重入次数是否为0： 为0，说明锁全部释放，删除key：DEL lock 大于0，说明锁还在使用，重置有效时间：EXPIRE lock 20 对应的Lua脚本如下：\n首先是获取锁：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 local key = KEYS[1]; -- 锁的key local threadId = ARGV[1]; -- 线程唯一标识 local releaseTime = ARGV[2]; -- 锁的自动释放时间 if(redis.call(\u0026#39;exists\u0026#39;, key) == 0) then -- 判断是否存在 redis.call(\u0026#39;hset\u0026#39;, key, threadId, \u0026#39;1\u0026#39;); -- 不存在, 获取锁 redis.call(\u0026#39;expire\u0026#39;, key, releaseTime); -- 设置有效期 return 1; -- 返回结果 end; if(redis.call(\u0026#39;hexists\u0026#39;, key, threadId) == 1) then -- 锁已经存在，判断threadId是否是自己\tredis.call(\u0026#39;hincrby\u0026#39;, key, threadId, \u0026#39;1\u0026#39;); -- 不存在, 获取锁，重入次数+1 redis.call(\u0026#39;expire\u0026#39;, key, releaseTime); -- 设置有效期 return 1; -- 返回结果 end; return 0; -- 代码走到这里,说明获取锁的不是自己，获取锁失败 Copied! 然后是释放锁：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 local key = KEYS[1]; -- 锁的key local threadId = ARGV[1]; -- 线程唯一标识 local releaseTime = ARGV[2]; -- 锁的自动释放时间 if (redis.call(\u0026#39;HEXISTS\u0026#39;, key, threadId) == 0) then -- 判断当前锁是否还是被自己持有 return nil; -- 如果已经不是自己，则直接返回 end; local count = redis.call(\u0026#39;HINCRBY\u0026#39;, key, threadId, -1); -- 是自己的锁，则重入次数-1 if (count \u0026gt; 0) then -- 判断是否重入次数是否已经为0 redis.call(\u0026#39;EXPIRE\u0026#39;, key, releaseTime); -- 大于0说明不能释放锁，重置有效期然后返回 return nil; else redis.call(\u0026#39;DEL\u0026#39;, key); -- 等于0说明可以释放锁，直接删除 return nil; end; Copied! 3）高可用的锁 面试官问题：redis分布式锁依赖与redis，如果redis宕机则锁失效。如何解决？\n此时大多数同学会回答说：搭建主从集群，做数据备份。\n这样就进入了陷阱，因为面试官的下一个问题就来了：\n面试官问题：如果搭建主从集群做数据备份时，进程A获取锁，master还没有把数据备份到slave，master宕机，slave升级为master，此时原来锁失效，其它进程也可以获取锁，出现安全问题。如何解决？\n关于这个问题，Redis官网给出了解决方案，使用RedLock思路可以解决：\n在Redis的分布式环境中，我们假设有N个Redis master。这些节点完全互相独立，不存在主从复制或者其他集群协调机制。之前我们已经描述了在Redis单实例下怎么安全地获取和释放锁。我们确保将在每（N)个实例上使用此方法获取和释放锁。在这个样例中，我们假设有5个Redis master节点，这是一个比较合理的设置，所以我们需要在5台机器上面或者5台虚拟机上面运行这些实例，这样保证他们不会同时都宕掉。\n为了取到锁，客户端应该执行以下操作:\n获取当前Unix时间，以毫秒为单位。 依次尝试从N个实例，使用相同的key和随机值获取锁。在步骤2，当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间，这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒，则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下，客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应，客户端应该尽快尝试另外一个Redis实例。 客户端使用当前时间减去开始获取锁时间（步骤1记录的时间）就得到获取锁使用的时间。当且仅当从大多数（这里是3个节点）的Redis节点都取到锁，并且使用的时间小于锁失效时间时，锁才算获取成功。 如果取到了锁，key的真正有效时间等于有效时间减去获取锁所使用的时间（步骤3计算的结果）。 如果因为某些原因，获取锁失败（没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间），客户端应该在所有的Redis实例上进行解锁（即便某些Redis实例根本就没有加锁成功）。 3.11.如何实现数据库与缓存数据一致？ 面试话术：\n实现方案有下面几种：\n本地缓存同步：当前微服务的数据库数据与缓存数据同步，可以直接在数据库修改时加入对Redis的修改逻辑，保证一致。 跨服务缓存同步：服务A调用了服务B，并对查询结果缓存。服务B数据库修改，可以通过MQ通知服务A，服务A修改Redis缓存数据 通用方案：使用Canal框架，伪装成MySQL的salve节点，监听MySQL的binLog变化，然后修改Redis缓存数据 ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/51178k51/","title":"15.微服务常见面试题"},{"content":" SpringCloud实用篇02 0.学习目标 1.Nacos配置管理 Nacos除了可以做注册中心，同样可以做配置管理来使用。\n1.1.统一配置管理 当微服务部署的实例越来越多，达到数十、数百时，逐个修改微服务配置就会让人抓狂，而且很容易出错。我们需要一种统一配置管理方案，可以集中管理所有实例的配置。\nNacos一方面可以将配置集中管理，另一方可以在配置变更时，及时通知微服务，实现配置的热更新。\n1.1.1.在nacos中添加配置文件 如何在nacos中管理配置呢？\n然后在弹出的表单中，填写配置信息：\n注意：项目的核心配置，需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。\n1.1.2.从微服务拉取配置 微服务要拉取nacos中管理的配置，并且与本地的application.yml配置合并，才能完成项目启动。\n但如果尚未读取application.yml，又如何得知nacos地址呢？\n因此spring引入了一种新的配置文件：bootstrap.yaml文件，会在application.yml之前被读取，流程如下：\n1）引入nacos-config依赖\n首先，在user-service服务中，引入nacos-config的客户端依赖：\n1 2 3 4 5 \u0026lt;!--nacos配置管理依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-nacos-config\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 2）添加bootstrap.yaml\n然后，在user-service中添加一个bootstrap.yaml文件，内容如下：\n1 2 3 4 5 6 7 8 9 10 spring: application: name: userservice # 服务名称 profiles: active: dev #开发环境，这里是dev cloud: nacos: server-addr: localhost:8848 # Nacos地址 config: file-extension: yaml # 文件后缀名 Copied! 这里会根据spring.cloud.nacos.server-addr获取nacos地址，再根据\n${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件id，来读取配置。\n本例中，就是去读取userservice-dev.yaml：\n3）读取nacos配置\n在user-service中的UserController中添加业务逻辑，读取pattern.dateformat配置：\n完整代码：\n1 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 package cn.itcast.user.web; import cn.itcast.user.pojo.User; import cn.itcast.user.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @Slf4j @RestController @RequestMapping(\u0026#34;/user\u0026#34;) public class UserController { @Autowired private UserService userService; @Value(\u0026#34;${pattern.dateformat}\u0026#34;) private String dateformat; @GetMapping(\u0026#34;now\u0026#34;) public String now(){ return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat)); } // ...略 } Copied! 在页面访问，可以看到效果：\n1.2.配置热更新 我们最终的目的，是修改nacos中的配置后，微服务中无需重启即可让配置生效，也就是配置热更新。\n要实现配置热更新，可以使用两种方式：\n1.2.1.方式一 在@Value注入的变量所在类上添加注解@RefreshScope：\n1.2.2.方式二 使用@ConfigurationProperties注解代替@Value注解。\n在user-service服务中，添加一个类，读取patterrn.dateformat属性：\n1 2 3 4 5 6 7 8 9 10 11 12 package cn.itcast.user.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @Data @ConfigurationProperties(prefix = \u0026#34;pattern\u0026#34;) public class PatternProperties { private String dateformat; } Copied! 在UserController中使用这个类代替@Value：\n完整代码：\n1 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 package cn.itcast.user.web; import cn.itcast.user.config.PatternProperties; import cn.itcast.user.pojo.User; import cn.itcast.user.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @Slf4j @RestController @RequestMapping(\u0026#34;/user\u0026#34;) public class UserController { @Autowired private UserService userService; @Autowired private PatternProperties patternProperties; @GetMapping(\u0026#34;now\u0026#34;) public String now(){ return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat())); } // 略 } Copied! 1.3.配置共享 其实微服务启动时，会去nacos读取多个配置文件，例如：\n[spring.application.name]-[spring.profiles.active].yaml，例如：userservice-dev.yaml\n[spring.application.name].yaml，例如：userservice.yaml\n而[spring.application.name].yaml不包含环境，因此可以被多个环境共享。\n下面我们通过案例来测试配置共享\n1）添加一个环境共享配置 我们在nacos中添加一个userservice.yaml文件：\n2）在user-service中读取共享配置 在user-service服务中，修改PatternProperties类，读取新添加的属性：\n在user-service服务中，修改UserController，添加一个方法：\n3）运行两个UserApplication，使用不同的profile 修改UserApplication2这个启动项，改变其profile值：\n这样，UserApplication(8081)使用的profile是dev，UserApplication2(8082)使用的profile是test。\n启动UserApplication和UserApplication2\n访问http://localhost:8081/user/prop，结果：\n访问http://localhost:8082/user/prop，结果：\n可以看出来，不管是dev，还是test环境，都读取到了envSharedValue这个属性的值。\n4）配置共享的优先级 当nacos、服务本地同时出现相同属性时，优先级有高低之分：\n1.4.搭建Nacos集群 Nacos生产环境下一定要部署为集群状态，部署方式参考课前资料中的文档：\n2.Feign远程调用 先来看我们以前利用RestTemplate发起远程调用的代码：\n存在下面的问题：\n•代码可读性差，编程体验不统一\n•参数复杂URL难以维护\nFeign是一个声明式的http客户端，官方地址：https://github.com/OpenFeign/feign\n其作用就是帮助我们优雅的实现http请求的发送，解决上面提到的问题。\n2.1.Feign替代RestTemplate Fegin的使用步骤如下：\n1）引入依赖 我们在order-service服务的pom文件中引入feign的依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-openfeign\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 2）添加注解 在order-service的启动类添加注解开启Feign的功能：\n3）编写Feign的客户端 在order-service中新建一个接口，内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 package cn.itcast.order.client; import cn.itcast.order.pojo.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(\u0026#34;userservice\u0026#34;) public interface UserClient { @GetMapping(\u0026#34;/user/{id}\u0026#34;) User findById(@PathVariable(\u0026#34;id\u0026#34;) Long id); } Copied! 这个客户端主要是基于SpringMVC的注解来声明远程调用的信息，比如：\n服务名称：userservice 请求方式：GET 请求路径：/user/{id} 请求参数：Long id 返回值类型：User 这样，Feign就可以帮助我们发送http请求，无需自己使用RestTemplate来发送了。\n4）测试 修改order-service中的OrderService类中的queryOrderById方法，使用Feign客户端代替RestTemplate：\n是不是看起来优雅多了。\n5）总结 使用Feign的步骤：\n① 引入依赖\n② 添加@EnableFeignClients注解\n③ 编写FeignClient接口\n④ 使用FeignClient中定义的方法代替RestTemplate\n2.2.自定义配置 Feign可以支持很多的自定义配置，如下表所示：\n类型 作用 说明 feign.Logger.Level 修改日志级别 包含四种不同的级别：NONE、BASIC、HEADERS、FULL feign.codec.Decoder 响应结果的解析器 http远程调用的结果做解析，例如解析json字符串为java对象 feign.codec.Encoder 请求参数编码 将请求参数编码，便于通过http请求发送 feign. Contract 支持的注解格式 默认是SpringMVC的注解 feign. Retryer 失败重试机制 请求失败的重试机制，默认是没有，不过会使用Ribbon的重试 一般情况下，默认值就能满足我们使用，如果要自定义时，只需要创建自定义的@Bean覆盖默认Bean即可。\n下面以日志为例来演示如何自定义配置。\n2.2.1.配置文件方式 基于配置文件修改feign的日志级别可以针对单个服务：\n1 2 3 4 5 feign: client: config: userservice: # 针对某个微服务的配置 loggerLevel: FULL # 日志级别 Copied! 也可以针对所有服务：\n1 2 3 4 5 feign: client: config: default: # 这里用default就是全局配置，如果是写服务名称，则是针对某个微服务的配置 loggerLevel: FULL # 日志级别 Copied! 而日志的级别分为四种：\nNONE：不记录任何日志信息，这是默认值。 BASIC：仅记录请求的方法，URL以及响应状态码和执行时间 HEADERS：在BASIC的基础上，额外记录了请求和响应的头信息 FULL：记录所有请求和响应的明细，包括头信息、请求体、元数据。 2.2.2.Java代码方式 也可以基于Java代码来修改日志级别，先声明一个类，然后声明一个Logger.Level的对象：\n1 2 3 4 5 6 public class DefaultFeignConfiguration { @Bean public Logger.Level feignLogLevel(){ return Logger.Level.BASIC; // 日志级别为BASIC } } Copied! 如果要全局生效，将其放到启动类的@EnableFeignClients这个注解中：\n1 @EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class) Copied! 如果是局部生效，则把它放到对应的@FeignClient这个注解中：\n1 @FeignClient(value = \u0026#34;userservice\u0026#34;, configuration = DefaultFeignConfiguration .class) Copied! 2.3.Feign使用优化 Feign底层发起http请求，依赖于其它的框架。其底层客户端实现包括：\n•URLConnection：默认实现，不支持连接池\n•Apache HttpClient ：支持连接池\n•OKHttp：支持连接池\n因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。\n这里我们用Apache的HttpClient来演示。\n1）引入依赖\n在order-service的pom文件中引入Apache的HttpClient依赖：\n1 2 3 4 5 \u0026lt;!--httpClient的依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.github.openfeign\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;feign-httpclient\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 2）配置连接池\n在order-service的application.yml中添加配置：\n1 2 3 4 5 6 7 8 9 feign: client: config: default: # default全局的配置 loggerLevel: BASIC # 日志级别，BASIC就是基本的请求和响应信息 httpclient: enabled: true # 开启feign对HttpClient的支持 max-connections: 200 # 最大的连接数 max-connections-per-route: 50 # 每个路径的最大连接数 Copied! 接下来，在FeignClientFactoryBean中的loadBalance方法中打断点：\nDebug方式启动order-service服务，可以看到这里的client，底层就是Apache HttpClient：\n总结，Feign的优化：\n1.日志级别尽量用basic\n2.使用HttpClient或OKHttp代替URLConnection\n① 引入feign-httpClient依赖\n② 配置文件开启httpClient功能，设置连接池参数\n2.4.最佳实践 所谓最近实践，就是使用过程中总结的经验，最好的一种使用方式。\n自习观察可以发现，Feign的客户端与服务提供者的controller代码非常相似：\nfeign客户端：\nUserController：\n有没有一种办法简化这种重复的代码编写呢？\n2.4.1.继承方式 一样的代码可以通过继承来共享：\n1）定义一个API接口，利用定义方法，并基于SpringMVC注解做声明。\n2）Feign客户端和Controller都集成改接口\n优点：\n简单 实现了代码共享 缺点：\n服务提供方、服务消费方紧耦合\n参数列表中的注解映射并不会继承，因此Controller中必须再次声明方法、参数列表、注解\n2.4.2.抽取方式 将Feign的Client抽取为独立模块，并且把接口有关的POJO、默认的Feign配置都放到这个模块中，提供给所有消费者使用。\n例如，将UserClient、User、Feign的默认配置都抽取到一个feign-api包中，所有微服务引用该依赖包，即可直接使用。\n2.4.3.实现基于抽取的最佳实践 1）抽取 首先创建一个module，命名为feign-api：\n项目结构：\n在feign-api中然后引入feign的starter依赖\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-openfeign\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 然后，order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中\n2）在order-service中使用feign-api 首先，删除order-service中的UserClient、User、DefaultFeignConfiguration等类或接口。\n在order-service的pom文件中中引入feign-api的依赖：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;cn.itcast.demo\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;feign-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 修改order-service中的所有与上述三个组件有关的导包部分，改成导入feign-api中的包\n3）重启测试 重启后，发现服务报错了：\n这是因为UserClient现在在cn.itcast.feign.clients包下，\n而order-service的@EnableFeignClients注解是在cn.itcast.order包下，不在同一个包，无法扫描到UserClient。\n4）解决扫描包问题 方式一：\n指定Feign应该扫描的包：\n1 @EnableFeignClients(basePackages = \u0026#34;cn.itcast.feign.clients\u0026#34;) Copied! 方式二：\n指定需要加载的Client接口：\n1 @EnableFeignClients(clients = {UserClient.class}) Copied! 3.Gateway服务网关 Spring Cloud Gateway 是 Spring Cloud 的一个全新项目，该项目是基于 Spring 5.0，Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关，它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。\n3.1.为什么需要网关 Gateway网关是我们服务的守门神，所有微服务的统一入口。\n网关的核心功能特性：\n请求路由 权限控制 限流 架构图：\n权限控制：网关作为微服务入口，需要校验用户是是否有请求资格，如果没有则进行拦截。\n路由和负载均衡：一切请求都必须先经过gateway，但网关不处理业务，而是根据某种规则，把请求转发到某个微服务，这个过程叫做路由。当然路由的目标服务有多个时，还需要做负载均衡。\n限流：当请求流量过高时，在网关中按照下流的微服务能够接受的速度来放行请求，避免服务压力过大。\n在SpringCloud中网关的实现包括两种：\ngateway zuul Zuul是基于Servlet的实现，属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux，属于响应式编程的实现，具备更好的性能。\n3.2.gateway快速入门 下面，我们就演示下网关的基本路由功能。基本步骤如下：\n创建SpringBoot工程gateway，引入网关依赖 编写启动类 编写基础配置和路由规则 启动网关服务进行测试 1）创建gateway服务，引入依赖 创建服务：\n引入依赖：\n1 2 3 4 5 6 7 8 9 10 \u0026lt;!--网关--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-gateway\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--nacos服务发现依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-nacos-discovery\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 2）编写启动类 1 2 3 4 5 6 7 8 9 10 11 12 package cn.itcast.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } } Copied! 3）编写基础配置和路由规则 创建application.yml文件，内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 10010 # 网关端口 spring: application: name: gateway # 服务名称 cloud: nacos: server-addr: localhost:8848 # nacos地址 gateway: routes: # 网关路由配置 - id: user-service # 路由id，自定义，只要唯一即可 # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址 uri: lb://userservice # 路由的目标地址 lb就是负载均衡，后面跟服务名称 predicates: # 路由断言，也就是判断请求是否符合路由规则的条件 - Path=/user/** # 这个是按照路径匹配，只要以/user/开头就符合要求 Copied! 我们将符合Path 规则的一切请求，都代理到 uri参数指定的地址。\n本例中，我们将 /user/**开头的请求，代理到lb://userservice，lb是负载均衡，根据服务名拉取服务列表，实现负载均衡。\n4）重启测试 重启网关，访问http://localhost:10010/user/1时，符合/user/**规则，请求转发到uri：http://userservice/user/1，得到了结果：\n5）网关路由的流程图 整个访问的流程如下：\n总结：\n网关搭建步骤：\n创建项目，引入nacos服务发现和gateway依赖\n配置application.yml，包括服务基本信息、nacos地址、路由\n路由配置包括：\n路由id：路由的唯一标示\n路由目标（uri）：路由的目标地址，http代表固定地址，lb代表根据服务名负载均衡\n路由断言（predicates）：判断路由的规则，\n路由过滤器（filters）：对请求或响应做处理\n接下来，就重点来学习路由断言和路由过滤器的详细知识\n3.3.断言工厂 我们在配置文件中写的断言规则只是字符串，这些字符串会被Predicate Factory读取并处理，转变为路由判断的条件\n例如Path=/user/**是按照路径匹配，这个规则是由\norg.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来\n处理的，像这样的断言工厂在SpringCloudGateway还有十几个:\n名称 说明 示例 After 是某个时间点后的请求 - After=2037-01-20T17:42:47.789-07:00[America/Denver] Before 是某个时间点之前的请求 - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] Between 是某两个时间点之前的请求 - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] Cookie 请求必须包含某些cookie - Cookie=chocolate, ch.p Header 请求必须包含某些header - Header=X-Request-Id, \\d+ Host 请求必须是访问某个host（域名） - Host=.somehost.org,.anotherhost.org Method 请求方式必须是指定方式 - Method=GET,POST Path 请求路径必须符合指定规则 - Path=/red/{segment},/blue/** Query 请求参数必须包含指定参数 - Query=name, Jack或者- Query=name RemoteAddr 请求者的ip必须是指定范围 - RemoteAddr=192.168.1.1/24 Weight 权重处理 我们只需要掌握Path这种路由工程就可以了。\n3.4.过滤器工厂 GatewayFilter是网关中提供的一种过滤器，可以对进入网关的请求和微服务返回的响应做处理：\n3.4.1.路由过滤器的种类 Spring提供了31种不同的路由过滤器工厂。例如：\n名称 说明 AddRequestHeader 给当前请求添加一个请求头 RemoveRequestHeader 移除请求中的一个请求头 AddResponseHeader 给响应结果中添加一个响应头 RemoveResponseHeader 从响应结果中移除有一个响应头 RequestRateLimiter 限制请求的流量 3.4.2.请求头过滤器 下面我们以AddRequestHeader 为例来讲解。\n需求：给所有进入userservice的请求添加一个请求头：Truth=itcast is freaking awesome!\n只需要修改gateway服务的application.yml文件，添加路由过滤即可：\n1 2 3 4 5 6 7 8 9 10 spring: cloud: gateway: routes: - id: user-service uri: lb://userservice predicates: - Path=/user/** filters: # 过滤器 - AddRequestHeader=Truth, Itcast is freaking awesome! # 添加请求头 Copied! 当前过滤器写在userservice路由下，因此仅仅对访问userservice的请求有效。\n3.4.3.默认过滤器 如果要对所有的路由都生效，则可以将过滤器工厂写到default下。格式如下：\n1 2 3 4 5 6 7 8 9 10 spring: cloud: gateway: routes: - id: user-service uri: lb://userservice predicates: - Path=/user/** default-filters: # 默认过滤项 - AddRequestHeader=Truth, Itcast is freaking awesome! Copied! 3.4.4.总结 过滤器的作用是什么？\n① 对路由的请求或响应做加工处理，比如添加请求头\n② 配置在路由下的过滤器只对当前路由的请求生效\ndefaultFilters的作用是什么？\n① 对所有路由都生效的过滤器\n3.5.全局过滤器 上一节学习的过滤器，网关提供了31种，但每一种过滤器的作用都是固定的。如果我们希望拦截请求，做自己的业务逻辑则没办法实现。\n3.5.1.全局过滤器作用 全局过滤器的作用也是处理一切进入网关的请求和微服务响应，与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义，处理逻辑是固定的；而GlobalFilter的逻辑需要自己写代码实现。\n定义方式是实现GlobalFilter接口。\n1 2 3 4 5 6 7 8 9 10 public interface GlobalFilter { /** * 处理当前请求，有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理 * * @param exchange 请求上下文，里面可以获取Request、Response等信息 * @param chain 用来把请求委托给下一个过滤器 * @return {@code Mono\u0026lt;Void\u0026gt;} 返回标示当前过滤器业务结束 */ Mono\u0026lt;Void\u0026gt; filter(ServerWebExchange exchange, GatewayFilterChain chain); } Copied! 在filter中编写自定义逻辑，可以实现下列功能：\n登录状态判断 权限校验 请求限流等 3.5.2.自定义全局过滤器 需求：定义全局过滤器，拦截请求，判断请求的参数是否满足下面条件：\n参数中是否有authorization，\nauthorization参数值是否为admin\n如果同时满足则放行，否则拦截\n实现：\n在gateway中定义一个过滤器：\n1 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 package cn.itcast.gateway.filters; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Order(-1) @Component public class AuthorizeFilter implements GlobalFilter { @Override public Mono\u0026lt;Void\u0026gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1.获取请求参数 MultiValueMap\u0026lt;String, String\u0026gt; params = exchange.getRequest().getQueryParams(); // 2.获取authorization参数 String auth = params.getFirst(\u0026#34;authorization\u0026#34;); // 3.校验 if (\u0026#34;admin\u0026#34;.equals(auth)) { // 放行 return chain.filter(exchange); } // 4.拦截 // 4.1.禁止访问，设置状态码 exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); // 4.2.结束处理 return exchange.getResponse().setComplete(); } } Copied! 3.5.3.过滤器执行顺序 请求进入网关会碰到三类过滤器：当前路由的过滤器、DefaultFilter、GlobalFilter\n请求路由后，会将当前路由过滤器和DefaultFilter、GlobalFilter，合并到一个过滤器链（集合）中，排序后依次执行每个过滤器：\n排序的规则是什么呢？\n每一个过滤器都必须指定一个int类型的order值，order值越小，优先级越高，执行顺序越靠前。 GlobalFilter通过实现Ordered接口，或者添加@Order注解来指定order值，由我们自己指定 路由过滤器和defaultFilter的order由Spring指定，默认是按照声明顺序从1递增。 当过滤器的order值一样时，会按照 defaultFilter \u0026gt; 路由过滤器 \u0026gt; GlobalFilter的顺序执行。 详细内容，可以查看源码：\norg.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()方法是先加载defaultFilters，然后再加载某个route的filters，然后合并。\norg.springframework.cloud.gateway.handler.FilteringWebHandler#handle()方法会加载全局过滤器，与前面的过滤器合并后根据order排序，组织过滤器链\n3.6.跨域问题 3.6.1.什么是跨域问题 跨域：域名不一致就是跨域，主要包括：\n域名不同： www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com\n域名相同，端口不同：localhost:8080和localhost8081\n跨域问题：浏览器禁止请求的发起者与服务端发生跨域ajax请求，请求被浏览器拦截的问题\n解决方案：CORS，这个以前应该学习过，这里不再赘述了。不知道的小伙伴可以查看https://www.ruanyifeng.com/blog/2016/04/cors.html\n3.6.2.模拟跨域问题 找到课前资料的页面文件：\n放入tomcat或者nginx这样的web服务器中，启动并访问。\n可以在浏览器控制台看到下面的错误：\n从localhost:8090访问localhost:10010，端口不同，显然是跨域的请求。\n3.6.3.解决跨域问题 在gateway服务的application.yml文件中，添加下面的配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 spring: cloud: gateway: # 。。。 globalcors: # 全局的跨域处理 add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题 corsConfigurations: \u0026#39;[/**]\u0026#39;: allowedOrigins: # 允许哪些网站的跨域请求 - \u0026#34;http://localhost:8090\u0026#34; allowedMethods: # 允许的跨域ajax的请求方式 - \u0026#34;GET\u0026#34; - \u0026#34;POST\u0026#34; - \u0026#34;DELETE\u0026#34; - \u0026#34;PUT\u0026#34; - \u0026#34;OPTIONS\u0026#34; allowedHeaders: \u0026#34;*\u0026#34; # 允许在请求中携带的头信息 allowCredentials: true # 是否允许携带cookie maxAge: 360000 # 这次跨域检测的有效期 Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/138f5613/","title":"2.SpringCloud实用篇02"},{"content":" Docker实用篇 0.学习目标 1.初识Docker 1.1.什么是Docker 微服务虽然具备各种各样的优势，但服务的拆分通用给部署带来了很大的麻烦。\n分布式系统中，依赖的组件非常多，不同组件之间部署时往往会产生一些冲突。 在数百上千台服务中重复部署，环境不一定一致，会遇到各种问题 1.1.1.应用部署的环境问题 大型项目组件较多，运行环境也较为复杂，部署时会碰到一些问题：\n依赖关系复杂，容易出现兼容性问题\n开发、测试、生产环境有差异\n例如一个项目中，部署时需要依赖于node.js、Redis、RabbitMQ、MySQL等，这些服务部署时所需要的函数库、依赖项各不相同，甚至会有冲突。给部署带来了极大的困难。\n1.1.2.Docker解决依赖兼容问题 而Docker确巧妙的解决了这些问题，Docker是如何实现的呢？\nDocker为了解决依赖的兼容问题的，采用了两个手段：\n将应用的Libs（函数库）、Deps（依赖）、配置与应用一起打包\n将每个应用放到一个隔离容器去运行，避免互相干扰\n这样打包好的应用包中，既包含应用本身，也保护应用所需要的Libs、Deps，无需再操作系统上安装这些，自然就不存在不同应用之间的兼容问题了。\n虽然解决了不同应用的兼容问题，但是开发、测试等环境会存在差异，操作系统版本也会有差异，怎么解决这些问题呢？\n1.1.3.Docker解决操作系统环境差异 要解决不同操作系统环境差异问题，必须先了解操作系统结构。以一个Ubuntu操作系统为例，结构如下：\n结构包括：\n计算机硬件：例如CPU、内存、磁盘等 系统内核：所有Linux发行版的内核都是Linux，例如CentOS、Ubuntu、Fedora等。内核可以与计算机硬件交互，对外提供内核指令，用于操作计算机硬件。 系统应用：操作系统本身提供的应用、函数库。这些函数库是对内核指令的封装，使用更加方便。 应用于计算机交互的流程如下：\n1）应用调用操作系统应用（函数库），实现各种功能\n2）系统函数库是对内核指令集的封装，会调用内核指令\n3）内核指令操作计算机硬件\nUbuntu和CentOSpringBoot都是基于Linux内核，无非是系统应用不同，提供的函数库有差异：\n此时，如果将一个Ubuntu版本的MySQL应用安装到CentOS系统，MySQL在调用Ubuntu函数库时，会发现找不到或者不匹配，就会报错了：\nDocker如何解决不同系统环境的问题？\nDocker将用户程序与所需要调用的系统(比如Ubuntu)函数库一起打包 Docker运行到不同操作系统时，直接基于打包的函数库，借助于操作系统的Linux内核来运行 如图：\n1.1.4.小结 Docker如何解决大型项目依赖关系复杂，不同组件依赖的兼容性问题？\nDocker允许开发中将应用、依赖、函数库、配置一起打包，形成可移植镜像 Docker应用运行在容器中，使用沙箱机制，相互隔离 Docker如何解决开发、测试、生产环境有差异的问题？\nDocker镜像中包含完整运行环境，包括系统函数库，仅依赖系统的Linux内核，因此可以在任意Linux操作系统上运行 Docker是一个快速交付应用、运行应用的技术，具备下列优势：\n可以将程序及其依赖、运行环境一起打包为一个镜像，可以迁移到任意Linux操作系统 运行时利用沙箱机制形成隔离容器，各个应用互不干扰 启动、移除都可以通过一行命令完成，方便快捷 1.2.Docker和虚拟机的区别 Docker可以让一个应用在任何操作系统中非常方便的运行。而以前我们接触的虚拟机，也能在一个操作系统中，运行另外一个操作系统，保护系统中的任何应用。\n两者有什么差异呢？\n虚拟机（virtual machine）是在操作系统中模拟硬件设备，然后运行另一个操作系统，比如在 Windows 系统里面运行 Ubuntu 系统，这样就可以运行任意的Ubuntu应用了。\nDocker仅仅是封装函数库，并没有模拟完整的操作系统，如图：\n对比来看：\n小结：\nDocker和虚拟机的差异：\ndocker是一个系统进程；虚拟机是在操作系统中的操作系统\ndocker体积小、启动速度快、性能好；虚拟机体积大、启动速度慢、性能一般\n1.3.Docker架构 1.3.1.镜像和容器 Docker中有几个重要的概念：\n镜像（Image）：Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起，称为镜像。\n容器（Container）：镜像中的应用程序运行后形成的进程就是容器，只是Docker会给容器进程做隔离，对外不可见。\n一切应用最终都是代码组成，都是硬盘中的一个个的字节形成的文件。只有运行时，才会加载到内存，形成进程。\n而镜像，就是把一个应用在硬盘上的文件、及其运行环境、部分系统函数库文件一起打包形成的文件包。这个文件包是只读的。\n容器呢，就是将这些文件中编写的程序、函数加载到内存中允许，形成进程，只不过要隔离起来。因此一个镜像可以启动多次，形成多个容器进程。\n例如你下载了一个QQ，如果我们将QQ在磁盘上的运行文件及其运行的操作系统依赖打包，形成QQ镜像。然后你可以启动多次，双开、甚至三开QQ，跟多个妹子聊天。\n1.3.2.DockerHub 开源应用程序非常多，打包这些应用往往是重复的劳动。为了避免这些重复劳动，人们就会将自己打包的应用镜像，例如Redis、MySQL镜像放到网络上，共享使用，就像GitHub的代码共享一样。\nDockerHub：DockerHub是一个官方的Docker镜像的托管平台。这样的平台称为Docker Registry。\n国内也有类似于DockerHub 的公开服务，比如 网易云镜像服务 、阿里云镜像库 等。\n我们一方面可以将自己的镜像共享到DockerHub，另一方面也可以从DockerHub拉取镜像：\n1.3.3.Docker架构 我们要使用Docker来操作镜像、容器，就必须要安装Docker。\nDocker是一个CS架构的程序，由两部分组成：\n服务端(server)：Docker守护进程，负责处理Docker指令，管理镜像、容器等\n客户端(client)：通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。\n如图：\n1.3.4.小结 镜像：\n将应用程序及其依赖、环境、配置打包在一起 容器：\n镜像运行起来就是容器，一个镜像可以运行多个容器 Docker结构：\n服务端：接收命令或远程请求，操作镜像或容器\n客户端：发送命令或者请求到Docker服务端\nDockerHub：\n一个镜像托管的服务器，类似的还有阿里云镜像服务，统称为DockerRegistry 1.4.安装Docker 企业部署一般都是采用Linux操作系统，而其中又数CentOS发行版占比最多，因此我们在CentOS下安装Docker。参考课前资料中的文档：\n2.Docker的基本操作 2.1.镜像操作 2.1.1.镜像名称 首先来看下镜像的名称组成：\n镜名称一般分两部分组成：[repository]:[tag]。 在没有指定tag时，默认是latest，代表最新版本的镜像 如图：\n这里的mysql就是repository，5.7就是tag，合一起就是镜像名称，代表5.7版本的MySQL镜像。\n2.1.2.镜像命令 常见的镜像操作命令如图：\n2.1.3.案例1-拉取、查看镜像 需求：从DockerHub中拉取一个nginx镜像并查看\n1）首先去镜像仓库搜索nginx镜像，比如DockerHub :\n2）根据查看到的镜像名称，拉取自己需要的镜像，通过命令：docker pull nginx\n3）通过命令：docker images 查看拉取到的镜像\n2.1.4.案例2-保存、导入镜像 需求：利用docker save将nginx镜像导出磁盘，然后再通过load加载回来\n1）利用docker xx \u0026ndash;help命令查看docker save和docker load的语法\n例如，查看save命令用法，可以输入命令：\n1 docker save --help Copied! 结果：\n命令格式：\n1 docker save -o [保存的目标文件名称] [镜像名称] Copied! 2）使用docker save导出镜像到磁盘\n运行命令：\n1 docker save -o nginx.tar nginx:latest Copied! 结果如图：\n3）使用docker load加载镜像\n先删除本地的nginx镜像：\n1 docker rmi nginx:latest Copied! 然后运行命令，加载本地文件：\n1 docker load -i nginx.tar Copied! 结果：\n2.1.5.练习 需求：去DockerHub搜索并拉取一个Redis镜像\n目标：\n1）去DockerHub搜索Redis镜像\n2）查看Redis镜像的名称和版本\n3）利用docker pull命令拉取镜像\n4）利用docker save命令将 redis:latest打包为一个redis.tar包\n5）利用docker rmi 删除本地的redis:latest\n6）利用docker load 重新加载 redis.tar文件\n2.2.容器操作 2.2.1.容器相关命令 容器操作的命令如图：\n容器保护三个状态：\n运行：进程正常运行 暂停：进程暂停，CPU不再运行，并不释放内存 停止：进程终止，回收进程占用的内存、CPU等资源 其中：\ndocker run：创建并运行一个容器，处于运行状态\ndocker pause：让一个运行的容器暂停\ndocker unpause：让一个容器从暂停状态恢复运行\ndocker stop：停止一个运行的容器\ndocker start：让一个停止的容器再次运行\ndocker rm：删除一个容器\n2.2.2.案例-创建并运行一个容器 创建并运行nginx容器的命令：\n1 docker run --name containerName -p 80:80 -d nginx Copied! 命令解读：\ndocker run ：创建并运行一个容器 \u0026ndash;name : 给容器起一个名字，比如叫做mn -p ：将宿主机端口与容器端口映射，冒号左侧是宿主机端口，右侧是容器端口 -d：后台运行容器 nginx：镜像名称，例如nginx 这里的-p参数，是将容器端口映射到宿主机端口。\n默认情况下，容器是隔离环境，我们直接访问宿主机的80端口，肯定访问不到容器中的nginx。\n现在，将容器的80与宿主机的80关联起来，当我们访问宿主机的80端口时，就会被映射到容器的80，这样就能访问到nginx了：\n2.2.3.案例-进入容器，修改文件 需求：进入Nginx容器，修改HTML文件内容，添加“传智教育欢迎您”\n提示：进入容器要用到docker exec命令。\n步骤：\n1）进入容器。进入我们刚刚创建的nginx容器的命令为：\n1 docker exec -it mn bash Copied! 命令解读：\ndocker exec ：进入容器内部，执行一个命令\n-it : 给当前进入的容器创建一个标准输入、输出终端，允许我们与容器交互\nmn ：要进入的容器的名称\nbash：进入容器后执行的命令，bash是一个linux终端交互命令\n2）进入nginx的HTML所在目录 /usr/share/nginx/html\n容器内部会模拟一个独立的Linux文件系统，看起来如同一个linux服务器一样：\nnginx的环境、配置、运行文件全部都在这个文件系统中，包括我们要修改的html文件。\n查看DockerHub网站中的nginx页面，可以知道nginx的html目录位置在/usr/share/nginx/html\n我们执行命令，进入该目录：\n1 cd /usr/share/nginx/html Copied! 查看目录下文件：\n3）修改index.html的内容\n容器内没有vi命令，无法直接修改，我们用下面的命令来修改：\n1 sed -i -e \u0026#39;s#Welcome to nginx#传智教育欢迎您#g\u0026#39; -e \u0026#39;s#\u0026lt;head\u0026gt;#\u0026lt;head\u0026gt;\u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt;#g\u0026#39; index.html Copied! 在浏览器访问自己的虚拟机地址，例如我的是：http://192.168.150.101，即可看到结果：\n2.2.4.小结 docker run命令的常见参数有哪些？\n\u0026ndash;name：指定容器名称 -p：指定端口映射 -d：让容器后台运行 查看容器日志的命令：\ndocker logs 添加 -f 参数可以持续查看日志 查看容器状态：\ndocker ps docker ps -a 查看所有容器，包括已经停止的 2.3.数据卷（容器数据管理） 在之前的nginx案例中，修改nginx的html页面时，需要进入nginx内部。并且因为没有编辑器，修改文件也很麻烦。\n这就是因为容器与数据（容器内文件）耦合带来的后果。\n要解决这个问题，必须将数据与容器解耦，这就要用到数据卷了。\n2.3.1.什么是数据卷 **数据卷（volume）**是一个虚拟目录，指向宿主机文件系统中的某个目录。\n一旦完成数据卷挂载，对容器的一切操作都会作用在数据卷对应的宿主机目录了。\n这样，我们操作宿主机的/var/lib/docker/volumes/html目录，就等于操作容器内的/usr/share/nginx/html目录了\n2.3.2.数据集操作命令 数据卷操作的基本语法如下：\n1 docker volume [COMMAND] Copied! docker volume命令是数据卷操作，根据命令后跟随的command来确定下一步的操作：\ncreate 创建一个volume inspect 显示一个或多个volume的信息 ls 列出所有的volume prune 删除未使用的volume rm 删除一个或多个指定的volume 2.3.3.创建和查看数据卷 需求：创建一个数据卷，并查看数据卷在宿主机的目录位置\n① 创建数据卷\n1 docker volume create html Copied! ② 查看所有数据\n1 docker volume ls Copied! 结果：\n③ 查看数据卷详细信息卷\n1 docker volume inspect html Copied! 结果：\n可以看到，我们创建的html这个数据卷关联的宿主机目录为/var/lib/docker/volumes/html/_data目录。\n小结：\n数据卷的作用：\n将容器与数据分离，解耦合，方便操作容器内数据，保证数据安全 数据卷操作：\ndocker volume create：创建数据卷 docker volume ls：查看所有数据卷 docker volume inspect：查看数据卷详细信息，包括关联的宿主机目录位置 docker volume rm：删除指定数据卷 docker volume prune：删除所有未使用的数据卷 2.3.4.挂载数据卷 我们在创建容器时，可以通过 -v 参数来挂载一个数据卷到某个容器内目录，命令格式如下：\n1 2 3 4 5 docker run \\ --name mn \\ -v html:/root/html \\ -p 8080:80 nginx \\ Copied! 这里的-v就是挂载数据卷的命令：\n-v html:/root/htm ：把html数据卷挂载到容器内的/root/html这个目录中 2.3.5.案例-给nginx挂载数据卷 需求：创建一个nginx容器，修改容器内的html目录内的index.html内容\n分析：上个案例中，我们进入nginx容器内部，已经知道nginx的html目录所在位置/usr/share/nginx/html ，我们需要把这个目录挂载到html这个数据卷上，方便操作其中的内容。\n提示：运行容器时使用 -v 参数挂载数据卷\n步骤：\n① 创建容器并挂载数据卷到容器内的HTML目录\n1 docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx Copied! ② 进入html数据卷所在位置，并修改HTML内容\n1 2 3 4 5 6 # 查看html数据卷的位置 docker volume inspect html # 进入该目录 cd /var/lib/docker/volumes/html/_data # 修改文件 vi index.html Copied! 2.3.6.案例-给MySQL挂载本地目录 容器不仅仅可以挂载数据卷，也可以直接挂载到宿主机目录上。关联关系如下：\n带数据卷模式：宿主机目录 \u0026ndash;\u0026gt; 数据卷 \u0026mdash;\u0026gt; 容器内目录 直接挂载模式：宿主机目录 \u0026mdash;\u0026gt; 容器内目录 如图：\n语法：\n目录挂载与数据卷挂载的语法是类似的：\n-v [宿主机目录]:[容器内目录] -v [宿主机文件]:[容器内文件] 需求：创建并运行一个MySQL容器，将宿主机目录直接挂载到容器\n实现思路如下：\n1）在将课前资料中的mysql.tar文件上传到虚拟机，通过load命令加载为镜像\n2）创建目录/tmp/mysql/data\n3）创建目录/tmp/mysql/conf，将课前资料提供的hmy.cnf文件上传到/tmp/mysql/conf\n4）去DockerHub查阅资料，创建并运行MySQL容器，要求：\n① 挂载/tmp/mysql/data到mysql容器内数据存储目录\n② 挂载/tmp/mysql/conf/hmy.cnf到mysql容器的配置文件\n③ 设置MySQL密码\n2.3.7.小结 docker run的命令中通过 -v 参数挂载文件或目录到容器中：\n-v volume名称:容器内目录 -v 宿主机文件:容器内文 -v 宿主机目录:容器内目录 数据卷挂载与目录直接挂载的\n数据卷挂载耦合度低，由docker来管理目录，但是目录较深，不好找 目录挂载耦合度高，需要我们自己管理目录，不过目录容易寻找查看 3.Dockerfile自定义镜像 常见的镜像在DockerHub就能找到，但是我们自己写的项目就必须自己构建镜像了。\n而要自定义镜像，就必须先了解镜像的结构才行。\n3.1.镜像结构 镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。\n我们以MySQL为例，来看看镜像的组成结构：\n简单来说，镜像就是在系统函数库、运行环境基础上，添加应用程序文件、配置文件、依赖文件等组合，然后编写好启动脚本打包在一起形成的文件。\n我们要构建镜像，其实就是实现上述打包的过程。\n3.2.Dockerfile语法 构建自定义的镜像时，并不需要一个个文件去拷贝，打包。\n我们只需要告诉Docker，我们的镜像的组成，需要哪些BaseImage、需要拷贝什么文件、需要安装什么依赖、启动脚本是什么，将来Docker会帮助我们构建镜像。\n而描述上述信息的文件就是Dockerfile文件。\nDockerfile就是一个文本文件，其中包含一个个的指令(Instruction)，用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer。\n更新详细语法说明，请参考官网文档： https://docs.docker.com/engine/reference/builder 3.3.构建Java项目 3.3.1.基于Ubuntu构建Java项目 需求：基于Ubuntu镜像构建一个新镜像，运行一个java项目\n步骤1：新建一个空文件夹docker-demo\n步骤2：拷贝课前资料中的docker-demo.jar文件到docker-demo这个目录\n步骤3：拷贝课前资料中的jdk8.tar.gz文件到docker-demo这个目录\n步骤4：拷贝课前资料提供的Dockerfile到docker-demo这个目录\n其中的内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 指定基础镜像 FROM ubuntu:16.04 # 配置环境变量，JDK的安装目录 ENV JAVA_DIR=/usr/local # 拷贝jdk和java项目的包 COPY ./jdk8.tar.gz $JAVA_DIR/ COPY ./docker-demo.jar /tmp/app.jar # 安装JDK RUN cd $JAVA_DIR \\ \u0026amp;\u0026amp; tar -xf ./jdk8.tar.gz \\ \u0026amp;\u0026amp; mv ./jdk1.8.0_144 ./java8 # 配置环境变量 ENV JAVA_HOME=$JAVA_DIR/java8 ENV PATH=$PATH:$JAVA_HOME/bin # 暴露端口 EXPOSE 8090 # 入口，java项目的启动命令 ENTRYPOINT java -jar /tmp/app.jar Copied! 步骤5：进入docker-demo\n将准备好的docker-demo上传到虚拟机任意目录，然后进入docker-demo目录下\n步骤6：运行命令：\n1 docker build -t javaweb:1.0 . Copied! 最后访问 http://192.168.150.101:8090/hello/count，其中的ip改成你的虚拟机ip\n3.3.2.基于java8构建Java项目 虽然我们可以基于Ubuntu基础镜像，添加任意自己需要的安装包，构建镜像，但是却比较麻烦。所以大多数情况下，我们都可以在一些安装了部分软件的基础镜像上做改造。\n例如，构建java项目的镜像，可以在已经准备了JDK的基础镜像基础上构建。\n需求：基于java:8-alpine镜像，将一个Java项目构建为镜像\n实现思路如下：\n① 新建一个空的目录，然后在目录中新建一个文件，命名为Dockerfile\n② 拷贝课前资料提供的docker-demo.jar到这个目录中\n③ 编写Dockerfile文件：\na ）基于java:8-alpine作为基础镜像\nb ）将app.jar拷贝到镜像中\nc ）暴露端口\nd ）编写入口ENTRYPOINT\n内容如下：\n1 2 3 4 FROM java:8-alpine COPY ./app.jar /tmp/app.jar EXPOSE 8090 ENTRYPOINT java -jar /tmp/app.jar Copied! ④ 使用docker build命令构建镜像\n⑤ 使用docker run创建容器并运行\n3.4.小结 小结：\nDockerfile的本质是一个文件，通过指令描述镜像的构建过程\nDockerfile的第一行必须是FROM，从一个基础镜像来构建\n基础镜像可以是基本操作系统，如Ubuntu。也可以是其他人制作好的镜像，例如：java:8-alpine\n4.Docker-Compose Docker Compose可以基于Compose文件帮我们快速的部署分布式应用，而无需手动一个个创建和运行容器！\n4.1.初识DockerCompose Compose文件是一个文本文件，通过指令定义集群中的每个容器如何运行。格式如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 version: \u0026#34;3.8\u0026#34; services: mysql: image: mysql:5.7.25 environment: MYSQL_ROOT_PASSWORD: 123 volumes: - \u0026#34;/tmp/mysql/data:/var/lib/mysql\u0026#34; - \u0026#34;/tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf\u0026#34; web: build: . ports: - \u0026#34;8090:8090\u0026#34; Copied! 上面的Compose文件就描述一个项目，其中包含两个容器：\nmysql：一个基于mysql:5.7.25镜像构建的容器，并且挂载了两个目录 web：一个基于docker build临时构建的镜像容器，映射端口时8090 DockerCompose的详细语法参考官网：https://docs.docker.com/compose/compose-file/\n其实DockerCompose文件可以看做是将多个docker run命令写到一个文件，只是语法稍有差异。\n4.2.安装DockerCompose 参考课前资料\n4.3.部署微服务集群 需求：将之前学习的cloud-demo微服务集群利用DockerCompose部署\n实现思路：\n① 查看课前资料提供的cloud-demo文件夹，里面已经编写好了docker-compose文件\n② 修改自己的cloud-demo项目，将数据库、nacos地址都命名为docker-compose中的服务名\n③ 使用maven打包工具，将项目中的每个微服务都打包为app.jar\n④ 将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录中\n⑤ 将cloud-demo上传至虚拟机，利用 docker-compose up -d 来部署\n4.3.1.compose文件 查看课前资料提供的cloud-demo文件夹，里面已经编写好了docker-compose文件，而且每个微服务都准备了一个独立的目录：\n内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 version: \u0026#34;3.2\u0026#34; services: nacos: image: nacos/nacos-server environment: MODE: standalone ports: - \u0026#34;8848:8848\u0026#34; mysql: image: mysql:5.7.25 environment: MYSQL_ROOT_PASSWORD: 123 volumes: - \u0026#34;$PWD/mysql/data:/var/lib/mysql\u0026#34; - \u0026#34;$PWD/mysql/conf:/etc/mysql/conf.d/\u0026#34; userservice: build: ./user-service orderservice: build: ./order-service gateway: build: ./gateway ports: - \u0026#34;10010:10010\u0026#34; Copied! 可以看到，其中包含5个service服务：\nnacos：作为注册中心和配置中心 image: nacos/nacos-server： 基于nacos/nacos-server镜像构建 environment：环境变量 MODE: standalone：单点模式启动 ports：端口映射，这里暴露了8848端口 mysql：数据库 image: mysql:5.7.25：镜像版本是mysql:5.7.25 environment：环境变量 MYSQL_ROOT_PASSWORD: 123：设置数据库root账户的密码为123 volumes：数据卷挂载，这里挂载了mysql的data、conf目录，其中有我提前准备好的数据 userservice、orderservice、gateway：都是基于Dockerfile临时构建的 查看mysql目录，可以看到其中已经准备好了cloud_order、cloud_user表：\n查看微服务目录，可以看到都包含Dockerfile文件：\n内容如下：\n1 2 3 FROM java:8-alpine COPY ./app.jar /tmp/app.jar ENTRYPOINT java -jar /tmp/app.jar Copied! 4.3.2.修改微服务配置 因为微服务将来要部署为docker容器，而容器之间互联不是通过IP地址，而是通过容器名。这里我们将order-service、user-service、gateway服务的mysql、nacos地址都修改为基于容器名的访问。\n如下所示：\n1 2 3 4 5 6 7 8 9 10 11 spring: datasource: url: jdbc:mysql://mysql:3306/cloud_order?useSSL=false username: root password: 123 driver-class-name: com.mysql.jdbc.Driver application: name: orderservice cloud: nacos: server-addr: nacos:8848 # nacos服务地址 Copied! 4.3.3.打包 接下来需要将我们的每个微服务都打包。因为之前查看到Dockerfile中的jar包名称都是app.jar，因此我们的每个微服务都需要用这个名称。\n可以通过修改pom.xml中的打包名称来实现，每个微服务都需要修改：\n1 2 3 4 5 6 7 8 9 10 \u0026lt;build\u0026gt; \u0026lt;!-- 服务打包的最终名称 --\u0026gt; \u0026lt;finalName\u0026gt;app\u0026lt;/finalName\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; Copied! 打包后：\n4.3.4.拷贝jar包到部署目录 编译打包好的app.jar文件，需要放到Dockerfile的同级目录中。注意：每个微服务的app.jar放到与服务名称对应的目录，别搞错了。\nuser-service：\norder-service：\ngateway：\n4.3.5.部署 最后，我们需要将文件整个cloud-demo文件夹上传到虚拟机中，理由DockerCompose部署。\n上传到任意目录：\n部署：\n进入cloud-demo目录，然后运行下面的命令：\n1 docker-compose up -d Copied! 5.Docker镜像仓库 5.1.搭建私有镜像仓库 参考课前资料《CentOS7安装Docker.md》\n5.2.推送、拉取镜像 推送镜像到私有镜像服务必须先tag，步骤如下：\n① 重新tag本地镜像，名称前缀为私有仓库的地址：192.168.150.101:8080/\n1 docker tag nginx:latest 192.168.150.101:8080/nginx:1.0 Copied! ② 推送镜像\n1 docker push 192.168.150.101:8080/nginx:1.0 Copied! ③ 拉取镜像\n1 docker pull 192.168.150.101:8080/nginx:1.0 Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/83491673/","title":"3.Docker实用篇"},{"content":" RabbitMQ 1.初识MQ 1.1.同步和异步通讯 微服务间通讯有同步和异步两种方式：\n同步通讯：就像打电话，需要实时响应。\n异步通讯：就像发邮件，不需要马上回复。\n两种方式各有优劣，打电话可以立即得到响应，但是你却不能跟多个人同时通话。发送邮件可以同时与多个人收发邮件，但是往往响应会有延迟。\n1.1.1.同步通讯 我们之前学习的Feign调用就属于同步方式，虽然调用可以实时得到结果，但存在下面的问题：\n总结：\n同步调用的优点：\n时效性较强，可以立即得到结果 同步调用的问题：\n耦合度高 性能和吞吐能力下降 有额外的资源消耗 有级联失败问题 1.1.2.异步通讯 异步调用则可以避免上述问题：\n我们以购买商品为例，用户支付后需要调用订单服务完成订单状态修改，调用物流服务，从仓库分配响应的库存并准备发货。\n在事件模式中，支付服务是事件发布者（publisher），在支付完成后只需要发布一个支付成功的事件（event），事件中带上订单id。\n订单服务和物流服务是事件订阅者（Consumer），订阅支付成功的事件，监听到事件后完成自己业务即可。\n为了解除事件发布者与订阅者之间的耦合，两者并不是直接通信，而是有一个中间人（Broker）。发布者发布事件到Broker，不关心谁来订阅事件。订阅者从Broker订阅事件，不关心谁发来的消息。\nBroker 是一个像数据总线一样的东西，所有的服务要接收数据和发送数据都发到这个总线上，这个总线就像协议一样，让服务间的通讯变得标准和可控。\n好处：\n吞吐量提升：无需等待订阅者处理完成，响应更快速\n故障隔离：服务没有直接调用，不存在级联失败问题\n调用间没有阻塞，不会造成无效的资源占用\n耦合度极低，每个服务都可以灵活插拔，可替换\n流量削峰：不管发布事件的流量波动多大，都由Broker接收，订阅者可以按照自己的速度去处理事件\n缺点：\n架构复杂了，业务没有明显的流程线，不好管理 需要依赖于Broker的可靠、安全、性能 好在现在开源软件或云平台上 Broker 的软件是非常成熟的，比较常见的一种就是我们今天要学习的MQ技术。\n1.2.技术对比： MQ，中文是消息队列（MessageQueue），字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。\n比较常见的MQ实现：\nActiveMQ RabbitMQ RocketMQ Kafka 几种常见MQ的对比：\nRabbitMQ ActiveMQ RocketMQ Kafka 公司/社区 Rabbit Apache 阿里 Apache 开发语言 Erlang Java Java Scala\u0026amp;Java 协议支持 AMQP，XMPP，SMTP，STOMP OpenWire,STOMP，REST,XMPP,AMQP 自定义协议 自定义协议 可用性 高 一般 高 高 单机吞吐量 一般 差 高 非常高 消息延迟 微秒级 毫秒级 毫秒级 毫秒以内 消息可靠性 高 一般 高 一般 追求可用性：Kafka、 RocketMQ 、RabbitMQ\n追求可靠性：RabbitMQ、RocketMQ\n追求吞吐能力：RocketMQ、Kafka\n追求消息低延迟：RabbitMQ、Kafka\n2.快速入门 2.1.安装RabbitMQ 安装RabbitMQ，参考课前资料：\nMQ的基本结构：\nRabbitMQ中的一些角色：\npublisher：生产者 consumer：消费者 exchange个：交换机，负责消息路由 queue：队列，存储消息 virtualHost：虚拟主机，隔离不同租户的exchange、queue、消息的隔离 2.2.RabbitMQ消息模型 RabbitMQ官方提供了5个不同的Demo示例，对应了不同的消息模型：\n2.3.导入Demo工程 课前资料提供了一个Demo工程，mq-demo:\n导入后可以看到结构如下：\n包括三部分：\nmq-demo：父工程，管理项目依赖 publisher：消息的发送者 consumer：消息的消费者 2.4.入门案例 简单队列模式的模型图：\n官方的HelloWorld是基于最基础的消息队列模型来实现的，只包括三个角色：\npublisher：消息发布者，将消息发送到队列queue queue：消息队列，负责接受并缓存消息 consumer：订阅队列，处理队列中的消息 2.4.1.publisher实现 思路：\n建立连接 创建Channel 声明队列 发送消息 关闭连接和channel 代码实现：\n1 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 37 38 39 40 41 42 package cn.itcast.mq.helloworld; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import org.junit.Test; import java.io.IOException; import java.util.concurrent.TimeoutException; public class PublisherTest { @Test public void testSendMessage() throws IOException, TimeoutException { // 1.建立连接 ConnectionFactory factory = new ConnectionFactory(); // 1.1.设置连接参数，分别是：主机名、端口号、vhost、用户名、密码 factory.setHost(\u0026#34;192.168.150.101\u0026#34;); factory.setPort(5672); factory.setVirtualHost(\u0026#34;/\u0026#34;); factory.setUsername(\u0026#34;itcast\u0026#34;); factory.setPassword(\u0026#34;123321\u0026#34;); // 1.2.建立连接 Connection connection = factory.newConnection(); // 2.创建通道Channel Channel channel = connection.createChannel(); // 3.创建队列 String queueName = \u0026#34;simple.queue\u0026#34;; channel.queueDeclare(queueName, false, false, false, null); // 4.发送消息 String message = \u0026#34;hello, rabbitmq!\u0026#34;; channel.basicPublish(\u0026#34;\u0026#34;, queueName, null, message.getBytes()); System.out.println(\u0026#34;发送消息成功：【\u0026#34; + message + \u0026#34;】\u0026#34;); // 5.关闭通道和连接 channel.close(); connection.close(); } } Copied! 2.4.2.consumer实现 代码思路：\n建立连接 创建Channel 声明队列 订阅消息 代码实现：\n1 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 37 38 39 40 41 package cn.itcast.mq.helloworld; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class ConsumerTest { public static void main(String[] args) throws IOException, TimeoutException { // 1.建立连接 ConnectionFactory factory = new ConnectionFactory(); // 1.1.设置连接参数，分别是：主机名、端口号、vhost、用户名、密码 factory.setHost(\u0026#34;192.168.150.101\u0026#34;); factory.setPort(5672); factory.setVirtualHost(\u0026#34;/\u0026#34;); factory.setUsername(\u0026#34;itcast\u0026#34;); factory.setPassword(\u0026#34;123321\u0026#34;); // 1.2.建立连接 Connection connection = factory.newConnection(); // 2.创建通道Channel Channel channel = connection.createChannel(); // 3.创建队列 String queueName = \u0026#34;simple.queue\u0026#34;; channel.queueDeclare(queueName, false, false, false, null); // 4.订阅消息 channel.basicConsume(queueName, true, new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { // 5.处理消息 String message = new String(body); System.out.println(\u0026#34;接收到消息：【\u0026#34; + message + \u0026#34;】\u0026#34;); } }); System.out.println(\u0026#34;等待接收消息。。。。\u0026#34;); } } Copied! 2.5.总结 基本消息队列的消息发送流程：\n建立connection\n创建channel\n利用channel声明队列\n利用channel向队列发送消息\n基本消息队列的消息接收流程：\n建立connection\n创建channel\n利用channel声明队列\n定义consumer的消费行为handleDelivery()\n利用channel将消费者与队列绑定\n3.SpringAMQP SpringAMQP是基于RabbitMQ封装的一套模板，并且还利用SpringBoot对其实现了自动装配，使用起来非常方便。\nSpringAmqp的官方地址：https://spring.io/projects/spring-amqp\nSpringAMQP提供了三个功能：\n自动声明队列、交换机及其绑定关系 基于注解的监听器模式，异步接收消息 封装了RabbitTemplate工具，用于发送消息 3.1.Basic Queue 简单队列模型 在父工程mq-demo中引入依赖\n1 2 3 4 5 \u0026lt;!--AMQP依赖，包含RabbitMQ--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-amqp\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 3.1.1.消息发送 首先配置MQ地址，在publisher服务的application.yml中添加配置：\n1 2 3 4 5 6 7 spring: rabbitmq: host: 192.168.150.101 # 主机名 port: 5672 # 端口 virtual-host: / # 虚拟主机 username: itcast # 用户名 password: 123321 # 密码 Copied! 然后在publisher服务中编写测试类SpringAmqpTest，并利用RabbitTemplate实现消息发送：\n1 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 package cn.itcast.mq.spring; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSimpleQueue() { // 队列名称 String queueName = \u0026#34;simple.queue\u0026#34;; // 消息 String message = \u0026#34;hello, spring amqp!\u0026#34;; // 发送消息 rabbitTemplate.convertAndSend(queueName, message); } } Copied! 3.1.2.消息接收 首先配置MQ地址，在consumer服务的application.yml中添加配置：\n1 2 3 4 5 6 7 spring: rabbitmq: host: 192.168.150.101 # 主机名 port: 5672 # 端口 virtual-host: / # 虚拟主机 username: itcast # 用户名 password: 123321 # 密码 Copied! 然后在consumer服务的cn.itcast.mq.listener包中新建一个类SpringRabbitListener，代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 package cn.itcast.mq.listener; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class SpringRabbitListener { @RabbitListener(queues = \u0026#34;simple.queue\u0026#34;) public void listenSimpleQueueMessage(String msg) throws InterruptedException { System.out.println(\u0026#34;spring 消费者接收到消息：【\u0026#34; + msg + \u0026#34;】\u0026#34;); } } Copied! 3.1.3.测试 启动consumer服务，然后在publisher服务中运行测试代码，发送MQ消息\n3.2.WorkQueue Work queues，也被称为（Task queues），任务模型。简单来说就是让多个消费者绑定到一个队列，共同消费队列中的消息。\n当消息处理比较耗时的时候，可能生产消息的速度会远远大于消息的消费速度。长此以往，消息就会堆积越来越多，无法及时处理。\n此时就可以使用work 模型，多个消费者共同处理消息处理，速度就能大大提高了。\n3.2.1.消息发送 这次我们循环发送，模拟大量消息堆积现象。\n在publisher服务中的SpringAmqpTest类中添加一个测试方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /** * workQueue * 向队列中不停发送消息，模拟消息堆积。 */ @Test public void testWorkQueue() throws InterruptedException { // 队列名称 String queueName = \u0026#34;simple.queue\u0026#34;; // 消息 String message = \u0026#34;hello, message_\u0026#34;; for (int i = 0; i \u0026lt; 50; i++) { // 发送消息 rabbitTemplate.convertAndSend(queueName, message + i); Thread.sleep(20); } } Copied! 3.2.2.消息接收 要模拟多个消费者绑定同一个队列，我们在consumer服务的SpringRabbitListener中添加2个新的方法：\n1 2 3 4 5 6 7 8 9 10 11 @RabbitListener(queues = \u0026#34;simple.queue\u0026#34;) public void listenWorkQueue1(String msg) throws InterruptedException { System.out.println(\u0026#34;消费者1接收到消息：【\u0026#34; + msg + \u0026#34;】\u0026#34; + LocalTime.now()); Thread.sleep(20); } @RabbitListener(queues = \u0026#34;simple.queue\u0026#34;) public void listenWorkQueue2(String msg) throws InterruptedException { System.err.println(\u0026#34;消费者2........接收到消息：【\u0026#34; + msg + \u0026#34;】\u0026#34; + LocalTime.now()); Thread.sleep(200); } Copied! 注意到这个消费者sleep了1000秒，模拟任务耗时。\n3.2.3.测试 启动ConsumerApplication后，在执行publisher服务中刚刚编写的发送测试方法testWorkQueue。\n可以看到消费者1很快完成了自己的25条消息。消费者2却在缓慢的处理自己的25条消息。\n也就是说消息是平均分配给每个消费者，并没有考虑到消费者的处理能力。这样显然是有问题的。\n3.2.4.能者多劳 在spring中有一个简单的配置，可以解决这个问题。我们修改consumer服务的application.yml文件，添加配置：\n1 2 3 4 5 spring: rabbitmq: listener: simple: prefetch: 1 # 每次只能获取一条消息，处理完成才能获取下一个消息 Copied! 3.2.5.总结 Work模型的使用：\n多个消费者绑定到一个队列，同一条消息只会被一个消费者处理 通过设置prefetch来控制消费者预取的消息数量 3.3.发布/订阅 发布订阅的模型如图：\n可以看到，在订阅模型中，多了一个exchange角色，而且过程略有变化：\nPublisher：生产者，也就是要发送消息的程序，但是不再发送到队列中，而是发给X（交换机） Exchange：交换机，图中的X。一方面，接收生产者发送的消息。另一方面，知道如何处理消息，例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作，取决于Exchange的类型。Exchange有以下3种类型： Fanout：广播，将消息交给所有绑定到交换机的队列 Direct：定向，把消息交给符合指定routing key 的队列 Topic：通配符，把消息交给符合routing pattern（路由模式） 的队列 Consumer：消费者，与以前一样，订阅队列，没有变化 Queue：消息队列也与以前一样，接收消息、缓存消息。 Exchange（交换机）只负责转发消息，不具备存储消息的能力，因此如果没有任何队列与Exchange绑定，或者没有符合路由规则的队列，那么消息会丢失！\n3.4.Fanout Fanout，英文翻译是扇出，我觉得在MQ中叫广播更合适。\n在广播模式下，消息发送流程是这样的：\n1） 可以有多个队列 2） 每个队列都要绑定到Exchange（交换机） 3） 生产者发送的消息，只能发送到交换机，交换机来决定要发给哪个队列，生产者无法决定 4） 交换机把消息发送给绑定过的所有队列 5） 订阅队列的消费者都能拿到消息 我们的计划是这样的：\n创建一个交换机 itcast.fanout，类型是Fanout 创建两个队列fanout.queue1和fanout.queue2，绑定到交换机itcast.fanout 3.4.1.声明队列和交换机 Spring提供了一个接口Exchange，来表示所有不同类型的交换机：\n在consumer中创建一个类，声明队列和交换机：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package cn.itcast.mq.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.FanoutExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class FanoutConfig { /** * 声明交换机 * @return Fanout类型交换机 */ @Bean public FanoutExchange fanoutExchange(){ return new FanoutExchange(\u0026#34;itcast.fanout\u0026#34;); } /** * 第1个队列 */ @Bean public Queue fanoutQueue1(){ return new Queue(\u0026#34;fanout.queue1\u0026#34;); } /** * 绑定队列和交换机 */ @Bean public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange); } /** * 第2个队列 */ @Bean public Queue fanoutQueue2(){ return new Queue(\u0026#34;fanout.queue2\u0026#34;); } /** * 绑定队列和交换机 */ @Bean public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange); } } Copied! 3.4.2.消息发送 在publisher服务的SpringAmqpTest类中添加测试方法：\n1 2 3 4 5 6 7 8 @Test public void testFanoutExchange() { // 队列名称 String exchangeName = \u0026#34;itcast.fanout\u0026#34;; // 消息 String message = \u0026#34;hello, everyone!\u0026#34;; rabbitTemplate.convertAndSend(exchangeName, \u0026#34;\u0026#34;, message); } Copied! 3.4.3.消息接收 在consumer服务的SpringRabbitListener中添加两个方法，作为消费者：\n1 2 3 4 5 6 7 8 9 @RabbitListener(queues = \u0026#34;fanout.queue1\u0026#34;) public void listenFanoutQueue1(String msg) { System.out.println(\u0026#34;消费者1接收到Fanout消息：【\u0026#34; + msg + \u0026#34;】\u0026#34;); } @RabbitListener(queues = \u0026#34;fanout.queue2\u0026#34;) public void listenFanoutQueue2(String msg) { System.out.println(\u0026#34;消费者2接收到Fanout消息：【\u0026#34; + msg + \u0026#34;】\u0026#34;); } Copied! 3.4.4.总结 交换机的作用是什么？\n接收publisher发送的消息 将消息按照规则路由到与之绑定的队列 不能缓存消息，路由失败，消息丢失 FanoutExchange的会将消息路由到每个绑定的队列 声明队列、交换机、绑定关系的Bean是什么？\nQueue FanoutExchange Binding 3.5.Direct 在Fanout模式中，一条消息，会被所有订阅的队列都消费。但是，在某些场景下，我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。\n在Direct模型下：\n队列与交换机的绑定，不能是任意绑定了，而是要指定一个RoutingKey（路由key） 消息的发送方在 向 Exchange发送消息时，也必须指定消息的 RoutingKey。 Exchange不再把消息交给每一个绑定的队列，而是根据消息的Routing Key进行判断，只有队列的Routingkey与消息的 Routing key完全一致，才会接收到消息 案例需求如下：\n利用@RabbitListener声明Exchange、Queue、RoutingKey\n在consumer服务中，编写两个消费者方法，分别监听direct.queue1和direct.queue2\n在publisher中编写测试方法，向itcast. direct发送消息\n3.5.1.基于注解声明队列和交换机 基于@Bean的方式声明队列和交换机比较麻烦，Spring还提供了基于注解方式来声明。\n在consumer的SpringRabbitListener中添加两个消费者，同时基于注解来声明队列和交换机：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @RabbitListener(bindings = @QueueBinding( value = @Queue(name = \u0026#34;direct.queue1\u0026#34;), exchange = @Exchange(name = \u0026#34;itcast.direct\u0026#34;, type = ExchangeTypes.DIRECT), key = {\u0026#34;red\u0026#34;, \u0026#34;blue\u0026#34;} )) public void listenDirectQueue1(String msg){ System.out.println(\u0026#34;消费者接收到direct.queue1的消息：【\u0026#34; + msg + \u0026#34;】\u0026#34;); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = \u0026#34;direct.queue2\u0026#34;), exchange = @Exchange(name = \u0026#34;itcast.direct\u0026#34;, type = ExchangeTypes.DIRECT), key = {\u0026#34;red\u0026#34;, \u0026#34;yellow\u0026#34;} )) public void listenDirectQueue2(String msg){ System.out.println(\u0026#34;消费者接收到direct.queue2的消息：【\u0026#34; + msg + \u0026#34;】\u0026#34;); } Copied! 3.5.2.消息发送 在publisher服务的SpringAmqpTest类中添加测试方法：\n1 2 3 4 5 6 7 8 9 @Test public void testSendDirectExchange() { // 交换机名称 String exchangeName = \u0026#34;itcast.direct\u0026#34;; // 消息 String message = \u0026#34;红色警报！日本乱排核废水，导致海洋生物变异，惊现哥斯拉！\u0026#34;; // 发送消息 rabbitTemplate.convertAndSend(exchangeName, \u0026#34;red\u0026#34;, message); } Copied! 3.5.3.总结 描述下Direct交换机与Fanout交换机的差异？\nFanout交换机将消息路由给每一个与之绑定的队列 Direct交换机根据RoutingKey判断路由给哪个队列 如果多个队列具有相同的RoutingKey，则与Fanout功能类似 基于@RabbitListener注解声明队列和交换机有哪些常见注解？\n@Queue @Exchange 3.6.Topic 3.6.1.说明 Topic类型的Exchange与Direct相比，都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符！\nRoutingkey 一般都是有一个或多个单词组成，多个单词之间以”.”分割，例如： item.insert\n通配符规则：\n#：匹配一个或多个词\n*：匹配不多不少恰好1个词\n举例：\nitem.#：能够匹配item.spu.insert 或者 item.spu\nitem.*：只能匹配item.spu\n​\n图示：\n解释：\nQueue1：绑定的是china.# ，因此凡是以 china.开头的routing key 都会被匹配到。包括china.news和china.weather Queue2：绑定的是#.news ，因此凡是以 .news结尾的 routing key 都会被匹配。包括china.news和japan.news 案例需求：\n实现思路如下：\n并利用@RabbitListener声明Exchange、Queue、RoutingKey\n在consumer服务中，编写两个消费者方法，分别监听topic.queue1和topic.queue2\n在publisher中编写测试方法，向itcast. topic发送消息\n3.6.2.消息发送 在publisher服务的SpringAmqpTest类中添加测试方法：\n1 2 3 4 5 6 7 8 9 10 11 12 /** * topicExchange */ @Test public void testSendTopicExchange() { // 交换机名称 String exchangeName = \u0026#34;itcast.topic\u0026#34;; // 消息 String message = \u0026#34;喜报！孙悟空大战哥斯拉，胜!\u0026#34;; // 发送消息 rabbitTemplate.convertAndSend(exchangeName, \u0026#34;china.news\u0026#34;, message); } Copied! 3.6.3.消息接收 在consumer服务的SpringRabbitListener中添加方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @RabbitListener(bindings = @QueueBinding( value = @Queue(name = \u0026#34;topic.queue1\u0026#34;), exchange = @Exchange(name = \u0026#34;itcast.topic\u0026#34;, type = ExchangeTypes.TOPIC), key = \u0026#34;china.#\u0026#34; )) public void listenTopicQueue1(String msg){ System.out.println(\u0026#34;消费者接收到topic.queue1的消息：【\u0026#34; + msg + \u0026#34;】\u0026#34;); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = \u0026#34;topic.queue2\u0026#34;), exchange = @Exchange(name = \u0026#34;itcast.topic\u0026#34;, type = ExchangeTypes.TOPIC), key = \u0026#34;#.news\u0026#34; )) public void listenTopicQueue2(String msg){ System.out.println(\u0026#34;消费者接收到topic.queue2的消息：【\u0026#34; + msg + \u0026#34;】\u0026#34;); } Copied! 3.6.4.总结 描述下Direct交换机与Topic交换机的差异？\nTopic交换机接收的消息RoutingKey必须是多个单词，以 **.** 分割 Topic交换机与队列绑定时的bindingKey可以指定通配符 #：代表0个或多个词 *：代表1个词 3.7.消息转换器 之前说过，Spring会把你发送的消息序列化为字节发送给MQ，接收消息的时候，还会把字节反序列化为Java对象。\n只不过，默认情况下Spring采用的序列化方式是JDK序列化。众所周知，JDK序列化存在下列问题：\n数据体积过大 有安全漏洞 可读性差 我们来测试一下。\n3.7.1.测试默认转换器 我们修改消息发送的代码，发送一个Map对象：\n1 2 3 4 5 6 7 8 9 @Test public void testSendMap() throws InterruptedException { // 准备消息 Map\u0026lt;String,Object\u0026gt; msg = new HashMap\u0026lt;\u0026gt;(); msg.put(\u0026#34;name\u0026#34;, \u0026#34;Jack\u0026#34;); msg.put(\u0026#34;age\u0026#34;, 21); // 发送消息 rabbitTemplate.convertAndSend(\u0026#34;simple.queue\u0026#34;,\u0026#34;\u0026#34;, msg); } Copied! 停止consumer服务\n发送消息后查看控制台：\n3.7.2.配置JSON转换器 显然，JDK序列化方式并不合适。我们希望消息体的体积更小、可读性更高，因此可以使用JSON方式来做序列化和反序列化。\n在publisher和consumer两个服务中都引入依赖：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.dataformat\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-dataformat-xml\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.9.10\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 配置消息转换器。\n在启动类中添加一个Bean即可：\n1 2 3 4 @Bean public MessageConverter jsonMessageConverter(){ return new Jackson2JsonMessageConverter(); } Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/95611894/","title":"4.RabbitMQ"},{"content":" 分布式搜索引擎01 \u0026ndash; elasticsearch基础\n0.学习目标 1.初识elasticsearch 1.1.了解ES 1.1.1.elasticsearch的作用 elasticsearch是一款非常强大的开源搜索引擎，具备非常多强大功能，可以帮助我们从海量数据中快速找到需要的内容\n例如：\n在GitHub搜索代码\n在电商网站搜索商品\n在百度搜索答案\n在打车软件搜索附近的车\n1.1.2.ELK技术栈 elasticsearch结合kibana、Logstash、Beats，也就是elastic stack（ELK）。被广泛应用在日志数据分析、实时监控等领域：\n而elasticsearch是elastic stack的核心，负责存储、搜索、分析数据。\n1.1.3.elasticsearch和lucene elasticsearch底层是基于lucene来实现的。\nLucene是一个Java语言的搜索引擎类库，是Apache公司的顶级项目，由DougCutting于1999年研发。官网地址：https://lucene.apache.org/ 。\nelasticsearch的发展历史：\n2004年Shay Banon基于Lucene开发了Compass 2010年Shay Banon 重写了Compass，取名为Elasticsearch。 1.1.4.为什么不是其他搜索技术？ 目前比较知名的搜索引擎技术排名：\n虽然在早期，Apache Solr是最主要的搜索引擎技术，但随着发展elasticsearch已经渐渐超越了Solr，独占鳌头：\n1.1.5.总结 什么是elasticsearch？\n一个开源的分布式搜索引擎，可以用来实现搜索、日志统计、分析、系统监控等功能 什么是elastic stack（ELK）？\n是以elasticsearch为核心的技术栈，包括beats、Logstash、kibana、elasticsearch 什么是Lucene？\n是Apache的开源搜索引擎类库，提供了搜索引擎的核心API 1.2.倒排索引 倒排索引的概念是基于MySQL这样的正向索引而言的。\n1.2.1.正向索引 那么什么是正向索引呢？例如给下表（tb_goods）中的id创建索引：\n如果是根据id查询，那么直接走索引，查询速度非常快。\n但如果是基于title做模糊查询，只能是逐行扫描数据，流程如下：\n1）用户搜索数据，条件是title符合\u0026quot;%手机%\u0026quot;\n2）逐行获取数据，比如id为1的数据\n3）判断数据中的title是否符合用户搜索条件\n4）如果符合则放入结果集，不符合则丢弃。回到步骤1\n逐行扫描，也就是全表扫描，随着数据量增加，其查询效率也会越来越低。当数据量达到数百万时，就是一场灾难。\n1.2.2.倒排索引 倒排索引中有两个非常重要的概念：\n文档（Document）：用来搜索的数据，其中的每一条数据就是一个文档。例如一个网页、一个商品信息 词条（Term）：对文档数据或用户搜索数据，利用某种算法分词，得到的具备含义的词语就是词条。例如：我是中国人，就可以分为：我、是、中国人、中国、国人这样的几个词条 创建倒排索引是对正向索引的一种特殊处理，流程如下：\n将每一个文档的数据利用算法分词，得到一个个词条 创建表，每行数据包括词条、词条所在文档id、位置等信息 因为词条唯一性，可以给词条创建索引，例如hash表结构索引 如图：\n倒排索引的搜索流程如下（以搜索\u0026quot;华为手机\u0026quot;为例）：\n1）用户输入条件\u0026quot;华为手机\u0026quot;进行搜索。\n2）对用户输入内容分词，得到词条：华为、手机。\n3）拿着词条在倒排索引中查找，可以得到包含词条的文档id：1、2、3。\n4）拿着文档id到正向索引中查找具体文档。\n如图：\n虽然要先查询倒排索引，再查询倒排索引，但是无论是词条、还是文档id都建立了索引，查询速度非常快！无需全表扫描。\n1.2.3.正向和倒排 那么为什么一个叫做正向索引，一个叫做倒排索引呢？\n正向索引是最传统的，根据id索引的方式。但根据词条查询时，必须先逐条获取每个文档，然后判断文档中是否包含所需要的词条，是根据文档找词条的过程。\n而倒排索引则相反，是先找到用户要搜索的词条，根据词条得到保护词条的文档的id，然后根据id获取文档。是根据词条找文档的过程。\n是不是恰好反过来了？\n那么两者方式的优缺点是什么呢？\n正向索引：\n优点： 可以给多个字段创建索引 根据索引字段搜索、排序速度非常快 缺点： 根据非索引字段，或者索引字段中的部分词条查找时，只能全表扫描。 倒排索引：\n优点： 根据词条搜索、模糊搜索时，速度非常快 缺点： 只能给词条创建索引，而不是字段 无法根据字段做排序 1.3.es的一些概念 elasticsearch中有很多独有的概念，与mysql中略有差别，但也有相似之处。\n1.3.1.文档和字段 elasticsearch是面向**文档（Document）**存储的，可以是数据库中的一条商品数据，一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中：\n而Json文档中往往包含很多的字段（Field），类似于数据库中的列。\n1.3.2.索引和映射 索引（Index），就是相同类型的文档的集合。\n例如：\n所有用户文档，就可以组织在一起，称为用户的索引； 所有商品的文档，可以组织在一起，称为商品的索引； 所有订单的文档，可以组织在一起，称为订单的索引； 因此，我们可以把索引当做是数据库中的表。\n数据库的表会有约束信息，用来定义表的结构、字段的名称、类型等信息。因此，索引库中就有映射（mapping），是索引中文档的字段约束信息，类似表的结构约束。\n1.3.3.mysql与elasticsearch 我们统一的把mysql与elasticsearch的概念做一下对比：\nMySQL Elasticsearch 说明 Table Index 索引(index)，就是文档的集合，类似数据库的表(table) Row Document 文档（Document），就是一条条的数据，类似数据库中的行（Row），文档都是JSON格式 Column Field 字段（Field），就是JSON文档中的字段，类似数据库中的列（Column） Schema Mapping Mapping（映射）是索引中文档的约束，例如字段类型约束。类似数据库的表结构（Schema） SQL DSL DSL是elasticsearch提供的JSON风格的请求语句，用来操作elasticsearch，实现CRUD 是不是说，我们学习了elasticsearch就不再需要mysql了呢？\n并不是如此，两者各自有自己的擅长支出：\nMysql：擅长事务类型操作，可以确保数据的安全和一致性\nElasticsearch：擅长海量数据的搜索、分析、计算\n因此在企业中，往往是两者结合使用：\n对安全性要求较高的写操作，使用mysql实现 对查询性能要求较高的搜索需求，使用elasticsearch实现 两者再基于某种方式，实现数据的同步，保证一致性 1.4.安装es、kibana 1.4.1.安装 参考课前资料：\n1.4.2.分词器 参考课前资料：\n1.4.3.总结 分词器的作用是什么？\n创建倒排索引时对文档分词 用户搜索时，对输入的内容分词 IK分词器有几种模式？\nik_smart：智能切分，粗粒度 ik_max_word：最细切分，细粒度 IK分词器如何拓展词条？如何停用词条？\n利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典 在词典中添加拓展词条或者停用词条 2.索引库操作 索引库就类似数据库表，mapping映射就类似表的结构。\n我们要向es中存储数据，必须先创建“库”和“表”。\n2.1.mapping映射属性 mapping是对索引库中文档的约束，常见的mapping属性包括：\ntype：字段数据类型，常见的简单类型有： 字符串：text（可分词的文本）、keyword（精确值，例如：品牌、国家、ip地址） 数值：long、integer、short、byte、double、float、 布尔：boolean 日期：date 对象：object index：是否创建索引，默认为true analyzer：使用哪种分词器 properties：该字段的子字段 例如下面的json文档：\n1 2 3 4 5 6 7 8 9 10 11 12 { \u0026#34;age\u0026#34;: 21, \u0026#34;weight\u0026#34;: 52.1, \u0026#34;isMarried\u0026#34;: false, \u0026#34;info\u0026#34;: \u0026#34;黑马程序员Java讲师\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;zy@itcast.cn\u0026#34;, \u0026#34;score\u0026#34;: [99.1, 99.5, 98.9], \u0026#34;name\u0026#34;: { \u0026#34;firstName\u0026#34;: \u0026#34;云\u0026#34;, \u0026#34;lastName\u0026#34;: \u0026#34;赵\u0026#34; } } Copied! 对应的每个字段映射（mapping）：\nage：类型为 integer；参与搜索，因此需要index为true；无需分词器 weight：类型为float；参与搜索，因此需要index为true；无需分词器 isMarried：类型为boolean；参与搜索，因此需要index为true；无需分词器 info：类型为字符串，需要分词，因此是text；参与搜索，因此需要index为true；分词器可以用ik_smart email：类型为字符串，但是不需要分词，因此是keyword；不参与搜索，因此需要index为false；无需分词器 score：虽然是数组，但是我们只看元素的类型，类型为float；参与搜索，因此需要index为true；无需分词器 name：类型为object，需要定义多个子属性 name.firstName；类型为字符串，但是不需要分词，因此是keyword；参与搜索，因此需要index为true；无需分词器 name.lastName；类型为字符串，但是不需要分词，因此是keyword；参与搜索，因此需要index为true；无需分词器 2.2.索引库的CRUD 这里我们统一使用Kibana编写DSL的方式来演示。\n2.2.1.创建索引库和映射 基本语法： 请求方式：PUT 请求路径：/索引库名，可以自定义 请求参数：mapping映射 格式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 PUT /索引库名称 { \u0026#34;mappings\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;字段名\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;ik_smart\u0026#34; }, \u0026#34;字段名2\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;index\u0026#34;: \u0026#34;false\u0026#34; }, \u0026#34;字段名3\u0026#34;:{ \u0026#34;properties\u0026#34;: { \u0026#34;子字段\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; } } }, // ...略 } } } Copied! 示例： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 PUT /heima { \u0026#34;mappings\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;info\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;ik_smart\u0026#34; }, \u0026#34;email\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;index\u0026#34;: \u0026#34;falsae\u0026#34; }, \u0026#34;name\u0026#34;:{ \u0026#34;properties\u0026#34;: { \u0026#34;firstName\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; } } }, // ... 略 } } } Copied! 2.2.2.查询索引库 基本语法：\n请求方式：GET\n请求路径：/索引库名\n请求参数：无\n格式：\n1 GET /索引库名 Copied! 示例：\n2.2.3.修改索引库 倒排索引结构虽然不复杂，但是一旦数据结构改变（比如改变了分词器），就需要重新创建倒排索引，这简直是灾难。因此索引库一旦创建，无法修改mapping。\n虽然无法修改mapping中已有的字段，但是却允许添加新的字段到mapping中，因为不会对倒排索引产生影响。\n语法说明：\n1 2 3 4 5 6 7 8 PUT /索引库名/_mapping { \u0026#34;properties\u0026#34;: { \u0026#34;新字段名\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; } } } Copied! 示例：\n2.2.4.删除索引库 语法：\n请求方式：DELETE\n请求路径：/索引库名\n请求参数：无\n格式：\n1 DELETE /索引库名 Copied! 在kibana中测试：\n2.2.5.总结 索引库操作有哪些？\n创建索引库：PUT /索引库名 查询索引库：GET /索引库名 删除索引库：DELETE /索引库名 添加字段：PUT /索引库名/_mapping 3.文档操作 3.1.新增文档 语法：\n1 2 3 4 5 6 7 8 9 10 POST /索引库名/_doc/文档id { \u0026#34;字段1\u0026#34;: \u0026#34;值1\u0026#34;, \u0026#34;字段2\u0026#34;: \u0026#34;值2\u0026#34;, \u0026#34;字段3\u0026#34;: { \u0026#34;子属性1\u0026#34;: \u0026#34;值3\u0026#34;, \u0026#34;子属性2\u0026#34;: \u0026#34;值4\u0026#34; }, // ... } Copied! 示例：\n1 2 3 4 5 6 7 8 9 POST /heima/_doc/1 { \u0026#34;info\u0026#34;: \u0026#34;黑马程序员Java讲师\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;zy@itcast.cn\u0026#34;, \u0026#34;name\u0026#34;: { \u0026#34;firstName\u0026#34;: \u0026#34;云\u0026#34;, \u0026#34;lastName\u0026#34;: \u0026#34;赵\u0026#34; } } Copied! 响应：\n3.2.查询文档 根据rest风格，新增是post，查询应该是get，不过查询一般都需要条件，这里我们把文档id带上。\n语法：\n1 GET /{索引库名称}/_doc/{id} Copied! 通过kibana查看数据：\n1 GET /heima/_doc/1 Copied! 查看结果：\n3.3.删除文档 删除使用DELETE请求，同样，需要根据id进行删除：\n语法：\n1 DELETE /{索引库名}/_doc/id值 Copied! 示例：\n1 2 # 根据id删除数据 DELETE /heima/_doc/1 Copied! 结果：\n3.4.修改文档 修改有两种方式：\n全量修改：直接覆盖原来的文档 增量修改：修改文档中的部分字段 3.4.1.全量修改 全量修改是覆盖原来的文档，其本质是：\n根据指定的id删除文档 新增一个相同id的文档 注意：如果根据id删除时，id不存在，第二步的新增也会执行，也就从修改变成了新增操作了。\n语法：\n1 2 3 4 5 6 PUT /{索引库名}/_doc/文档id { \u0026#34;字段1\u0026#34;: \u0026#34;值1\u0026#34;, \u0026#34;字段2\u0026#34;: \u0026#34;值2\u0026#34;, // ... 略 } Copied! 示例：\n1 2 3 4 5 6 7 8 9 PUT /heima/_doc/1 { \u0026#34;info\u0026#34;: \u0026#34;黑马程序员高级Java讲师\u0026#34;, \u0026#34;email\u0026#34;: \u0026#34;zy@itcast.cn\u0026#34;, \u0026#34;name\u0026#34;: { \u0026#34;firstName\u0026#34;: \u0026#34;云\u0026#34;, \u0026#34;lastName\u0026#34;: \u0026#34;赵\u0026#34; } } Copied! 3.4.2.增量修改 增量修改是只修改指定id匹配的文档中的部分字段。\n语法：\n1 2 3 4 5 6 POST /{索引库名}/_update/文档id { \u0026#34;doc\u0026#34;: { \u0026#34;字段名\u0026#34;: \u0026#34;新的值\u0026#34;, } } Copied! 示例：\n1 2 3 4 5 6 POST /heima/_update/1 { \u0026#34;doc\u0026#34;: { \u0026#34;email\u0026#34;: \u0026#34;ZhaoYun@itcast.cn\u0026#34; } } Copied! 3.5.总结 文档操作有哪些？\n创建文档：POST /{索引库名}/_doc/文档id { json文档 } 查询文档：GET /{索引库名}/_doc/文档id 删除文档：DELETE /{索引库名}/_doc/文档id 修改文档： 全量修改：PUT /{索引库名}/_doc/文档id { json文档 } 增量修改：POST /{索引库名}/_update/文档id { \u0026ldquo;doc\u0026rdquo;: {字段}} 4.RestAPI ES官方提供了各种不同语言的客户端，用来操作ES。这些客户端的本质就是组装DSL语句，通过http请求发送给ES。官方文档地址：https://www.elastic.co/guide/en/elasticsearch/client/index.html\n其中的Java Rest Client又包括两种：\nJava Low Level Rest Client Java High Level Rest Client 我们学习的是Java HighLevel Rest Client客户端API\n4.0.导入Demo工程 4.0.1.导入数据 首先导入课前资料提供的数据库数据：\n数据结构如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CREATE TABLE `tb_hotel` ( `id` bigint(20) NOT NULL COMMENT \u0026#39;酒店id\u0026#39;, `name` varchar(255) NOT NULL COMMENT \u0026#39;酒店名称；例：7天酒店\u0026#39;, `address` varchar(255) NOT NULL COMMENT \u0026#39;酒店地址；例：航头路\u0026#39;, `price` int(10) NOT NULL COMMENT \u0026#39;酒店价格；例：329\u0026#39;, `score` int(2) NOT NULL COMMENT \u0026#39;酒店评分；例：45，就是4.5分\u0026#39;, `brand` varchar(32) NOT NULL COMMENT \u0026#39;酒店品牌；例：如家\u0026#39;, `city` varchar(32) NOT NULL COMMENT \u0026#39;所在城市；例：上海\u0026#39;, `star_name` varchar(16) DEFAULT NULL COMMENT \u0026#39;酒店星级，从低到高分别是：1星到5星，1钻到5钻\u0026#39;, `business` varchar(255) DEFAULT NULL COMMENT \u0026#39;商圈；例：虹桥\u0026#39;, `latitude` varchar(32) NOT NULL COMMENT \u0026#39;纬度；例：31.2497\u0026#39;, `longitude` varchar(32) NOT NULL COMMENT \u0026#39;经度；例：120.3925\u0026#39;, `pic` varchar(255) DEFAULT NULL COMMENT \u0026#39;酒店图片；例:/img/1.jpg\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; Copied! 4.0.2.导入项目 然后导入课前资料提供的项目:\n项目结构如图：\n4.0.3.mapping映射分析 创建索引库，最关键的是mapping映射，而mapping映射要考虑的信息包括：\n字段名 字段数据类型 是否参与搜索 是否需要分词 如果分词，分词器是什么？ 其中：\n字段名、字段数据类型，可以参考数据表结构的名称和类型 是否参与搜索要分析业务来判断，例如图片地址，就无需参与搜索 是否分词呢要看内容，内容如果是一个整体就无需分词，反之则要分词 分词器，我们可以统一使用ik_max_word 来看下酒店数据的索引库结构:\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 PUT /hotel { \u0026#34;mappings\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;id\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;name\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;copy_to\u0026#34;: \u0026#34;all\u0026#34; }, \u0026#34;address\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;index\u0026#34;: false }, \u0026#34;price\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; }, \u0026#34;score\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; }, \u0026#34;brand\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;copy_to\u0026#34;: \u0026#34;all\u0026#34; }, \u0026#34;city\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;copy_to\u0026#34;: \u0026#34;all\u0026#34; }, \u0026#34;starName\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;business\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;location\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;geo_point\u0026#34; }, \u0026#34;pic\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;index\u0026#34;: false }, \u0026#34;all\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34; } } } } Copied! 几个特殊字段说明：\nlocation：地理坐标，里面包含精度、纬度 all：一个组合字段，其目的是将多字段的值 利用copy_to合并，提供给用户搜索 地理坐标说明：\ncopy_to说明：\n4.0.4.初始化RestClient 在elasticsearch提供的API中，与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中，必须先完成这个对象的初始化，建立与elasticsearch的连接。\n分为三步：\n1）引入es的RestHighLevelClient依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.elasticsearch.client\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;elasticsearch-rest-high-level-client\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 2）因为SpringBoot默认的ES版本是7.6.2，所以我们需要覆盖默认的ES版本：\n1 2 3 4 \u0026lt;properties\u0026gt; \u0026lt;java.version\u0026gt;1.8\u0026lt;/java.version\u0026gt; \u0026lt;elasticsearch.version\u0026gt;7.12.1\u0026lt;/elasticsearch.version\u0026gt; \u0026lt;/properties\u0026gt; Copied! 3）初始化RestHighLevelClient：\n初始化的代码如下：\n1 2 3 RestHighLevelClient client = new RestHighLevelClient(RestClient.builder( HttpHost.create(\u0026#34;http://192.168.150.101:9200\u0026#34;) )); Copied! 这里为了单元测试方便，我们创建一个测试类HotelIndexTest，然后将初始化的代码编写在@BeforeEach方法中：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package cn.itcast.hotel; import org.apache.http.HttpHost; import org.elasticsearch.client.RestHighLevelClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; public class HotelIndexTest { private RestHighLevelClient client; @BeforeEach void setUp() { this.client = new RestHighLevelClient(RestClient.builder( HttpHost.create(\u0026#34;http://192.168.150.101:9200\u0026#34;) )); } @AfterEach void tearDown() throws IOException { this.client.close(); } } Copied! 4.1.创建索引库 4.1.1.代码解读 创建索引库的API如下：\n代码分为三步：\n1）创建Request对象。因为是创建索引库的操作，因此Request是CreateIndexRequest。 2）添加请求参数，其实就是DSL的JSON参数部分。因为json字符串很长，这里是定义了静态字符串常量MAPPING_TEMPLATE，让代码看起来更加优雅。 3）发送请求，client.indices()方法的返回值是IndicesClient类型，封装了所有与索引库操作有关的方法。 4.1.2.完整示例 在hotel-demo的cn.itcast.hotel.constants包下，创建一个类，定义mapping映射的JSON字符串常量：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package cn.itcast.hotel.constants; public class HotelConstants { public static final String MAPPING_TEMPLATE = \u0026#34;{\\n\u0026#34; + \u0026#34; \\\u0026#34;mappings\\\u0026#34;: {\\n\u0026#34; + \u0026#34; \\\u0026#34;properties\\\u0026#34;: {\\n\u0026#34; + \u0026#34; \\\u0026#34;id\\\u0026#34;: {\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: \\\u0026#34;keyword\\\u0026#34;\\n\u0026#34; + \u0026#34; },\\n\u0026#34; + \u0026#34; \\\u0026#34;name\\\u0026#34;:{\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: \\\u0026#34;text\\\u0026#34;,\\n\u0026#34; + \u0026#34; \\\u0026#34;analyzer\\\u0026#34;: \\\u0026#34;ik_max_word\\\u0026#34;,\\n\u0026#34; + \u0026#34; \\\u0026#34;copy_to\\\u0026#34;: \\\u0026#34;all\\\u0026#34;\\n\u0026#34; + \u0026#34; },\\n\u0026#34; + \u0026#34; \\\u0026#34;address\\\u0026#34;:{\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: \\\u0026#34;keyword\\\u0026#34;,\\n\u0026#34; + \u0026#34; \\\u0026#34;index\\\u0026#34;: false\\n\u0026#34; + \u0026#34; },\\n\u0026#34; + \u0026#34; \\\u0026#34;price\\\u0026#34;:{\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: \\\u0026#34;integer\\\u0026#34;\\n\u0026#34; + \u0026#34; },\\n\u0026#34; + \u0026#34; \\\u0026#34;score\\\u0026#34;:{\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: \\\u0026#34;integer\\\u0026#34;\\n\u0026#34; + \u0026#34; },\\n\u0026#34; + \u0026#34; \\\u0026#34;brand\\\u0026#34;:{\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: \\\u0026#34;keyword\\\u0026#34;,\\n\u0026#34; + \u0026#34; \\\u0026#34;copy_to\\\u0026#34;: \\\u0026#34;all\\\u0026#34;\\n\u0026#34; + \u0026#34; },\\n\u0026#34; + \u0026#34; \\\u0026#34;city\\\u0026#34;:{\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: \\\u0026#34;keyword\\\u0026#34;,\\n\u0026#34; + \u0026#34; \\\u0026#34;copy_to\\\u0026#34;: \\\u0026#34;all\\\u0026#34;\\n\u0026#34; + \u0026#34; },\\n\u0026#34; + \u0026#34; \\\u0026#34;starName\\\u0026#34;:{\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: \\\u0026#34;keyword\\\u0026#34;\\n\u0026#34; + \u0026#34; },\\n\u0026#34; + \u0026#34; \\\u0026#34;business\\\u0026#34;:{\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: \\\u0026#34;keyword\\\u0026#34;\\n\u0026#34; + \u0026#34; },\\n\u0026#34; + \u0026#34; \\\u0026#34;location\\\u0026#34;:{\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: \\\u0026#34;geo_point\\\u0026#34;\\n\u0026#34; + \u0026#34; },\\n\u0026#34; + \u0026#34; \\\u0026#34;pic\\\u0026#34;:{\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: \\\u0026#34;keyword\\\u0026#34;,\\n\u0026#34; + \u0026#34; \\\u0026#34;index\\\u0026#34;: false\\n\u0026#34; + \u0026#34; },\\n\u0026#34; + \u0026#34; \\\u0026#34;all\\\u0026#34;:{\\n\u0026#34; + \u0026#34; \\\u0026#34;type\\\u0026#34;: \\\u0026#34;text\\\u0026#34;,\\n\u0026#34; + \u0026#34; \\\u0026#34;analyzer\\\u0026#34;: \\\u0026#34;ik_max_word\\\u0026#34;\\n\u0026#34; + \u0026#34; }\\n\u0026#34; + \u0026#34; }\\n\u0026#34; + \u0026#34; }\\n\u0026#34; + \u0026#34;}\u0026#34;; } Copied! 在hotel-demo中的HotelIndexTest测试类中，编写单元测试，实现创建索引：\n1 2 3 4 5 6 7 8 9 @Test void createHotelIndex() throws IOException { // 1.创建Request对象 CreateIndexRequest request = new CreateIndexRequest(\u0026#34;hotel\u0026#34;); // 2.准备请求的参数：DSL语句 request.source(MAPPING_TEMPLATE, XContentType.JSON); // 3.发送请求 client.indices().create(request, RequestOptions.DEFAULT); } Copied! 4.2.删除索引库 删除索引库的DSL语句非常简单：\n1 DELETE /hotel Copied! 与创建索引库相比：\n请求方式从PUT变为DELTE 请求路径不变 无请求参数 所以代码的差异，注意体现在Request对象上。依然是三步走：\n1）创建Request对象。这次是DeleteIndexRequest对象 2）准备参数。这里是无参 3）发送请求。改用delete方法 在hotel-demo中的HotelIndexTest测试类中，编写单元测试，实现删除索引：\n1 2 3 4 5 6 7 @Test void testDeleteHotelIndex() throws IOException { // 1.创建Request对象 DeleteIndexRequest request = new DeleteIndexRequest(\u0026#34;hotel\u0026#34;); // 2.发送请求 client.indices().delete(request, RequestOptions.DEFAULT); } Copied! 4.3.判断索引库是否存在 判断索引库是否存在，本质就是查询，对应的DSL是：\n1 GET /hotel Copied! 因此与删除的Java代码流程是类似的。依然是三步走：\n1）创建Request对象。这次是GetIndexRequest对象 2）准备参数。这里是无参 3）发送请求。改用exists方法 1 2 3 4 5 6 7 8 9 @Test void testExistsHotelIndex() throws IOException { // 1.创建Request对象 GetIndexRequest request = new GetIndexRequest(\u0026#34;hotel\u0026#34;); // 2.发送请求 boolean exists = client.indices().exists(request, RequestOptions.DEFAULT); // 3.输出 System.err.println(exists ? \u0026#34;索引库已经存在！\u0026#34; : \u0026#34;索引库不存在！\u0026#34;); } Copied! 4.4.总结 JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。\n索引库操作的基本步骤：\n初始化RestHighLevelClient 创建XxxIndexRequest。XXX是Create、Get、Delete 准备DSL（ Create时需要，其它是无参） 发送请求。调用RestHighLevelClient#indices().xxx()方法，xxx是create、exists、delete 5.RestClient操作文档 为了与索引库操作分离，我们再次参加一个测试类，做两件事情：\n初始化RestHighLevelClient 我们的酒店数据在数据库，需要利用IHotelService去查询，所以注入这个接口 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 package cn.itcast.hotel; import cn.itcast.hotel.pojo.Hotel; import cn.itcast.hotel.service.IHotelService; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.io.IOException; import java.util.List; @SpringBootTest public class HotelDocumentTest { @Autowired private IHotelService hotelService; private RestHighLevelClient client; @BeforeEach void setUp() { this.client = new RestHighLevelClient(RestClient.builder( HttpHost.create(\u0026#34;http://192.168.150.101:9200\u0026#34;) )); } @AfterEach void tearDown() throws IOException { this.client.close(); } } Copied! 5.1.新增文档 我们要将数据库的酒店数据查询出来，写入elasticsearch中。\n5.1.1.索引库实体类 数据库查询后的结果是一个Hotel类型的对象。结构如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data @TableName(\u0026#34;tb_hotel\u0026#34;) public class Hotel { @TableId(type = IdType.INPUT) private Long id; private String name; private String address; private Integer price; private Integer score; private String brand; private String city; private String starName; private String business; private String longitude; private String latitude; private String pic; } Copied! 与我们的索引库结构存在差异：\nlongitude和latitude需要合并为location 因此，我们需要定义一个新的类型，与索引库结构吻合：\n1 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 package cn.itcast.hotel.pojo; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class HotelDoc { private Long id; private String name; private String address; private Integer price; private Integer score; private String brand; private String city; private String starName; private String business; private String location; private String pic; public HotelDoc(Hotel hotel) { this.id = hotel.getId(); this.name = hotel.getName(); this.address = hotel.getAddress(); this.price = hotel.getPrice(); this.score = hotel.getScore(); this.brand = hotel.getBrand(); this.city = hotel.getCity(); this.starName = hotel.getStarName(); this.business = hotel.getBusiness(); this.location = hotel.getLatitude() + \u0026#34;, \u0026#34; + hotel.getLongitude(); this.pic = hotel.getPic(); } } Copied! 5.1.2.语法说明 新增文档的DSL语句如下：\n1 2 3 4 5 POST /{索引库名}/_doc/1 { \u0026#34;name\u0026#34;: \u0026#34;Jack\u0026#34;, \u0026#34;age\u0026#34;: 21 } Copied! 对应的java代码如图：\n可以看到与创建索引库类似，同样是三步走：\n1）创建Request对象 2）准备请求参数，也就是DSL中的JSON文档 3）发送请求 变化的地方在于，这里直接使用client.xxx()的API，不再需要client.indices()了。\n5.1.3.完整代码 我们导入酒店数据，基本流程一致，但是需要考虑几点变化：\n酒店数据来自于数据库，我们需要先查询出来，得到hotel对象 hotel对象需要转为HotelDoc对象 HotelDoc需要序列化为json格式 因此，代码整体步骤如下：\n1）根据id查询酒店数据Hotel 2）将Hotel封装为HotelDoc 3）将HotelDoc序列化为JSON 4）创建IndexRequest，指定索引库名和id 5）准备请求参数，也就是JSON文档 6）发送请求 在hotel-demo的HotelDocumentTest测试类中，编写单元测试：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test void testAddDocument() throws IOException { // 1.根据id查询酒店数据 Hotel hotel = hotelService.getById(61083L); // 2.转换为文档类型 HotelDoc hotelDoc = new HotelDoc(hotel); // 3.将HotelDoc转json String json = JSON.toJSONString(hotelDoc); // 1.准备Request对象 IndexRequest request = new IndexRequest(\u0026#34;hotel\u0026#34;).id(hotelDoc.getId().toString()); // 2.准备Json文档 request.source(json, XContentType.JSON); // 3.发送请求 client.index(request, RequestOptions.DEFAULT); } Copied! 5.2.查询文档 5.2.1.语法说明 查询的DSL语句如下：\n1 GET /hotel/_doc/{id} Copied! 非常简单，因此代码大概分两步：\n准备Request对象 发送请求 不过查询的目的是得到结果，解析为HotelDoc，因此难点是结果的解析。完整代码如下：\n可以看到，结果是一个JSON，其中文档放在一个_source属性中，因此解析就是拿到_source，反序列化为Java对象即可。\n与之前类似，也是三步走：\n1）准备Request对象。这次是查询，所以是GetRequest 2）发送请求，得到结果。因为是查询，这里调用client.get()方法 3）解析结果，就是对JSON做反序列化 5.2.2.完整代码 在hotel-demo的HotelDocumentTest测试类中，编写单元测试：\n1 2 3 4 5 6 7 8 9 10 11 12 @Test void testGetDocumentById() throws IOException { // 1.准备Request GetRequest request = new GetRequest(\u0026#34;hotel\u0026#34;, \u0026#34;61082\u0026#34;); // 2.发送请求，得到响应 GetResponse response = client.get(request, RequestOptions.DEFAULT); // 3.解析响应结果 String json = response.getSourceAsString(); HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); System.out.println(hotelDoc); } Copied! 5.3.删除文档 删除的DSL为是这样的：\n1 DELETE /hotel/_doc/{id} Copied! 与查询相比，仅仅是请求方式从DELETE变成GET，可以想象Java代码应该依然是三步走：\n1）准备Request对象，因为是删除，这次是DeleteRequest对象。要指定索引库名和id 2）准备参数，无参 3）发送请求。因为是删除，所以是client.delete()方法 在hotel-demo的HotelDocumentTest测试类中，编写单元测试：\n1 2 3 4 5 6 7 @Test void testDeleteDocument() throws IOException { // 1.准备Request DeleteRequest request = new DeleteRequest(\u0026#34;hotel\u0026#34;, \u0026#34;61083\u0026#34;); // 2.发送请求 client.delete(request, RequestOptions.DEFAULT); } Copied! 5.4.修改文档 5.4.1.语法说明 修改我们讲过两种方式：\n全量修改：本质是先根据id删除，再新增 增量修改：修改文档中的指定字段值 在RestClient的API中，全量修改与新增的API完全一致，判断依据是ID：\n如果新增时，ID已经存在，则修改 如果新增时，ID不存在，则新增 这里不再赘述，我们主要关注增量修改。\n代码示例如图：\n与之前类似，也是三步走：\n1）准备Request对象。这次是修改，所以是UpdateRequest 2）准备参数。也就是JSON文档，里面包含要修改的字段 3）更新文档。这里调用client.update()方法 5.4.2.完整代码 在hotel-demo的HotelDocumentTest测试类中，编写单元测试：\n1 2 3 4 5 6 7 8 9 10 11 12 @Test void testUpdateDocument() throws IOException { // 1.准备Request UpdateRequest request = new UpdateRequest(\u0026#34;hotel\u0026#34;, \u0026#34;61083\u0026#34;); // 2.准备请求参数 request.doc( \u0026#34;price\u0026#34;, \u0026#34;952\u0026#34;, \u0026#34;starName\u0026#34;, \u0026#34;四钻\u0026#34; ); // 3.发送请求 client.update(request, RequestOptions.DEFAULT); } Copied! 5.5.批量导入文档 案例需求：利用BulkRequest批量将数据库数据导入到索引库中。\n步骤如下：\n利用mybatis-plus查询酒店数据\n将查询到的酒店数据（Hotel）转换为文档类型数据（HotelDoc）\n利用JavaRestClient中的BulkRequest批处理，实现批量新增文档\n5.5.1.语法说明 批量处理BulkRequest，其本质就是将多个普通的CRUD请求组合在一起发送。\n其中提供了一个add方法，用来添加其他请求：\n可以看到，能添加的请求包括：\nIndexRequest，也就是新增 UpdateRequest，也就是修改 DeleteRequest，也就是删除 因此Bulk中添加了多个IndexRequest，就是批量新增功能了。示例：\n其实还是三步走：\n1）创建Request对象。这里是BulkRequest 2）准备参数。批处理的参数，就是其它Request对象，这里就是多个IndexRequest 3）发起请求。这里是批处理，调用的方法为client.bulk()方法 我们在导入酒店数据时，将上述代码改造成for循环处理即可。\n5.5.2.完整代码 在hotel-demo的HotelDocumentTest测试类中，编写单元测试：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test void testBulkRequest() throws IOException { // 批量查询酒店数据 List\u0026lt;Hotel\u0026gt; hotels = hotelService.list(); // 1.创建Request BulkRequest request = new BulkRequest(); // 2.准备参数，添加多个新增的Request for (Hotel hotel : hotels) { // 2.1.转换为文档类型HotelDoc HotelDoc hotelDoc = new HotelDoc(hotel); // 2.2.创建新增文档的Request对象 request.add(new IndexRequest(\u0026#34;hotel\u0026#34;) .id(hotelDoc.getId().toString()) .source(JSON.toJSONString(hotelDoc), XContentType.JSON)); } // 3.发送请求 client.bulk(request, RequestOptions.DEFAULT); } Copied! 5.6.小结 文档操作的基本步骤：\n初始化RestHighLevelClient 创建XxxRequest。XXX是Index、Get、Update、Delete、Bulk 准备参数（Index、Update、Bulk时需要） 发送请求。调用RestHighLevelClient#.xxx()方法，xxx是index、get、update、delete、bulk 解析结果（Get时需要） ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/5117b451/","title":"5.分布式搜索引擎01"},{"content":" 分布式搜索引擎02 在昨天的学习中，我们已经导入了大量数据到elasticsearch中，实现了elasticsearch的数据存储功能。但elasticsearch最擅长的还是搜索和数据分析。\n所以今天，我们研究下elasticsearch的数据搜索功能。我们会分别使用DSL和RestClient实现搜索。\n0.学习目标 1.DSL查询文档 elasticsearch的查询依然是基于JSON风格的DSL来实现的。\n1.1.DSL查询分类 Elasticsearch提供了基于JSON的DSL（Domain Specific Language ）来定义查询。常见的查询类型包括：\n查询所有：查询出所有数据，一般测试用。例如：match_all\n全文检索（full text）查询：利用分词器对用户输入内容分词，然后去倒排索引库中匹配。例如：\nmatch_query multi_match_query 精确查询：根据精确词条值查找数据，一般是查找keyword、数值、日期、boolean等类型字段。例如：\nids range term 地理（geo）查询：根据经纬度查询。例如：\ngeo_distance geo_bounding_box 复合（compound）查询：复合查询可以将上述各种查询条件组合起来，合并查询条件。例如：\nbool function_score 查询的语法基本一致：\n1 2 3 4 5 6 7 8 GET /indexName/_search { \u0026#34;query\u0026#34;: { \u0026#34;查询类型\u0026#34;: { \u0026#34;查询条件\u0026#34;: \u0026#34;条件值\u0026#34; } } } Copied! 我们以查询所有为例，其中：\n查询类型为match_all 没有查询条件 1 2 3 4 5 6 7 8 // 查询所有 GET /indexName/_search { \u0026#34;query\u0026#34;: { \u0026#34;match_all\u0026#34;: { } } } Copied! 其它查询无非就是查询类型、查询条件的变化。\n1.2.全文检索查询 1.2.1.使用场景 全文检索查询的基本流程如下：\n对用户搜索的内容做分词，得到词条 根据词条去倒排索引库中匹配，得到文档id 根据文档id找到文档，返回给用户 比较常用的场景包括：\n商城的输入框搜索 百度输入框搜索 例如京东：\n因为是拿着词条去匹配，因此参与搜索的字段也必须是可分词的text类型的字段。\n1.2.2.基本语法 常见的全文检索查询包括：\nmatch查询：单字段查询 multi_match查询：多字段查询，任意一个字段符合条件就算符合查询条件 match查询语法如下：\n1 2 3 4 5 6 7 8 GET /indexName/_search { \u0026#34;query\u0026#34;: { \u0026#34;match\u0026#34;: { \u0026#34;FIELD\u0026#34;: \u0026#34;TEXT\u0026#34; } } } Copied! mulit_match语法如下：\n1 2 3 4 5 6 7 8 9 GET /indexName/_search { \u0026#34;query\u0026#34;: { \u0026#34;multi_match\u0026#34;: { \u0026#34;query\u0026#34;: \u0026#34;TEXT\u0026#34;, \u0026#34;fields\u0026#34;: [\u0026#34;FIELD1\u0026#34;, \u0026#34; FIELD12\u0026#34;] } } } Copied! 1.2.3.示例 match查询示例：\nmulti_match查询示例：\n可以看到，两种查询结果是一样的，为什么？\n因为我们将brand、name、business值都利用copy_to复制到了all字段中。因此你根据三个字段搜索，和根据all字段搜索效果当然一样了。\n但是，搜索字段越多，对查询性能影响越大，因此建议采用copy_to，然后单字段查询的方式。\n1.2.4.总结 match和multi_match的区别是什么？\nmatch：根据一个字段查询 multi_match：根据多个字段查询，参与查询字段越多，查询性能越差 1.3.精准查询 精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有：\nterm：根据词条精确值查询 range：根据值的范围查询 1.3.1.term查询 因为精确查询的字段搜是不分词的字段，因此查询的条件也必须是不分词的词条。查询时，用户输入的内容跟自动值完全匹配时才认为符合条件。如果用户输入的内容过多，反而搜索不到数据。\n语法说明：\n1 2 3 4 5 6 7 8 9 10 11 // term查询 GET /indexName/_search { \u0026#34;query\u0026#34;: { \u0026#34;term\u0026#34;: { \u0026#34;FIELD\u0026#34;: { \u0026#34;value\u0026#34;: \u0026#34;VALUE\u0026#34; } } } } Copied! 示例：\n当我搜索的是精确词条时，能正确查询出结果：\n但是，当我搜索的内容不是词条，而是多个词语形成的短语时，反而搜索不到：\n1.3.2.range查询 范围查询，一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。\n基本语法：\n1 2 3 4 5 6 7 8 9 10 11 12 // range查询 GET /indexName/_search { \u0026#34;query\u0026#34;: { \u0026#34;range\u0026#34;: { \u0026#34;FIELD\u0026#34;: { \u0026#34;gte\u0026#34;: 10, // 这里的gte代表大于等于，gt则代表大于 \u0026#34;lte\u0026#34;: 20 // lte代表小于等于，lt则代表小于 } } } } Copied! 示例：\n1.3.3.总结 精确查询常见的有哪些？\nterm查询：根据词条精确匹配，一般搜索keyword类型、数值类型、布尔类型、日期类型字段 range查询：根据数值范围查询，可以是数值、日期的范围 1.4.地理坐标查询 所谓的地理坐标查询，其实就是根据经纬度查询，官方文档：https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html\n常见的使用场景包括：\n携程：搜索我附近的酒店 滴滴：搜索我附近的出租车 微信：搜索我附近的人 附近的酒店：\n附近的车：\n1.4.1.矩形范围查询 矩形范围查询，也就是geo_bounding_box查询，查询坐标落在某个矩形范围的所有文档：\n查询时，需要指定矩形的左上、右下两个点的坐标，然后画出一个矩形，落在该矩形内的都是符合条件的点。\n语法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // geo_bounding_box查询 GET /indexName/_search { \u0026#34;query\u0026#34;: { \u0026#34;geo_bounding_box\u0026#34;: { \u0026#34;FIELD\u0026#34;: { \u0026#34;top_left\u0026#34;: { // 左上点 \u0026#34;lat\u0026#34;: 31.1, \u0026#34;lon\u0026#34;: 121.5 }, \u0026#34;bottom_right\u0026#34;: { // 右下点 \u0026#34;lat\u0026#34;: 30.9, \u0026#34;lon\u0026#34;: 121.7 } } } } } Copied! 这种并不符合“附近的人”这样的需求，所以我们就不做了。\n1.4.2.附近查询 附近查询，也叫做距离查询（geo_distance）：查询到指定中心点小于某个距离值的所有文档。\n换句话来说，在地图上找一个点作为圆心，以指定距离为半径，画一个圆，落在圆内的坐标都算符合条件：\n语法说明：\n1 2 3 4 5 6 7 8 9 10 // geo_distance 查询 GET /indexName/_search { \u0026#34;query\u0026#34;: { \u0026#34;geo_distance\u0026#34;: { \u0026#34;distance\u0026#34;: \u0026#34;15km\u0026#34;, // 半径 \u0026#34;FIELD\u0026#34;: \u0026#34;31.21,121.5\u0026#34; // 圆心 } } } Copied! 示例：\n我们先搜索陆家嘴附近15km的酒店：\n发现共有47家酒店。\n然后把半径缩短到3公里：\n可以发现，搜索到的酒店数量减少到了5家。\n1.5.复合查询 复合（compound）查询：复合查询可以将其它简单查询组合起来，实现更复杂的搜索逻辑。常见的有两种：\nfuction score：算分函数查询，可以控制文档相关性算分，控制文档排名 bool query：布尔查询，利用逻辑关系组合多个其它的查询，实现复杂搜索 1.5.1.相关性算分 当我们利用match查询时，文档结果会根据与搜索词条的关联度打分（_score），返回结果时按照分值降序排列。\n例如，我们搜索 \u0026ldquo;虹桥如家\u0026rdquo;，结果如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [ { \u0026#34;_score\u0026#34; : 17.850193, \u0026#34;_source\u0026#34; : { \u0026#34;name\u0026#34; : \u0026#34;虹桥如家酒店真不错\u0026#34;, } }, { \u0026#34;_score\u0026#34; : 12.259849, \u0026#34;_source\u0026#34; : { \u0026#34;name\u0026#34; : \u0026#34;外滩如家酒店真不错\u0026#34;, } }, { \u0026#34;_score\u0026#34; : 11.91091, \u0026#34;_source\u0026#34; : { \u0026#34;name\u0026#34; : \u0026#34;迪士尼如家酒店真不错\u0026#34;, } } ] Copied! 在elasticsearch中，早期使用的打分算法是TF-IDF算法，公式如下：\n在后来的5.1版本升级中，elasticsearch将算法改进为BM25算法，公式如下：\nTF-IDF算法有一各缺陷，就是词条频率越高，文档得分也会越高，单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限，曲线更加平滑：\n小结：elasticsearch会根据词条和文档的相关度做打分，算法由两种：\nTF-IDF算法 BM25算法，elasticsearch5.1版本后采用的算法 1.5.2.算分函数查询 根据相关度打分是比较合理的需求，但合理的不一定是产品经理需要的。\n以百度为例，你搜索的结果中，并不是相关度越高排名越靠前，而是谁掏的钱多排名就越靠前。如图：\n要想认为控制相关性算分，就需要利用elasticsearch中的function score 查询了。\n1）语法说明 function score 查询中包含四部分内容：\n原始查询条件：query部分，基于这个条件搜索文档，并且基于BM25算法给文档打分，原始算分（query score) 过滤条件：filter部分，符合该条件的文档才会重新算分 算分函数：符合filter条件的文档要根据这个函数做运算，得到的函数算分（function score），有四种函数 weight：函数结果是常量 field_value_factor：以文档中的某个字段值作为函数结果 random_score：以随机数作为函数结果 script_score：自定义算分函数算法 运算模式：算分函数的结果、原始查询的相关性算分，两者之间的运算方式，包括： multiply：相乘 replace：用function score替换query score 其它，例如：sum、avg、max、min function score的运行流程如下：\n1）根据原始条件查询搜索文档，并且计算相关性算分，称为原始算分（query score） 2）根据过滤条件，过滤文档 3）符合过滤条件的文档，基于算分函数运算，得到函数算分（function score） 4）将原始算分（query score）和函数算分（function score）基于运算模式做运算，得到最终结果，作为相关性算分。 因此，其中的关键点是：\n过滤条件：决定哪些文档的算分被修改 算分函数：决定函数算分的算法 运算模式：决定最终算分结果 2）示例 需求：给“如家”这个品牌的酒店排名靠前一些\n翻译一下这个需求，转换为之前说的四个要点：\n原始条件：不确定，可以任意变化 过滤条件：brand = \u0026ldquo;如家\u0026rdquo; 算分函数：可以简单粗暴，直接给固定的算分结果，weight 运算模式：比如求和 因此最终的DSL语句如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 GET /hotel/_search { \u0026#34;query\u0026#34;: { \u0026#34;function_score\u0026#34;: { \u0026#34;query\u0026#34;: { .... }, // 原始查询，可以是任意条件 \u0026#34;functions\u0026#34;: [ // 算分函数 { \u0026#34;filter\u0026#34;: { // 满足的条件，品牌必须是如家 \u0026#34;term\u0026#34;: { \u0026#34;brand\u0026#34;: \u0026#34;如家\u0026#34; } }, \u0026#34;weight\u0026#34;: 2 // 算分权重为2 } ], \u0026#34;boost_mode\u0026#34;: \u0026#34;sum\u0026#34; // 加权模式，求和 } } } Copied! 测试，在未添加算分函数时，如家得分如下：\n添加了算分函数后，如家得分就提升了：\n3）小结 function score query定义的三要素是什么？\n过滤条件：哪些文档要加分 算分函数：如何计算function score 加权方式：function score 与 query score如何运算 1.5.3.布尔查询 布尔查询是一个或多个查询子句的组合，每一个子句就是一个子查询。子查询的组合方式有：\nmust：必须匹配每个子查询，类似“与” should：选择性匹配子查询，类似“或” must_not：必须不匹配，不参与算分，类似“非” filter：必须匹配，不参与算分 比如在搜索酒店时，除了关键字搜索外，我们还可能根据品牌、价格、城市等字段做过滤：\n每一个不同的字段，其查询的条件、方式都不一样，必须是多个不同的查询，而要组合这些查询，就必须用bool查询了。\n需要注意的是，搜索时，参与打分的字段越多，查询的性能也越差。因此这种多条件查询时，建议这样做：\n搜索框的关键字搜索，是全文检索查询，使用must查询，参与算分 其它过滤条件，采用filter查询。不参与算分 1）语法示例： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 GET /hotel/_search { \u0026#34;query\u0026#34;: { \u0026#34;bool\u0026#34;: { \u0026#34;must\u0026#34;: [ {\u0026#34;term\u0026#34;: {\u0026#34;city\u0026#34;: \u0026#34;上海\u0026#34; }} ], \u0026#34;should\u0026#34;: [ {\u0026#34;term\u0026#34;: {\u0026#34;brand\u0026#34;: \u0026#34;皇冠假日\u0026#34; }}, {\u0026#34;term\u0026#34;: {\u0026#34;brand\u0026#34;: \u0026#34;华美达\u0026#34; }} ], \u0026#34;must_not\u0026#34;: [ { \u0026#34;range\u0026#34;: { \u0026#34;price\u0026#34;: { \u0026#34;lte\u0026#34;: 500 } }} ], \u0026#34;filter\u0026#34;: [ { \u0026#34;range\u0026#34;: {\u0026#34;score\u0026#34;: { \u0026#34;gte\u0026#34;: 45 } }} ] } } } Copied! 2）示例 需求：搜索名字包含“如家”，价格不高于400，在坐标31.21,121.5周围10km范围内的酒店。\n分析：\n名称搜索，属于全文检索查询，应该参与算分。放到must中 价格不高于400，用range查询，属于过滤条件，不参与算分。放到must_not中 周围10km范围内，用geo_distance查询，属于过滤条件，不参与算分。放到filter中 3）小结 bool查询有几种逻辑关系？\nmust：必须匹配的条件，可以理解为“与” should：选择性匹配的条件，可以理解为“或” must_not：必须不匹配的条件，不参与打分 filter：必须匹配的条件，不参与打分 2.搜索结果处理 搜索的结果可以按照用户指定的方式去处理或展示。\n2.1.排序 elasticsearch默认是根据相关度算分（_score）来排序，但是也支持自定义方式对搜索结果排序 。可以排序字段类型有：keyword类型、数值类型、地理坐标类型、日期类型等。\n2.1.1.普通字段排序 keyword、数值、日期类型排序的语法基本一致。\n语法：\n1 2 3 4 5 6 7 8 9 10 11 GET /indexName/_search { \u0026#34;query\u0026#34;: { \u0026#34;match_all\u0026#34;: {} }, \u0026#34;sort\u0026#34;: [ { \u0026#34;FIELD\u0026#34;: \u0026#34;desc\u0026#34; // 排序字段、排序方式ASC、DESC } ] } Copied! 排序条件是一个数组，也就是可以写多个排序条件。按照声明的顺序，当第一个条件相等时，再按照第二个条件排序，以此类推\n示例：\n需求描述：酒店数据按照用户评价（score)降序排序，评价相同的按照价格(price)升序排序\n2.1.2.地理坐标排序 地理坐标排序略有不同。\n语法说明：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 GET /indexName/_search { \u0026#34;query\u0026#34;: { \u0026#34;match_all\u0026#34;: {} }, \u0026#34;sort\u0026#34;: [ { \u0026#34;_geo_distance\u0026#34; : { \u0026#34;FIELD\u0026#34; : \u0026#34;纬度，经度\u0026#34;, // 文档中geo_point类型的字段名、目标坐标点 \u0026#34;order\u0026#34; : \u0026#34;asc\u0026#34;, // 排序方式 \u0026#34;unit\u0026#34; : \u0026#34;km\u0026#34; // 排序的距离单位 } } ] } Copied! 这个查询的含义是：\n指定一个坐标，作为目标点 计算每一个文档中，指定字段（必须是geo_point类型）的坐标 到目标点的距离是多少 根据距离排序 示例：\n需求描述：实现对酒店数据按照到你的位置坐标的距离升序排序\n提示：获取你的位置的经纬度的方式：https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/\n假设我的位置是：31.034661，121.612282，寻找我周围距离最近的酒店。\n2.2.分页 elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果：\nfrom：从第几个文档开始 size：总共查询几个文档 类似于mysql中的limit ?, ?\n2.2.1.基本的分页 分页的基本语法如下：\n1 2 3 4 5 6 7 8 9 10 11 GET /hotel/_search { \u0026#34;query\u0026#34;: { \u0026#34;match_all\u0026#34;: {} }, \u0026#34;from\u0026#34;: 0, // 分页开始的位置，默认为0 \u0026#34;size\u0026#34;: 10, // 期望获取的文档总数 \u0026#34;sort\u0026#34;: [ {\u0026#34;price\u0026#34;: \u0026#34;asc\u0026#34;} ] } Copied! 2.2.2.深度分页问题 现在，我要查询990~1000的数据，查询逻辑要这么写：\n1 2 3 4 5 6 7 8 9 10 11 GET /hotel/_search { \u0026#34;query\u0026#34;: { \u0026#34;match_all\u0026#34;: {} }, \u0026#34;from\u0026#34;: 990, // 分页开始的位置，默认为0 \u0026#34;size\u0026#34;: 10, // 期望获取的文档总数 \u0026#34;sort\u0026#34;: [ {\u0026#34;price\u0026#34;: \u0026#34;asc\u0026#34;} ] } Copied! 这里是查询990开始的数据，也就是 第990~第1000条 数据。\n不过，elasticsearch内部分页时，必须先查询 0~1000条，然后截取其中的990 ~ 1000的这10条：\n查询TOP1000，如果es是单点模式，这并无太大影响。\n但是elasticsearch将来一定是集群，例如我集群有5个节点，我要查询TOP1000的数据，并不是每个节点查询200条就可以了。\n因为节点A的TOP200，在另一个节点可能排到10000名以外了。\n因此要想获取整个集群的TOP1000，必须先查询出每个节点的TOP1000，汇总结果后，重新排名，重新截取TOP1000。\n那如果我要查询9900~10000的数据呢？是不是要先查询TOP10000呢？那每个节点都要查询10000条？汇总到内存中？\n当查询分页深度较大时，汇总数据过多，对内存和CPU会产生非常大的压力，因此elasticsearch会禁止from+ size 超过10000的请求。\n针对深度分页，ES提供了两种解决方案，官方文档 ：\nsearch after：分页时需要排序，原理是从上一次的排序值开始，查询下一页数据。官方推荐使用的方式。 scroll：原理将排序后的文档id形成快照，保存在内存。官方已经不推荐使用。 2.2.3.小结 分页查询的常见实现方案以及优缺点：\nfrom + size：\n优点：支持随机翻页 缺点：深度分页问题，默认查询上限（from + size）是10000 场景：百度、京东、谷歌、淘宝这样的随机翻页搜索 after search：\n优点：没有查询上限（单次查询的size不超过10000） 缺点：只能向后逐页查询，不支持随机翻页 场景：没有随机翻页需求的搜索，例如手机向下滚动翻页 scroll：\n优点：没有查询上限（单次查询的size不超过10000） 缺点：会有额外内存消耗，并且搜索结果是非实时的 场景：海量数据的获取和迁移。从ES7.1开始不推荐，建议用 after search方案。 2.3.高亮 2.3.1.高亮原理 什么是高亮显示呢？\n我们在百度，京东搜索时，关键字会变成红色，比较醒目，这叫高亮显示：\n高亮显示的实现分为两步：\n1）给文档中的所有关键字都添加一个标签，例如\u0026lt;em\u0026gt;标签 2）页面给\u0026lt;em\u0026gt;标签编写CSS样式 2.3.2.实现高亮 高亮的语法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 GET /hotel/_search { \u0026#34;query\u0026#34;: { \u0026#34;match\u0026#34;: { \u0026#34;FIELD\u0026#34;: \u0026#34;TEXT\u0026#34; // 查询条件，高亮一定要使用全文检索查询 } }, \u0026#34;highlight\u0026#34;: { \u0026#34;fields\u0026#34;: { // 指定要高亮的字段 \u0026#34;FIELD\u0026#34;: { \u0026#34;pre_tags\u0026#34;: \u0026#34;\u0026lt;em\u0026gt;\u0026#34;, // 用来标记高亮字段的前置标签 \u0026#34;post_tags\u0026#34;: \u0026#34;\u0026lt;/em\u0026gt;\u0026#34; // 用来标记高亮字段的后置标签 } } } } Copied! 注意：\n高亮是对关键字高亮，因此搜索条件必须带有关键字，而不能是范围这样的查询。 默认情况下，高亮的字段，必须与搜索指定的字段一致，否则无法高亮 如果要对非搜索字段高亮，则需要添加一个属性：required_field_match=false 示例：\n2.4.总结 查询的DSL是一个大的JSON对象，包含下列属性：\nquery：查询条件 from和size：分页条件 sort：排序条件 highlight：高亮条件 示例：\n3.RestClient查询文档 文档的查询同样适用昨天学习的 RestHighLevelClient对象，基本步骤包括：\n1）准备Request对象 2）准备请求参数 3）发起请求 4）解析响应 3.1.快速入门 我们以match_all查询为例\n3.1.1.发起查询请求 代码解读：\n第一步，创建SearchRequest对象，指定索引库名\n第二步，利用request.source()构建DSL，DSL中可以包含查询、分页、排序、高亮等\nquery()：代表查询条件，利用QueryBuilders.matchAllQuery()构建一个match_all查询的DSL 第三步，利用client.search()发送请求，得到响应\n这里关键的API有两个，一个是request.source()，其中包含了查询、排序、分页、高亮等所有功能：\n另一个是QueryBuilders，其中包含match、term、function_score、bool等各种查询：\n3.1.2.解析响应 响应结果的解析：\nelasticsearch返回的结果是一个JSON字符串，结构包含：\nhits：命中的结果 total：总条数，其中的value是具体的总条数值 max_score：所有结果中得分最高的文档的相关性算分 hits：搜索结果的文档数组，其中的每个文档都是一个json对象 _source：文档中的原始数据，也是json对象 因此，我们解析响应结果，就是逐层解析JSON字符串，流程如下：\nSearchHits：通过response.getHits()获取，就是JSON中的最外层的hits，代表命中的结果 SearchHits#getTotalHits().value：获取总条数信息 SearchHits#getHits()：获取SearchHit数组，也就是文档数组 SearchHit#getSourceAsString()：获取文档结果中的_source，也就是原始的json文档数据 3.1.3.完整代码 完整代码如下：\n1 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 @Test void testMatchAll() throws IOException { // 1.准备Request SearchRequest request = new SearchRequest(\u0026#34;hotel\u0026#34;); // 2.准备DSL request.source() .query(QueryBuilders.matchAllQuery()); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应 handleResponse(response); } private void handleResponse(SearchResponse response) { // 4.解析响应 SearchHits searchHits = response.getHits(); // 4.1.获取总条数 long total = searchHits.getTotalHits().value; System.out.println(\u0026#34;共搜索到\u0026#34; + total + \u0026#34;条数据\u0026#34;); // 4.2.文档数组 SearchHit[] hits = searchHits.getHits(); // 4.3.遍历 for (SearchHit hit : hits) { // 获取文档source String json = hit.getSourceAsString(); // 反序列化 HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); System.out.println(\u0026#34;hotelDoc = \u0026#34; + hotelDoc); } } Copied! 3.1.4.小结 查询的基本步骤是：\n创建SearchRequest对象\n准备Request.source()，也就是DSL。\n① QueryBuilders来构建查询条件\n② 传入Request.source() 的 query() 方法\n发送请求，得到结果\n解析结果（参考JSON结果，从外到内，逐层解析）\n3.2.match查询 全文检索的match和multi_match查询与match_all的API基本一致。差别是查询条件，也就是query的部分。\n因此，Java代码上的差异主要是request.source().query()中的参数了。同样是利用QueryBuilders提供的方法：\n而结果解析代码则完全一致，可以抽取并共享。\n完整代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 @Test void testMatch() throws IOException { // 1.准备Request SearchRequest request = new SearchRequest(\u0026#34;hotel\u0026#34;); // 2.准备DSL request.source() .query(QueryBuilders.matchQuery(\u0026#34;all\u0026#34;, \u0026#34;如家\u0026#34;)); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应 handleResponse(response); } Copied! 3.3.精确查询 精确查询主要是两者：\nterm：词条精确匹配 range：范围查询 与之前的查询相比，差异同样在查询条件，其它都一样。\n查询条件构造的API如下：\n3.4.布尔查询 布尔查询是用must、must_not、filter等方式组合其它查询，代码示例如下：\n可以看到，API与其它查询的差别同样是在查询条件的构建，QueryBuilders，结果解析等其他代码完全不变。\n完整代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test void testBool() throws IOException { // 1.准备Request SearchRequest request = new SearchRequest(\u0026#34;hotel\u0026#34;); // 2.准备DSL // 2.1.准备BooleanQuery BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); // 2.2.添加term boolQuery.must(QueryBuilders.termQuery(\u0026#34;city\u0026#34;, \u0026#34;杭州\u0026#34;)); // 2.3.添加range boolQuery.filter(QueryBuilders.rangeQuery(\u0026#34;price\u0026#34;).lte(250)); request.source().query(boolQuery); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应 handleResponse(response); } Copied! 3.5.排序、分页 搜索结果的排序和分页是与query同级的参数，因此同样是使用request.source()来设置。\n对应的API如下：\n完整代码示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test void testPageAndSort() throws IOException { // 页码，每页大小 int page = 1, size = 5; // 1.准备Request SearchRequest request = new SearchRequest(\u0026#34;hotel\u0026#34;); // 2.准备DSL // 2.1.query request.source().query(QueryBuilders.matchAllQuery()); // 2.2.排序 sort request.source().sort(\u0026#34;price\u0026#34;, SortOrder.ASC); // 2.3.分页 from、size request.source().from((page - 1) * size).size(5); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应 handleResponse(response); } Copied! 3.6.高亮 高亮的代码与之前代码差异较大，有两点：\n查询的DSL：其中除了查询条件，还需要添加高亮条件，同样是与query同级。 结果解析：结果除了要解析_source文档数据，还要解析高亮结果 3.6.1.高亮请求构建 高亮请求的构建API如下：\n上述代码省略了查询条件部分，但是大家不要忘了：高亮查询必须使用全文检索查询，并且要有搜索关键字，将来才可以对关键字高亮。\n完整代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test void testHighlight() throws IOException { // 1.准备Request SearchRequest request = new SearchRequest(\u0026#34;hotel\u0026#34;); // 2.准备DSL // 2.1.query request.source().query(QueryBuilders.matchQuery(\u0026#34;all\u0026#34;, \u0026#34;如家\u0026#34;)); // 2.2.高亮 request.source().highlighter(new HighlightBuilder().field(\u0026#34;name\u0026#34;).requireFieldMatch(false)); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应 handleResponse(response); } Copied! 3.6.2.高亮结果解析 高亮的结果与查询的文档结果默认是分离的，并不在一起。\n因此解析高亮的代码需要额外处理：\n代码解读：\n第一步：从结果中获取source。hit.getSourceAsString()，这部分是非高亮结果，json字符串。还需要反序列为HotelDoc对象 第二步：获取高亮结果。hit.getHighlightFields()，返回值是一个Map，key是高亮字段名称，值是HighlightField对象，代表高亮值 第三步：从map中根据高亮字段名称，获取高亮字段值对象HighlightField 第四步：从HighlightField中获取Fragments，并且转为字符串。这部分就是真正的高亮字符串了 第五步：用高亮的结果替换HotelDoc中的非高亮结果 完整代码如下：\n1 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 private void handleResponse(SearchResponse response) { // 4.解析响应 SearchHits searchHits = response.getHits(); // 4.1.获取总条数 long total = searchHits.getTotalHits().value; System.out.println(\u0026#34;共搜索到\u0026#34; + total + \u0026#34;条数据\u0026#34;); // 4.2.文档数组 SearchHit[] hits = searchHits.getHits(); // 4.3.遍历 for (SearchHit hit : hits) { // 获取文档source String json = hit.getSourceAsString(); // 反序列化 HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); // 获取高亮结果 Map\u0026lt;String, HighlightField\u0026gt; highlightFields = hit.getHighlightFields(); if (!CollectionUtils.isEmpty(highlightFields)) { // 根据字段名获取高亮结果 HighlightField highlightField = highlightFields.get(\u0026#34;name\u0026#34;); if (highlightField != null) { // 获取高亮值 String name = highlightField.getFragments()[0].string(); // 覆盖非高亮结果 hotelDoc.setName(name); } } System.out.println(\u0026#34;hotelDoc = \u0026#34; + hotelDoc); } } Copied! 4.黑马旅游案例 下面，我们通过黑马旅游的案例来实战演练下之前学习的知识。\n我们实现四部分功能：\n酒店搜索和分页 酒店结果过滤 我周边的酒店 酒店竞价排名 启动我们提供的hotel-demo项目，其默认端口是8089，访问http://localhost:8090，就能看到项目页面了：\n4.1.酒店搜索和分页 案例需求：实现黑马旅游的酒店搜索功能，完成关键字搜索和分页\n4.1.1.需求分析 在项目的首页，有一个大大的搜索框，还有分页按钮：\n点击搜索按钮，可以看到浏览器控制台发出了请求：\n请求参数如下：\n由此可以知道，我们这个请求的信息如下：\n请求方式：POST 请求路径：/hotel/list 请求参数：JSON对象，包含4个字段： key：搜索关键字 page：页码 size：每页大小 sortBy：排序，目前暂不实现 返回值：分页查询，需要返回分页结果PageResult，包含两个属性： total：总条数 List\u0026lt;HotelDoc\u0026gt;：当前页的数据 因此，我们实现业务的流程如下：\n步骤一：定义实体类，接收请求参数的JSON对象 步骤二：编写controller，接收页面的请求 步骤三：编写业务实现，利用RestHighLevelClient实现搜索、分页 4.1.2.定义实体类 实体类有两个，一个是前端的请求参数实体，一个是服务端应该返回的响应结果实体。\n1）请求参数\n前端请求的json结构如下：\n1 2 3 4 5 6 { \u0026#34;key\u0026#34;: \u0026#34;搜索关键字\u0026#34;, \u0026#34;page\u0026#34;: 1, \u0026#34;size\u0026#34;: 3, \u0026#34;sortBy\u0026#34;: \u0026#34;default\u0026#34; } Copied! 因此，我们在cn.itcast.hotel.pojo包下定义一个实体类：\n1 2 3 4 5 6 7 8 9 10 11 package cn.itcast.hotel.pojo; import lombok.Data; @Data public class RequestParams { private String key; private Integer page; private Integer size; private String sortBy; } Copied! 2）返回值\n分页查询，需要返回分页结果PageResult，包含两个属性：\ntotal：总条数 List\u0026lt;HotelDoc\u0026gt;：当前页的数据 因此，我们在cn.itcast.hotel.pojo中定义返回结果：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package cn.itcast.hotel.pojo; import lombok.Data; import java.util.List; @Data public class PageResult { private Long total; private List\u0026lt;HotelDoc\u0026gt; hotels; public PageResult() { } public PageResult(Long total, List\u0026lt;HotelDoc\u0026gt; hotels) { this.total = total; this.hotels = hotels; } } Copied! 4.1.3.定义controller 定义一个HotelController，声明查询接口，满足下列要求：\n请求方式：Post 请求路径：/hotel/list 请求参数：对象，类型为RequestParam 返回值：PageResult，包含两个属性 Long total：总条数 List\u0026lt;HotelDoc\u0026gt; hotels：酒店数据 因此，我们在cn.itcast.hotel.web中定义HotelController：\n1 2 3 4 5 6 7 8 9 10 11 12 @RestController @RequestMapping(\u0026#34;/hotel\u0026#34;) public class HotelController { @Autowired private IHotelService hotelService; // 搜索酒店数据 @PostMapping(\u0026#34;/list\u0026#34;) public PageResult search(@RequestBody RequestParams params){ return hotelService.search(params); } } Copied! 4.1.4.实现搜索业务 我们在controller调用了IHotelService，并没有实现该方法，因此下面我们就在IHotelService中定义方法，并且去实现业务逻辑。\n1）在cn.itcast.hotel.service中的IHotelService接口中定义一个方法：\n1 2 3 4 5 6 /** * 根据关键字搜索酒店信息 * @param params 请求参数对象，包含用户输入的关键字 * @return 酒店文档列表 */ PageResult search(RequestParams params); Copied! 2）实现搜索业务，肯定离不开RestHighLevelClient，我们需要把它注册到Spring中作为一个Bean。在cn.itcast.hotel中的HotelDemoApplication中声明这个Bean：\n1 2 3 4 5 6 @Bean public RestHighLevelClient client(){ return new RestHighLevelClient(RestClient.builder( HttpHost.create(\u0026#34;http://192.168.150.101:9200\u0026#34;) )); } Copied! 3）在cn.itcast.hotel.service.impl中的HotelService中实现search方法：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 @Override public PageResult search(RequestParams params) { try { // 1.准备Request SearchRequest request = new SearchRequest(\u0026#34;hotel\u0026#34;); // 2.准备DSL // 2.1.query String key = params.getKey(); if (key == null || \u0026#34;\u0026#34;.equals(key)) { boolQuery.must(QueryBuilders.matchAllQuery()); } else { boolQuery.must(QueryBuilders.matchQuery(\u0026#34;all\u0026#34;, key)); } // 2.2.分页 int page = params.getPage(); int size = params.getSize(); request.source().from((page - 1) * size).size(size); // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应 return handleResponse(response); } catch (IOException e) { throw new RuntimeException(e); } } // 结果解析 private PageResult handleResponse(SearchResponse response) { // 4.解析响应 SearchHits searchHits = response.getHits(); // 4.1.获取总条数 long total = searchHits.getTotalHits().value; // 4.2.文档数组 SearchHit[] hits = searchHits.getHits(); // 4.3.遍历 List\u0026lt;HotelDoc\u0026gt; hotels = new ArrayList\u0026lt;\u0026gt;(); for (SearchHit hit : hits) { // 获取文档source String json = hit.getSourceAsString(); // 反序列化 HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class); // 放入集合 hotels.add(hotelDoc); } // 4.4.封装返回 return new PageResult(total, hotels); } Copied! 4.2.酒店结果过滤 需求：添加品牌、城市、星级、价格等过滤功能\n4.2.1.需求分析 在页面搜索框下面，会有一些过滤项：\n传递的参数如图：\n包含的过滤条件有：\nbrand：品牌值 city：城市 minPrice~maxPrice：价格范围 starName：星级 我们需要做两件事情：\n修改请求参数的对象RequestParams，接收上述参数 修改业务逻辑，在搜索条件之外，添加一些过滤条件 4.2.2.修改实体类 修改在cn.itcast.hotel.pojo包下的实体类RequestParams：\n1 2 3 4 5 6 7 8 9 10 11 12 13 @Data public class RequestParams { private String key; private Integer page; private Integer size; private String sortBy; // 下面是新增的过滤条件参数 private String city; private String brand; private String starName; private Integer minPrice; private Integer maxPrice; } Copied! 4.2.3.修改搜索业务 在HotelService的search方法中，只有一个地方需要修改：requet.source().query( \u0026hellip; )其中的查询条件。\n在之前的业务中，只有match查询，根据关键字搜索，现在要添加条件过滤，包括：\n品牌过滤：是keyword类型，用term查询 星级过滤：是keyword类型，用term查询 价格过滤：是数值类型，用range查询 城市过滤：是keyword类型，用term查询 多个查询条件组合，肯定是boolean查询来组合：\n关键字搜索放到must中，参与算分 其它过滤条件放到filter中，不参与算分 因为条件构建的逻辑比较复杂，这里先封装为一个函数：\nbuildBasicQuery的代码如下：\n1 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 private void buildBasicQuery(RequestParams params, SearchRequest request) { // 1.构建BooleanQuery BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); // 2.关键字搜索 String key = params.getKey(); if (key == null || \u0026#34;\u0026#34;.equals(key)) { boolQuery.must(QueryBuilders.matchAllQuery()); } else { boolQuery.must(QueryBuilders.matchQuery(\u0026#34;all\u0026#34;, key)); } // 3.城市条件 if (params.getCity() != null \u0026amp;\u0026amp; !params.getCity().equals(\u0026#34;\u0026#34;)) { boolQuery.filter(QueryBuilders.termQuery(\u0026#34;city\u0026#34;, params.getCity())); } // 4.品牌条件 if (params.getBrand() != null \u0026amp;\u0026amp; !params.getBrand().equals(\u0026#34;\u0026#34;)) { boolQuery.filter(QueryBuilders.termQuery(\u0026#34;brand\u0026#34;, params.getBrand())); } // 5.星级条件 if (params.getStarName() != null \u0026amp;\u0026amp; !params.getStarName().equals(\u0026#34;\u0026#34;)) { boolQuery.filter(QueryBuilders.termQuery(\u0026#34;starName\u0026#34;, params.getStarName())); } // 6.价格 if (params.getMinPrice() != null \u0026amp;\u0026amp; params.getMaxPrice() != null) { boolQuery.filter(QueryBuilders .rangeQuery(\u0026#34;price\u0026#34;) .gte(params.getMinPrice()) .lte(params.getMaxPrice()) ); } // 7.放入source request.source().query(boolQuery); } Copied! 4.3.我周边的酒店 需求：我附近的酒店\n4.3.1.需求分析 在酒店列表页的右侧，有一个小地图，点击地图的定位按钮，地图会找到你所在的位置：\n并且，在前端会发起查询请求，将你的坐标发送到服务端：\n我们要做的事情就是基于这个location坐标，然后按照距离对周围酒店排序。实现思路如下：\n修改RequestParams参数，接收location字段 修改search方法业务逻辑，如果location有值，添加根据geo_distance排序的功能 4.3.2.修改实体类 修改在cn.itcast.hotel.pojo包下的实体类RequestParams：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package cn.itcast.hotel.pojo; import lombok.Data; @Data public class RequestParams { private String key; private Integer page; private Integer size; private String sortBy; private String city; private String brand; private String starName; private Integer minPrice; private Integer maxPrice; // 我当前的地理坐标 private String location; } Copied! 4.3.3.距离排序API 我们以前学习过排序功能，包括两种：\n普通字段排序 地理坐标排序 我们只讲了普通字段排序对应的java写法。地理坐标排序只学过DSL语法，如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 GET /indexName/_search { \u0026#34;query\u0026#34;: { \u0026#34;match_all\u0026#34;: {} }, \u0026#34;sort\u0026#34;: [ { \u0026#34;price\u0026#34;: \u0026#34;asc\u0026#34; }, { \u0026#34;_geo_distance\u0026#34; : { \u0026#34;FIELD\u0026#34; : \u0026#34;纬度，经度\u0026#34;, \u0026#34;order\u0026#34; : \u0026#34;asc\u0026#34;, \u0026#34;unit\u0026#34; : \u0026#34;km\u0026#34; } } ] } Copied! 对应的java代码示例：\n4.3.4.添加距离排序 在cn.itcast.hotel.service.impl的HotelService的search方法中，添加一个排序功能：\n完整代码：\n1 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 @Override public PageResult search(RequestParams params) { try { // 1.准备Request SearchRequest request = new SearchRequest(\u0026#34;hotel\u0026#34;); // 2.准备DSL // 2.1.query buildBasicQuery(params, request); // 2.2.分页 int page = params.getPage(); int size = params.getSize(); request.source().from((page - 1) * size).size(size); // 2.3.排序 String location = params.getLocation(); if (location != null \u0026amp;\u0026amp; !location.equals(\u0026#34;\u0026#34;)) { request.source().sort(SortBuilders .geoDistanceSort(\u0026#34;location\u0026#34;, new GeoPoint(location)) .order(SortOrder.ASC) .unit(DistanceUnit.KILOMETERS) ); } // 3.发送请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析响应 return handleResponse(response); } catch (IOException e) { throw new RuntimeException(e); } } Copied! 4.3.5.排序距离显示 重启服务后，测试我的酒店功能：\n发现确实可以实现对我附近酒店的排序，不过并没有看到酒店到底距离我多远，这该怎么办？\n排序完成后，页面还要获取我附近每个酒店的具体距离值，这个值在响应结果中是独立的：\n因此，我们在结果解析阶段，除了解析source部分以外，还要得到sort部分，也就是排序的距离，然后放到响应结果中。\n我们要做两件事：\n修改HotelDoc，添加排序距离字段，用于页面显示 修改HotelService类中的handleResponse方法，添加对sort值的获取 1）修改HotelDoc类，添加距离字段\n1 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 37 package cn.itcast.hotel.pojo; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class HotelDoc { private Long id; private String name; private String address; private Integer price; private Integer score; private String brand; private String city; private String starName; private String business; private String location; private String pic; // 排序时的 距离值 private Object distance; public HotelDoc(Hotel hotel) { this.id = hotel.getId(); this.name = hotel.getName(); this.address = hotel.getAddress(); this.price = hotel.getPrice(); this.score = hotel.getScore(); this.brand = hotel.getBrand(); this.city = hotel.getCity(); this.starName = hotel.getStarName(); this.business = hotel.getBusiness(); this.location = hotel.getLatitude() + \u0026#34;, \u0026#34; + hotel.getLongitude(); this.pic = hotel.getPic(); } } Copied! 2）修改HotelService中的handleResponse方法\n重启后测试，发现页面能成功显示距离了：\n4.4.酒店竞价排名 需求：让指定的酒店在搜索结果中排名置顶\n4.4.1.需求分析 要让指定酒店在搜索结果中排名置顶，效果如图：\n页面会给指定的酒店添加广告标记。\n那怎样才能让指定的酒店排名置顶呢？\n我们之前学习过的function_score查询可以影响算分，算分高了，自然排名也就高了。而function_score包含3个要素：\n过滤条件：哪些文档要加分 算分函数：如何计算function score 加权方式：function score 与 query score如何运算 这里的需求是：让指定酒店排名靠前。因此我们需要给这些酒店添加一个标记，这样在过滤条件中就可以根据这个标记来判断，是否要提高算分。\n比如，我们给酒店添加一个字段：isAD，Boolean类型：\ntrue：是广告 false：不是广告 这样function_score包含3个要素就很好确定了：\n过滤条件：判断isAD 是否为true 算分函数：我们可以用最简单暴力的weight，固定加权值 加权方式：可以用默认的相乘，大大提高算分 因此，业务的实现步骤包括：\n给HotelDoc类添加isAD字段，Boolean类型\n挑选几个你喜欢的酒店，给它的文档数据添加isAD字段，值为true\n修改search方法，添加function score功能，给isAD值为true的酒店增加权重\n4.4.2.修改HotelDoc实体 给cn.itcast.hotel.pojo包下的HotelDoc类添加isAD字段：\n4.4.3.添加广告标记 接下来，我们挑几个酒店，添加isAD字段，设置为true：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 POST /hotel/_update/1902197537 { \u0026#34;doc\u0026#34;: { \u0026#34;isAD\u0026#34;: true } } POST /hotel/_update/2056126831 { \u0026#34;doc\u0026#34;: { \u0026#34;isAD\u0026#34;: true } } POST /hotel/_update/1989806195 { \u0026#34;doc\u0026#34;: { \u0026#34;isAD\u0026#34;: true } } POST /hotel/_update/2056105938 { \u0026#34;doc\u0026#34;: { \u0026#34;isAD\u0026#34;: true } } Copied! 4.4.4.添加算分函数查询 接下来我们就要修改查询条件了。之前是用的boolean 查询，现在要改成function_socre查询。\nfunction_score查询结构如下：\n对应的JavaAPI如下：\n我们可以将之前写的boolean查询作为原始查询条件放到query中，接下来就是添加过滤条件、算分函数、加权模式了。所以原来的代码依然可以沿用。\n修改cn.itcast.hotel.service.impl包下的HotelService类中的buildBasicQuery方法，添加算分函数查询：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 private void buildBasicQuery(RequestParams params, SearchRequest request) { // 1.构建BooleanQuery BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); // 关键字搜索 String key = params.getKey(); if (key == null || \u0026#34;\u0026#34;.equals(key)) { boolQuery.must(QueryBuilders.matchAllQuery()); } else { boolQuery.must(QueryBuilders.matchQuery(\u0026#34;all\u0026#34;, key)); } // 城市条件 if (params.getCity() != null \u0026amp;\u0026amp; !params.getCity().equals(\u0026#34;\u0026#34;)) { boolQuery.filter(QueryBuilders.termQuery(\u0026#34;city\u0026#34;, params.getCity())); } // 品牌条件 if (params.getBrand() != null \u0026amp;\u0026amp; !params.getBrand().equals(\u0026#34;\u0026#34;)) { boolQuery.filter(QueryBuilders.termQuery(\u0026#34;brand\u0026#34;, params.getBrand())); } // 星级条件 if (params.getStarName() != null \u0026amp;\u0026amp; !params.getStarName().equals(\u0026#34;\u0026#34;)) { boolQuery.filter(QueryBuilders.termQuery(\u0026#34;starName\u0026#34;, params.getStarName())); } // 价格 if (params.getMinPrice() != null \u0026amp;\u0026amp; params.getMaxPrice() != null) { boolQuery.filter(QueryBuilders .rangeQuery(\u0026#34;price\u0026#34;) .gte(params.getMinPrice()) .lte(params.getMaxPrice()) ); } // 2.算分控制 FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery( // 原始查询，相关性算分的查询 boolQuery, // function score的数组 new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{ // 其中的一个function score 元素 new FunctionScoreQueryBuilder.FilterFunctionBuilder( // 过滤条件 QueryBuilders.termQuery(\u0026#34;isAD\u0026#34;, true), // 算分函数 ScoreFunctionBuilders.weightFactorFunction(10) ) }); request.source().query(functionScoreQuery); } Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/13895613/","title":"6.分布式搜索引擎02"},{"content":" 分布式搜索引擎03 0.学习目标 1.数据聚合 **聚合（ aggregations ） **可以让我们极其方便的实现对数据的统计、分析、运算。例如：\n什么品牌的手机最受欢迎？ 这些手机的平均价格、最高价格、最低价格？ 这些手机每月的销售情况如何？ 实现这些统计功能的比数据库的sql要方便的多，而且查询速度非常快，可以实现近实时搜索效果。\n1.1.聚合的种类 聚合常见的有三类：\n**桶（Bucket）**聚合：用来对文档做分组\nTermAggregation：按照文档字段值分组，例如按照品牌值分组、按照国家分组 Date Histogram：按照日期阶梯分组，例如一周为一组，或者一月为一组 **度量（Metric）**聚合：用以计算一些值，比如：最大值、最小值、平均值等\nAvg：求平均值 Max：求最大值 Min：求最小值 Stats：同时求max、min、avg、sum等 **管道（pipeline）**聚合：其它聚合的结果为基础做聚合\n**注意：**参加聚合的字段必须是keyword、日期、数值、布尔类型\n1.2.DSL实现聚合 现在，我们要统计所有数据中的酒店品牌有几种，其实就是按照品牌对数据分组。此时可以根据酒店品牌的名称做聚合，也就是Bucket聚合。\n1.2.1.Bucket聚合语法 语法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 GET /hotel/_search { \u0026#34;size\u0026#34;: 0, // 设置size为0，结果中不包含文档，只包含聚合结果 \u0026#34;aggs\u0026#34;: { // 定义聚合 \u0026#34;brandAgg\u0026#34;: { //给聚合起个名字 \u0026#34;terms\u0026#34;: { // 聚合的类型，按照品牌值聚合，所以选择term \u0026#34;field\u0026#34;: \u0026#34;brand\u0026#34;, // 参与聚合的字段 \u0026#34;size\u0026#34;: 20 // 希望获取的聚合结果数量 } } } } Copied! 结果如图：\n1.2.2.聚合结果排序 默认情况下，Bucket聚合会统计Bucket内的文档数量，记为_count，并且按照_count降序排序。\n我们可以指定order属性，自定义聚合的排序方式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 GET /hotel/_search { \u0026#34;size\u0026#34;: 0, \u0026#34;aggs\u0026#34;: { \u0026#34;brandAgg\u0026#34;: { \u0026#34;terms\u0026#34;: { \u0026#34;field\u0026#34;: \u0026#34;brand\u0026#34;, \u0026#34;order\u0026#34;: { \u0026#34;_count\u0026#34;: \u0026#34;asc\u0026#34; // 按照_count升序排列 }, \u0026#34;size\u0026#34;: 20 } } } } Copied! 1.2.3.限定聚合范围 默认情况下，Bucket聚合是对索引库的所有文档做聚合，但真实场景下，用户会输入搜索条件，因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。\n我们可以限定要聚合的文档范围，只要添加query条件即可：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 GET /hotel/_search { \u0026#34;query\u0026#34;: { \u0026#34;range\u0026#34;: { \u0026#34;price\u0026#34;: { \u0026#34;lte\u0026#34;: 200 // 只对200元以下的文档聚合 } } }, \u0026#34;size\u0026#34;: 0, \u0026#34;aggs\u0026#34;: { \u0026#34;brandAgg\u0026#34;: { \u0026#34;terms\u0026#34;: { \u0026#34;field\u0026#34;: \u0026#34;brand\u0026#34;, \u0026#34;size\u0026#34;: 20 } } } } Copied! 这次，聚合得到的品牌明显变少了：\n1.2.4.Metric聚合语法 上节课，我们对酒店按照品牌分组，形成了一个个桶。现在我们需要对桶内的酒店做运算，获取每个品牌的用户评分的min、max、avg等值。\n这就要用到Metric聚合了，例如stat聚合：就可以获取min、max、avg等结果。\n语法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 GET /hotel/_search { \u0026#34;size\u0026#34;: 0, \u0026#34;aggs\u0026#34;: { \u0026#34;brandAgg\u0026#34;: { \u0026#34;terms\u0026#34;: { \u0026#34;field\u0026#34;: \u0026#34;brand\u0026#34;, \u0026#34;size\u0026#34;: 20 }, \u0026#34;aggs\u0026#34;: { // 是brands聚合的子聚合，也就是分组后对每组分别计算 \u0026#34;score_stats\u0026#34;: { // 聚合名称 \u0026#34;stats\u0026#34;: { // 聚合类型，这里stats可以计算min、max、avg等 \u0026#34;field\u0026#34;: \u0026#34;score\u0026#34; // 聚合字段，这里是score } } } } } } Copied! 这次的score_stats聚合是在brandAgg的聚合内部嵌套的子聚合。因为我们需要在每个桶分别计算。\n另外，我们还可以给聚合结果做个排序，例如按照每个桶的酒店平均分做排序：\n1.2.5.小结 aggs代表聚合，与query同级，此时query的作用是？\n限定聚合的的文档范围 聚合必须的三要素：\n聚合名称 聚合类型 聚合字段 聚合可配置属性有：\nsize：指定聚合结果数量 order：指定聚合结果排序方式 field：指定聚合字段 1.3.RestAPI实现聚合 1.3.1.API语法 聚合条件与query条件同级别，因此需要使用request.source()来指定聚合条件。\n聚合条件的语法：\n聚合的结果也与查询结果不同，API也比较特殊。不过同样是JSON逐层解析：\n1.3.2.业务需求 需求：搜索页面的品牌、城市等信息不应该是在页面写死，而是通过聚合索引库中的酒店数据得来的：\n分析：\n目前，页面的城市列表、星级列表、品牌列表都是写死的，并不会随着搜索结果的变化而变化。但是用户搜索条件改变时，搜索结果会跟着变化。\n例如：用户搜索“东方明珠”，那搜索的酒店肯定是在上海东方明珠附近，因此，城市只能是上海，此时城市列表中就不应该显示北京、深圳、杭州这些信息了。\n也就是说，搜索结果中包含哪些城市，页面就应该列出哪些城市；搜索结果中包含哪些品牌，页面就应该列出哪些品牌。\n如何得知搜索结果中包含哪些品牌？如何得知搜索结果中包含哪些城市？\n使用聚合功能，利用Bucket聚合，对搜索结果中的文档基于品牌分组、基于城市分组，就能得知包含哪些品牌、哪些城市了。\n因为是对搜索结果聚合，因此聚合是限定范围的聚合，也就是说聚合的限定条件跟搜索文档的条件一致。\n查看浏览器可以发现，前端其实已经发出了这样的一个请求：\n请求参数与搜索文档的参数完全一致。\n返回值类型就是页面要展示的最终结果：\n结果是一个Map结构：\nkey是字符串，城市、星级、品牌、价格 value是集合，例如多个城市的名称 1.3.3.业务实现 在cn.itcast.hotel.web包的HotelController中添加一个方法，遵循下面的要求：\n请求方式：POST 请求路径：/hotel/filters 请求参数：RequestParams，与搜索文档的参数一致 返回值类型：Map\u0026lt;String, List\u0026lt;String\u0026gt;\u0026gt; 代码：\n1 2 3 4 @PostMapping(\u0026#34;filters\u0026#34;) public Map\u0026lt;String, List\u0026lt;String\u0026gt;\u0026gt; getFilters(@RequestBody RequestParams params){ return hotelService.getFilters(params); } Copied! 这里调用了IHotelService中的getFilters方法，尚未实现。\n在cn.itcast.hotel.service.IHotelService中定义新方法：\n1 Map\u0026lt;String, List\u0026lt;String\u0026gt;\u0026gt; filters(RequestParams params); Copied! 在cn.itcast.hotel.service.impl.HotelService中实现该方法：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 @Override public Map\u0026lt;String, List\u0026lt;String\u0026gt;\u0026gt; filters(RequestParams params) { try { // 1.准备Request SearchRequest request = new SearchRequest(\u0026#34;hotel\u0026#34;); // 2.准备DSL // 2.1.query buildBasicQuery(params, request); // 2.2.设置size request.source().size(0); // 2.3.聚合 buildAggregation(request); // 3.发出请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析结果 Map\u0026lt;String, List\u0026lt;String\u0026gt;\u0026gt; result = new HashMap\u0026lt;\u0026gt;(); Aggregations aggregations = response.getAggregations(); // 4.1.根据品牌名称，获取品牌结果 List\u0026lt;String\u0026gt; brandList = getAggByName(aggregations, \u0026#34;brandAgg\u0026#34;); result.put(\u0026#34;品牌\u0026#34;, brandList); // 4.2.根据品牌名称，获取品牌结果 List\u0026lt;String\u0026gt; cityList = getAggByName(aggregations, \u0026#34;cityAgg\u0026#34;); result.put(\u0026#34;城市\u0026#34;, cityList); // 4.3.根据品牌名称，获取品牌结果 List\u0026lt;String\u0026gt; starList = getAggByName(aggregations, \u0026#34;starAgg\u0026#34;); result.put(\u0026#34;星级\u0026#34;, starList); return result; } catch (IOException e) { throw new RuntimeException(e); } } private void buildAggregation(SearchRequest request) { request.source().aggregation(AggregationBuilders .terms(\u0026#34;brandAgg\u0026#34;) .field(\u0026#34;brand\u0026#34;) .size(100) ); request.source().aggregation(AggregationBuilders .terms(\u0026#34;cityAgg\u0026#34;) .field(\u0026#34;city\u0026#34;) .size(100) ); request.source().aggregation(AggregationBuilders .terms(\u0026#34;starAgg\u0026#34;) .field(\u0026#34;starName\u0026#34;) .size(100) ); } private List\u0026lt;String\u0026gt; getAggByName(Aggregations aggregations, String aggName) { // 4.1.根据聚合名称获取聚合结果 Terms brandTerms = aggregations.get(aggName); // 4.2.获取buckets List\u0026lt;? extends Terms.Bucket\u0026gt; buckets = brandTerms.getBuckets(); // 4.3.遍历 List\u0026lt;String\u0026gt; brandList = new ArrayList\u0026lt;\u0026gt;(); for (Terms.Bucket bucket : buckets) { // 4.4.获取key String key = bucket.getKeyAsString(); brandList.add(key); } return brandList; } Copied! 2.自动补全 当用户在搜索框输入字符时，我们应该提示出与该字符有关的搜索项，如图：\n这种根据用户输入的字母，提示完整词条的功能，就是自动补全了。\n因为需要根据拼音字母来推断，因此要用到拼音分词功能。\n2.1.拼音分词器 要实现根据字母做补全，就必须对文档按照拼音分词。在GitHub上恰好有elasticsearch的拼音分词插件。地址：https://github.com/medcl/elasticsearch-analysis-pinyin\n课前资料中也提供了拼音分词器的安装包：\n安装方式与IK分词器一样，分三步：\n​\t①解压\n​\t②上传到虚拟机中，elasticsearch的plugin目录\n​\t③重启elasticsearch\n​\t④测试\n详细安装步骤可以参考IK分词器的安装过程。\n测试用法如下：\n1 2 3 4 5 POST /_analyze { \u0026#34;text\u0026#34;: \u0026#34;如家酒店还不错\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;pinyin\u0026#34; } Copied! 结果：\n2.2.自定义分词器 默认的拼音分词器会将每个汉字单独分为拼音，而我们希望的是每个词条形成一组拼音，需要对拼音分词器做个性化定制，形成自定义分词器。\nelasticsearch中分词器（analyzer）的组成包含三部分：\ncharacter filters：在tokenizer之前对文本进行处理。例如删除字符、替换字符 tokenizer：将文本按照一定的规则切割成词条（term）。例如keyword，就是不分词；还有ik_smart tokenizer filter：将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等 文档分词时会依次由这三部分来处理文档：\n声明自定义分词器的语法如下：\n1 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 PUT /test { \u0026#34;settings\u0026#34;: { \u0026#34;analysis\u0026#34;: { \u0026#34;analyzer\u0026#34;: { // 自定义分词器 \u0026#34;my_analyzer\u0026#34;: { // 分词器名称 \u0026#34;tokenizer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;filter\u0026#34;: \u0026#34;py\u0026#34; } }, \u0026#34;filter\u0026#34;: { // 自定义tokenizer filter \u0026#34;py\u0026#34;: { // 过滤器名称 \u0026#34;type\u0026#34;: \u0026#34;pinyin\u0026#34;, // 过滤器类型，这里是pinyin \u0026#34;keep_full_pinyin\u0026#34;: false, \u0026#34;keep_joined_full_pinyin\u0026#34;: true, \u0026#34;keep_original\u0026#34;: true, \u0026#34;limit_first_letter_length\u0026#34;: 16, \u0026#34;remove_duplicated_term\u0026#34;: true, \u0026#34;none_chinese_pinyin_tokenize\u0026#34;: false } } } }, \u0026#34;mappings\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;name\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;my_analyzer\u0026#34;, \u0026#34;search_analyzer\u0026#34;: \u0026#34;ik_smart\u0026#34; } } } } Copied! 测试：\n总结：\n如何使用拼音分词器？\n①下载pinyin分词器\n②解压并放到elasticsearch的plugin目录\n③重启即可\n如何自定义分词器？\n①创建索引库时，在settings中配置，可以包含三部分\n②character filter\n③tokenizer\n④filter\n拼音分词器注意事项？\n为了避免搜索到同音字，搜索时不要使用拼音分词器 2.3.自动补全查询 elasticsearch提供了Completion Suggester 查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率，对于文档中字段的类型有一些约束：\n参与补全查询的字段必须是completion类型。\n字段的内容一般是用来补全的多个词条形成的数组。\n比如，一个这样的索引库：\n1 2 3 4 5 6 7 8 9 10 11 // 创建索引库 PUT test { \u0026#34;mappings\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;title\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;completion\u0026#34; } } } } Copied! 然后插入下面的数据：\n1 2 3 4 5 6 7 8 9 10 11 12 13 // 示例数据 POST test/_doc { \u0026#34;title\u0026#34;: [\u0026#34;Sony\u0026#34;, \u0026#34;WH-1000XM3\u0026#34;] } POST test/_doc { \u0026#34;title\u0026#34;: [\u0026#34;SK-II\u0026#34;, \u0026#34;PITERA\u0026#34;] } POST test/_doc { \u0026#34;title\u0026#34;: [\u0026#34;Nintendo\u0026#34;, \u0026#34;switch\u0026#34;] } Copied! 查询的DSL语句如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 自动补全查询 GET /test/_search { \u0026#34;suggest\u0026#34;: { \u0026#34;title_suggest\u0026#34;: { \u0026#34;text\u0026#34;: \u0026#34;s\u0026#34;, // 关键字 \u0026#34;completion\u0026#34;: { \u0026#34;field\u0026#34;: \u0026#34;title\u0026#34;, // 补全查询的字段 \u0026#34;skip_duplicates\u0026#34;: true, // 跳过重复的 \u0026#34;size\u0026#34;: 10 // 获取前10条结果 } } } } Copied! 2.4.实现酒店搜索框自动补全 现在，我们的hotel索引库还没有设置拼音分词器，需要修改索引库中的配置。但是我们知道索引库是无法修改的，只能删除然后重新创建。\n另外，我们需要添加一个字段，用来做自动补全，将brand、suggestion、city等都放进去，作为自动补全的提示。\n因此，总结一下，我们需要做的事情包括：\n修改hotel索引库结构，设置自定义拼音分词器\n修改索引库的name、all字段，使用自定义分词器\n索引库添加一个新字段suggestion，类型为completion类型，使用自定义的分词器\n给HotelDoc类添加suggestion字段，内容包含brand、business\n重新导入数据到hotel库\n2.4.1.修改酒店映射结构 代码如下：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 // 酒店数据索引库 PUT /hotel { \u0026#34;settings\u0026#34;: { \u0026#34;analysis\u0026#34;: { \u0026#34;analyzer\u0026#34;: { \u0026#34;text_anlyzer\u0026#34;: { \u0026#34;tokenizer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;filter\u0026#34;: \u0026#34;py\u0026#34; }, \u0026#34;completion_analyzer\u0026#34;: { \u0026#34;tokenizer\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;filter\u0026#34;: \u0026#34;py\u0026#34; } }, \u0026#34;filter\u0026#34;: { \u0026#34;py\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;pinyin\u0026#34;, \u0026#34;keep_full_pinyin\u0026#34;: false, \u0026#34;keep_joined_full_pinyin\u0026#34;: true, \u0026#34;keep_original\u0026#34;: true, \u0026#34;limit_first_letter_length\u0026#34;: 16, \u0026#34;remove_duplicated_term\u0026#34;: true, \u0026#34;none_chinese_pinyin_tokenize\u0026#34;: false } } } }, \u0026#34;mappings\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;id\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;name\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;text_anlyzer\u0026#34;, \u0026#34;search_analyzer\u0026#34;: \u0026#34;ik_smart\u0026#34;, \u0026#34;copy_to\u0026#34;: \u0026#34;all\u0026#34; }, \u0026#34;address\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;index\u0026#34;: false }, \u0026#34;price\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; }, \u0026#34;score\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;integer\u0026#34; }, \u0026#34;brand\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;copy_to\u0026#34;: \u0026#34;all\u0026#34; }, \u0026#34;city\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;starName\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34; }, \u0026#34;business\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;copy_to\u0026#34;: \u0026#34;all\u0026#34; }, \u0026#34;location\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;geo_point\u0026#34; }, \u0026#34;pic\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;keyword\u0026#34;, \u0026#34;index\u0026#34;: false }, \u0026#34;all\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;text_anlyzer\u0026#34;, \u0026#34;search_analyzer\u0026#34;: \u0026#34;ik_smart\u0026#34; }, \u0026#34;suggestion\u0026#34;:{ \u0026#34;type\u0026#34;: \u0026#34;completion\u0026#34;, \u0026#34;analyzer\u0026#34;: \u0026#34;completion_analyzer\u0026#34; } } } } Copied! 2.4.2.修改HotelDoc实体 HotelDoc中要添加一个字段，用来做自动补全，内容可以是酒店品牌、城市、商圈等信息。按照自动补全字段的要求，最好是这些字段的数组。\n因此我们在HotelDoc中添加一个suggestion字段，类型为List\u0026lt;String\u0026gt;，然后将brand、city、business等信息放到里面。\n代码如下：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package cn.itcast.hotel.pojo; import lombok.Data; import lombok.NoArgsConstructor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @Data @NoArgsConstructor public class HotelDoc { private Long id; private String name; private String address; private Integer price; private Integer score; private String brand; private String city; private String starName; private String business; private String location; private String pic; private Object distance; private Boolean isAD; private List\u0026lt;String\u0026gt; suggestion; public HotelDoc(Hotel hotel) { this.id = hotel.getId(); this.name = hotel.getName(); this.address = hotel.getAddress(); this.price = hotel.getPrice(); this.score = hotel.getScore(); this.brand = hotel.getBrand(); this.city = hotel.getCity(); this.starName = hotel.getStarName(); this.business = hotel.getBusiness(); this.location = hotel.getLatitude() + \u0026#34;, \u0026#34; + hotel.getLongitude(); this.pic = hotel.getPic(); // 组装suggestion if(this.business.contains(\u0026#34;/\u0026#34;)){ // business有多个值，需要切割 String[] arr = this.business.split(\u0026#34;/\u0026#34;); // 添加元素 this.suggestion = new ArrayList\u0026lt;\u0026gt;(); this.suggestion.add(this.brand); Collections.addAll(this.suggestion, arr); }else { this.suggestion = Arrays.asList(this.brand, this.business); } } } Copied! 2.4.3.重新导入 重新执行之前编写的导入数据功能，可以看到新的酒店数据中包含了suggestion：\n2.4.4.自动补全查询的JavaAPI 之前我们学习了自动补全查询的DSL，而没有学习对应的JavaAPI，这里给出一个示例：\n而自动补全的结果也比较特殊，解析的代码如下：\n2.4.5.实现搜索框自动补全 查看前端页面，可以发现当我们在输入框键入时，前端会发起ajax请求：\n返回值是补全词条的集合，类型为List\u0026lt;String\u0026gt;\n1）在cn.itcast.hotel.web包下的HotelController中添加新接口，接收新的请求：\n1 2 3 4 @GetMapping(\u0026#34;suggestion\u0026#34;) public List\u0026lt;String\u0026gt; getSuggestions(@RequestParam(\u0026#34;key\u0026#34;) String prefix) { return hotelService.getSuggestions(prefix); } Copied! 2）在cn.itcast.hotel.service包下的IhotelService中添加方法：\n1 List\u0026lt;String\u0026gt; getSuggestions(String prefix); Copied! 3）在cn.itcast.hotel.service.impl.HotelService中实现该方法：\n1 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 @Override public List\u0026lt;String\u0026gt; getSuggestions(String prefix) { try { // 1.准备Request SearchRequest request = new SearchRequest(\u0026#34;hotel\u0026#34;); // 2.准备DSL request.source().suggest(new SuggestBuilder().addSuggestion( \u0026#34;suggestions\u0026#34;, SuggestBuilders.completionSuggestion(\u0026#34;suggestion\u0026#34;) .prefix(prefix) .skipDuplicates(true) .size(10) )); // 3.发起请求 SearchResponse response = client.search(request, RequestOptions.DEFAULT); // 4.解析结果 Suggest suggest = response.getSuggest(); // 4.1.根据补全查询名称，获取补全结果 CompletionSuggestion suggestions = suggest.getSuggestion(\u0026#34;suggestions\u0026#34;); // 4.2.获取options List\u0026lt;CompletionSuggestion.Entry.Option\u0026gt; options = suggestions.getOptions(); // 4.3.遍历 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(options.size()); for (CompletionSuggestion.Entry.Option option : options) { String text = option.getText().toString(); list.add(text); } return list; } catch (IOException e) { throw new RuntimeException(e); } } Copied! 3.数据同步 elasticsearch中的酒店数据来自于mysql数据库，因此mysql数据发生改变时，elasticsearch也必须跟着改变，这个就是elasticsearch与mysql之间的数据同步。\n3.1.思路分析 常见的数据同步方案有三种：\n同步调用 异步通知 监听binlog 3.1.1.同步调用 方案一：同步调用\n基本步骤如下：\nhotel-demo对外提供接口，用来修改elasticsearch中的数据 酒店管理服务在完成数据库操作后，直接调用hotel-demo提供的接口， 3.1.2.异步通知 方案二：异步通知\n流程如下：\nhotel-admin对mysql数据库数据完成增、删、改后，发送MQ消息 hotel-demo监听MQ，接收到消息后完成elasticsearch数据修改 3.1.3.监听binlog 方案三：监听binlog\n流程如下：\n给mysql开启binlog功能 mysql完成增、删、改操作都会记录在binlog中 hotel-demo基于canal监听binlog变化，实时更新elasticsearch中的内容 3.1.4.选择 方式一：同步调用\n优点：实现简单，粗暴 缺点：业务耦合度高 方式二：异步通知\n优点：低耦合，实现难度一般 缺点：依赖mq的可靠性 方式三：监听binlog\n优点：完全解除服务间耦合 缺点：开启binlog增加数据库负担、实现复杂度高 3.2.实现数据同步 3.2.1.思路 利用课前资料提供的hotel-admin项目作为酒店管理的微服务。当酒店数据发生增、删、改时，要求对elasticsearch中数据也要完成相同操作。\n步骤：\n导入课前资料提供的hotel-admin项目，启动并测试酒店数据的CRUD\n声明exchange、queue、RoutingKey\n在hotel-admin中的增、删、改业务中完成消息发送\n在hotel-demo中完成消息监听，并更新elasticsearch中数据\n启动并测试数据同步功能\n3.2.2.导入demo 导入课前资料提供的hotel-admin项目：\n运行后，访问 http://localhost:8099\n其中包含了酒店的CRUD功能：\n3.2.3.声明交换机、队列 MQ结构如图：\n1）引入依赖 在hotel-admin、hotel-demo中引入rabbitmq的依赖：\n1 2 3 4 5 \u0026lt;!--amqp--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-amqp\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 2）声明队列交换机名称 在hotel-admin和hotel-demo中的cn.itcast.hotel.constatnts包下新建一个类MqConstants：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package cn.itcast.hotel.constatnts; public class MqConstants { /** * 交换机 */ public final static String HOTEL_EXCHANGE = \u0026#34;hotel.topic\u0026#34;; /** * 监听新增和修改的队列 */ public final static String HOTEL_INSERT_QUEUE = \u0026#34;hotel.insert.queue\u0026#34;; /** * 监听删除的队列 */ public final static String HOTEL_DELETE_QUEUE = \u0026#34;hotel.delete.queue\u0026#34;; /** * 新增或修改的RoutingKey */ public final static String HOTEL_INSERT_KEY = \u0026#34;hotel.insert\u0026#34;; /** * 删除的RoutingKey */ public final static String HOTEL_DELETE_KEY = \u0026#34;hotel.delete\u0026#34;; } Copied! 3）声明队列交换机 在hotel-demo中，定义配置类，声明队列、交换机：\n1 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 37 package cn.itcast.hotel.config; import cn.itcast.hotel.constants.MqConstants; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.TopicExchange; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MqConfig { @Bean public TopicExchange topicExchange(){ return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false); } @Bean public Queue insertQueue(){ return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true); } @Bean public Queue deleteQueue(){ return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true); } @Bean public Binding insertQueueBinding(){ return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY); } @Bean public Binding deleteQueueBinding(){ return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY); } } Copied! 3.2.4.发送MQ消息 在hotel-admin中的增、删、改业务中分别发送MQ消息：\n3.2.5.接收MQ消息 hotel-demo接收到MQ消息要做的事情包括：\n新增消息：根据传递的hotel的id查询hotel信息，然后新增一条数据到索引库 删除消息：根据传递的hotel的id删除索引库中的一条数据 1）首先在hotel-demo的cn.itcast.hotel.service包下的IHotelService中新增新增、删除业务\n1 2 3 void deleteById(Long id); void insertById(Long id); Copied! 2）给hotel-demo中的cn.itcast.hotel.service.impl包下的HotelService中实现业务：\n1 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 @Override public void deleteById(Long id) { try { // 1.准备Request DeleteRequest request = new DeleteRequest(\u0026#34;hotel\u0026#34;, id.toString()); // 2.发送请求 client.delete(request, RequestOptions.DEFAULT); } catch (IOException e) { throw new RuntimeException(e); } } @Override public void insertById(Long id) { try { // 0.根据id查询酒店数据 Hotel hotel = getById(id); // 转换为文档类型 HotelDoc hotelDoc = new HotelDoc(hotel); // 1.准备Request对象 IndexRequest request = new IndexRequest(\u0026#34;hotel\u0026#34;).id(hotel.getId().toString()); // 2.准备Json文档 request.source(JSON.toJSONString(hotelDoc), XContentType.JSON); // 3.发送请求 client.index(request, RequestOptions.DEFAULT); } catch (IOException e) { throw new RuntimeException(e); } } Copied! 3）编写监听器\n在hotel-demo中的cn.itcast.hotel.mq包新增一个类：\n1 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 package cn.itcast.hotel.mq; import cn.itcast.hotel.constants.MqConstants; import cn.itcast.hotel.service.IHotelService; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class HotelListener { @Autowired private IHotelService hotelService; /** * 监听酒店新增或修改的业务 * @param id 酒店id */ @RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE) public void listenHotelInsertOrUpdate(Long id){ hotelService.insertById(id); } /** * 监听酒店删除的业务 * @param id 酒店id */ @RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE) public void listenHotelDelete(Long id){ hotelService.deleteById(id); } } Copied! 4.集群 单机的elasticsearch做数据存储，必然面临两个问题：海量数据存储问题、单点故障问题。\n海量数据存储问题：将索引库从逻辑上拆分为N个分片（shard），存储到多个节点 单点故障问题：将分片数据在不同节点备份（replica ） ES集群相关概念:\n集群（cluster）：一组拥有共同的 cluster name 的 节点。\n节点（node) ：集群中的一个 Elasticearch 实例\n分片（shard）：索引可以被拆分为不同的部分进行存储，称为分片。在集群环境下，一个索引的不同分片可以拆分到不同的节点中\n解决问题：数据量太大，单点存储量有限的问题。\n此处，我们把数据分成3片：shard0、shard1、shard2\n主分片（Primary shard）：相对于副本分片的定义。\n副本分片（Replica shard）每个主分片可以有一个或者多个副本，数据和主分片一样。\n​\n数据备份可以保证高可用，但是每个分片备份一份，所需要的节点数量就会翻一倍，成本实在是太高了！\n为了在高可用和成本间寻求平衡，我们可以这样做：\n首先对数据分片，存储到不同节点 然后对每个分片进行备份，放到对方节点，完成互相备份 这样可以大大减少所需要的服务节点数量，如图，我们以3分片，每个分片备份一份为例：\n现在，每个分片都有1个备份，存储在3个节点：\nnode0：保存了分片0和1 node1：保存了分片0和2 node2：保存了分片1和2 4.1.搭建ES集群 参考课前资料的文档：\n其中的第四章节：\n4.2.集群脑裂问题 4.2.1.集群职责划分 elasticsearch中集群节点有不同的职责划分：\n默认情况下，集群中的任何一个节点都同时具备上述四种角色。\n但是真实的集群一定要将集群职责分离：\nmaster节点：对CPU要求高，但是内存要求第 data节点：对CPU和内存要求都高 coordinating节点：对网络带宽、CPU要求高 职责分离可以让我们根据不同节点的需求分配不同的硬件去部署。而且避免业务之间的互相干扰。\n一个典型的es集群职责划分如图：\n4.2.2.脑裂问题 脑裂是因为集群中的节点失联导致的。\n例如一个集群中，主节点与其它节点失联：\n此时，node2和node3认为node1宕机，就会重新选主：\n当node3当选后，集群继续对外提供服务，node2和node3自成集群，node1自成集群，两个集群数据不同步，出现数据差异。\n当网络恢复后，因为集群中有两个master节点，集群状态的不一致，出现脑裂的情况：\n解决脑裂的方案是，要求选票超过 ( eligible节点数量 + 1 ）/ 2 才能当选为主，因此eligible节点数量最好是奇数。对应配置项是discovery.zen.minimum_master_nodes，在es7.0以后，已经成为默认配置，因此一般不会发生脑裂问题\n例如：3个节点形成的集群，选票必须超过 （3 + 1） / 2 ，也就是2票。node3得到node2和node3的选票，当选为主。node1只有自己1票，没有当选。集群中依然只有1个主节点，没有出现脑裂。\n4.2.3.小结 master eligible节点的作用是什么？\n参与集群选主 主节点可以管理集群状态、管理分片信息、处理创建和删除索引库的请求 data节点的作用是什么？\n数据的CRUD coordinator节点的作用是什么？\n路由请求到其它节点\n合并查询到的结果，返回给用户\n4.3.集群分布式存储 当新增文档时，应该保存到不同分片，保证数据均衡，那么coordinating node如何确定数据该存储到哪个分片呢？\n4.3.1.分片存储测试 插入三条数据：\n测试可以看到，三条数据分别在不同分片：\n结果：\n4.3.2.分片存储原理 elasticsearch会通过hash算法来计算文档应该存储到哪个分片：\n说明：\n_routing默认是文档的id 算法与分片数量有关，因此索引库一旦创建，分片数量不能修改！ 新增文档的流程如下：\n解读：\n1）新增一个id=1的文档 2）对id做hash运算，假如得到的是2，则应该存储到shard-2 3）shard-2的主分片在node3节点，将数据路由到node3 4）保存文档 5）同步给shard-2的副本replica-2，在node2节点 6）返回结果给coordinating-node节点 4.4.集群分布式查询 elasticsearch的查询分成两个阶段：\nscatter phase：分散阶段，coordinating node会把请求分发到每一个分片\ngather phase：聚集阶段，coordinating node汇总data node的搜索结果，并处理为最终结果集返回给用户\n4.5.集群故障转移 集群的master节点会监控集群中的节点状态，如果发现有节点宕机，会立即将宕机节点的分片数据迁移到其它节点，确保数据安全，这个叫做故障转移。\n1）例如一个集群结构如图：\n现在，node1是主节点，其它两个节点是从节点。\n2）突然，node1发生了故障：\n宕机后的第一件事，需要重新选主，例如选中了node2：\nnode2成为主节点后，会检测集群监控状态，发现：shard-1、shard-0没有副本节点。因此需要将node1上的数据迁移到node2、node3：\n","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/73491673/","title":"7.分布式搜索引擎03"},{"content":" Sentinel 规则持久化 一、修改order-service服务 修改OrderService，让其监听Nacos中的sentinel规则配置。\n具体步骤如下：\n1.引入依赖 在order-service中引入sentinel监听nacos的依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.csp\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;sentinel-datasource-nacos\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 2.配置nacos地址 在order-service中的application.yml文件配置nacos地址及监听的配置信息：\n1 2 3 4 5 6 7 8 9 10 spring: cloud: sentinel: datasource: flow: nacos: server-addr: localhost:8848 # nacos地址 dataId: orderservice-flow-rules groupId: SENTINEL_GROUP rule-type: flow # 还可以是：degrade、authority、param-flow Copied! 二、修改sentinel-dashboard源码 SentinelDashboard默认不支持nacos的持久化，需要修改源码。\n1. 解压 解压课前资料中的sentinel源码包：\n然后并用IDEA打开这个项目，结构如下：\n2. 修改nacos依赖 在sentinel-dashboard源码的pom文件中，nacos的依赖默认的scope是test，只能在测试时使用，这里要去除：\n将sentinel-datasource-nacos依赖的scope去掉：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.csp\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;sentinel-datasource-nacos\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 3. 添加nacos支持 在sentinel-dashboard的test包下，已经编写了对nacos的支持，我们需要将其拷贝到main下。\n4. 修改nacos地址 然后，还需要修改测试代码中的NacosConfig类：\n修改其中的nacos地址，让其读取application.properties中的配置：\n在sentinel-dashboard的application.properties中添加nacos地址配置：\n1 nacos.addr=localhost:8848 Copied! 5. 配置nacos数据源 另外，还需要修改com.alibaba.csp.sentinel.dashboard.controller.v2包下的FlowControllerV2类：\n让我们添加的Nacos数据源生效：\n6. 修改前端页面 接下来，还要修改前端页面，添加一个支持nacos的菜单。\n修改src/main/webapp/resources/app/scripts/directives/sidebar/目录下的sidebar.html文件：\n将其中的这部分注释打开：\n修改其中的文本：\n7. 重新编译、打包项目 运行IDEA中的maven插件，编译和打包修改好的Sentinel-Dashboard：\n8.启动 启动方式跟官方一样：\n1 java -jar sentinel-dashboard.jar Copied! 如果要修改nacos地址，需要添加参数：\n1 java -jar -Dnacos.addr=localhost:8848 sentinel-dashboard.jar Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/4916u149/","title":"8.sentinel规则持久化"},{"content":" 微服务保护 1.初识Sentinel 1.1.雪崩问题及解决方案 1.1.1.雪崩问题 微服务中，服务间调用关系错综复杂，一个微服务往往依赖于多个其它微服务。\n如图，如果服务提供者I发生了故障，当前的应用的部分业务因为依赖于服务I，因此也会被阻塞。此时，其它不依赖于服务I的业务似乎不受影响。\n但是，依赖服务I的业务请求被阻塞，用户不会得到响应，则tomcat的这个线程不会释放，于是越来越多的用户请求到来，越来越多的线程会阻塞：\n服务器支持的线程和并发数有限，请求一直阻塞，会导致服务器资源耗尽，从而导致所有其它服务都不可用，那么当前服务也就不可用了。\n那么，依赖于当前服务的其它服务随着时间的推移，最终也都会变的不可用，形成级联失败，雪崩就发生了：\n1.1.2.超时处理 解决雪崩问题的常见方式有四种：\n•超时处理：设定超时时间，请求超过一定时间没有响应就返回错误信息，不会无休止等待\n1.1.3.仓壁模式 方案2：仓壁模式\n仓壁模式来源于船舱的设计：\n船舱都会被隔板分离为多个独立空间，当船体破损时，只会导致部分空间进入，将故障控制在一定范围内，避免整个船体都被淹没。\n于此类似，我们可以限定每个业务能使用的线程数，避免耗尽整个tomcat的资源，因此也叫线程隔离。\n1.1.4.断路器 断路器模式：由断路器统计业务执行的异常比例，如果超出阈值则会熔断该业务，拦截访问该业务的一切请求。\n断路器会统计访问某个服务的请求数量，异常比例：\n当发现访问服务D的请求异常比例过高时，认为服务D有导致雪崩的风险，会拦截访问服务D的一切请求，形成熔断：\n1.1.5.限流 流量控制：限制业务访问的QPS，避免服务因流量的突增而故障。\n1.1.6.总结 什么是雪崩问题？\n微服务之间相互调用，因为调用链中的一个服务故障，引起整个链路都无法访问的情况。 可以认为：\n限流是对服务的保护，避免因瞬间高并发流量而导致服务故障，进而避免雪崩。是一种预防措施。\n超时处理、线程隔离、降级熔断是在部分服务故障时，将故障控制在一定范围，避免雪崩。是一种补救措施。\n1.2.服务保护技术对比 在SpringCloud当中支持多种服务保护技术：\nNetfix Hystrix Sentinel Resilience4J 早期比较流行的是Hystrix框架，但目前国内实用最广泛的还是阿里巴巴的Sentinel框架，这里我们做下对比：\nSentinel Hystrix 隔离策略 信号量隔离 线程池隔离/信号量隔离 熔断降级策略 基于慢调用比例或异常比例 基于失败比率 实时指标实现 滑动窗口 滑动窗口（基于 RxJava） 规则配置 支持多种数据源 支持多种数据源 扩展性 多个扩展点 插件的形式 基于注解的支持 支持 支持 限流 基于 QPS，支持基于调用关系的限流 有限的支持 流量整形 支持慢启动、匀速排队模式 不支持 系统自适应保护 支持 不支持 控制台 开箱即用，可配置规则、查看秒级监控、机器发现等 不完善 常见框架的适配 Servlet、Spring Cloud、Dubbo、gRPC 等 Servlet、Spring Cloud Netflix 1.3.Sentinel介绍和安装 1.3.1.初识Sentinel Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址：https://sentinelguard.io/zh-cn/index.html\nSentinel 具有以下特征:\n•丰富的应用场景：Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景，例如秒杀（即突发流量控制在系统容量可以承受的范围）、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。\n•完备的实时监控：Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据，甚至 500 台以下规模的集群的汇总运行情况。\n•广泛的开源生态：Sentinel 提供开箱即用的与其它开源框架/库的整合模块，例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。\n•完善的 SPI 扩展点：Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。\n1.3.2.安装Sentinel 1）下载\nsentinel官方提供了UI控制台，方便我们对系统做限流设置。大家可以在GitHub 下载。\n课前资料也提供了下载好的jar包：\n2）运行\n将jar包放到任意非中文目录，执行命令：\n1 java -jar sentinel-dashboard-1.8.1.jar Copied! 如果要修改Sentinel的默认端口、账户、密码，可以通过下列配置：\n配置项 默认值 说明 server.port 8080 服务端口 sentinel.dashboard.auth.username sentinel 默认用户名 sentinel.dashboard.auth.password sentinel 默认密码 例如，修改端口：\n1 java -Dserver.port=8090 -jar sentinel-dashboard-1.8.1.jar Copied! 3）访问\n访问http://localhost:8080页面，就可以看到sentinel的控制台了：\n需要输入账号和密码，默认都是：sentinel\n登录后，发现一片空白，什么都没有：\n这是因为我们还没有与微服务整合。\n1.4.微服务整合Sentinel 我们在order-service中整合sentinel，并连接sentinel的控制台，步骤如下：\n1）引入sentinel依赖\n1 2 3 4 5 \u0026lt;!--sentinel--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-sentinel\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 2）配置控制台\n修改application.yaml文件，添加下面内容：\n1 2 3 4 5 6 7 server: port: 8088 spring: cloud: sentinel: transport: dashboard: localhost:8080 Copied! 3）访问order-service的任意端点\n打开浏览器，访问http://localhost:8088/order/101，这样才能触发sentinel的监控。\n然后再访问sentinel的控制台，查看效果：\n2.流量控制 雪崩问题虽然有四种方案，但是限流是避免服务因突发的流量而发生故障，是对微服务雪崩问题的预防。我们先学习这种模式。\n2.1.簇点链路 当请求进入微服务时，首先会访问DispatcherServlet，然后进入Controller、Service、Mapper，这样的一个调用链就叫做簇点链路。簇点链路中被监控的每一个接口就是一个资源。\n默认情况下sentinel会监控SpringMVC的每一个端点（Endpoint，也就是controller中的方法），因此SpringMVC的每一个端点（Endpoint）就是调用链路中的一个资源。\n例如，我们刚才访问的order-service中的OrderController中的端点：/order/{orderId}\n流控、熔断等都是针对簇点链路中的资源来设置的，因此我们可以点击对应资源后面的按钮来设置规则：\n流控：流量控制 降级：降级熔断 热点：热点参数限流，是限流的一种 授权：请求的权限控制 2.1.快速入门 2.1.1.示例 点击资源/order/{orderId}后面的流控按钮，就可以弹出表单。\n表单中可以填写限流规则，如下：\n其含义是限制 /order/{orderId}这个资源的单机QPS为1，即每秒只允许1次请求，超出的请求会被拦截并报错。\n2.1.2.练习： 需求：给 /order/{orderId}这个资源设置流控规则，QPS不能超过 5，然后测试。\n1）首先在sentinel控制台添加限流规则\n2）利用jmeter测试\n如果没有用过jmeter，可以参考课前资料提供的文档《Jmeter快速入门.md》\n课前资料提供了编写好的Jmeter测试样例：\n打开jmeter，导入课前资料提供的测试样例：\n选择：\n20个用户，2秒内运行完，QPS是10，超过了5.\n选中流控入门，QPS\u0026lt;5右键运行：\n注意，不要点击菜单中的执行按钮来运行。\n结果：\n可以看到，成功的请求每次只有5个\n2.2.流控模式 在添加限流规则时，点击高级选项，可以选择三种流控模式：\n直接：统计当前资源的请求，触发阈值时对当前资源直接限流，也是默认的模式 关联：统计与当前资源相关的另一个资源，触发阈值时，对当前资源限流 链路：统计从指定链路访问到本资源的请求，触发阈值时，对指定链路限流 快速入门测试的就是直接模式。\n2.2.1.关联模式 关联模式：统计与当前资源相关的另一个资源，触发阈值时，对当前资源限流\n配置规则：\n语法说明：当/write资源访问量触发阈值时，就会对/read资源限流，避免影响/write资源。\n使用场景：比如用户支付时需要修改订单状态，同时用户要查询订单。查询和修改操作会争抢数据库锁，产生竞争。业务需求是优先支付和更新订单的业务，因此当修改订单业务触发阈值时，需要对查询订单业务限流。\n需求说明：\n在OrderController新建两个端点：/order/query和/order/update，无需实现业务\n配置流控规则，当/order/ update资源被访问的QPS超过5时，对/order/query请求限流\n1）定义/order/query端点，模拟订单查询\n1 2 3 4 @GetMapping(\u0026#34;/query\u0026#34;) public String queryOrder() { return \u0026#34;查询订单成功\u0026#34;; } Copied! 2）定义/order/update端点，模拟订单更新\n1 2 3 4 @GetMapping(\u0026#34;/update\u0026#34;) public String updateOrder() { return \u0026#34;更新订单成功\u0026#34;; } Copied! 重启服务，查看sentinel控制台的簇点链路：\n3）配置流控规则\n对哪个端点限流，就点击哪个端点后面的按钮。我们是对订单查询/order/query限流，因此点击它后面的按钮：\n在表单中填写流控规则：\n4）在Jmeter测试\n选择《流控模式-关联》：\n可以看到1000个用户，100秒，因此QPS为10，超过了我们设定的阈值：5\n查看http请求：\n请求的目标是/order/update，这样这个断点就会触发阈值。\n但限流的目标是/order/query，我们在浏览器访问，可以发现：\n确实被限流了。\n5）总结\n2.2.2.链路模式 链路模式：只针对从指定链路访问到本资源的请求做统计，判断是否超过阈值。\n配置示例：\n例如有两条请求链路：\n/test1 \u0026ndash;\u0026gt; /common\n/test2 \u0026ndash;\u0026gt; /common\n如果只希望统计从/test2进入到/common的请求，则可以这样配置：\n实战案例\n需求：有查询订单和创建订单业务，两者都需要查询商品。针对从查询订单进入到查询商品的请求统计，并设置限流。\n步骤：\n在OrderService中添加一个queryGoods方法，不用实现业务\n在OrderController中，改造/order/query端点，调用OrderService中的queryGoods方法\n在OrderController中添加一个/order/save的端点，调用OrderService的queryGoods方法\n给queryGoods设置限流规则，从/order/query进入queryGoods的方法限制QPS必须小于2\n实现：\n1）添加查询商品方法 在order-service服务中，给OrderService类添加一个queryGoods方法：\n1 2 3 public void queryGoods(){ System.err.println(\u0026#34;查询商品\u0026#34;); } Copied! 2）查询订单时，查询商品 在order-service的OrderController中，修改/order/query端点的业务逻辑：\n1 2 3 4 5 6 7 8 @GetMapping(\u0026#34;/query\u0026#34;) public String queryOrder() { // 查询商品 orderService.queryGoods(); // 查询订单 System.out.println(\u0026#34;查询订单\u0026#34;); return \u0026#34;查询订单成功\u0026#34;; } Copied! 3）新增订单，查询商品 在order-service的OrderController中，修改/order/save端点，模拟新增订单：\n1 2 3 4 5 6 7 8 @GetMapping(\u0026#34;/save\u0026#34;) public String saveOrder() { // 查询商品 orderService.queryGoods(); // 查询订单 System.err.println(\u0026#34;新增订单\u0026#34;); return \u0026#34;新增订单成功\u0026#34;; } Copied! 4）给查询商品添加资源标记 默认情况下，OrderService中的方法是不被Sentinel监控的，需要我们自己通过注解来标记要监控的方法。\n给OrderService的queryGoods方法添加@SentinelResource注解：\n1 2 3 4 @SentinelResource(\u0026#34;goods\u0026#34;) public void queryGoods(){ System.err.println(\u0026#34;查询商品\u0026#34;); } Copied! 链路模式中，是对不同来源的两个链路做监控。但是sentinel默认会给进入SpringMVC的所有请求设置同一个root资源，会导致链路模式失效。\n我们需要关闭这种对SpringMVC的资源聚合，修改order-service服务的application.yml文件：\n1 2 3 4 spring: cloud: sentinel: web-context-unify: false # 关闭context整合 Copied! 重启服务，访问/order/query和/order/save，可以查看到sentinel的簇点链路规则中，出现了新的资源：\n5）添加流控规则 点击goods资源后面的流控按钮，在弹出的表单中填写下面信息：\n只统计从/order/query进入/goods的资源，QPS阈值为2，超出则被限流。\n6）Jmeter测试 选择《流控模式-链路》：\n可以看到这里200个用户，50秒内发完，QPS为4，超过了我们设定的阈值2\n一个http请求是访问/order/save：\n运行的结果：\n完全不受影响。\n另一个是访问/order/query：\n运行结果：\n每次只有2个通过。\n2.2.3.总结 流控模式有哪些？\n•直接：对当前资源限流\n•关联：高优先级资源触发阈值，对低优先级资源限流。\n•链路：阈值统计时，只统计从指定资源进入当前资源的请求，是对请求来源的限流\n2.3.流控效果 在流控的高级选项中，还有一个流控效果选项：\n流控效果是指请求达到流控阈值时应该采取的措施，包括三种：\n快速失败：达到阈值后，新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。\nwarm up：预热模式，对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化，从一个较小值逐渐增加到最大阈值。\n排队等待：让所有的请求按照先后次序排队执行，两个请求的间隔不能小于指定时长\n2.3.1.warm up 阈值一般是一个微服务能承担的最大QPS，但是一个服务刚刚启动时，一切资源尚未初始化（冷启动），如果直接将QPS跑到最大值，可能导致服务瞬间宕机。\nwarm up也叫预热模式，是应对服务冷启动的一种方案。请求阈值初始值是 maxThreshold / coldFactor，持续指定时长后，逐渐提高到maxThreshold值。而coldFactor的默认值是3.\n例如，我设置QPS的maxThreshold为10，预热时间为5秒，那么初始阈值就是 10 / 3 ，也就是3，然后在5秒后逐渐增长到10.\n案例\n需求：给/order/{orderId}这个资源设置限流，最大QPS为10，利用warm up效果，预热时长为5秒\n1）配置流控规则： 2）Jmeter测试 选择《流控效果，warm up》：\nQPS为10.\n刚刚启动时，大部分请求失败，成功的只有3个，说明QPS被限定在3：\n随着时间推移，成功比例越来越高：\n到Sentinel控制台查看实时监控：\n一段时间后：\n2.3.2.排队等待 当请求超过QPS阈值时，快速失败和warm up 会拒绝新的请求并抛出异常。\n而排队等待则是让所有请求进入一个队列中，然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成，如果请求预期的等待时间超出最大时长，则会被拒绝。\n工作原理\n例如：QPS = 5，意味着每200ms处理一个队列中的请求；timeout = 2000，意味着预期等待时长超过2000ms的请求会被拒绝并抛出异常。\n那什么叫做预期等待时长呢？\n比如现在一下子来了12 个请求，因为每200ms执行一个请求，那么：\n第6个请求的预期等待时长 = 200 * （6 - 1） = 1000ms 第12个请求的预期等待时长 = 200 * （12-1） = 2200ms 现在，第1秒同时接收到10个请求，但第2秒只有1个请求，此时QPS的曲线这样的：\n如果使用队列模式做流控，所有进入的请求都要排队，以固定的200ms的间隔执行，QPS会变的很平滑：\n平滑的QPS曲线，对于服务器来说是更友好的。\n案例\n需求：给/order/{orderId}这个资源设置限流，最大QPS为10，利用排队的流控效果，超时时长设置为5s\n1）添加流控规则 2）Jmeter测试 选择《流控效果，队列》：\nQPS为15，已经超过了我们设定的10。\n如果是之前的 快速失败、warmup模式，超出的请求应该会直接报错。\n但是我们看看队列模式的运行结果：\n全部都通过了。\n再去sentinel查看实时监控的QPS曲线：\nQPS非常平滑，一致保持在10，但是超出的请求没有被拒绝，而是放入队列。因此响应时间（等待时间）会越来越长。\n当队列满了以后，才会有部分请求失败：\n2.3.3.总结 流控效果有哪些？\n快速失败：QPS超过阈值时，拒绝新的请求\nwarm up： QPS超过阈值时，拒绝新的请求；QPS阈值是逐渐提升的，可以避免冷启动时高并发导致服务宕机。\n排队等待：请求会进入队列，按照阈值允许的时间间隔依次执行请求；如果请求预期等待时长大于超时时间，直接拒绝\n2.4.热点参数限流 之前的限流是统计访问某个资源的所有请求，判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求，判断是否超过QPS阈值。\n2.4.1.全局参数限流 例如，一个根据id查询商品的接口：\n访问/goods/{id}的请求中，id参数值会有变化，热点参数限流会根据参数值分别统计QPS，统计结果：\n当id=1的请求触发阈值被限流时，id值不为1的请求不受影响。\n配置示例：\n代表的含义是：对hot这个资源的0号参数（第一个参数）做统计，每1秒相同参数值的请求数不能超过5\n2.4.2.热点参数限流 刚才的配置中，对查询商品这个接口的所有商品一视同仁，QPS都限定为5.\n而在实际开发中，可能部分商品是热点商品，例如秒杀商品，我们希望这部分商品的QPS限制与其它商品不一样，高一些。那就需要配置热点参数限流的高级选项了：\n结合上一个配置，这里的含义是对0号的long类型参数限流，每1秒相同参数的QPS不能超过5，有两个例外：\n•如果参数值是100，则每1秒允许的QPS为10\n•如果参数值是101，则每1秒允许的QPS为15\n2.4.4.案例 案例需求：给/order/{orderId}这个资源添加热点参数限流，规则如下：\n•默认的热点参数规则是每1秒请求量不超过2\n•给102这个参数设置例外：每1秒请求量不超过4\n•给103这个参数设置例外：每1秒请求量不超过10\n注意事项：热点参数限流对默认的SpringMVC资源无效，需要利用@SentinelResource注解标记资源\n1）标记资源 给order-service中的OrderController中的/order/{orderId}资源添加注解：\n2）热点参数限流规则 访问该接口，可以看到我们标记的hot资源出现了：\n这里不要点击hot后面的按钮，页面有BUG\n点击左侧菜单中热点规则菜单：\n点击新增，填写表单：\n3）Jmeter测试 选择《热点参数限流 QPS1》：\n这里发起请求的QPS为5.\n包含3个http请求：\n普通参数，QPS阈值为2\n运行结果：\n例外项，QPS阈值为4\n运行结果：\n例外项，QPS阈值为10\n运行结果：\n3.隔离和降级 限流是一种预防措施，虽然限流可以尽量避免因高并发而引起的服务故障，但服务还会因为其它原因而故障。\n而要将这些故障控制在一定范围，避免雪崩，就要靠线程隔离（舱壁模式）和熔断降级手段了。\n线程隔离之前讲到过：调用者在调用服务提供者时，给每个调用的请求分配独立线程池，出现故障时，最多消耗这个线程池内资源，避免把调用者的所有资源耗尽。\n熔断降级：是在调用方这边加入断路器，统计对服务提供者的调用，如果调用的失败比例过高，则熔断该业务，不允许访问该服务的提供者了。\n可以看到，不管是线程隔离还是熔断降级，都是对客户端（调用方）的保护。需要在调用方 发起远程调用时做线程隔离、或者服务熔断。\n而我们的微服务远程调用都是基于Feign来完成的，因此我们需要将Feign与Sentinel整合，在Feign里面实现线程隔离和服务熔断。\n3.1.FeignClient整合Sentinel SpringCloud中，微服务调用都是通过Feign来实现的，因此做客户端保护必须整合Feign和Sentinel。\n3.1.1.修改配置，开启sentinel功能 修改OrderService的application.yml文件，开启Feign的Sentinel功能：\n1 2 3 feign: sentinel: enabled: true # 开启feign对sentinel的支持 Copied! 3.1.2.编写失败降级逻辑 业务失败后，不能直接报错，而应该返回用户一个友好提示或者默认结果，这个就是失败降级逻辑。\n给FeignClient编写失败后的降级逻辑\n①方式一：FallbackClass，无法对远程调用的异常做处理\n②方式二：FallbackFactory，可以对远程调用的异常做处理，我们选择这种\n这里我们演示方式二的失败降级处理。\n步骤一：在feing-api项目中定义类，实现FallbackFactory：\n代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package cn.itcast.feign.clients.fallback; import cn.itcast.feign.clients.UserClient; import cn.itcast.feign.pojo.User; import feign.hystrix.FallbackFactory; import lombok.extern.slf4j.Slf4j; @Slf4j public class UserClientFallbackFactory implements FallbackFactory\u0026lt;UserClient\u0026gt; { @Override public UserClient create(Throwable throwable) { return new UserClient() { @Override public User findById(Long id) { log.error(\u0026#34;查询用户异常\u0026#34;, throwable); return new User(); } }; } } Copied! 步骤二：在feing-api项目中的DefaultFeignConfiguration类中将UserClientFallbackFactory注册为一个Bean：\n1 2 3 4 @Bean public UserClientFallbackFactory userClientFallbackFactory(){ return new UserClientFallbackFactory(); } Copied! 步骤三：在feing-api项目中的UserClient接口中使用UserClientFallbackFactory：\n1 2 3 4 5 6 7 8 9 10 11 12 import cn.itcast.feign.clients.fallback.UserClientFallbackFactory; import cn.itcast.feign.pojo.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(value = \u0026#34;userservice\u0026#34;, fallbackFactory = UserClientFallbackFactory.class) public interface UserClient { @GetMapping(\u0026#34;/user/{id}\u0026#34;) User findById(@PathVariable(\u0026#34;id\u0026#34;) Long id); } Copied! 重启后，访问一次订单查询业务，然后查看sentinel控制台，可以看到新的簇点链路：\n3.1.3.总结 Sentinel支持的雪崩解决方案：\n线程隔离（仓壁模式） 降级熔断 Feign整合Sentinel的步骤：\n在application.yml中配置：feign.sentienl.enable=true 给FeignClient编写FallbackFactory并注册为Bean 将FallbackFactory配置到FeignClient 3.2.线程隔离（舱壁模式） 3.2.1.线程隔离的实现方式 线程隔离有两种方式实现：\n线程池隔离\n信号量隔离（Sentinel默认采用）\n如图：\n线程池隔离：给每个服务调用业务分配一个线程池，利用线程池本身实现隔离效果\n信号量隔离：不创建线程池，而是计数器模式，记录业务使用的线程数量，达到信号量上限时，禁止新的请求。\n两者的优缺点：\n3.2.2.sentinel的线程隔离 用法说明：\n在添加限流规则时，可以选择两种阈值类型：\nQPS：就是每秒的请求数，在快速入门中已经演示过\n线程数：是该资源能使用用的tomcat线程数的最大值。也就是通过限制线程数量，实现线程隔离（舱壁模式）。\n案例需求：给 order-service服务中的UserClient的查询用户接口设置流控规则，线程数不能超过 2。然后利用jemeter测试。\n1）配置隔离规则 选择feign接口后面的流控按钮：\n填写表单：\n2）Jmeter测试 选择《阈值类型-线程数\u0026lt;2》：\n一次发生10个请求，有较大概率并发线程数超过2，而超出的请求会走之前定义的失败降级逻辑。\n查看运行结果：\n发现虽然结果都是通过了，不过部分请求得到的响应是降级返回的null信息。\n3.2.3.总结 线程隔离的两种手段是？\n信号量隔离\n线程池隔离\n信号量隔离的特点是？\n基于计数器模式，简单，开销小 线程池隔离的特点是？\n基于线程池模式，有额外开销，但隔离控制更强 3.3.熔断降级 熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例，如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求；而当服务恢复时，断路器会放行访问该服务的请求。\n断路器控制熔断和放行是通过状态机来完成的：\n状态机包括三个状态：\nclosed：关闭状态，断路器放行所有请求，并开始统计异常比例、慢请求比例。超过阈值则切换到open状态 open：打开状态，服务调用被熔断，访问被熔断服务的请求会被拒绝，快速失败，直接走降级逻辑。Open状态5秒后会进入half-open状态 half-open：半开状态，放行一次请求，根据执行结果来判断接下来的操作。 请求成功：则切换到closed状态 请求失败：则切换到open状态 断路器熔断策略有三种：慢调用、异常比例、异常数\n3.3.1.慢调用 慢调用：业务的响应时长（RT）大于指定时长的请求认定为慢调用请求。在指定时间内，如果请求数量超过设定的最小数量，慢调用比例大于设定的阈值，则触发熔断。\n例如：\n解读：RT超过500ms的调用是慢调用，统计最近10000ms内的请求，如果请求量超过10次，并且慢调用比例不低于0.5，则触发熔断，熔断时长为5秒。然后进入half-open状态，放行一次请求做测试。\n案例\n需求：给 UserClient的查询用户接口设置降级规则，慢调用的RT阈值为50ms，统计时间为1秒，最小请求数量为5，失败阈值比例为0.4，熔断时长为5\n1）设置慢调用 修改user-service中的/user/{id}这个接口的业务。通过休眠模拟一个延迟时间：\n此时，orderId=101的订单，关联的是id为1的用户，调用时长为60ms：\norderId=102的订单，关联的是id为2的用户，调用时长为非常短；\n2）设置熔断规则 下面，给feign接口设置降级规则：\n规则：\n超过50ms的请求都会被认为是慢请求\n3）测试 在浏览器访问：http://localhost:8088/order/101，快速刷新5次，可以发现：\n触发了熔断，请求时长缩短至5ms，快速失败了，并且走降级逻辑，返回的null\n在浏览器访问：http://localhost:8088/order/102，竟然也被熔断了：\n3.3.2.异常比例、异常数 异常比例或异常数：统计指定时间内的调用，如果调用次数超过指定请求数，并且出现异常的比例达到设定的比例阈值（或超过指定异常数），则触发熔断。\n例如，一个异常比例设置：\n解读：统计最近1000ms内的请求，如果请求量超过10次，并且异常比例不低于0.4，则触发熔断。\n一个异常数设置：\n解读：统计最近1000ms内的请求，如果请求量超过10次，并且异常比例不低于2次，则触发熔断。\n案例\n需求：给 UserClient的查询用户接口设置降级规则，统计时间为1秒，最小请求数量为5，失败阈值比例为0.4，熔断时长为5s\n1）设置异常请求 首先，修改user-service中的/user/{id}这个接口的业务。手动抛出异常，以触发异常比例的熔断：\n也就是说，id 为 2时，就会触发异常\n2）设置熔断规则 下面，给feign接口设置降级规则：\n规则：\n在5次请求中，只要异常比例超过0.4，也就是有2次以上的异常，就会触发熔断。\n3）测试 在浏览器快速访问：http://localhost:8088/order/102，快速刷新5次，触发熔断：\n此时，我们去访问本来应该正常的103：\n4.授权规则 授权规则可以对请求方来源做判断和控制。\n4.1.授权规则 4.1.1.基本规则 授权规则可以对调用方的来源做控制，有白名单和黑名单两种方式。\n白名单：来源（origin）在白名单内的调用者允许访问\n黑名单：来源（origin）在黑名单内的调用者不允许访问\n点击左侧菜单的授权，可以看到授权规则：\n资源名：就是受保护的资源，例如/order/{orderId}\n流控应用：是来源者的名单，\n如果是勾选白名单，则名单中的来源被许可访问。 如果是勾选黑名单，则名单中的来源被禁止访问。 比如：\n我们允许请求从gateway到order-service，不允许浏览器访问order-service，那么白名单中就要填写网关的来源名称（origin）。\n4.1.2.如何获取origin Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的。\n1 2 3 4 5 6 public interface RequestOriginParser { /** * 从请求request对象中获取origin，获取方式自定义 */ String parseOrigin(HttpServletRequest request); } Copied! 这个方法的作用就是从request对象中，获取请求者的origin值并返回。\n默认情况下，sentinel不管请求者从哪里来，返回值永远是default，也就是说一切请求的来源都被认为是一样的值default。\n因此，我们需要自定义这个接口的实现，让不同的请求，返回不同的origin。\n例如order-service服务中，我们定义一个RequestOriginParser的实现类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package cn.itcast.order.sentinel; import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.servlet.http.HttpServletRequest; @Component public class HeaderOriginParser implements RequestOriginParser { @Override public String parseOrigin(HttpServletRequest request) { // 1.获取请求头 String origin = request.getHeader(\u0026#34;origin\u0026#34;); // 2.非空判断 if (StringUtils.isEmpty(origin)) { origin = \u0026#34;blank\u0026#34;; } return origin; } } Copied! 我们会尝试从request-header中获取origin值。\n4.1.3.给网关添加请求头 既然获取请求origin的方式是从reques-header中获取origin值，我们必须让所有从gateway路由到微服务的请求都带上origin头。\n这个需要利用之前学习的一个GatewayFilter来实现，AddRequestHeaderGatewayFilter。\n修改gateway服务中的application.yml，添加一个defaultFilter：\n1 2 3 4 5 6 7 spring: cloud: gateway: default-filters: - AddRequestHeader=origin,gateway routes: # ...略 Copied! 这样，从gateway路由的所有请求都会带上origin头，值为gateway。而从其它地方到达微服务的请求则没有这个头。\n4.1.4.配置授权规则 接下来，我们添加一个授权规则，放行origin值为gateway的请求。\n配置如下：\n现在，我们直接跳过网关，访问order-service服务：\n通过网关访问：\n4.2.自定义异常结果 默认情况下，发生限流、降级、授权拦截时，都会抛出异常到调用方。异常结果都是flow limmiting（限流）。这样不够友好，无法得知是限流还是降级还是授权拦截。\n4.2.1.异常类型 而如果要自定义异常时的返回结果，需要实现BlockExceptionHandler接口：\n1 2 3 4 5 6 public interface BlockExceptionHandler { /** * 处理请求被限流、降级、授权拦截时抛出的异常：BlockException */ void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception; } Copied! 这个方法有三个参数：\nHttpServletRequest request：request对象 HttpServletResponse response：response对象 BlockException e：被sentinel拦截时抛出的异常 这里的BlockException包含多个不同的子类：\n异常 说明 FlowException 限流异常 ParamFlowException 热点参数限流的异常 DegradeException 降级异常 AuthorityException 授权规则异常 SystemBlockException 系统规则异常 4.2.2.自定义异常处理 下面，我们就在order-service定义一个自定义异常处理类：\n1 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 package cn.itcast.order.sentinel; import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; import com.alibaba.csp.sentinel.slots.block.flow.FlowException; import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Component public class SentinelExceptionHandler implements BlockExceptionHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception { String msg = \u0026#34;未知异常\u0026#34;; int status = 429; if (e instanceof FlowException) { msg = \u0026#34;请求被限流了\u0026#34;; } else if (e instanceof ParamFlowException) { msg = \u0026#34;请求被热点参数限流\u0026#34;; } else if (e instanceof DegradeException) { msg = \u0026#34;请求被降级了\u0026#34;; } else if (e instanceof AuthorityException) { msg = \u0026#34;没有权限访问\u0026#34;; status = 401; } response.setContentType(\u0026#34;application/json;charset=utf-8\u0026#34;); response.setStatus(status); response.getWriter().println(\u0026#34;{\\\u0026#34;msg\\\u0026#34;: \u0026#34; + msg + \u0026#34;, \\\u0026#34;status\\\u0026#34;: \u0026#34; + status + \u0026#34;}\u0026#34;); } } Copied! 重启测试，在不同场景下，会返回不同的异常消息.\n限流：\n授权拦截时：\n5.规则持久化 现在，sentinel的所有规则都是内存存储，重启后所有规则都会丢失。在生产环境下，我们必须确保这些规则的持久化，避免丢失。\n5.1.规则管理模式 规则是否能持久化，取决于规则管理模式，sentinel支持三种规则管理模式：\n原始模式：Sentinel的默认模式，将规则保存在内存，重启服务会丢失。 pull模式 push模式 5.1.1.pull模式 pull模式：控制台将配置的规则推送到Sentinel客户端，而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询，更新本地规则。\n5.1.2.push模式 push模式：控制台将配置规则推送到远程配置中心，例如Nacos。Sentinel客户端监听Nacos，获取配置变更的推送消息，完成本地配置更新。\n5.2.实现push模式 详细步骤可以参考课前资料的《sentinel规则持久化》：\n","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/157v3815/","title":"8.微服务保护"},{"content":" seata的部署和集成 一、部署Seata的tc-server 1.下载 首先我们要下载seata-server包，地址在http ://seata.io/zh-cn/blog/download . html 当然，课前资料也准备好了：\n2.解压 在非中文目录解压缩这个zip包，其目录结构如下：\n3.修改配置 修改conf目录下的registry.conf文件：\n内容如下：\n1 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 registry { # tc服务的注册中心类，这里选择nacos，也可以是eureka、zookeeper等 type = \u0026#34;nacos\u0026#34; nacos { # seata tc 服务注册到 nacos的服务名称，可以自定义 application = \u0026#34;seata-tc-server\u0026#34; serverAddr = \u0026#34;127.0.0.1:8848\u0026#34; group = \u0026#34;DEFAULT_GROUP\u0026#34; namespace = \u0026#34;\u0026#34; cluster = \u0026#34;SH\u0026#34; username = \u0026#34;nacos\u0026#34; password = \u0026#34;nacos\u0026#34; } } config { # 读取tc服务端的配置文件的方式，这里是从nacos配置中心读取，这样如果tc是集群，可以共享配置 type = \u0026#34;nacos\u0026#34; # 配置nacos地址等信息 nacos { serverAddr = \u0026#34;127.0.0.1:8848\u0026#34; namespace = \u0026#34;\u0026#34; group = \u0026#34;SEATA_GROUP\u0026#34; username = \u0026#34;nacos\u0026#34; password = \u0026#34;nacos\u0026#34; dataId = \u0026#34;seataServer.properties\u0026#34; } } Copied! 4.在nacos添加配置 特别注意，为了让tc服务的集群可以共享配置，我们选择了nacos作为统一配置中心。因此服务端配置文件seataServer.properties文件需要在nacos中配好。\n格式如下：\n配置内容如下：\n1 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 # 数据存储方式，db代表数据库 store.mode=db store.db.datasource=druid store.db.dbType=mysql store.db.driverClassName=com.mysql.jdbc.Driver store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true\u0026amp;rewriteBatchedStatements=true store.db.user=root store.db.password=123 store.db.minConn=5 store.db.maxConn=30 store.db.globalTable=global_table store.db.branchTable=branch_table store.db.queryLimit=100 store.db.lockTable=lock_table store.db.maxWait=5000 # 事务、日志等配置 server.recovery.committingRetryPeriod=1000 server.recovery.asynCommittingRetryPeriod=1000 server.recovery.rollbackingRetryPeriod=1000 server.recovery.timeoutRetryPeriod=1000 server.maxCommitRetryTimeout=-1 server.maxRollbackRetryTimeout=-1 server.rollbackRetryTimeoutUnlockEnable=false server.undo.logSaveDays=7 server.undo.logDeletePeriod=86400000 # 客户端与服务端传输方式 transport.serialization=seata transport.compressor=none # 关闭metrics功能，提高性能 metrics.enabled=false metrics.registryType=compact metrics.exporterList=prometheus metrics.exporterPrometheusPort=9898 Copied! ==其中的数据库地址、用户名、密码都需要修改成你自己的数据库信息。==\n5.创建数据库表 特别注意：tc服务在管理分布式事务时，需要记录事务相关数据到数据库中，你需要提前创建好这些表。\n新建一个名为seata的数据库，运行课前资料提供的sql文件：\n这些表主要记录全局事务、分支事务、全局锁信息：\n1 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 37 38 39 40 41 42 43 44 45 46 SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- 分支事务表 -- ---------------------------- DROP TABLE IF EXISTS `branch_table`; CREATE TABLE `branch_table` ( `branch_id` bigint(20) NOT NULL, `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `transaction_id` bigint(20) NULL DEFAULT NULL, `resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `status` tinyint(4) NULL DEFAULT NULL, `client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `gmt_create` datetime(6) NULL DEFAULT NULL, `gmt_modified` datetime(6) NULL DEFAULT NULL, PRIMARY KEY (`branch_id`) USING BTREE, INDEX `idx_xid`(`xid`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- 全局事务表 -- ---------------------------- DROP TABLE IF EXISTS `global_table`; CREATE TABLE `global_table` ( `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `transaction_id` bigint(20) NULL DEFAULT NULL, `status` tinyint(4) NOT NULL, `application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `timeout` int(11) NULL DEFAULT NULL, `begin_time` bigint(20) NULL DEFAULT NULL, `application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `gmt_create` datetime NULL DEFAULT NULL, `gmt_modified` datetime NULL DEFAULT NULL, PRIMARY KEY (`xid`) USING BTREE, INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE, INDEX `idx_transaction_id`(`transaction_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; SET FOREIGN_KEY_CHECKS = 1; Copied! 6.启动TC服务 进入bin目录，运行其中的seata-server.bat即可：\n启动成功后，seata-server应该已经注册到nacos注册中心了。\n打开浏览器，访问nacos地址：http://localhost:8848，然后进入服务列表页面，可以看到seata-tc-server的信息：\n二、微服务集成seata 1.引入依赖 首先，我们需要在微服务中引入seata依赖：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-seata\u0026lt;/artifactId\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;!--版本较低，1.3.0，因此排除--\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;artifactId\u0026gt;seata-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;io.seata\u0026lt;/groupId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--seata starter 采用1.4.2版本--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.seata\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;seata-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${seata.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 2.修改配置文件 需要修改application.yml文件，添加一些配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 seata: registry: # TC服务注册中心的配置，微服务根据这些信息去注册中心获取tc服务地址 # 参考tc服务自己的registry.conf中的配置 type: nacos nacos: # tc server-addr: 127.0.0.1:8848 namespace: \u0026#34;\u0026#34; group: DEFAULT_GROUP application: seata-tc-server # tc服务在nacos中的服务名称 cluster: SH tx-service-group: seata-demo # 事务组，根据这个获取tc服务的cluster名称 service: vgroup-mapping: # 事务组与TC服务cluster的映射关系 seata-demo: SH Copied! 三、TC服务的高可用和异地容灾 1.模拟异地容灾的TC集群 计划启动两台seata的tc服务节点：\n节点名称 ip地址 端口号 集群名称 seata 127.0.0.1 8091 SH seata2 127.0.0.1 8092 HZ 之前我们已经启动了一台seata服务，端口是8091，集群名为SH。\n现在，将seata目录复制一份，起名为seata2\n修改seata2/conf/registry.conf内容如下：\n1 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 registry { # tc服务的注册中心类，这里选择nacos，也可以是eureka、zookeeper等 type = \u0026#34;nacos\u0026#34; nacos { # seata tc 服务注册到 nacos的服务名称，可以自定义 application = \u0026#34;seata-tc-server\u0026#34; serverAddr = \u0026#34;127.0.0.1:8848\u0026#34; group = \u0026#34;DEFAULT_GROUP\u0026#34; namespace = \u0026#34;\u0026#34; cluster = \u0026#34;HZ\u0026#34; username = \u0026#34;nacos\u0026#34; password = \u0026#34;nacos\u0026#34; } } config { # 读取tc服务端的配置文件的方式，这里是从nacos配置中心读取，这样如果tc是集群，可以共享配置 type = \u0026#34;nacos\u0026#34; # 配置nacos地址等信息 nacos { serverAddr = \u0026#34;127.0.0.1:8848\u0026#34; namespace = \u0026#34;\u0026#34; group = \u0026#34;SEATA_GROUP\u0026#34; username = \u0026#34;nacos\u0026#34; password = \u0026#34;nacos\u0026#34; dataId = \u0026#34;seataServer.properties\u0026#34; } } Copied! 进入seata2/bin目录，然后运行命令：\n1 seata-server.bat -p 8092 Copied! 打开nacos控制台，查看服务列表：\n点进详情查看：\n2.将事务组映射配置到nacos 接下来，我们需要将tx-service-group与cluster的映射关系都配置到nacos配置中心。\n新建一个配置：\n配置的内容如下：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 # 事务组映射关系 service.vgroupMapping.seata-demo=SH service.enableDegrade=false service.disableGlobalTransaction=false # 与TC服务的通信配置 transport.type=TCP transport.server=NIO transport.heartbeat=true transport.enableClientBatchSendRequest=false transport.threadFactory.bossThreadPrefix=NettyBoss transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler transport.threadFactory.shareBossWorker=false transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector transport.threadFactory.clientSelectorThreadSize=1 transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread transport.threadFactory.bossThreadSize=1 transport.threadFactory.workerThreadSize=default transport.shutdown.wait=3 # RM配置 client.rm.asyncCommitBufferLimit=10000 client.rm.lock.retryInterval=10 client.rm.lock.retryTimes=30 client.rm.lock.retryPolicyBranchRollbackOnConflict=true client.rm.reportRetryCount=5 client.rm.tableMetaCheckEnable=false client.rm.tableMetaCheckerInterval=60000 client.rm.sqlParserType=druid client.rm.reportSuccessEnable=false client.rm.sagaBranchRegisterEnable=false # TM配置 client.tm.commitRetryCount=5 client.tm.rollbackRetryCount=5 client.tm.defaultGlobalTransactionTimeout=60000 client.tm.degradeCheck=false client.tm.degradeCheckAllowTimes=10 client.tm.degradeCheckPeriod=2000 # undo日志配置 client.undo.dataValidation=true client.undo.logSerialization=jackson client.undo.onlyCareUpdateColumns=true client.undo.logTable=undo_log client.undo.compress.enable=true client.undo.compress.type=zip client.undo.compress.threshold=64k client.log.exceptionRate=100 Copied! 3.微服务读取nacos配置 接下来，需要修改每一个微服务的application.yml文件，让微服务读取nacos中的client.properties文件：\n1 2 3 4 5 6 7 8 9 seata: config: type: nacos nacos: server-addr: 127.0.0.1:8848 username: nacos password: nacos group: SEATA_GROUP data-id: client.properties Copied! 重启微服务，现在微服务到底是连接tc的SH集群，还是tc的HZ集群，都统一由nacos的client.properties来决定了。\n","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/57149157/","title":"9.seata的部署和集成"},{"content":" 分布式事务 0.学习目标 1.分布式事务问题 1.1.本地事务 本地事务，也就是传统的单机事务。在传统数据库事务中，必须要满足四个原则：\n1.2.分布式事务 分布式事务，就是指不是在单个服务或单个数据库架构下，产生的事务，例如：\n跨数据源的分布式事务 跨服务的分布式事务 综合情况 在数据库水平拆分、服务垂直拆分之后，一个业务操作通常要跨多个数据库、服务才能完成。例如电商行业中比较常见的下单付款案例，包括下面几个行为：\n创建新订单 扣减商品库存 从用户账户余额扣除金额 完成上面的操作需要访问三个不同的微服务和三个不同的数据库。\n订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务，可以保证ACID原则。\n但是当我们把三件事情看做一个\u0026quot;业务\u0026quot;，要满足保证“业务”的原子性，要么所有操作全部成功，要么全部失败，不允许出现部分成功部分失败的现象，这就是分布式系统下的事务了。\n此时ACID难以满足，这是分布式事务要解决的问题\n1.3.演示分布式事务问题 我们通过一个案例来演示分布式事务的问题：\n1）创建数据库，名为seata_demo，然后导入课前资料提供的SQL文件：\n2）导入课前资料提供的微服务：\n微服务结构如下：\n其中：\nseata-demo：父工程，负责管理项目依赖\naccount-service：账户服务，负责管理用户的资金账户。提供扣减余额的接口 storage-service：库存服务，负责管理商品库存。提供扣减库存的接口 order-service：订单服务，负责管理订单。创建订单时，需要调用account-service和storage-service 3）启动nacos、所有微服务\n4）测试下单功能，发出Post请求：\n请求如下：\n1 curl --location --request POST \u0026#39;http://localhost:8082/order?userId=user202103032042012\u0026amp;commodityCode=100202003032041\u0026amp;count=20\u0026amp;money=200\u0026#39; Copied! 如图：\n测试发现，当库存不足时，如果余额已经扣减，并不会回滚，出现了分布式事务问题。\n2.理论基础 解决分布式事务问题，需要一些分布式系统的基础知识作为理论指导。\n2.1.CAP定理 1998年，加州大学的计算机科学家 Eric Brewer 提出，分布式系统有三个指标。\nConsistency（一致性） Availability（可用性） Partition tolerance （分区容错性） 它们的第一个字母分别是 C、A、P。\nEric Brewer 说，这三个指标不可能同时做到。这个结论就叫做 CAP 定理。\n2.1.1.一致性 Consistency（一致性）：用户访问分布式系统中的任意节点，得到的数据必须一致。\n比如现在包含两个节点，其中的初始数据是一致的：\n当我们修改其中一个节点的数据时，两者的数据产生了差异：\n要想保住一致性，就必须实现node01 到 node02的数据 同步：\n2.1.2.可用性 Availability （可用性）：用户访问集群中的任意健康节点，必须能得到响应，而不是超时或拒绝。\n如图，有三个节点的集群，访问任何一个都可以及时得到响应：\n当有部分节点因为网络故障或其它原因无法访问时，代表节点不可用：\n2.1.3.分区容错 Partition（分区）：因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接，形成独立分区。\nTolerance（容错）：在集群出现分区时，整个系统也要持续对外提供服务\n2.1.4.矛盾 在分布式系统中，系统间的网络不能100%保证健康，一定会有故障的时候，而服务有必须对外保证服务。因此Partition Tolerance不可避免。\n当节点接收到新的数据变更时，就会出现问题了：\n如果此时要保证一致性，就必须等待网络恢复，完成数据同步后，整个集群才对外提供服务，服务处于阻塞状态，不可用。\n如果此时要保证可用性，就不能等待网络恢复，那node01、node02与node03之间就会出现数据不一致。\n也就是说，在P一定会出现的情况下，A和C之间只能实现一个。\n2.2.BASE理论 BASE理论是对CAP的一种解决思路，包含三个思想：\nBasically Available （基本可用）：分布式系统在出现故障时，允许损失部分可用性，即保证核心可用。 **Soft State（软状态）：**在一定时间内，允许出现中间状态，比如临时的不一致状态。 Eventually Consistent（最终一致性）：虽然无法保证强一致性，但是在软状态结束后，最终达到数据一致。 2.3.解决分布式事务的思路 分布式事务最大的问题是各个子事务的一致性问题，因此可以借鉴CAP定理和BASE理论，有两种解决思路：\nAP模式：各子事务分别执行和提交，允许出现结果不一致，然后采用弥补措施恢复数据即可，实现最终一致。\nCP模式：各个子事务执行后互相等待，同时提交，同时回滚，达成强一致。但事务等待过程中，处于弱可用状态。\n但不管是哪一种模式，都需要在子系统事务之间互相通讯，协调事务状态，也就是需要一个事务协调者(TC)：\n这里的子系统事务，称为分支事务；有关联的各个分支事务在一起称为全局事务。\n3.初识Seata Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务，为用户打造一站式的分布式解决方案。\n官网地址：http://seata.io/，其中的文档、播客中提供了大量的使用说明、源码分析。\n3.1.Seata的架构 Seata事务管理中有三个重要的角色：\nTC (Transaction Coordinator) - **事务协调者：**维护全局和分支事务的状态，协调全局事务提交或回滚。\nTM (Transaction Manager) - **事务管理器：**定义全局事务的范围、开始全局事务、提交或回滚全局事务。\nRM (Resource Manager) - **资源管理器：**管理分支事务处理的资源，与TC交谈以注册分支事务和报告分支事务的状态，并驱动分支事务提交或回滚。\n整体的架构如图：\nSeata基于上述架构提供了四种不同的分布式事务解决方案：\nXA模式：强一致性分阶段事务模式，牺牲了一定的可用性，无业务侵入 TCC模式：最终一致的分阶段事务模式，有业务侵入 AT模式：最终一致的分阶段事务模式，无业务侵入，也是Seata的默认模式 SAGA模式：长事务模式，有业务侵入 无论哪种方案，都离不开TC，也就是事务的协调者。\n3.2.部署TC服务 参考课前资料提供的文档《 seata的部署和集成.md 》：\n3.3.微服务集成Seata 我们以order-service为例来演示。\n3.3.1.引入依赖 首先，在order-service中引入依赖：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026lt;!--seata--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-seata\u0026lt;/artifactId\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;!--版本较低，1.3.0，因此排除--\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;artifactId\u0026gt;seata-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;io.seata\u0026lt;/groupId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.seata\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;seata-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;!--seata starter 采用1.4.2版本--\u0026gt; \u0026lt;version\u0026gt;${seata.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 3.3.2.配置TC地址 在order-service中的application.yml中，配置TC服务信息，通过注册中心nacos，结合服务名称获取TC地址：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 seata: registry: # TC服务注册中心的配置，微服务根据这些信息去注册中心获取tc服务地址 type: nacos # 注册中心类型 nacos nacos: server-addr: 127.0.0.1:8848 # nacos地址 namespace: \u0026#34;\u0026#34; # namespace，默认为空 group: DEFAULT_GROUP # 分组，默认是DEFAULT_GROUP application: seata-tc-server # seata服务名称 username: nacos password: nacos tx-service-group: seata-demo # 事务组名称 service: vgroup-mapping: # 事务组与cluster的映射关系 seata-demo: SH Copied! 微服务如何根据这些配置寻找TC的地址呢？\n我们知道注册到Nacos中的微服务，确定一个具体实例需要四个信息：\nnamespace：命名空间 group：分组 application：服务名 cluster：集群名 以上四个信息，在刚才的yaml文件中都能找到：\nnamespace为空，就是默认的public\n结合起来，TC服务的信息就是：public@DEFAULT_GROUP@seata-tc-server@SH，这样就能确定TC服务集群了。然后就可以去Nacos拉取对应的实例信息了。\n3.3.3.其它服务 其它两个微服务也都参考order-service的步骤来做，完全一样。\n4.动手实践 下面我们就一起学习下Seata中的四种不同的事务模式。\n4.1.XA模式 XA 规范 是 X/Open 组织定义的分布式事务处理（DTP，Distributed Transaction Processing）标准，XA 规范 描述了全局的TM与局部的RM之间的接口，几乎所有主流的数据库都对 XA 规范 提供了支持。\n4.1.1.两阶段提交 XA是规范，目前主流数据库都实现了这种规范，实现的原理都是基于两阶段提交。\n正常情况：\n异常情况：\n一阶段：\n事务协调者通知每个事物参与者执行本地事务 本地事务执行完成后报告事务执行状态给事务协调者，此时事务不提交，继续持有数据库锁 二阶段：\n事务协调者基于一阶段的报告来判断下一步操作 如果一阶段都成功，则通知所有事务参与者，提交事务 如果一阶段任意一个参与者失败，则通知所有事务参与者回滚事务 4.1.2.Seata的XA模型 Seata对原始的XA模式做了简单的封装和改造，以适应自己的事务模型，基本架构如图：\nRM一阶段的工作：\n​\t① 注册分支事务到TC\n​\t② 执行分支业务sql但不提交\n​\t③ 报告执行状态到TC\nTC二阶段的工作：\nTC检测各分支事务执行状态\na.如果都成功，通知所有RM提交事务\nb.如果有失败，通知所有RM回滚事务\nRM二阶段的工作：\n接收TC指令，提交或回滚事务 4.1.3.优缺点 XA模式的优点是什么？\n事务的强一致性，满足ACID原则。 常用数据库都支持，实现简单，并且没有代码侵入 XA模式的缺点是什么？\n因为一阶段需要锁定数据库资源，等待二阶段结束才释放，性能较差 依赖关系型数据库实现事务 4.1.4.实现XA模式 Seata的starter已经完成了XA模式的自动装配，实现非常简单，步骤如下：\n1）修改application.yml文件（每个参与事务的微服务），开启XA模式：\n1 2 seata: data-source-proxy-mode: XA Copied! 2）给发起全局事务的入口方法添加@GlobalTransactional注解:\n本例中是OrderServiceImpl中的create方法.\n3）重启服务并测试\n重启order-service，再次测试，发现无论怎样，三个微服务都能成功回滚。\n4.2.AT模式 AT模式同样是分阶段提交的事务模型，不过缺弥补了XA模型中资源锁定周期过长的缺陷。\n4.2.1.Seata的AT模型 基本流程图：\n阶段一RM的工作：\n注册分支事务 记录undo-log（数据快照） 执行业务sql并提交 报告事务状态 阶段二提交时RM的工作：\n删除undo-log即可 阶段二回滚时RM的工作：\n根据undo-log恢复数据到更新前 4.2.2.流程梳理 我们用一个真实的业务来梳理下AT模式的原理。\n比如，现在又一个数据库表，记录用户余额：\nid money 1 100 其中一个分支业务要执行的SQL为：\n1 update tb_account set money = money - 10 where id = 1 Copied! AT模式下，当前分支事务执行流程如下：\n一阶段：\n1）TM发起并注册全局事务到TC\n2）TM调用分支事务\n3）分支事务准备执行业务SQL\n4）RM拦截业务SQL，根据where条件查询原始数据，形成快照。\n1 2 3 { \u0026#34;id\u0026#34;: 1, \u0026#34;money\u0026#34;: 100 } Copied! 5）RM执行业务SQL，提交本地事务，释放数据库锁。此时 money = 90\n6）RM报告本地事务状态给TC\n二阶段：\n1）TM通知TC事务结束\n2）TC检查分支事务状态\n​\ta）如果都成功，则立即删除快照\n​\tb）如果有分支事务失败，需要回滚。读取快照数据（{\u0026quot;id\u0026quot;: 1, \u0026quot;money\u0026quot;: 100}），将快照恢复到数据库。此时数据库再次恢复为100\n流程图：\n4.2.3.AT与XA的区别 简述AT模式与XA模式最大的区别是什么？\nXA模式一阶段不提交事务，锁定资源；AT模式一阶段直接提交，不锁定资源。 XA模式依赖数据库机制实现回滚；AT模式利用数据快照实现数据回滚。 XA模式强一致；AT模式最终一致 4.2.4.脏写问题 在多线程并发访问AT模式的分布式事务时，有可能出现脏写问题，如图：\n解决思路就是引入了全局锁的概念。在释放DB锁之前，先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。\n4.2.5.优缺点 AT模式的优点：\n一阶段完成直接提交事务，释放数据库资源，性能比较好 利用全局锁实现读写隔离 没有代码侵入，框架自动完成回滚和提交 AT模式的缺点：\n两阶段之间属于软状态，属于最终一致 框架的快照功能会影响性能，但比XA模式要好很多 4.2.6.实现AT模式 AT模式中的快照生成、回滚等动作都是由框架自动完成，没有任何代码侵入，因此实现非常简单。\n只不过，AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。\n1）导入数据库表，记录全局锁\n导入课前资料提供的Sql文件：seata-at.sql，其中lock_table导入到TC服务关联的数据库，undo_log表导入到微服务关联的数据库：\n2）修改application.yml文件，将事务模式修改为AT模式即可：\n1 2 seata: data-source-proxy-mode: AT # 默认就是AT Copied! 3）重启服务并测试\n4.3.TCC模式 TCC模式与AT模式非常相似，每阶段都是独立事务，不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法：\nTry：资源的检测和预留；\nConfirm：完成资源操作业务；要求 Try 成功 Confirm 一定要能成功。\nCancel：预留资源释放，可以理解为try的反向操作。\n4.3.1.流程分析 举例，一个扣减用户余额的业务。假设账户A原来余额是100，需要余额扣减30元。\n阶段一（ Try ）：检查余额是否充足，如果充足则冻结金额增加30元，可用余额扣除30 初识余额：\n余额充足，可以冻结：\n此时，总金额 = 冻结金额 + 可用金额，数量依然是100不变。事务直接提交无需等待其它事务。\n阶段二（Confirm)：假如要提交（Confirm），则冻结金额扣减30 确认可以提交，不过之前可用金额已经扣减过了，这里只要清除冻结金额就好了：\n此时，总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元\n阶段二(Canncel)：如果要回滚（Cancel），则冻结金额扣减30，可用余额增加30 需要回滚，那么就要释放冻结金额，恢复可用金额：\n4.3.2.Seata的TCC模型 Seata中的TCC模型依然延续之前的事务架构，如图：\n4.3.3.优缺点 TCC模式的每个阶段是做什么的？\nTry：资源检查和预留 Confirm：业务执行和提交 Cancel：预留资源的释放 TCC的优点是什么？\n一阶段完成直接提交事务，释放数据库资源，性能好 相比AT模型，无需生成快照，无需使用全局锁，性能最强 不依赖数据库事务，而是依赖补偿操作，可以用于非事务型数据库 TCC的缺点是什么？\n有代码侵入，需要人为编写try、Confirm和Cancel接口，太麻烦 软状态，事务是最终一致 需要考虑Confirm和Cancel的失败情况，做好幂等处理 4.3.4.事务悬挂和空回滚 1）空回滚 当某分支事务的try阶段阻塞时，可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作，这时cancel不能做回滚，就是空回滚。\n如图：\n执行cancel操作时，应当判断try是否已经执行，如果尚未执行，则应该空回滚。\n2）业务悬挂 对于已经空回滚的业务，之前被阻塞的try操作恢复，继续执行try，就永远不可能confirm或cancel ，事务一直处于中间状态，这就是业务悬挂。\n执行try操作时，应当判断cancel是否已经执行过了，如果已经执行，应当阻止空回滚后的try操作，避免悬挂\n4.3.5.实现TCC模式 解决空回滚和业务悬挂问题，必须要记录当前事务状态，是在try、还是cancel？\n1）思路分析 这里我们定义一张表：\n1 2 3 4 5 6 7 CREATE TABLE `account_freeze_tbl` ( `xid` varchar(128) NOT NULL, `user_id` varchar(255) DEFAULT NULL COMMENT \u0026#39;用户id\u0026#39;, `freeze_money` int(11) unsigned DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;冻结金额\u0026#39;, `state` int(1) DEFAULT NULL COMMENT \u0026#39;事务状态，0:try，1:confirm，2:cancel\u0026#39;, PRIMARY KEY (`xid`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; Copied! 其中：\nxid：是全局事务id freeze_money：用来记录用户冻结金额 state：用来记录事务状态 那此时，我们的业务开怎么做呢？\nTry业务： 记录冻结金额和事务状态到account_freeze表 扣减account表可用金额 Confirm业务 根据xid删除account_freeze表的冻结记录 Cancel业务 修改account_freeze表，冻结金额为0，state为2 修改account表，恢复可用金额 如何判断是否空回滚？ cancel业务中，根据xid查询account_freeze，如果为null则说明try还没做，需要空回滚 如何避免业务悬挂？ try业务中，根据xid查询account_freeze ，如果已经存在则证明Cancel已经执行，拒绝执行try业务 接下来，我们改造account-service，利用TCC实现余额扣减功能。\n2）声明TCC接口 TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明，\n我们在account-service项目中的cn.itcast.account.service包中新建一个接口，声明TCC三个接口：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package cn.itcast.account.service; import io.seata.rm.tcc.api.BusinessActionContext; import io.seata.rm.tcc.api.BusinessActionContextParameter; import io.seata.rm.tcc.api.LocalTCC; import io.seata.rm.tcc.api.TwoPhaseBusinessAction; @LocalTCC public interface AccountTCCService { @TwoPhaseBusinessAction(name = \u0026#34;deduct\u0026#34;, commitMethod = \u0026#34;confirm\u0026#34;, rollbackMethod = \u0026#34;cancel\u0026#34;) void deduct(@BusinessActionContextParameter(paramName = \u0026#34;userId\u0026#34;) String userId, @BusinessActionContextParameter(paramName = \u0026#34;money\u0026#34;)int money); boolean confirm(BusinessActionContext ctx); boolean cancel(BusinessActionContext ctx); } Copied! 3）编写实现类 在account-service服务中的cn.itcast.account.service.impl包下新建一个类，实现TCC业务：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package cn.itcast.account.service.impl; import cn.itcast.account.entity.AccountFreeze; import cn.itcast.account.mapper.AccountFreezeMapper; import cn.itcast.account.mapper.AccountMapper; import cn.itcast.account.service.AccountTCCService; import io.seata.core.context.RootContext; import io.seata.rm.tcc.api.BusinessActionContext; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service @Slf4j public class AccountTCCServiceImpl implements AccountTCCService { @Autowired private AccountMapper accountMapper; @Autowired private AccountFreezeMapper freezeMapper; @Override @Transactional public void deduct(String userId, int money) { // 0.获取事务id String xid = RootContext.getXID(); // 1.扣减可用余额 accountMapper.deduct(userId, money); // 2.记录冻结金额，事务状态 AccountFreeze freeze = new AccountFreeze(); freeze.setUserId(userId); freeze.setFreezeMoney(money); freeze.setState(AccountFreeze.State.TRY); freeze.setXid(xid); freezeMapper.insert(freeze); } @Override public boolean confirm(BusinessActionContext ctx) { // 1.获取事务id String xid = ctx.getXid(); // 2.根据id删除冻结记录 int count = freezeMapper.deleteById(xid); return count == 1; } @Override public boolean cancel(BusinessActionContext ctx) { // 0.查询冻结记录 String xid = ctx.getXid(); AccountFreeze freeze = freezeMapper.selectById(xid); // 1.恢复可用余额 accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney()); // 2.将冻结金额清零，状态改为CANCEL freeze.setFreezeMoney(0); freeze.setState(AccountFreeze.State.CANCEL); int count = freezeMapper.updateById(freeze); return count == 1; } } Copied! 4.4.SAGA模式 Saga 模式是 Seata 即将开源的长事务解决方案，将由蚂蚁金服主要贡献。\n其理论基础是Hector \u0026amp; Kenneth 在1987年发表的论文Sagas 。\nSeata官网对于Saga的指南：https://seata.io/zh-cn/docs/user/saga.html\n4.4.1.原理 在 Saga 模式下，分布式事务内有多个参与者，每一个参与者都是一个冲正补偿服务，需要用户根据业务场景实现其正向操作和逆向回滚操作。\n分布式事务执行过程中，依次执行各参与者的正向操作，如果所有正向操作均执行成功，那么分布式事务提交。如果任何一个正向操作执行失败，那么分布式事务会去退回去执行前面各参与者的逆向回滚操作，回滚已提交的参与者，使分布式事务回到初始状态。\nSaga也分为两个阶段：\n一阶段：直接提交本地事务 二阶段：成功则什么都不做；失败则通过编写补偿业务来回滚 4.4.2.优缺点 优点：\n事务参与者可以基于事件驱动实现异步调用，吞吐高 一阶段直接提交事务，无锁，性能好 不用编写TCC中的三个阶段，实现简单 缺点：\n软状态持续时间不确定，时效性差 没有锁，没有事务隔离，会有脏写 4.5.四种模式对比 我们从以下几个方面来对比四种实现：\n一致性：能否保证事务的一致性？强一致还是最终一致？ 隔离性：事务之间的隔离性如何？ 代码侵入：是否需要对业务代码改造？ 性能：有无性能损耗？ 场景：常见的业务场景 如图：\n5.高可用 Seata的TC服务作为分布式事务核心，一定要保证集群的高可用性。\n5.1.高可用架构模型 搭建TC服务集群非常简单，启动多个TC服务，注册到nacos即可。\n但集群并不能确保100%安全，万一集群所在机房故障怎么办？所以如果要求较高，一般都会做异地多机房容灾。\n比如一个TC集群在上海，另一个TC集群在杭州：\n微服务基于事务组（tx-service-group)与TC集群的映射关系，来查找当前应该使用哪个TC集群。当SH集群故障时，只需要将vgroup-mapping中的映射关系改成HZ。则所有微服务就会切换到HZ的TC集群了。\n5.2.实现高可用 具体实现请参考课前资料提供的文档《seata的部署和集成.md》：\n第三章节：\n","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/17845117/","title":"9.分布式事务"},{"content":" Jmeter快速入门 1.安装Jmeter Jmeter依赖于JDK，所以必须确保当前计算机上已经安装了JDK，并且配置了环境变量。\n1.1.下载 可以Apache Jmeter官网下载，地址：http://jmeter.apache.org/download_jmeter.cgi\n当然，我们课前资料也提供了下载好的安装包：\n1.2.解压 因为下载的是zip包，解压缩即可使用，目录结构如下：\n其中的bin目录就是执行的脚本，其中包含启动脚本：\n1.3.运行 双击即可运行，但是有两点注意：\n启动速度比较慢，要耐心等待 启动后黑窗口不能关闭，否则Jmeter也跟着关闭了 2.快速入门 2.1.设置中文语言 默认Jmeter的语言是英文，需要设置：\n效果：\n注意：上面的配置只能保证本次运行是中文，如果要永久中文，需要修改Jmeter的配置文件\n打开jmeter文件夹，在bin目录中找到 jmeter.properties，添加下面配置：\n1 language=zh_CN Copied! 注意：前面不要出现#，#代表注释，另外这里是下划线，不是中划线\n2.2.基本用法 在测试计划上点鼠标右键，选择添加 \u0026gt; 线程（用户） \u0026gt; 线程组：\n在新增的线程组中，填写线程信息：\n给线程组点鼠标右键，添加http取样器：\n编写取样器内容：\n添加监听报告：\n添加监听结果树：\n汇总报告结果：\n结果树：\n","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/11783511/","title":"Jmeter快速入门"},{"content":" Nacos安装指南 1.Windows安装 开发阶段采用单机安装即可。\n1.1.下载安装包 在Nacos的GitHub页面，提供有下载链接，可以下载编译好的Nacos服务端或者源代码：\nGitHub主页：https://github.com/alibaba/nacos\nGitHub的Release下载页：https://github.com/alibaba/nacos/releases\n如图：\n本课程采用1.4.1.版本的Nacos，课前资料已经准备了安装包：\nwindows版本使用nacos-server-1.4.1.zip包即可。\n1.2.解压 将这个包解压到任意非中文目录下，如图：\n目录说明：\nbin：启动脚本 conf：配置文件 1.3.端口配置 Nacos的默认端口是8848，如果你电脑上的其它进程占用了8848端口，请先尝试关闭该进程。\n如果无法关闭占用8848端口的进程，也可以进入nacos的conf目录，修改配置文件中的端口：\n修改其中的内容：\n1.4.启动 启动非常简单，进入bin目录，结构如下：\n然后执行命令即可：\nwindows命令：\n1 startup.cmd -m standalone Copied! 执行后的效果如图：\n1.5.访问 在浏览器输入地址：http://127.0.0.1:8848/nacos即可：\n默认的账号和密码都是nacos，进入后：\n2.Linux安装 Linux或者Mac安装方式与Windows类似。\n2.1.安装JDK Nacos依赖于JDK运行，索引Linux上也需要安装JDK才行。\n上传jdk安装包：\n上传到某个目录，例如：/usr/local/\n然后解压缩：\n1 tar -xvf jdk-8u144-linux-x64.tar.gz Copied! 然后重命名为java\n配置环境变量：\n1 2 export JAVA_HOME=/usr/local/java export PATH=$PATH:$JAVA_HOME/bin Copied! 设置环境变量：\n1 source /etc/profile Copied! 2.2.上传安装包 如图：\n也可以直接使用课前资料中的tar.gz：\n上传到Linux服务器的某个目录，例如/usr/local/src目录下：\n2.3.解压 命令解压缩安装包：\n1 tar -xvf nacos-server-1.4.1.tar.gz Copied! 然后删除安装包：\n1 rm -rf nacos-server-1.4.1.tar.gz Copied! 目录中最终样式：\n目录内部：\n2.4.端口配置 与windows中类似\n2.5.启动 在nacos/bin目录中，输入命令启动Nacos：\n1 sh startup.sh -m standalone Copied! 3.Nacos的依赖 父工程：\n1 2 3 4 5 6 7 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-alibaba-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.2.5.RELEASE\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; Copied! 客户端：\n1 2 3 4 5 \u0026lt;!-- nacos客户端依赖包 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-nacos-discovery\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/38z56138/","title":"Nacos安装指南"},{"content":" Nacos集群搭建 1.集群结构图 官方给出的Nacos集群图：\n其中包含3个nacos节点，然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx。\n我们计划的集群结构：\n三个nacos节点的地址：\n节点 ip port nacos1 192.168.150.1 8845 nacos2 192.168.150.1 8846 nacos3 192.168.150.1 8847 2.搭建集群 搭建集群的基本步骤：\n搭建数据库，初始化数据库表结构 下载nacos安装包 配置nacos 启动nacos集群 nginx反向代理 2.1.初始化数据库 Nacos默认数据存储在内嵌数据库Derby中，不属于生产可用的数据库。\n官方推荐的最佳实践是使用带有主从的高可用数据库集群，主从模式的高可用数据库可以参考传智教育的后续高手课程。\n这里我们以单点的数据库为例来讲解。\n首先新建一个数据库，命名为nacos，而后导入下面的SQL：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 CREATE TABLE `config_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;id\u0026#39;, `data_id` varchar(255) NOT NULL COMMENT \u0026#39;data_id\u0026#39;, `group_id` varchar(255) DEFAULT NULL, `content` longtext NOT NULL COMMENT \u0026#39;content\u0026#39;, `md5` varchar(32) DEFAULT NULL COMMENT \u0026#39;md5\u0026#39;, `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;创建时间\u0026#39;, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;修改时间\u0026#39;, `src_user` text COMMENT \u0026#39;source user\u0026#39;, `src_ip` varchar(50) DEFAULT NULL COMMENT \u0026#39;source ip\u0026#39;, `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT \u0026#39;\u0026#39; COMMENT \u0026#39;租户字段\u0026#39;, `c_desc` varchar(256) DEFAULT NULL, `c_use` varchar(64) DEFAULT NULL, `effect` varchar(64) DEFAULT NULL, `type` varchar(64) DEFAULT NULL, `c_schema` text, PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=\u0026#39;config_info\u0026#39;; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_aggr */ /******************************************/ CREATE TABLE `config_info_aggr` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;id\u0026#39;, `data_id` varchar(255) NOT NULL COMMENT \u0026#39;data_id\u0026#39;, `group_id` varchar(255) NOT NULL COMMENT \u0026#39;group_id\u0026#39;, `datum_id` varchar(255) NOT NULL COMMENT \u0026#39;datum_id\u0026#39;, `content` longtext NOT NULL COMMENT \u0026#39;内容\u0026#39;, `gmt_modified` datetime NOT NULL COMMENT \u0026#39;修改时间\u0026#39;, `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT \u0026#39;\u0026#39; COMMENT \u0026#39;租户字段\u0026#39;, PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=\u0026#39;增加租户字段\u0026#39;; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_beta */ /******************************************/ CREATE TABLE `config_info_beta` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;id\u0026#39;, `data_id` varchar(255) NOT NULL COMMENT \u0026#39;data_id\u0026#39;, `group_id` varchar(128) NOT NULL COMMENT \u0026#39;group_id\u0026#39;, `app_name` varchar(128) DEFAULT NULL COMMENT \u0026#39;app_name\u0026#39;, `content` longtext NOT NULL COMMENT \u0026#39;content\u0026#39;, `beta_ips` varchar(1024) DEFAULT NULL COMMENT \u0026#39;betaIps\u0026#39;, `md5` varchar(32) DEFAULT NULL COMMENT \u0026#39;md5\u0026#39;, `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;创建时间\u0026#39;, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;修改时间\u0026#39;, `src_user` text COMMENT \u0026#39;source user\u0026#39;, `src_ip` varchar(50) DEFAULT NULL COMMENT \u0026#39;source ip\u0026#39;, `tenant_id` varchar(128) DEFAULT \u0026#39;\u0026#39; COMMENT \u0026#39;租户字段\u0026#39;, PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=\u0026#39;config_info_beta\u0026#39;; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_tag */ /******************************************/ CREATE TABLE `config_info_tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;id\u0026#39;, `data_id` varchar(255) NOT NULL COMMENT \u0026#39;data_id\u0026#39;, `group_id` varchar(128) NOT NULL COMMENT \u0026#39;group_id\u0026#39;, `tenant_id` varchar(128) DEFAULT \u0026#39;\u0026#39; COMMENT \u0026#39;tenant_id\u0026#39;, `tag_id` varchar(128) NOT NULL COMMENT \u0026#39;tag_id\u0026#39;, `app_name` varchar(128) DEFAULT NULL COMMENT \u0026#39;app_name\u0026#39;, `content` longtext NOT NULL COMMENT \u0026#39;content\u0026#39;, `md5` varchar(32) DEFAULT NULL COMMENT \u0026#39;md5\u0026#39;, `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;创建时间\u0026#39;, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;修改时间\u0026#39;, `src_user` text COMMENT \u0026#39;source user\u0026#39;, `src_ip` varchar(50) DEFAULT NULL COMMENT \u0026#39;source ip\u0026#39;, PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=\u0026#39;config_info_tag\u0026#39;; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_tags_relation */ /******************************************/ CREATE TABLE `config_tags_relation` ( `id` bigint(20) NOT NULL COMMENT \u0026#39;id\u0026#39;, `tag_name` varchar(128) NOT NULL COMMENT \u0026#39;tag_name\u0026#39;, `tag_type` varchar(64) DEFAULT NULL COMMENT \u0026#39;tag_type\u0026#39;, `data_id` varchar(255) NOT NULL COMMENT \u0026#39;data_id\u0026#39;, `group_id` varchar(128) NOT NULL COMMENT \u0026#39;group_id\u0026#39;, `tenant_id` varchar(128) DEFAULT \u0026#39;\u0026#39; COMMENT \u0026#39;tenant_id\u0026#39;, `nid` bigint(20) NOT NULL AUTO_INCREMENT, PRIMARY KEY (`nid`), UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=\u0026#39;config_tag_relation\u0026#39;; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = group_capacity */ /******************************************/ CREATE TABLE `group_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT \u0026#39;主键ID\u0026#39;, `group_id` varchar(128) NOT NULL DEFAULT \u0026#39;\u0026#39; COMMENT \u0026#39;Group ID，空字符表示整个集群\u0026#39;, `quota` int(10) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;配额，0表示使用默认值\u0026#39;, `usage` int(10) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;使用量\u0026#39;, `max_size` int(10) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;单个配置大小上限，单位为字节，0表示使用默认值\u0026#39;, `max_aggr_count` int(10) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;聚合子配置最大个数，，0表示使用默认值\u0026#39;, `max_aggr_size` int(10) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;单个聚合数据的子配置大小上限，单位为字节，0表示使用默认值\u0026#39;, `max_history_count` int(10) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;最大变更历史数量\u0026#39;, `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;创建时间\u0026#39;, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;修改时间\u0026#39;, PRIMARY KEY (`id`), UNIQUE KEY `uk_group_id` (`group_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=\u0026#39;集群、各Group容量信息表\u0026#39;; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = his_config_info */ /******************************************/ CREATE TABLE `his_config_info` ( `id` bigint(64) unsigned NOT NULL, `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `data_id` varchar(255) NOT NULL, `group_id` varchar(128) NOT NULL, `app_name` varchar(128) DEFAULT NULL COMMENT \u0026#39;app_name\u0026#39;, `content` longtext NOT NULL, `md5` varchar(32) DEFAULT NULL, `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `src_user` text, `src_ip` varchar(50) DEFAULT NULL, `op_type` char(10) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT \u0026#39;\u0026#39; COMMENT \u0026#39;租户字段\u0026#39;, PRIMARY KEY (`nid`), KEY `idx_gmt_create` (`gmt_create`), KEY `idx_gmt_modified` (`gmt_modified`), KEY `idx_did` (`data_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=\u0026#39;多租户改造\u0026#39;; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = tenant_capacity */ /******************************************/ CREATE TABLE `tenant_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT \u0026#39;主键ID\u0026#39;, `tenant_id` varchar(128) NOT NULL DEFAULT \u0026#39;\u0026#39; COMMENT \u0026#39;Tenant ID\u0026#39;, `quota` int(10) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;配额，0表示使用默认值\u0026#39;, `usage` int(10) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;使用量\u0026#39;, `max_size` int(10) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;单个配置大小上限，单位为字节，0表示使用默认值\u0026#39;, `max_aggr_count` int(10) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;聚合子配置最大个数\u0026#39;, `max_aggr_size` int(10) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;单个聚合数据的子配置大小上限，单位为字节，0表示使用默认值\u0026#39;, `max_history_count` int(10) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;最大变更历史数量\u0026#39;, `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;创建时间\u0026#39;, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;修改时间\u0026#39;, PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=\u0026#39;租户容量信息表\u0026#39;; CREATE TABLE `tenant_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;id\u0026#39;, `kp` varchar(128) NOT NULL COMMENT \u0026#39;kp\u0026#39;, `tenant_id` varchar(128) default \u0026#39;\u0026#39; COMMENT \u0026#39;tenant_id\u0026#39;, `tenant_name` varchar(128) default \u0026#39;\u0026#39; COMMENT \u0026#39;tenant_name\u0026#39;, `tenant_desc` varchar(256) DEFAULT NULL COMMENT \u0026#39;tenant_desc\u0026#39;, `create_source` varchar(32) DEFAULT NULL COMMENT \u0026#39;create_source\u0026#39;, `gmt_create` bigint(20) NOT NULL COMMENT \u0026#39;创建时间\u0026#39;, `gmt_modified` bigint(20) NOT NULL COMMENT \u0026#39;修改时间\u0026#39;, PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=\u0026#39;tenant_info\u0026#39;; CREATE TABLE `users` ( `username` varchar(50) NOT NULL PRIMARY KEY, `password` varchar(500) NOT NULL, `enabled` boolean NOT NULL ); CREATE TABLE `roles` ( `username` varchar(50) NOT NULL, `role` varchar(50) NOT NULL, UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE ); CREATE TABLE `permissions` ( `role` varchar(50) NOT NULL, `resource` varchar(255) NOT NULL, `action` varchar(8) NOT NULL, UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE ); INSERT INTO users (username, password, enabled) VALUES (\u0026#39;nacos\u0026#39;, \u0026#39;$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu\u0026#39;, TRUE); INSERT INTO roles (username, role) VALUES (\u0026#39;nacos\u0026#39;, \u0026#39;ROLE_ADMIN\u0026#39;); Copied! 2.2.下载nacos nacos在GitHub上有下载地址：https://github.com/alibaba/nacos/tags，可以选择任意版本下载。\n本例中才用1.4.1版本：\n2.3.配置Nacos 将这个包解压到任意非中文目录下，如图：\n目录说明：\nbin：启动脚本 conf：配置文件 进入nacos的conf目录，修改配置文件cluster.conf.example，重命名为cluster.conf：\n然后添加内容：\n1 2 3 127.0.0.1:8845 127.0.0.1.8846 127.0.0.1.8847 Copied! 然后修改application.properties文件，添加数据库配置\n1 2 3 4 5 6 7 spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8\u0026amp;connectTimeout=1000\u0026amp;socketTimeout=3000\u0026amp;autoReconnect=true\u0026amp;useUnicode=true\u0026amp;useSSL=false\u0026amp;serverTimezone=UTC db.user.0=root db.password.0=123 Copied! 2.4.启动 将nacos文件夹复制三份，分别命名为：nacos1、nacos2、nacos3\n然后分别修改三个文件夹中的application.properties，\nnacos1:\n1 server.port=8845 Copied! nacos2:\n1 server.port=8846 Copied! nacos3:\n1 server.port=8847 Copied! 然后分别启动三个nacos节点：\n1 startup.cmd Copied! 2.5.nginx反向代理 找到课前资料提供的nginx安装包：\n解压到任意非中文目录下：\n修改conf/nginx.conf文件，配置如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 upstream nacos-cluster { server 127.0.0.1:8845; server 127.0.0.1:8846; server 127.0.0.1:8847; } server { listen 80; server_name localhost; location /nacos { proxy_pass http://nacos-cluster; } } Copied! 而后在浏览器访问：http://localhost/nacos即可。\n代码中application.yml文件配置如下：\n1 2 3 4 spring: cloud: nacos: server-addr: localhost:80 # Nacos地址 Copied! 2.6.优化 实际部署时，需要给做反向代理的nginx服务器设置一个域名，这样后续如果有服务器迁移nacos的客户端也无需更改配置.\nNacos的各个节点应该部署到多个不同服务器，做好容灾和隔离\n","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/956118f5/","title":"nacos集群搭建"},{"content":" RabbitMQ部署指南 1.单机部署 我们在Centos7虚拟机中使用Docker来安装。\n1.1.下载镜像 方式一：在线拉取\n1 docker pull rabbitmq:3-management Copied! 方式二：从本地加载\n在课前资料已经提供了镜像包：\n上传到虚拟机中后，使用命令加载镜像即可：\n1 docker load -i mq.tar Copied! 1.2.安装MQ 执行下面的命令来运行MQ容器：\n1 2 3 4 5 6 7 8 9 docker run \\ -e RABBITMQ_DEFAULT_USER=itcast \\ -e RABBITMQ_DEFAULT_PASS=123321 \\ --name mq \\ --hostname mq1 \\ -p 15672:15672 \\ -p 5672:5672 \\ -d \\ rabbitmq:3-management Copied! 2.集群部署 接下来，我们看看如何安装RabbitMQ的集群。\n2.1.集群分类 在RabbitMQ的官方文档中，讲述了两种集群的配置方式：\n普通模式：普通模式集群不进行数据同步，每个MQ都有自己的队列、数据信息（其它元数据信息如交换机等会同步）。例如我们有2个MQ：mq1，和mq2，如果你的消息在mq1，而你连接到了mq2，那么mq2会去mq1拉取消息，然后返回给你。如果mq1宕机，消息就会丢失。 镜像模式：与普通模式不同，队列会在各个mq的镜像节点之间同步，因此你连接到任何一个镜像节点，均可获取到消息。而且如果一个节点宕机，并不会导致数据丢失。不过，这种方式增加了数据同步的带宽消耗。 我们先来看普通模式集群。\n2.2.设置网络 首先，我们需要让3台MQ互相知道对方的存在。\n分别在3台机器中，设置 /etc/hosts文件，添加如下内容：\n1 2 3 192.168.150.101 mq1 192.168.150.102 mq2 192.168.150.103 mq3 Copied! 并在每台机器上测试，是否可以ping通对方：\n","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/1784e117/","title":"RabbitMQ部署指南1"},{"content":" RabbitMQ部署指南 1.单机部署 我们在Centos7虚拟机中使用Docker来安装。\n1.1.下载镜像 方式一：在线拉取\n1 docker pull rabbitmq:3.8-management Copied! 方式二：从本地加载\n在课前资料已经提供了镜像包：\n上传到虚拟机中后，使用命令加载镜像即可：\n1 docker load -i mq.tar Copied! 1.2.安装MQ 执行下面的命令来运行MQ容器：\n1 2 3 4 5 6 7 8 9 10 docker run \\ -e RABBITMQ_DEFAULT_USER=itcast \\ -e RABBITMQ_DEFAULT_PASS=123321 \\ -v mq-plugins:/plugins \\ --name mq \\ --hostname mq1 \\ -p 15672:15672 \\ -p 5672:5672 \\ -d \\ rabbitmq:3.8-management Copied! 2.安装DelayExchange插件 官方的安装指南地址为：https://blog.rabbitmq.com/posts/2015/04/scheduling-messages-with-rabbitmq\n上述文档是基于linux原生安装RabbitMQ，然后安装插件。\n因为我们之前是基于Docker安装RabbitMQ，所以下面我们会讲解基于Docker来安装RabbitMQ插件。\n2.1.下载插件 RabbitMQ有一个官方的插件社区，地址为：https://www.rabbitmq.com/community-plugins.html\n其中包含各种各样的插件，包括我们要使用的DelayExchange插件：\n大家可以去对应的GitHub页面下载3.8.9版本的插件，地址为https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/3.8.9这个对应RabbitMQ的3.8.5以上版本。\n课前资料也提供了下载好的插件：\n2.2.上传插件 因为我们是基于Docker安装，所以需要先查看RabbitMQ的插件目录对应的数据卷。如果不是基于Docker的同学，请参考第一章部分，重新创建Docker容器。\n我们之前设定的RabbitMQ的数据卷名称为mq-plugins，所以我们使用下面命令查看数据卷：\n1 docker volume inspect mq-plugins Copied! 可以得到下面结果：\n接下来，将插件上传到这个目录即可：\n2.3.安装插件 最后就是安装了，需要进入MQ容器内部来执行安装。我的容器名为mq，所以执行下面命令：\n1 docker exec -it mq bash Copied! 执行时，请将其中的 -it 后面的mq替换为你自己的容器名.\n进入容器内部后，执行下面命令开启插件：\n1 rabbitmq-plugins enable rabbitmq_delayed_message_exchange Copied! 结果如下：\n3.集群部署 接下来，我们看看如何安装RabbitMQ的集群。\n2.1.集群分类 在RabbitMQ的官方文档中，讲述了两种集群的配置方式：\n普通模式：普通模式集群不进行数据同步，每个MQ都有自己的队列、数据信息（其它元数据信息如交换机等会同步）。例如我们有2个MQ：mq1，和mq2，如果你的消息在mq1，而你连接到了mq2，那么mq2会去mq1拉取消息，然后返回给你。如果mq1宕机，消息就会丢失。 镜像模式：与普通模式不同，队列会在各个mq的镜像节点之间同步，因此你连接到任何一个镜像节点，均可获取到消息。而且如果一个节点宕机，并不会导致数据丢失。不过，这种方式增加了数据同步的带宽消耗。 我们先来看普通模式集群，我们的计划部署3节点的mq集群：\n主机名 控制台端口 amqp通信端口 mq1 8081 \u0026mdash;\u0026gt; 15672 8071 \u0026mdash;\u0026gt; 5672 mq2 8082 \u0026mdash;\u0026gt; 15672 8072 \u0026mdash;\u0026gt; 5672 mq3 8083 \u0026mdash;\u0026gt; 15672 8073 \u0026mdash;\u0026gt; 5672 集群中的节点标示默认都是：rabbit@[hostname]，因此以上三个节点的名称分别为：\nrabbit@mq1 rabbit@mq2 rabbit@mq3 2.2.获取cookie RabbitMQ底层依赖于Erlang，而Erlang虚拟机就是一个面向分布式的语言，默认就支持集群模式。集群模式中的每个RabbitMQ 节点使用 cookie 来确定它们是否被允许相互通信。\n要使两个节点能够通信，它们必须具有相同的共享秘密，称为Erlang cookie。cookie 只是一串最多 255 个字符的字母数字字符。\n每个集群节点必须具有相同的 cookie。实例之间也需要它来相互通信。\n我们先在之前启动的mq容器中获取一个cookie值，作为集群的cookie。执行下面的命令：\n1 docker exec -it mq cat /var/lib/rabbitmq/.erlang.cookie Copied! 可以看到cookie值如下：\n1 FXZMCVGLBIXZCDEMMVZQ Copied! 接下来，停止并删除当前的mq容器，我们重新搭建集群。\n1 docker rm -f mq Copied! 2.3.准备集群配置 在/tmp目录新建一个配置文件 rabbitmq.conf：\n1 2 3 cd /tmp # 创建文件 touch rabbitmq.conf Copied! 文件内容如下：\n1 2 3 4 5 6 loopback_users.guest = false listeners.tcp.default = 5672 cluster_formation.peer_discovery_backend = rabbit_peer_discovery_classic_config cluster_formation.classic_config.nodes.1 = rabbit@mq1 cluster_formation.classic_config.nodes.2 = rabbit@mq2 cluster_formation.classic_config.nodes.3 = rabbit@mq3 Copied! 再创建一个文件，记录cookie\n1 2 3 4 5 6 7 cd /tmp # 创建cookie文件 touch .erlang.cookie # 写入cookie echo \u0026#34;FXZMCVGLBIXZCDEMMVZQ\u0026#34; \u0026gt; .erlang.cookie # 修改cookie文件的权限 chmod 600 .erlang.cookie Copied! 准备三个目录,mq1、mq2、mq3：\n1 2 3 cd /tmp # 创建目录 mkdir mq1 mq2 mq3 Copied! 然后拷贝rabbitmq.conf、cookie文件到mq1、mq2、mq3：\n1 2 3 4 5 6 7 8 9 # 进入/tmp cd /tmp # 拷贝 cp rabbitmq.conf mq1 cp rabbitmq.conf mq2 cp rabbitmq.conf mq3 cp .erlang.cookie mq1 cp .erlang.cookie mq2 cp .erlang.cookie mq3 Copied! 2.4.启动集群 创建一个网络：\n1 docker network create mq-net Copied! docker volume create\n运行命令\n1 2 3 4 5 6 7 8 9 10 docker run -d --net mq-net \\ -v ${PWD}/mq1/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \\ -v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \\ -e RABBITMQ_DEFAULT_USER=itcast \\ -e RABBITMQ_DEFAULT_PASS=123321 \\ --name mq1 \\ --hostname mq1 \\ -p 8071:5672 \\ -p 8081:15672 \\ rabbitmq:3.8-management Copied! 1 2 3 4 5 6 7 8 9 10 docker run -d --net mq-net \\ -v ${PWD}/mq2/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \\ -v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \\ -e RABBITMQ_DEFAULT_USER=itcast \\ -e RABBITMQ_DEFAULT_PASS=123321 \\ --name mq2 \\ --hostname mq2 \\ -p 8072:5672 \\ -p 8082:15672 \\ rabbitmq:3.8-management Copied! 1 2 3 4 5 6 7 8 9 10 docker run -d --net mq-net \\ -v ${PWD}/mq3/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \\ -v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \\ -e RABBITMQ_DEFAULT_USER=itcast \\ -e RABBITMQ_DEFAULT_PASS=123321 \\ --name mq3 \\ --hostname mq3 \\ -p 8073:5672 \\ -p 8083:15672 \\ rabbitmq:3.8-management Copied! 2.5.测试 在mq1这个节点上添加一个队列：\n如图，在mq2和mq3两个控制台也都能看到：\n2.5.1.数据共享测试 点击这个队列，进入管理页面：\n然后利用控制台发送一条消息到这个队列：\n结果在mq2、mq3上都能看到这条消息：\n2.5.2.可用性测试 我们让其中一台节点mq1宕机：\n1 docker stop mq1 Copied! 然后登录mq2或mq3的控制台，发现simple.queue也不可用了：\n说明数据并没有拷贝到mq2和mq3。\n4.镜像模式 在刚刚的案例中，一旦创建队列的主机宕机，队列就会不可用。不具备高可用能力。如果要解决这个问题，必须使用官方提供的镜像集群方案。\n官方文档地址：https://www.rabbitmq.com/ha.html\n4.1.镜像模式的特征 默认情况下，队列只保存在创建该队列的节点上。而镜像模式下，创建队列的节点被称为该队列的主节点，队列还会拷贝到集群中的其它节点，也叫做该队列的镜像节点。\n但是，不同队列可以在集群中的任意节点上创建，因此不同队列的主节点可以不同。甚至，一个队列的主节点可能是另一个队列的镜像节点。\n用户发送给队列的一切请求，例如发送消息、消息回执默认都会在主节点完成，如果是从节点接收到请求，也会路由到主节点去完成。镜像节点仅仅起到备份数据作用。\n当主节点接收到消费者的ACK时，所有镜像都会删除节点中的数据。\n总结如下：\n镜像队列结构是一主多从（从就是镜像） 所有操作都是主节点完成，然后同步给镜像节点 主宕机后，镜像节点会替代成新的主（如果在主从同步完成前，主就已经宕机，可能出现数据丢失） 不具备负载均衡功能，因为所有操作都会有主节点完成（但是不同队列，其主节点可以不同，可以利用这个提高吞吐量） 4.2.镜像模式的配置 镜像模式的配置有3种模式：\nha-mode ha-params 效果 准确模式exactly 队列的副本量count 集群中队列副本（主服务器和镜像服务器之和）的数量。count如果为1意味着单个副本：即队列主节点。count值为2表示2个副本：1个队列主和1个队列镜像。换句话说：count = 镜像数量 + 1。如果群集中的节点数少于count，则该队列将镜像到所有节点。如果有集群总数大于count+1，并且包含镜像的节点出现故障，则将在另一个节点上创建一个新的镜像。 all (none) 队列在群集中的所有节点之间进行镜像。队列将镜像到任何新加入的节点。镜像到所有节点将对所有群集节点施加额外的压力，包括网络I / O，磁盘I / O和磁盘空间使用情况。推荐使用exactly，设置副本数为（N / 2 +1）。 nodes node names 指定队列创建到哪些节点，如果指定的节点全部不存在，则会出现异常。如果指定的节点在集群中存在，但是暂时不可用，会创建节点到当前客户端连接到的节点。 这里我们以rabbitmqctl命令作为案例来讲解配置语法。\n语法示例：\n4.2.1.exactly模式 1 rabbitmqctl set_policy ha-two \u0026#34;^two\\.\u0026#34; \u0026#39;{\u0026#34;ha-mode\u0026#34;:\u0026#34;exactly\u0026#34;,\u0026#34;ha-params\u0026#34;:2,\u0026#34;ha-sync-mode\u0026#34;:\u0026#34;automatic\u0026#34;}\u0026#39; Copied! rabbitmqctl set_policy：固定写法 ha-two：策略名称，自定义 \u0026quot;^two\\.\u0026quot;：匹配队列的正则表达式，符合命名规则的队列才生效，这里是任何以two.开头的队列名称 '{\u0026quot;ha-mode\u0026quot;:\u0026quot;exactly\u0026quot;,\u0026quot;ha-params\u0026quot;:2,\u0026quot;ha-sync-mode\u0026quot;:\u0026quot;automatic\u0026quot;}': 策略内容 \u0026quot;ha-mode\u0026quot;:\u0026quot;exactly\u0026quot;：策略模式，此处是exactly模式，指定副本数量 \u0026quot;ha-params\u0026quot;:2：策略参数，这里是2，就是副本数量为2，1主1镜像 \u0026quot;ha-sync-mode\u0026quot;:\u0026quot;automatic\u0026quot;：同步策略，默认是manual，即新加入的镜像节点不会同步旧的消息。如果设置为automatic，则新加入的镜像节点会把主节点中所有消息都同步，会带来额外的网络开销 4.2.2.all模式 1 rabbitmqctl set_policy ha-all \u0026#34;^all\\.\u0026#34; \u0026#39;{\u0026#34;ha-mode\u0026#34;:\u0026#34;all\u0026#34;}\u0026#39; Copied! ha-all：策略名称，自定义 \u0026quot;^all\\.\u0026quot;：匹配所有以all.开头的队列名 '{\u0026quot;ha-mode\u0026quot;:\u0026quot;all\u0026quot;}'：策略内容 \u0026quot;ha-mode\u0026quot;:\u0026quot;all\u0026quot;：策略模式，此处是all模式，即所有节点都会称为镜像节点 4.2.3.nodes模式 1 rabbitmqctl set_policy ha-nodes \u0026#34;^nodes\\.\u0026#34; \u0026#39;{\u0026#34;ha-mode\u0026#34;:\u0026#34;nodes\u0026#34;,\u0026#34;ha-params\u0026#34;:[\u0026#34;rabbit@nodeA\u0026#34;, \u0026#34;rabbit@nodeB\u0026#34;]}\u0026#39; Copied! rabbitmqctl set_policy：固定写法 ha-nodes：策略名称，自定义 \u0026quot;^nodes\\.\u0026quot;：匹配队列的正则表达式，符合命名规则的队列才生效，这里是任何以nodes.开头的队列名称 '{\u0026quot;ha-mode\u0026quot;:\u0026quot;nodes\u0026quot;,\u0026quot;ha-params\u0026quot;:[\u0026quot;rabbit@nodeA\u0026quot;, \u0026quot;rabbit@nodeB\u0026quot;]}': 策略内容 \u0026quot;ha-mode\u0026quot;:\u0026quot;nodes\u0026quot;：策略模式，此处是nodes模式 \u0026quot;ha-params\u0026quot;:[\u0026quot;rabbit@mq1\u0026quot;, \u0026quot;rabbit@mq2\u0026quot;]：策略参数，这里指定副本所在节点名称 4.3.测试 我们使用exactly模式的镜像，因为集群节点数量为3，因此镜像数量就设置为2.\n运行下面的命令：\n1 docker exec -it mq1 rabbitmqctl set_policy ha-two \u0026#34;^two\\.\u0026#34; \u0026#39;{\u0026#34;ha-mode\u0026#34;:\u0026#34;exactly\u0026#34;,\u0026#34;ha-params\u0026#34;:2,\u0026#34;ha-sync-mode\u0026#34;:\u0026#34;automatic\u0026#34;}\u0026#39; Copied! 下面，我们创建一个新的队列：\n在任意一个mq控制台查看队列：\n4.3.1.测试数据共享 给two.queue发送一条消息：\n然后在mq1、mq2、mq3的任意控制台查看消息：\n4.3.2.测试高可用 现在，我们让two.queue的主节点mq1宕机：\n1 docker stop mq1 Copied! 查看集群状态：\n查看队列状态：\n发现依然是健康的！并且其主节点切换到了rabbit@mq2上\n5.仲裁队列 从RabbitMQ 3.8版本开始，引入了新的仲裁队列，他具备与镜像队里类似的功能，但使用更加方便。\n5.1.添加仲裁队列 在任意控制台添加一个队列，一定要选择队列类型为Quorum类型。\n在任意控制台查看队列：\n可以看到，仲裁队列的 + 2字样。代表这个队列有2个镜像节点。\n因为仲裁队列默认的镜像数为5。如果你的集群有7个节点，那么镜像数肯定是5；而我们集群只有3个节点，因此镜像数量就是3.\n5.2.测试 可以参考对镜像集群的测试，效果是一样的。\n5.3.集群扩容 5.3.1.加入集群 1）启动一个新的MQ容器：\n1 2 3 4 5 6 7 8 9 docker run -d --net mq-net \\ -v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \\ -e RABBITMQ_DEFAULT_USER=itcast \\ -e RABBITMQ_DEFAULT_PASS=123321 \\ --name mq4 \\ --hostname mq5 \\ -p 8074:15672 \\ -p 8084:15672 \\ rabbitmq:3.8-management Copied! 2）进入容器控制台：\n1 docker exec -it mq4 bash Copied! 3）停止mq进程\n1 rabbitmqctl stop_app Copied! 4）重置RabbitMQ中的数据：\n1 rabbitmqctl reset Copied! 5）加入mq1：\n1 rabbitmqctl join_cluster rabbit@mq1 Copied! 6）再次启动mq进程\n1 rabbitmqctl start_app Copied! 5.3.2.增加仲裁队列副本 我们先查看下quorum.queue这个队列目前的副本情况，进入mq1容器：\n1 docker exec -it mq1 bash Copied! 执行命令：\n1 rabbitmq-queues quorum_status \u0026#34;quorum.queue\u0026#34; Copied! 结果：\n现在，我们让mq4也加入进来：\n1 rabbitmq-queues add_member \u0026#34;quorum.queue\u0026#34; \u0026#34;rabbit@mq4\u0026#34; Copied! 结果：\n再次查看：\n1 rabbitmq-queues quorum_status \u0026#34;quorum.queue\u0026#34; Copied! 查看控制台，发现quorum.queue的镜像数量也从原来的 +2 变成了 +3：\n","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/45yhgx45/","title":"RabbitMQ部署指南2"},{"content":" 安装elasticsearch 1.部署单点es 1.1.创建网络 因为我们还需要部署kibana容器，因此需要让es和kibana容器互联。这里先创建一个网络：\n1 docker network create es-net Copied! 1.2.加载镜像 这里我们采用elasticsearch的7.12.1版本的镜像，这个镜像体积非常大，接近1G。不建议大家自己pull。\n课前资料提供了镜像的tar包：\n大家将其上传到虚拟机中，然后运行命令加载即可：\n1 2 # 导入数据 docker load -i es.tar Copied! 同理还有kibana的tar包也需要这样做。\n1.3.运行 运行docker命令，部署单点es：\n1 2 3 4 5 6 7 8 9 10 11 docker run -d \\ --name es \\ -e \u0026#34;ES_JAVA_OPTS=-Xms512m -Xmx512m\u0026#34; \\ -e \u0026#34;discovery.type=single-node\u0026#34; \\ -v es-data:/usr/share/elasticsearch/data \\ -v es-plugins:/usr/share/elasticsearch/plugins \\ --privileged \\ --network es-net \\ -p 9200:9200 \\ -p 9300:9300 \\ elasticsearch:7.12.1 Copied! 命令解释：\n-e \u0026quot;cluster.name=es-docker-cluster\u0026quot;：设置集群名称 -e \u0026quot;http.host=0.0.0.0\u0026quot;：监听的地址，可以外网访问 -e \u0026quot;ES_JAVA_OPTS=-Xms512m -Xmx512m\u0026quot;：内存大小 -e \u0026quot;discovery.type=single-node\u0026quot;：非集群模式 -v es-data:/usr/share/elasticsearch/data：挂载逻辑卷，绑定es的数据目录 -v es-logs:/usr/share/elasticsearch/logs：挂载逻辑卷，绑定es的日志目录 -v es-plugins:/usr/share/elasticsearch/plugins：挂载逻辑卷，绑定es的插件目录 --privileged：授予逻辑卷访问权 --network es-net ：加入一个名为es-net的网络中 -p 9200:9200：端口映射配置 在浏览器中输入：http://192.168.150.101:9200 即可看到elasticsearch的响应结果：\n2.部署kibana kibana可以给我们提供一个elasticsearch的可视化界面，便于我们学习。\n2.1.部署 运行docker命令，部署kibana\n1 2 3 4 5 6 docker run -d \\ --name kibana \\ -e ELASTICSEARCH_HOSTS=http://es:9200 \\ --network=es-net \\ -p 5601:5601 \\ kibana:7.12.1 Copied! --network es-net ：加入一个名为es-net的网络中，与elasticsearch在同一个网络中 -e ELASTICSEARCH_HOSTS=http://es:9200\u0026quot;：设置elasticsearch的地址，因为kibana已经与elasticsearch在一个网络，因此可以用容器名直接访问elasticsearch -p 5601:5601：端口映射配置 kibana启动一般比较慢，需要多等待一会，可以通过命令：\n1 docker logs -f kibana Copied! 查看运行日志，当查看到下面的日志，说明成功：\n此时，在浏览器输入地址访问：http://192.168.150.101:5601，即可看到结果\n2.2.DevTools kibana中提供了一个DevTools界面：\n这个界面中可以编写DSL来操作elasticsearch。并且对DSL语句有自动补全功能。\n3.安装IK分词器 3.1.在线安装ik插件（较慢） 1 2 3 4 5 6 7 8 9 10 # 进入容器内部 docker exec -it elasticsearch /bin/bash # 在线下载并安装 ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip #退出 exit #重启容器 docker restart elasticsearch Copied! 3.2.离线安装ik插件（推荐） 1）查看数据卷目录 安装插件需要知道elasticsearch的plugins目录位置，而我们用了数据卷挂载，因此需要查看elasticsearch的数据卷目录，通过下面命令查看:\n1 docker volume inspect es-plugins Copied! 显示结果：\n1 2 3 4 5 6 7 8 9 10 11 [ { \u0026#34;CreatedAt\u0026#34;: \u0026#34;2022-05-06T10:06:34+08:00\u0026#34;, \u0026#34;Driver\u0026#34;: \u0026#34;local\u0026#34;, \u0026#34;Labels\u0026#34;: null, \u0026#34;Mountpoint\u0026#34;: \u0026#34;/var/lib/docker/volumes/es-plugins/_data\u0026#34;, \u0026#34;Name\u0026#34;: \u0026#34;es-plugins\u0026#34;, \u0026#34;Options\u0026#34;: null, \u0026#34;Scope\u0026#34;: \u0026#34;local\u0026#34; } ] Copied! 说明plugins目录被挂载到了：/var/lib/docker/volumes/es-plugins/_data 这个目录中。\n2）解压缩分词器安装包 下面我们需要把课前资料中的ik分词器解压缩，重命名为ik\n3）上传到es容器的插件数据卷中 也就是/var/lib/docker/volumes/es-plugins/_data ：\n4）重启容器 1 2 # 4、重启容器 docker restart es Copied! 1 2 # 查看es日志 docker logs -f es Copied! 5）测试： IK分词器包含两种模式：\nik_smart：最少切分\nik_max_word：最细切分\n1 2 3 4 5 GET /_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;黑马程序员学习java太棒了\u0026#34; } Copied! 结果：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 { \u0026#34;tokens\u0026#34; : [ { \u0026#34;token\u0026#34; : \u0026#34;黑马\u0026#34;, \u0026#34;start_offset\u0026#34; : 0, \u0026#34;end_offset\u0026#34; : 2, \u0026#34;type\u0026#34; : \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34; : 0 }, { \u0026#34;token\u0026#34; : \u0026#34;程序员\u0026#34;, \u0026#34;start_offset\u0026#34; : 2, \u0026#34;end_offset\u0026#34; : 5, \u0026#34;type\u0026#34; : \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34; : 1 }, { \u0026#34;token\u0026#34; : \u0026#34;程序\u0026#34;, \u0026#34;start_offset\u0026#34; : 2, \u0026#34;end_offset\u0026#34; : 4, \u0026#34;type\u0026#34; : \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34; : 2 }, { \u0026#34;token\u0026#34; : \u0026#34;员\u0026#34;, \u0026#34;start_offset\u0026#34; : 4, \u0026#34;end_offset\u0026#34; : 5, \u0026#34;type\u0026#34; : \u0026#34;CN_CHAR\u0026#34;, \u0026#34;position\u0026#34; : 3 }, { \u0026#34;token\u0026#34; : \u0026#34;学习\u0026#34;, \u0026#34;start_offset\u0026#34; : 5, \u0026#34;end_offset\u0026#34; : 7, \u0026#34;type\u0026#34; : \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34; : 4 }, { \u0026#34;token\u0026#34; : \u0026#34;java\u0026#34;, \u0026#34;start_offset\u0026#34; : 7, \u0026#34;end_offset\u0026#34; : 11, \u0026#34;type\u0026#34; : \u0026#34;ENGLISH\u0026#34;, \u0026#34;position\u0026#34; : 5 }, { \u0026#34;token\u0026#34; : \u0026#34;太棒了\u0026#34;, \u0026#34;start_offset\u0026#34; : 11, \u0026#34;end_offset\u0026#34; : 14, \u0026#34;type\u0026#34; : \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34; : 6 }, { \u0026#34;token\u0026#34; : \u0026#34;太棒\u0026#34;, \u0026#34;start_offset\u0026#34; : 11, \u0026#34;end_offset\u0026#34; : 13, \u0026#34;type\u0026#34; : \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34; : 7 }, { \u0026#34;token\u0026#34; : \u0026#34;了\u0026#34;, \u0026#34;start_offset\u0026#34; : 13, \u0026#34;end_offset\u0026#34; : 14, \u0026#34;type\u0026#34; : \u0026#34;CN_CHAR\u0026#34;, \u0026#34;position\u0026#34; : 8 } ] } Copied! 3.3 扩展词词典 随着互联网的发展，“造词运动”也越发的频繁。出现了很多新的词语，在原有的词汇列表中并不存在。比如：“奥力给”，“传智播客” 等。\n所以我们的词汇也需要不断的更新，IK分词器提供了扩展词汇的功能。\n1）打开IK分词器config目录：\n2）在IKAnalyzer.cfg.xml配置文件内容添加：\n1 2 3 4 5 6 7 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE properties SYSTEM \u0026#34;http://java.sun.com/dtd/properties.dtd\u0026#34;\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;comment\u0026gt;IK Analyzer 扩展配置\u0026lt;/comment\u0026gt; \u0026lt;!--用户可以在这里配置自己的扩展字典 *** 添加扩展词典--\u0026gt; \u0026lt;entry key=\u0026#34;ext_dict\u0026#34;\u0026gt;ext.dic\u0026lt;/entry\u0026gt; \u0026lt;/properties\u0026gt; Copied! 3）新建一个 ext.dic，可以参考config目录下复制一个配置文件进行修改\n1 2 传智播客 奥力给 Copied! 4）重启elasticsearch\n1 2 3 4 docker restart es # 查看 日志 docker logs -f elasticsearch Copied! 日志中已经成功加载ext.dic配置文件\n5）测试效果：\n1 2 3 4 5 GET /_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;传智播客Java就业超过90%,奥力给！\u0026#34; } Copied! 注意当前文件的编码必须是 UTF-8 格式，严禁使用Windows记事本编辑\n3.4 停用词词典 在互联网项目中，在网络间传输的速度很快，所以很多语言是不允许在网络上传递的，如：关于宗教、政治等敏感词语，那么我们在搜索时也应该忽略当前词汇。\nIK分词器也提供了强大的停用词功能，让我们在索引时就直接忽略当前的停用词汇表中的内容。\n1）IKAnalyzer.cfg.xml配置文件内容添加：\n1 2 3 4 5 6 7 8 9 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE properties SYSTEM \u0026#34;http://java.sun.com/dtd/properties.dtd\u0026#34;\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;comment\u0026gt;IK Analyzer 扩展配置\u0026lt;/comment\u0026gt; \u0026lt;!--用户可以在这里配置自己的扩展字典--\u0026gt; \u0026lt;entry key=\u0026#34;ext_dict\u0026#34;\u0026gt;ext.dic\u0026lt;/entry\u0026gt; \u0026lt;!--用户可以在这里配置自己的扩展停止词字典 *** 添加停用词词典--\u0026gt; \u0026lt;entry key=\u0026#34;ext_stopwords\u0026#34;\u0026gt;stopword.dic\u0026lt;/entry\u0026gt; \u0026lt;/properties\u0026gt; Copied! 3）在 stopword.dic 添加停用词\n1 习大大 Copied! 4）重启elasticsearch\n1 2 3 4 5 6 # 重启服务 docker restart elasticsearch docker restart kibana # 查看 日志 docker logs -f elasticsearch Copied! 日志中已经成功加载stopword.dic配置文件\n5）测试效果：\n1 2 3 4 5 GET /_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;传智播客Java就业率超过95%,习大大都点赞,奥力给！\u0026#34; } Copied! 注意当前文件的编码必须是 UTF-8 格式，严禁使用Windows记事本编辑\n4.部署es集群 部署es集群可以直接使用docker-compose来完成，不过要求你的Linux虚拟机至少有4G的内存空间\n首先编写一个docker-compose文件，内容如下：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 version: \u0026#39;2.2\u0026#39; services: es01: image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1 container_name: es01 environment: - node.name=es01 - cluster.name=es-docker-cluster - discovery.seed_hosts=es02,es03 - cluster.initial_master_nodes=es01,es02,es03 - bootstrap.memory_lock=true - \u0026#34;ES_JAVA_OPTS=-Xms512m -Xmx512m\u0026#34; ulimits: memlock: soft: -1 hard: -1 volumes: - data01:/usr/share/elasticsearch/data ports: - 9200:9200 networks: - elastic es02: image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1 container_name: es02 environment: - node.name=es02 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es03 - cluster.initial_master_nodes=es01,es02,es03 - bootstrap.memory_lock=true - \u0026#34;ES_JAVA_OPTS=-Xms512m -Xmx512m\u0026#34; ulimits: memlock: soft: -1 hard: -1 volumes: - data02:/usr/share/elasticsearch/data networks: - elastic es03: image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1 container_name: es03 environment: - node.name=es03 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es02 - cluster.initial_master_nodes=es01,es02,es03 - bootstrap.memory_lock=true - \u0026#34;ES_JAVA_OPTS=-Xms512m -Xmx512m\u0026#34; ulimits: memlock: soft: -1 hard: -1 volumes: - data03:/usr/share/elasticsearch/data networks: - elastic volumes: data01: driver: local data02: driver: local data03: driver: local networks: elastic: driver: bridge Copied! Run docker-compose to bring up the cluster:\n1 docker-compose up Copied! ","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/34a16834/","title":"安装elasticsearch1"},{"content":" 安装elasticsearch 1.部署单点es 1.1.创建网络 因为我们还需要部署kibana容器，因此需要让es和kibana容器互联。这里先创建一个网络：\n1 docker network create es-net Copied! 1.2.加载镜像 这里我们采用elasticsearch的7.12.1版本的镜像，这个镜像体积非常大，接近1G。不建议大家自己pull。\n课前资料提供了镜像的tar包：\n大家将其上传到虚拟机中，然后运行命令加载即可：\n1 2 # 导入数据 docker load -i es.tar Copied! 同理还有kibana的tar包也需要这样做。\n1.3.运行 运行docker命令，部署单点es：\n1 2 3 4 5 6 7 8 9 10 11 docker run -d \\ --name es \\ -e \u0026#34;ES_JAVA_OPTS=-Xms512m -Xmx512m\u0026#34; \\ -e \u0026#34;discovery.type=single-node\u0026#34; \\ -v es-data:/usr/share/elasticsearch/data \\ -v es-plugins:/usr/share/elasticsearch/plugins \\ --privileged \\ --network es-net \\ -p 9200:9200 \\ -p 9300:9300 \\ elasticsearch:7.12.1 Copied! 命令解释：\n-e \u0026quot;cluster.name=es-docker-cluster\u0026quot;：设置集群名称 -e \u0026quot;http.host=0.0.0.0\u0026quot;：监听的地址，可以外网访问 -e \u0026quot;ES_JAVA_OPTS=-Xms512m -Xmx512m\u0026quot;：内存大小 -e \u0026quot;discovery.type=single-node\u0026quot;：非集群模式 -v es-data:/usr/share/elasticsearch/data：挂载逻辑卷，绑定es的数据目录 -v es-logs:/usr/share/elasticsearch/logs：挂载逻辑卷，绑定es的日志目录 -v es-plugins:/usr/share/elasticsearch/plugins：挂载逻辑卷，绑定es的插件目录 --privileged：授予逻辑卷访问权 --network es-net ：加入一个名为es-net的网络中 -p 9200:9200：端口映射配置 在浏览器中输入：http://192.168.150.101:9200 即可看到elasticsearch的响应结果：\n2.部署kibana kibana可以给我们提供一个elasticsearch的可视化界面，便于我们学习。\n2.1.部署 运行docker命令，部署kibana\n1 2 3 4 5 6 docker run -d \\ --name kibana \\ -e ELASTICSEARCH_HOSTS=http://es:9200 \\ --network=es-net \\ -p 5601:5601 \\ kibana:7.12.1 Copied! --network es-net ：加入一个名为es-net的网络中，与elasticsearch在同一个网络中 -e ELASTICSEARCH_HOSTS=http://es:9200\u0026quot;：设置elasticsearch的地址，因为kibana已经与elasticsearch在一个网络，因此可以用容器名直接访问elasticsearch -p 5601:5601：端口映射配置 kibana启动一般比较慢，需要多等待一会，可以通过命令：\n1 docker logs -f kibana Copied! 查看运行日志，当查看到下面的日志，说明成功：\n此时，在浏览器输入地址访问：http://192.168.150.101:5601，即可看到结果\n2.2.DevTools kibana中提供了一个DevTools界面：\n这个界面中可以编写DSL来操作elasticsearch。并且对DSL语句有自动补全功能。\n3.安装IK分词器 3.1.在线安装ik插件（较慢） 1 2 3 4 5 6 7 8 9 10 # 进入容器内部 docker exec -it elasticsearch /bin/bash # 在线下载并安装 ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip #退出 exit #重启容器 docker restart elasticsearch Copied! 3.2.离线安装ik插件（推荐） 1）查看数据卷目录 安装插件需要知道elasticsearch的plugins目录位置，而我们用了数据卷挂载，因此需要查看elasticsearch的数据卷目录，通过下面命令查看:\n1 docker volume inspect es-plugins Copied! 显示结果：\n1 2 3 4 5 6 7 8 9 10 11 [ { \u0026#34;CreatedAt\u0026#34;: \u0026#34;2022-05-06T10:06:34+08:00\u0026#34;, \u0026#34;Driver\u0026#34;: \u0026#34;local\u0026#34;, \u0026#34;Labels\u0026#34;: null, \u0026#34;Mountpoint\u0026#34;: \u0026#34;/var/lib/docker/volumes/es-plugins/_data\u0026#34;, \u0026#34;Name\u0026#34;: \u0026#34;es-plugins\u0026#34;, \u0026#34;Options\u0026#34;: null, \u0026#34;Scope\u0026#34;: \u0026#34;local\u0026#34; } ] Copied! 说明plugins目录被挂载到了：/var/lib/docker/volumes/es-plugins/_data 这个目录中。\n2）解压缩分词器安装包 下面我们需要把课前资料中的ik分词器解压缩，重命名为ik\n3）上传到es容器的插件数据卷中 也就是/var/lib/docker/volumes/es-plugins/_data ：\n4）重启容器 1 2 # 4、重启容器 docker restart es Copied! 1 2 # 查看es日志 docker logs -f es Copied! 5）测试： IK分词器包含两种模式：\nik_smart：最少切分\nik_max_word：最细切分\n1 2 3 4 5 GET /_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;黑马程序员学习java太棒了\u0026#34; } Copied! 结果：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 { \u0026#34;tokens\u0026#34; : [ { \u0026#34;token\u0026#34; : \u0026#34;黑马\u0026#34;, \u0026#34;start_offset\u0026#34; : 0, \u0026#34;end_offset\u0026#34; : 2, \u0026#34;type\u0026#34; : \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34; : 0 }, { \u0026#34;token\u0026#34; : \u0026#34;程序员\u0026#34;, \u0026#34;start_offset\u0026#34; : 2, \u0026#34;end_offset\u0026#34; : 5, \u0026#34;type\u0026#34; : \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34; : 1 }, { \u0026#34;token\u0026#34; : \u0026#34;程序\u0026#34;, \u0026#34;start_offset\u0026#34; : 2, \u0026#34;end_offset\u0026#34; : 4, \u0026#34;type\u0026#34; : \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34; : 2 }, { \u0026#34;token\u0026#34; : \u0026#34;员\u0026#34;, \u0026#34;start_offset\u0026#34; : 4, \u0026#34;end_offset\u0026#34; : 5, \u0026#34;type\u0026#34; : \u0026#34;CN_CHAR\u0026#34;, \u0026#34;position\u0026#34; : 3 }, { \u0026#34;token\u0026#34; : \u0026#34;学习\u0026#34;, \u0026#34;start_offset\u0026#34; : 5, \u0026#34;end_offset\u0026#34; : 7, \u0026#34;type\u0026#34; : \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34; : 4 }, { \u0026#34;token\u0026#34; : \u0026#34;java\u0026#34;, \u0026#34;start_offset\u0026#34; : 7, \u0026#34;end_offset\u0026#34; : 11, \u0026#34;type\u0026#34; : \u0026#34;ENGLISH\u0026#34;, \u0026#34;position\u0026#34; : 5 }, { \u0026#34;token\u0026#34; : \u0026#34;太棒了\u0026#34;, \u0026#34;start_offset\u0026#34; : 11, \u0026#34;end_offset\u0026#34; : 14, \u0026#34;type\u0026#34; : \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34; : 6 }, { \u0026#34;token\u0026#34; : \u0026#34;太棒\u0026#34;, \u0026#34;start_offset\u0026#34; : 11, \u0026#34;end_offset\u0026#34; : 13, \u0026#34;type\u0026#34; : \u0026#34;CN_WORD\u0026#34;, \u0026#34;position\u0026#34; : 7 }, { \u0026#34;token\u0026#34; : \u0026#34;了\u0026#34;, \u0026#34;start_offset\u0026#34; : 13, \u0026#34;end_offset\u0026#34; : 14, \u0026#34;type\u0026#34; : \u0026#34;CN_CHAR\u0026#34;, \u0026#34;position\u0026#34; : 8 } ] } Copied! 3.3 扩展词词典 随着互联网的发展，“造词运动”也越发的频繁。出现了很多新的词语，在原有的词汇列表中并不存在。比如：“奥力给”，“传智播客” 等。\n所以我们的词汇也需要不断的更新，IK分词器提供了扩展词汇的功能。\n1）打开IK分词器config目录：\n2）在IKAnalyzer.cfg.xml配置文件内容添加：\n1 2 3 4 5 6 7 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE properties SYSTEM \u0026#34;http://java.sun.com/dtd/properties.dtd\u0026#34;\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;comment\u0026gt;IK Analyzer 扩展配置\u0026lt;/comment\u0026gt; \u0026lt;!--用户可以在这里配置自己的扩展字典 *** 添加扩展词典--\u0026gt; \u0026lt;entry key=\u0026#34;ext_dict\u0026#34;\u0026gt;ext.dic\u0026lt;/entry\u0026gt; \u0026lt;/properties\u0026gt; Copied! 3）新建一个 ext.dic，可以参考config目录下复制一个配置文件进行修改\n1 2 传智播客 奥力给 Copied! 4）重启elasticsearch\n1 2 3 4 docker restart es # 查看 日志 docker logs -f elasticsearch Copied! 日志中已经成功加载ext.dic配置文件\n5）测试效果：\n1 2 3 4 5 GET /_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;传智播客Java就业超过90%,奥力给！\u0026#34; } Copied! 注意当前文件的编码必须是 UTF-8 格式，严禁使用Windows记事本编辑\n3.4 停用词词典 在互联网项目中，在网络间传输的速度很快，所以很多语言是不允许在网络上传递的，如：关于宗教、政治等敏感词语，那么我们在搜索时也应该忽略当前词汇。\nIK分词器也提供了强大的停用词功能，让我们在索引时就直接忽略当前的停用词汇表中的内容。\n1）IKAnalyzer.cfg.xml配置文件内容添加：\n1 2 3 4 5 6 7 8 9 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE properties SYSTEM \u0026#34;http://java.sun.com/dtd/properties.dtd\u0026#34;\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;comment\u0026gt;IK Analyzer 扩展配置\u0026lt;/comment\u0026gt; \u0026lt;!--用户可以在这里配置自己的扩展字典--\u0026gt; \u0026lt;entry key=\u0026#34;ext_dict\u0026#34;\u0026gt;ext.dic\u0026lt;/entry\u0026gt; \u0026lt;!--用户可以在这里配置自己的扩展停止词字典 *** 添加停用词词典--\u0026gt; \u0026lt;entry key=\u0026#34;ext_stopwords\u0026#34;\u0026gt;stopword.dic\u0026lt;/entry\u0026gt; \u0026lt;/properties\u0026gt; Copied! 3）在 stopword.dic 添加停用词\n1 习大大 Copied! 4）重启elasticsearch\n1 2 3 4 5 6 # 重启服务 docker restart elasticsearch docker restart kibana # 查看 日志 docker logs -f elasticsearch Copied! 日志中已经成功加载stopword.dic配置文件\n5）测试效果：\n1 2 3 4 5 GET /_analyze { \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34;, \u0026#34;text\u0026#34;: \u0026#34;传智播客Java就业率超过95%,习大大都点赞,奥力给！\u0026#34; } Copied! 注意当前文件的编码必须是 UTF-8 格式，严禁使用Windows记事本编辑\n4.部署es集群 我们会在单机上利用docker容器运行多个es实例来模拟es集群。不过生产环境推荐大家每一台服务节点仅部署一个es的实例。\n部署es集群可以直接使用docker-compose来完成，但这要求你的Linux虚拟机至少有4G的内存空间\n4.1.创建es集群 首先编写一个docker-compose文件，内容如下：\n1 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 version: \u0026#39;2.2\u0026#39; services: es01: image: elasticsearch:7.12.1 container_name: es01 environment: - node.name=es01 - cluster.name=es-docker-cluster - discovery.seed_hosts=es02,es03 - cluster.initial_master_nodes=es01,es02,es03 - \u0026#34;ES_JAVA_OPTS=-Xms512m -Xmx512m\u0026#34; volumes: - data01:/usr/share/elasticsearch/data ports: - 9200:9200 networks: - elastic es02: image: elasticsearch:7.12.1 container_name: es02 environment: - node.name=es02 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es03 - cluster.initial_master_nodes=es01,es02,es03 - \u0026#34;ES_JAVA_OPTS=-Xms512m -Xmx512m\u0026#34; volumes: - data02:/usr/share/elasticsearch/data ports: - 9201:9200 networks: - elastic es03: image: elasticsearch:7.12.1 container_name: es03 environment: - node.name=es03 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es02 - cluster.initial_master_nodes=es01,es02,es03 - \u0026#34;ES_JAVA_OPTS=-Xms512m -Xmx512m\u0026#34; volumes: - data03:/usr/share/elasticsearch/data networks: - elastic ports: - 9202:9200 volumes: data01: driver: local data02: driver: local data03: driver: local networks: elastic: driver: bridge Copied! es运行需要修改一些linux系统权限，修改/etc/sysctl.conf文件\n1 vi /etc/sysctl.conf Copied! 添加下面的内容：\n1 vm.max_map_count=262144 Copied! 然后执行命令，让配置生效：\n1 sysctl -p Copied! 通过docker-compose启动集群：\n1 docker-compose up -d Copied! 4.2.集群状态监控 kibana可以监控es集群，不过新版本需要依赖es的x-pack 功能，配置比较复杂。\n这里推荐使用cerebro来监控es集群状态，官方网址：https://github.com/lmenezes/cerebro\n课前资料已经提供了安装包：\n解压即可使用，非常方便。\n解压好的目录如下：\n进入对应的bin目录：\n双击其中的cerebro.bat文件即可启动服务。\n访问http://localhost:9000 即可进入管理界面：\n输入你的elasticsearch的任意节点的地址和端口，点击connect即可：\n绿色的条，代表集群处于绿色（健康状态）。\n4.3.创建索引库 1）利用kibana的DevTools创建索引库 在DevTools中输入指令：\n1 2 3 4 5 6 7 8 9 10 11 12 PUT /itcast { \u0026#34;settings\u0026#34;: { \u0026#34;number_of_shards\u0026#34;: 3, // 分片数量 \u0026#34;number_of_replicas\u0026#34;: 1 // 副本数量 }, \u0026#34;mappings\u0026#34;: { \u0026#34;properties\u0026#34;: { // mapping映射定义 ... } } } Copied! 2）利用cerebro创建索引库 利用cerebro还可以创建索引库：\n填写索引库信息：\n点击右下角的create按钮：\n4.4.查看分片效果 回到首页，即可查看索引库分片效果：\n","date":"2023-01-03T15:49:51+08:00","permalink":"https://qh.1357810.xyz/p/2023/01/11784511/","title":"安装elasticsearch2"},{"content":" 在google搜索页面输入“site:www.xx.com”就可以看到这个网页是否被google索引到， 如果没被索引到在搜索结果页面就会直接提示你使用Google Search Console。 登录后，如果是首次使用在Search Console中以下界面中选择“网页”类型资源，并将博客完整url填入其中，注意http或者https，www等最好能完全正确。\nHTML验证文件上传 只需要根据要求，下载HTML验证文件，把文件放在站点根目录的static目录下\nHTML标记 在config.yaml中加一个配置\n1 2 params: googleSiteVerification: \u0026#39;xxx\u0026#39; #写google给的字符串 Copied! 找到themes/meme/layouts/partials/head/custom.html或者head.html，在第一行加入：\n1 \u0026lt;meta name=\u0026#34;google-site-verification\u0026#34; content=\u0026#34;{{.Site.Params.googleSiteVerification}}\u0026#34;/\u0026gt; Copied! 然后执行hugo命令，就可以验证了\nGoogle Analytics 先到Google Analytics创建一个账号，并登录。 新建一个资源，填完后获得tracking code。 更新config.toml文件，配置GoogleAnalytics的id(G-xxxxxxx) 站点地图确认打开，默认都打开了\n在左侧点击“站点地图”，并在右侧点添加/测试站点地图，并添加url，\nBing 相似地，在Bing网站管理登陆、添加网站url。\n然后在左侧点击“配置我的网站\u0026gt;Sitemaps”，并在右侧加上sitemap的url，点击提交。\n测试 https://tagassistant.google.com/ ","date":"2022-04-09T00:00:00+08:00","image":"https://logan.1357810.xyz/cover/pic_099.jpg","permalink":"https://qh.1357810.xyz/articles/with-hugo/websearch/","title":"网站收录"},{"content":"This article offers a sample of basic Markdown syntax that can be used in Hugo content files, also it shows whether basic HTML elements are decorated with CSS in a Hugo theme.\nHeadings The following HTML \u0026lt;h1\u0026gt;—\u0026lt;h6\u0026gt; elements represent six levels of section headings. \u0026lt;h1\u0026gt; is the highest section level while \u0026lt;h6\u0026gt; is the lowest.\nH1 H2 H3 H4 H5 H6 Paragraph Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat.\nItatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat.\nBlockquotes The blockquote element represents content that is quoted from another source, optionally with a citation which must be within a footer or cite element, and optionally with in-line changes such as annotations and abbreviations.\nBlockquote without attribution Tiam, ad mint andaepu dandae nostion secatur sequo quae. Note that you can use Markdown syntax within a blockquote.\nBlockquote with attribution Don\u0026rsquo;t communicate by sharing memory, share memory by communicating.\n— Rob Pike1\nTables Tables aren\u0026rsquo;t part of the core Markdown spec, but Hugo supports supports them out-of-the-box.\nName Age Bob 27 Alice 23 Inline Markdown within tables Italics Bold Code italics bold code A B C D E F Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus ultricies, sapien non euismod aliquam, dui ligula tincidunt odio, at accumsan nulla sapien eget ex. Proin eleifend dictum ipsum, non euismod ipsum pulvinar et. Vivamus sollicitudin, quam in pulvinar aliquam, metus elit pretium purus Proin sit amet velit nec enim imperdiet vehicula. Ut bibendum vestibulum quam, eu egestas turpis gravida nec Sed scelerisque nec turpis vel viverra. Vivamus vitae pretium sapien Code Blocks Code block with backticks 1 2 3 4 5 6 7 8 9 10 \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Example HTML5 Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;Test\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Copied! Code block indented with four spaces \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026quot;en\u0026quot;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026quot;utf-8\u0026quot;\u0026gt; \u0026lt;title\u0026gt;Example HTML5 Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;Test\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Code block with Hugo\u0026rsquo;s internal highlight shortcode 1 2 3 4 5 6 7 8 9 10 \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Example HTML5 Document\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;Test\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Diff code block 1 2 3 4 5 [dependencies.bevy] git = \u0026#34;https://github.com/bevyengine/bevy\u0026#34; rev = \u0026#34;11f52b8c72fc3a568e8bb4a4cd1f3eb025ac2e13\u0026#34; - features = [\u0026#34;dynamic\u0026#34;] + features = [\u0026#34;jpeg\u0026#34;, \u0026#34;dynamic\u0026#34;] Copied! List Types Ordered List First item Second item Third item Unordered List List item Another item And another item Nested list Fruit Apple Orange Banana Dairy Milk Cheese Other Elements — abbr, sub, sup, kbd, mark GIF is a bitmap image format.\nH2O\nXn + Yn = Zn\nPress CTRL + ALT + Delete to end the session.\nMost salamanders are nocturnal, and hunt for insects, worms, and other small creatures.\nHyperlinked image The above quote is excerpted from Rob Pike\u0026rsquo;s talk during Gopherfest, November 18, 2015.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2022-03-11T00:00:00+08:00","image":"https://logan.1357810.xyz/cover/pic_074.jpg","permalink":"https://qh.1357810.xyz/example/markdown-syntax/","title":"Markdown Syntax Guide"},{"content":"Emoji can be enabled in a Hugo project in a number of ways.\nThe emojify function can be called directly in templates or Inline Shortcodes .\nTo enable emoji globally, set enableEmoji to true in your site\u0026rsquo;s configuration and then you can type emoji shorthand codes directly in content files; e.g.\n🙈 :see_no_evil: 🙉 :hear_no_evil: 🙊 :speak_no_evil:\nThe Emoji cheat sheet is a useful reference for emoji shorthand codes.\nN.B. The above steps enable Unicode Standard emoji characters and sequences in Hugo, however the rendering of these glyphs depends on the browser and the platform. To style the emoji you can either use a third party emoji font or a font stack; e.g.\n1 2 3 .emoji { font-family: Apple Color Emoji, Segoe UI Emoji, NotoColorEmoji, Segoe UI Symbol, Android Emoji, EmojiSymbols; } ","date":"2022-03-05T00:00:00+08:00","image":"https://logan.1357810.xyz/cover/pic_072.jpg","permalink":"https://qh.1357810.xyz/example/emoji-support/","title":"Emoji Support"},{"content":"Lorem est tota propiore conpellat pectoribus de pectora summo.\nRedit teque digerit hominumque toris verebor lumina non cervice subde tollit usus habet Arctonque, furores quas nec ferunt. Quoque montibus nunc caluere tempus inhospita parcite confusaque translucet patri vestro qui optatis lumine cognoscere flos nubis! Fronde ipsamque patulos Dryopen deorum.\nExierant elisi ambit vivere dedere Duce pollice Eris modo Spargitque ferrea quos palude Rursus nulli murmur; hastile inridet ut ab gravi sententia! Nomine potitus silentia flumen, sustinet placuit petis in dilapsa erat sunt. Atria tractus malis.\nComas hunc haec pietate fetum procerum dixit Post torum vates letum Tiresia Flumen querellas Arcanaque montibus omnes Quidem et Vagus elidunt The Van de Graaf Canon Mane refeci capiebant unda mulcebat Victa caducifer, malo vulnere contra dicere aurato, ludit regale, voca! Retorsit colit est profanae esse virescere furit nec; iaculi matertera et visa est, viribus. Divesque creatis, tecta novat collumque vulnus est, parvas. Faces illo pepulere tempus adest. Tendit flamma, ab opes virum sustinet, sidus sequendo urbis.\nIubar proles corpore raptos vero auctor imperium; sed et huic: manus caeli Lelegas tu lux. Verbis obstitit intus oblectamina fixis linguisque ausus sperare Echionides cornuaque tenent clausit possit. Omnia putatur. Praeteritae refert ausus; ferebant e primus lora nutat, vici quae mea ipse. Et iter nil spectatae vulnus haerentia iuste et exercebat, sui et.\nEurytus Hector, materna ipsumque ut Politen, nec, nate, ignari, vernum cohaesit sequitur. Vel mitis temploque vocatus, inque alis, oculos nomen non silvis corpore coniunx ne displicet illa. Crescunt non unus, vidit visa quantum inmiti flumina mortis facto sic: undique a alios vincula sunt iactata abdita! Suspenderat ego fuit tendit: luna, ante urbem Propoetides parte.\n","date":"2019-03-09T00:00:00+08:00","image":"https://logan.1357810.xyz/cover/pic_076.jpg","permalink":"https://qh.1357810.xyz/example/placeholder-text/","title":"Placeholder Text"},{"content":"Mathematical notation in a Hugo project can be enabled by using third party JavaScript libraries.\nIn this example we will be using KaTeX Create a partial under /layouts/partials/math.html Within this partial reference the Auto-render Extension or host these scripts locally. Include the partial in your templates like so: 1 2 3 {{ if or .Params.math .Site.Params.math }} {{ partial \u0026#34;math.html\u0026#34; . }} {{ end }} Copied! To enable KaTeX globally set the parameter math to true in a project\u0026rsquo;s configuration To enable KaTeX on a per page basis include the parameter math: true in content files Note: Use the online reference of Supported TeX Functions Examples Inline math: $\\varphi = \\dfrac{1+\\sqrt5}{2}= 1.6180339887…$\nBlock math: $$ \\varphi = 1+\\frac{1} {1+\\frac{1} {1+\\frac{1} {1+\\cdots} } } $$\n","date":"2019-02-09T00:00:00+08:00","image":"https://logan.1357810.xyz/cover/pic_075.jpg","permalink":"https://qh.1357810.xyz/example/math-typesetting/","title":"Math Typesetting"},{"content":" A页面-包含B页面的超链接 点击查看另一个Markdown文件 title_a text\n","date":"2008-04-09T00:00:00+08:00","permalink":"https://qh.1357810.xyz/articles/with-hugo/page-a/","title":"A页面"},{"content":" B页面-包含A页面的超链接 点击查看另一个Markdown文件 title_b text\n","date":"2008-04-09T00:00:00+08:00","permalink":"https://qh.1357810.xyz/articles/with-hugo/page-b/","title":"B页面"}]