Docker 精选

1.安装 Docker

环境 Ubuntu 20

Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。 Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。 容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。

我们可以在常用的服务器系统上安装 Docker 如:windows server、Linux 等。

•Docker-CE 指Docker社区版,由社区维护和提供技术支持,为免费版本,适合个人开发人员和小团队使用。

•Docker-EE指Docker企业版,为收费版本,由售后团队和技术团队提供技术支持,专为企业开发和IT团队而设计。 

通常情况下,Docker-CE足以满足我们的需求。主要针对Docker-CE进行学习。

1.安装 Docker

centos7 上安装 docker-ce

# 0 卸载老版本(新机器不用操作)
sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine
# yum list installed | grep docker
# yum remove docker-ce.x86_64 docker-ce-cli.x86_64
# rm -rf /var/lib/docker
# sudo yum update

# 1 安装必要的系统工具
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
# 2 添加源信息
	-官方:地址在国外,很慢
  sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
  -阿里云:(推荐)
  sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 3 检查一下/etc/yum.repos.d/docker-ce.repo 中的url地址是不是都是阿里云
cat /etc/yum.repos.d/docker-ce.repo 
# 4 安装
sudo yum -y install docker-ce
# 5 开启docker服务
systemctl start docker

# 6 docker info 查看docker信息

ubuntu 上安装 docker-ce

# 0 卸载
sudo apt-get remove docker docker-engine docker.io containerd runc
# 1 安装必要工具
sudo apt-get update
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
# 2 安装GPG证书
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
#换成阿里云
curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
# 2 写入软件源信息
#官方
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
#换成阿里云
sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"

# 3 更新并安装docker-ce
sudo apt-get -y install docker-ce
# 4 开启docker服务
systemctl status docker

2.启动 Docker

相类似的还有 stop,restart

systemctl start docker

3.开机自启

systemctl enable docker

4.配置镜像加速器
针对 Docker 客户端版本大于 1.10.0 的用户 您可以通过修改 daemon 配置文件 /etc/docker/daemon.json 来使用加速器(如果文件不存在请新建该文件),文件内容如下:修改后需要执行 systemctl daemon-reload 和 systemctl restart docker 命令。

{
  "registry-mirrors": ["https://tuv7rqqq.mirror.aliyuncs.com","https://docker.mirrors.ustc.edu.cn/","https://hub-mirror.c.163.com/","https://reg-mirror.qiniu.com"]
}

检查加速器是否生效:

检查加速器是否生效配置加速器之后,如果拉取镜像仍然十分缓慢,请手动检查加速器配置是否生效,在命令行执行 docker info,如果从结果中看到了如下内容,说明配置成功。

root@test:~# docker info
.....
 Registry Mirrors:
  https://tuv7rqqq.mirror.aliyuncs.com/
  https://docker.mirrors.ustc.edu.cn/
  https://hub-mirror.c.163.com/
  https://reg-mirror.qiniu.com/
 Live Restore Enabled: false

docker 配置远程连接

在 Linux 上使用时不能关闭防火墙,需要使用命令允许 2375 端口使用。

firewall-cmd --add-port=2375/tcp --permanent
firewall-cmd --reload

ubuntu

sudo ufw allow 2375

执行如下脚本获取证书(注意要修改 docker 主机 IP):

#创建 Docker TLS 证书
#!/bin/bash
#相关配置信息
#docker主机IP
SERVER="127.0.0.1"
PASSWORD="nihao"
COUNTRY="CN"
STATE="shanghai"
CITY="shanghai"
ORGANIZATION="shanghai"
ORGANIZATIONAL_UNIT="Dev"
EMAIL="ladidol-xiaoqiang@qq.com"
###开始生成文件###
echo "开始生成文件"
#切换到生产密钥的目录
cd /etc/docker
#生成ca私钥(使用aes256加密)
openssl genrsa -aes256 -passout "pass:$PASSWORD"  -out ca-key.pem 2048
#生成ca证书,填写配置信息
openssl req -new -x509 -passin "pass:$PASSWORD" -days 3650 -key ca-key.pem -sha256 -out ca.pem -subj "/C=$COUNTRY/ST=$STATE/L=$CITY/O=$ORGANIZATION/OU=$ORGANIZATIONAL_UNIT/CN=$SERVER/emailAddress=$EMAIL"
#生成server证书私钥文件
openssl genrsa -out server-key.pem 2048
#生成server证书请求文件
openssl req -subj "/CN=$SERVER" -new -key server-key.pem -out server.csr
echo "subjectAltName=IP:${SERVER},IP:0.0.0.0" >> extfile.cnf
echo "extendedKeyUsage=serverAuth" >> extfile.cnf
#使用CA证书及CA密钥以及上面的server证书请求文件进行签发,生成server自签证书
openssl x509 -req -days 3650 -in server.csr -CA ca.pem -CAkey ca-key.pem -passin "pass:$PASSWORD" -CAcreateserial  -out server-cert.pem -extfile extfile.cnf
#生成client证书RSA私钥文件
openssl genrsa -out key.pem 2048
#生成client证书请求文件
openssl req -subj '/CN=client' -new -key key.pem -out client.csr
sh -c 'echo "extendedKeyUsage=clientAuth" > extfile.cnf'
#生成client自签证书(根据上面的client私钥文件、client证书请求文件生成)
openssl x509 -req -days 3650 -in client.csr -CA ca.pem -CAkey ca-key.pem  -passin "pass:$PASSWORD" -CAcreateserial -out cert.pem  -extfile extfile.cnf
#更改密钥权限
chmod 0400 ca-key.pem key.pem server-key.pem
#更改密钥权限
chmod 0444 ca.pem server-cert.pem cert.pem
#删除无用文件
rm client.csr server.csr
echo "生成文件完成"

给脚本文件增加权限 chmod 777 脚本文件,并运行 ./脚本文件,在 Linux 系统中,每个权限字符可以用一个数字来表示。以下是对应关系:

  • r(读权限)用数字 4 表示。
  • w(写权限)用数字 2 表示。
  • x(执行权限)用数字 1 表示。

通过将这些数字相加,可以将权限字符转换为一个三位数。例如:

  • rwx表示读、写和执行权限,对应的数字是 4 + 2 + 1 = 7。
  • rw-表示读和写权限,对应的数字是 4 + 2 = 6。
  • r-x表示读和执行权限,对应的数字是 4 + 1 = 5。

因此,当我们使用chmod命令来修改文件权限时,可以使用这些数字来表示权限。例如,chmod 755 file.txt表示将文件file.txt的权限设置为-rwxr-xr-x,其中第一个数字 7 表示文件所有者具有读、写和执行权限,后面的两个数字 5 表示所属组和其他用户具有读和执行权限。

配置Docker守护程序:编辑Docker的配置文件 vi /lib/systemd/system/docker.service,添加以下内容:

ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock\
--tlsverify \
--tlscacert=/etc/docker/ca.pem \
--tlscert=/etc/docker/server-cert.pem \
--tlskey=/etc/docker/server-key.pem \
-H tcp://0.0.0.0:2375 \
-H unix:///var/run/docker.sock

重启 docker

systemctl restart docker

打开浏览器 https://124.70.51.123:2375/ 网址报错。

从服务器上复制 ca.pem、cert.pem、key.pem 三个文件到本地文件。并使用 idea 连接 docker

可以使用可视化界面对 Containers(容器)、images(镜像)、Networks(网络)Volumes(数据卷) 进行管理。

扩展:window 安装 docker

Docker Desktop 是 Docker 在 Windows 10 和 macOS 操作系统上的官方安装方式,这个方法依然属于先在虚拟机中安装 Linux 然后再安装 Docker 的方法。

Docker Desktop 官方下载地址: https://docs.docker.com/desktop/install/windows-install/

安装 Hyper-V

Hyper-V 是微软开发的虚拟机,类似于 VMWare 或 VirtualBox,仅适用于 Windows 10。这是 Docker Desktop for Windows 所使用的虚拟机。 但是,这个虚拟机一旦启用,QEMU、VirtualBox 或 VMWare Workstation 15 及以下版本将无法使用!如果你必须在电脑上使用其他虚拟机(例如开发 Android 应用必须使用的模拟器),请不要使用 Hyper-V!

在应用-> 可选功能中启用  Hyper-V 和 适用于 Linux 的 Windows 子系统,完成后重启计算机。

运行 docker 安装包进行安装。如果启动中遇到因 WSL2 导致地错误,请安装 WSL2。安装完成后重启 docker 即可。

配置配置镜像加速器,点击右上方设置按钮进入 docker engine 加入如下内容。

代码:

"registry-mirrors": [
  "https://docker.mirrors.ustc.edu.cn/",
  "https://reg-mirror.qiniu.com"
]

配置远程连接。开启一个本地代理将 ipv4 地址的请求转发到172.0.0.1 的 2375 端口上 cmd 命令行下管理员方式运行如下命令。

netsh interface portproxy add v4tov4 listenport=2375 connectaddress=127.0.0.1 connectport=2375 listenaddress=192.168.8.80 protocol=tcp

查看是否成功

netsh interface portproxy show v4tov4

开启如下开关即可通过第三方工具连接。

2.Docker 卸载

1:查看已安装的 docker 安装包

[root@localhost ~]# yum list installed|grep docker
docker.x86_64                       2:1.13.1-209.git7d71120.el7.centos @extras
docker-client.x86_64                2:1.13.1-209.git7d71120.el7.centos @extras
docker-common.x86_64                2:1.13.1-209.git7d71120.el7.centos @extras

2:删除安装包 

yum –y remove docker.x86_64 
yum –y remove docker-client.x86_64
yum –y remove docker-common.x86_64

3:删除 Docker 镜像 

rm -rf /var/lib/docker

windows 卸载就按照普通软件卸载即可,最后再恢复 Windows 可选功能里面的 Hyper-V 和 适用于 Linux 的window 子系统。

3.安装 mysql

docker 中的几个基本概念。

仓库:集中存放镜像文件的地方

镜像:一个特殊的文件系统,可以看作可运行的软件包

容器:镜像运行时的实体

镜像仓库:

https://hub.docker.com/

AtomHub 已下线

搜索并找到 mysql 8.0.21 / mysql:5.7,下载镜像。

docker pull mysql:8.0.21

关于镜像常用两个命令:

  • docker images 查看下载到本地的镜像
  • docker rmi 镜像 id:根据镜像 id 删除本地镜像。
  • docker image save 容器名 -o 文件名.image 将容器导出本地文件,防止没有网的时候获取容器。
  • docker image load -i 文件名.image 从本地文件加载 docker 容器。如果容器的名字和版本丢失了使用 docker tag 镜像ID 镜像名:版本 如:docker tag 84c5 redis:6.0.8 

使用 idea 可以使用可视化界面进行拉取镜像。

启动容器

docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=cvcode mysql:8.0.21

在 opt/docker 下创建 MySQL 的挂载目录 mysql,并将创建的容器内的配置文件拷贝到该目录下。

docker cp mysql:/var/lib/mysql/ /opt/docker/mysql/data
docker cp mysql:/etc/mysql/conf.d/ /opt/docker/mysql/config

结果如下:

删除前面创建的容器,重新运行如下命令,请确保主机上的 /opt/docker/mysql/config 和 /opt/docker/mysql/data 目录已存在,并具有适当的权限。运行命令后,MySQL8 容器将在后台运行,并将配置和数据目录挂载到指定的目录中。

docker run -d -p 3306:3306 --name=mysql8 \
-v /opt/docker/mysql/config:/etc/mysql/conf.d \
-v /opt/docker/mysql/data:/var/lib/mysql \
--privileged=true \
-e MYSQL_ROOT_PASSWORD=cvcode \
mysql:8.0.21

将在 Docker 中运行 MySQL8 容器,并挂载配置和数据目录。以下是对命令的解释:

  • -d:将容器放入后台运行。
  • -p 3306:3306:将主机的3306端口映射到容器的3306端口,允许从主机上访问MySQL服务器。
  • --name=mysql8:将容器命名为mysql8。
  • -v /opt/docker/mysql/config:/etc/mysql/conf.d:将主机上的/opt/docker/mysql/config目录挂载到容器中的/etc/mysql/conf.d目录,用于存储MySQL的配置文件。
  • -v /opt/docker/mysql/data:/var/lib/mysql:将主机上的/opt/docker/mysql/data目录挂载到容器中的/var/lib/mysql目录,用于存储MySQL的数据文件。
  • --privileged=true:赋予容器特权,以便在容器内部执行特权操作。
  • -e MYSQL_ROOT_PASSWORD=cvcode:设置MySQL root用户的密码为cvcode
  • mysql:8.0.21:指定要运行的MySQL 8的Docker镜像。

使用 idea 可视化界面启动容器只需要在镜像上右键创建镜像

启动完成后 mysql 即可远程连接,docker 会自动将防火墙等处理好,如果是云服务器需要配置安全组规则。

在使用过程中可能会用到如下命令:

1.如果启动失败,查看错误日志。

docker logs mysql

2.查看运行的容器状态

docker ps

3.查看全部容器的运行状态

docker ps -a

4.停止容器用 

docker stop 容器 id

5.再次启动停止的容器

docker start 容器 id 启动

6.删除容器 

docker rm 容器 id

以上所有命令除了 save 和 load 都可以通过 idea 可视化界面进行操作。

4.安装 oracle

查找 oracle 镜像,我们选择精简版 oracle-xe-11g

docker search oracle

拉取镜像

docker pull oracleinanutshell/oracle-xe-11g

启动镜像

docker run -d -p 1521:1521 -e ORACLE_ALLOW_REMOTE=true --name oracle11g oracleinanutshell/oracle-xe-11g

ORACLE_ALLOW_REMOTE 为 true 表示可以远程访问

连接数据库()system 和 sys 的默认密码是 oracle

hostname: localhost
port: 1521
sid: xe
username: system
password: oracle

使用 idea 连接 Oracle

创库:在 oracle 中创建数据库通常叫创建表空间,并需要创建该表空间用户,并赋予该用户权限

--创建表空间

create tablespace jwxt datafile 'jwxt.ora' size 500m 
autoextend on --自动扩展 
 next 50m maxsize 20480m --每次自动扩展50M,最大可到20480M 
extent management local;

--创建表空间默认用户

create user jwxt identified by 888 default tablespace jwxt quota 500m on jwxt;

--赋予其所有权限

grant all privileges to jwxt;

--拥有connect权限的用户只可以登录Oracle;拥有resource权限的用户只可以创建实体,不可以创建数据库结构;拥有全部特权,是--系统最高权限,只有DBA才可以创建数据库结构。

创表:oracle 数据库使用序列 sequence 完成数据库自增操作,通常每张表都会创建一个序列

create sequence USER_SEQ
    maxvalue 10000000

创表

create table "user"
(
    id int
        constraint USER_PK
        primary key,
    name varchar2(255)
)

在 oracle 中表名和字段名不能使用 TAB 上面的 ~(飘)而是使用双引号,值必须使用单引号。同时使用序列(sequence)的下一个值方法获取下一个值。每次使用该方法后该序列的下一个值会自动按照步长增加。

insert into "user"(ID,NAME) values (USER_SEQ.nextval,'张三')

Oracle 分页查询必须使用行号 rownum,每次查询的行号都在变化,所以一般使用 子查询完成,SQL 如下

select * from ( select rownum r_, row_.* from ( select * from student order by id ) row_ where rownum <=Y ) where r_>=X ;

X:起始索引位置。

Y:结束索引位置。

jdbc 操作时 url 前的驱动方式有两种,下面进行说明。最后的 sid 称为实例名,一般每个数据库服务都会有一个实例名。每个用户下有自己所属的数据库。查询也是针对该数据库。

同时需要导入相应的依赖:

<dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc8</artifactId>
    <version>12.2.0.1</version>
</dependency>

案例:

Class.forName("oracle.jdbc.driver.OracleDriver");
Connection connection = DriverManager.getConnection("jdbc:oracle:thin:@192.168.75.128:1521:xe", "jwxt", "888");
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select * from \"user\"");
while (resultSet.next()) {
    System.out.println(resultSet.getString(2));
}
resultSet.close();
statement.close();
connection.close();

thin 和 oci 的 url 写法上的区别:

1)从使用上来说,oci必须在客户机上安装oracle客户端或才能连接,而thin就不需要,因此从使用上来讲thin还是更加方便,这也是thin比较常见的原因。

2)原理上来看,thin是纯java实现tcp/ip的c/s通讯;而oci方式,客户端通过native java method调用c library访问服务端,而这个c library就是oci(oracle called interface),因此这个oci总是需要随着oracle客户端安装(从oracle10.1.0开始,单独提供OCI Instant Client,不用再完整的安装client)

3)它们分别是不同的驱动类别,oci是二类驱动, thin是四类驱动,但它们在功能上并无差异。

4)虽然很多人说oci的速度快于thin,但找了半天没有找到相关的测试报告。

5.数据卷 Volume

在 Docker 中,Volume 是一种用于持久化存储数据的机制。它允许容器之间共享和重用数据,同时也提供了一种方便的方式来在容器和主机之间共享文件。

使用 Volume 的好处包括:

  1. 数据持久化:Volume 允许将容器中的数据存储在主机上,即使容器被删除或重新创建,数据仍然存在。
  2. 数据共享:多个容器可以共享同一个 Volume,这样可以实现容器之间的数据共享和通信。
  3. 数据备份和恢复:通过将 Volume 挂载到主机上,可以方便地备份和恢复数据。
  4. 数据迁移:可以将 Volume 从一个容器迁移到另一个容器,或者从一个主机迁移到另一个主机,而不会丢失数据。

在 Docker 中,可以通过以下方式来管理 Volume:

  1. 创建 Volume:可以使用docker volume create命令来创建一个 Volume。例如:docker volume create mydata
  2. 查看 Volume:可以使用docker volume ls命令来列出所有的 Volume。
  3. 删除 Volume:可以使用docker volume rm命令来删除一个 Volume。例如:docker volume rm mydata
  4. 挂载 Volume:可以使用docker run命令的-v参数来将一个 Volume 挂载到容器中。例如:docker run -v mydata:/app/data myimage,这将把名为"mydata"的 Volume 挂载到容器的/app/data目录上。
  5. 备份和恢复 Volume:可以使用docker cp命令来备份和恢复 Volume 中的数据。例如:docker cp mycontainer:/app/data ./backup,这将把名为"mycontainer"的容器中的/app/data目录的数据复制到当前主机上的./backup目录中。

总之,Volume 是 Docker 中用于持久化存储数据的一种机制,它提供了方便的数据共享、备份和迁移的功能,使得容器之间的数据管理更加灵活和可靠。

6.安装 redis

从仓库拉取 redis 镜像

docker pull redis:5.0.4

因为 redis 默认配置你会发现只能够本地连接,不能进行远程访问,使用远程连接工具连接都会报错,因此需要手动挂载 redis 配置文件。   在本机上创建如下目录,用于挂载容器的配置文件、数据。

mkdir -p /opt/docker/redis/config
mkdir -p /opt/docker/redis/data

cd 到 /opt/docker/redis/conf 下后用 wget 指令下载 redis 的配置文件。Index of /releases/ 下载对应版本,并修改配置文件如下内容。

1.NETWORK 模块

################################## NETWORK #####################################

# By default, if no "bind" configuration directive is specified, Redis listens
# for connections from all the network interfaces available on the server.
# It is possible to listen to just one or multiple selected interfaces using
# the "bind" configuration directive, followed by one or more IP addresses.
#
# Examples:
#
# bind 192.168.1.100 10.0.0.1
# bind 127.0.0.1 ::1
#
# ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the
# internet, binding to all the interfaces is dangerous and will expose the
# instance to everybody on the internet. So by default we uncomment the
# following bind directive, that will force Redis to listen only into
# the IPv4 loopback interface address (this means Redis will be able to
# accept connections only from clients running into the same computer it
# is running).
#
# IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES
# JUST COMMENT THE FOLLOWING LINE.
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bind 127.0.0.1

注释掉 bind 127.0.0.1,解除本地连接限制。

2.Protected mode 保护模式

# Protected mode is a layer of security protection, in order to avoid that
# Redis instances left open on the internet are accessed and exploited.
#
# When protected mode is on and if:
#
# 1) The server is not binding explicitly to a set of addresses using the
#    "bind" directive.
# 2) No password is configured.
#
# The server only accepts connections from clients connecting from the
# IPv4 and IPv6 loopback addresses 127.0.0.1 and ::1, and from Unix domain
# sockets.
#
# By default protected mode is enabled. You should disable it only if
# you are sure you want clients from other hosts to connect to Redis
# even if no authentication is configured, nor a specific set of interfaces
# are explicitly listed using the "bind" directive.
protected-mode yes

修改 protected-mode,保护模式为 no,限制为本地访问,修改后解除保护模式。

3.requirepass 修改密码

################################## SECURITY ###################################

# Require clients to issue AUTH <PASSWORD> before processing any other
# commands.  This might be useful in environments in which you do not trust
# others with access to the host running redis-server.
#
# This should stay commented out for backward compatibility and because most
# people do not need auth (e.g. they run their own servers).
#
# Warning: since Redis is pretty fast an outside user can try up to
# 150k passwords per second against a good box. This means that you should
# use a very strong password otherwise it will be very easy to break.
#
requirepass foobared

将 requirepass foobared 注释取消,foobared就是连接 redis 的密码,根据实际情况修改即可。

docker run -p 6379:6379 --name redis \
-v /opt/docker/redis/config/redis.conf:/etc/redis/redis.conf \
-v /opt/docker/redis/data:/data \
--privileged=true \
-d redis:5.0.4 \
redis-server /etc/redis/redis.conf \
--appendonly yes

每个选项和参数的含义:

  • -p 6379:6379:将主机的6379端口映射到Redis容器的6379端口,以便可以从主机访问Redis服务。
  • --name redis:为Redis容器指定一个名称为"redis",以便后续可以使用该名称来管理容器。
  • -v /opt/docker/redis/config/redis.conf:/etc/redis/redis.conf:将主机上的Redis配置文件redis.conf挂载到容器内的/etc/redis/redis.conf路径,以便使用自定义配置。
  • -v /opt/docker/redis/data:/data:将主机上的Redis数据目录/opt/docker/redis/data挂载到容器内的/data路径,以便持久化Redis数据。
  • --privileged=true:以特权模式运行容器,允许容器内的进程访问主机的特权操作。
  • -d:以守护进程模式运行容器。 redis:5.0.4:使用Redis 5.0.4镜像。 redis-server /etc/redis/redis.conf:在容器内运行redis-server命令,并指定使用/etc/redis/redis.conf配置文件。
  • --appendonly yes:通过在命令行中指定--appendonly yes参数,启用Redis的持久化机制,将写操作追加到磁盘上的AOF(Append-Only File)文件中。  

这个命令的目的是创建一个名为"redis"的Redis容器,使用自定义的配置文件和数据目录,并启用持久化。通过将主机的6379端口映射到容器的6379端口,可以从主机上的应用程序访问Redis服务。

设置容器随 docker 容器自启

docker update redis --restart=always

7.FastDFS

分布式文件系统(Distributed File System)是指文件系统管理的物理存储资源不一定直接连接在本地节点上,而是通过计算机网络与节点相连。 

通俗来讲:  

传统文件系统管理的文件就存储在本机。

分布式文件系统管理的文件存储在很多机器,这些机器通过网络连接,要被统一管理。无论是上传或者访问文件,都需要通过管理中心来访问。

FastDFS:是由淘宝的余庆先生所开发的一个轻量级、高性能的开源分布式文件系统。用纯 C 语言开发,功能丰富:

  1. 文件存储
  2. 文件同步
  3. 文件访问(上传、下载)
  4. 存取负载均衡
  5. 在线扩容

适合有大容量存储需求的应用或系统。同类的分布式文件系统有 GFS(谷歌)、HDFS(Hadoop)、TFS(淘宝)等。

安装 fastDFS

1.首先下载 FastDFS 文件系统的 docker 镜像

docker search fastdfs

2.获取镜像

docker pull delron/fastdfs

3.使用docker镜像构建tracker容器(跟踪服务器,起到调度的作用):

docker run -d --network=host --name tracker delron/fastdfs tracker

4.使用 docker 镜像构建 storage 容器(存储服务器,提供容量和备份服务):

docker run -d --network=host --name storage -e TRACKER_SERVER=192.168.175.128:22122 -e GROUP_NAME=group1 delron/fastdfs storage

5.创建 springboot 项目并导入下面的依赖

<dependency>
    <groupId>com.github.tobato</groupId>
    <artifactId>fastdfs-client</artifactId>
    <version>1.26.1-RELEASE</version>
</dependency>

配置文件如下

fdfs:
  so-timeout: 1501 #超时时间
  connect-timeout: 601 #连接超时时间
  thumb-image: # 缩略图
    width: 60
    height: 60
  tracker-list: # tracker地址
    - 192.168.175.135:22122

6.增加配置类

import com.github.tobato.fastdfs.FdfsClientConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableMBeanExport;
import org.springframework.context.annotation.Import;
import org.springframework.jmx.support.RegistrationPolicy;

@Configuration
@Import(FdfsClientConfig.class)
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
public class FastClientImporter {
}

单元测试

import com.github.tobato.fastdfs.domain.StorePath;
import com.github.tobato.fastdfs.domain.ThumbImageConfig;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    private FastFileStorageClient storageClient;

    @Autowired
    private ThumbImageConfig thumbImageConfig;

    @Test
    public void testUpload() throws FileNotFoundException {
        File file = new File("C:/Users/Administrator/Desktop/up.png");
        // 上传
        StorePath storePath = this.storageClient.uploadFile(
                new FileInputStream(file), file.length(), "png", null);
        // 带分组的路径
        System.out.println(storePath.getFullPath());
        // 不带分组的路径
        System.out.println(storePath.getPath());
    }


    @Test
    public void testUploadAndCreateThumb() throws FileNotFoundException {
        File file = new File("G:\\LeYou\\upload\\spitter_logo_50.png");
        // 上传并且生成缩略图
        StorePath storePath = this.storageClient.uploadImageAndCrtThumbImage(
                new FileInputStream(file), file.length(), "png", null);
        // 带分组的路径
        System.out.println(storePath.getFullPath());
        // 不带分组的路径
        System.out.println(storePath.getPath());
        // 获取缩略图路径
        String path = thumbImageConfig.getThumbImagePath(storePath.getPath());
        System.out.println(path);
    }
}

访问图片地址:

http://192.168.175.128:8888/group1/M00/00/00/wKivgF6ZzFWAa9KfAACRNhf3vVk267.png

8.Dockerfile

Dockerfile 是一个文本文件,其内包含了一条条的指令(Instruction),用于构建镜像。每一条指令构建一层镜像,因此每一条指令的内容,就是描述该层镜像应当如何构建。一般而言, Dockerfile 分为四部分 : 基础镜像信息、维护者信息、镜像操作指令和容器启动时执行指令。

例如:

#第一行必须指定 基础镜像信息
FROM ibtech/java17#工作空间
WORKDIR /usr/local/tomcat
# 下载Tomcat二进制分发包
RUN wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.78/bin/apache-tomcat-9.0.78.tar.gz
RUN tar -xvf apache-tomcat-9.0.78.tar.gz
RUN mv apache-tomcat-9.0.78/* .
RUN rm -rf apache-tomcat-9.0.78.tar.gz apache-tomcat-9.0.78

# 暴露Tomcat的默认端口
EXPOSE 8080

# 启动Tomcat
CMD ["bin/catalina.sh", "run"]

Dockerfile 四部分说明:

  1. 一开始必须要指明所基于的镜像名称,关键字是 FROM,这是必须的。
  2. 接下来是维护者信息关键字是 MAINTAINER,非必须,但良好的习惯有利于后期的职责明确。
  3. 后面是镜像操作指令,如 RUN 等,每执行一条 RUN 命令,镜像添加新的一层。
  4. 最后是 CMD 指令,来指明运行容器时的操作命令。

构建镜像

docker build -t 镜像名字:版本 .

注意镜像名字和版本名称都是自己起。 注意 . 代表 Dockerfile 文件在当前路径。

FROM ibtech/java17
MAINTAINER WPF
CMD java -version

构建

docker build -t java-hello:17 .

咱们通过上面的命令已经把自己写的 Dockerfile 文件 build 成了一个镜像,现在咱们看看是否有这个镜像,然后创建并运行(run)

[root@hecs-16743 ~]# docker run java-hello:17
openjdk version "17.0.4.1" 2022-08-12 LTS
OpenJDK Runtime Environment Corretto-17.0.4.9.1 (build 17.0.4.1+9-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.4.9.1 (build 17.0.4.1+9-LTS, mixed mode, sharing)

如果想要运行上面的 tomcat 案例使用如下命令构建:

docker build -t java-tomcat:17 .

使用如下命令运行:

docker run -p 8080:8080 -d java-tomcat:17

Dockerfile 中包含了一系列的指令。以下是一些常用的 Dockerfile 指令:

  1. FROM:指定基础镜像,用于构建当前镜像的基础环境。
  2. RUN:在镜像中执行命令,可以用于安装软件包、运行脚本等操作。
  3. COPY:将文件或目录从构建环境复制到镜像中的指定路径。
  4. ADD:类似于 COPY,但功能更强大,可以自动解压缩文件、下载文件等。
  5. WORKDIR:设置工作目录,后续的指令将在该目录下执行。
  6. ENV:设置环境变量,可以在容器运行时使用。
  7. EXPOSE:声明容器运行时需要监听的端口。
  8. CMD:指定容器启动时要执行的命令,只能有一个 CMD 指令。
  9. ENTRYPOINT:指定容器启动时要执行的命令,可以与 CMD 指令配合使用。
  10. VOLUME:声明容器中的目录是持久化存储的,可以用于存储数据。
  11. ARG:定义构建时的参数,可以在构建命令中传入。
  12. LABEL:为镜像添加元数据,可以用于标记镜像的版本、作者等信息。

这些是 Dockerfile 中的一些常用指令,可以根据实际需求选择使用。

要挂载日志目录和项目目录,你可以在运行容器时使用-v参数来指定挂载的目录。例如:

docker run -d -p 8080:8080 -v /path/to/logs:/usr/local/tomcat/logs -v /path/to/project:/usr/local/tomcat/webapps/your_project your_image_name

其中:

  • -d表示以后台模式运行容器
  • -p 8080:8080表示将容器的 8080 端口映射到主机的 8080 端口
  • -v /path/to/logs:/usr/local/tomcat/logs表示将主机上的/path/to/logs目录挂载到容器的/usr/local/tomcat/logs目录,用于存放 Tomcat 的日志文件
  • -v /path/to/project:/usr/local/tomcat/webapps/your_project表示将主机上的/path/to/project目录挂载到容器的/usr/local/tomcat/webapps/your_project目录,用于部署你的项目

请将上述命令中的/path/to/logs/path/to/project替换为你实际的日志目录和项目目录的路径。

9.远程 Portainer

Docker 是一款常用的虚拟化工具,但是运行时如果没有可视化界面,则可能会显得不够直观。这时候通过使用 Docker 的可视化界面,可以更方便地管理和控制 Docker 容器和镜像。 为了实现 Docker 可视化界面,需要安装和使用一些工具。 首先需要安装 Docker Compose,可以使用以下命令:

yum install docker-compose

然后安装 Portainer,这是一款方便易用的 Docker 可视化管理工具。

docker volume create portainer_data
docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data --name portainer portainer/portainer

docker volume create portainer_data 这条命令是在 Docker 中创建一个名为 "portainer_data" 的 volume(卷)。Volume 是 Docker 用来持久化存储数据的一种机制,可以在容器之间共享和重用数据。在这个例子中,创建的"portainer_data" volume 用于存储 Portainer 容器的数据。

这样就成功安装好了 Portainer,并通过 9000 端口打开界面。在浏览器中访问 http://localhost:9000 即可开始使用。

Portainer 的使用非常简单,通过几个简单的步骤,就可以管理和控制 Docker 容器和镜像。 首先需要登录 Portainer,可以通过设置管理员用户名和密码来登录。

接下来,可以选择管理本地 Docker 环境,或者远程 Docker 环境。如果选择管理远程 Docker 环境,则需要输入 Docker 主机的 IP 地址和端口。

登录之后,可以在左边的菜单中看到已经安装的 Docker 容器和镜像。也可以通过右上角的“+”按钮来新建一个容器或镜像。 通过使用 Docker 的可视化界面,可以更方便地管理和控制 Docker 容器和镜像。使用 Portainer,可以轻松实现 Docker 的可视化界面,加速 Docker 的使用和管理。

10.rabbitmq 安装和简单队列

MQ(message queue),从字面意思上看,本质是个队列,FIFO 先入先出,只不过队列中存放的内容是 message (消息)而已,还是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ 是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了 MQ 之后,消息发送上游只需要依赖 MQ,不 用依赖其他服务。

官网:RabbitMQ: One broker to queue them all | RabbitMQ

rabbitmq 安装

拉取 rabbitmq 的镜像

docker pull rabbitmq:3.8.3-management

启动 rabbitmq 镜像

docker run --name rabbitmq -d -p 15672:15672 -p 5672:5672 rabbitmq:3.8.3-management

设置 rabbitmq 开机启动

docker update rabbitmq --restart=always

访问rabbitmq浏览器端口

http://192.168.175.128:15672/

默认用户名 guest,密码也是 guest。也可以启动容器时增加参数 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=123456 指定账号和密码。

RabbitMQ is a message broker: it accepts and forwards messages. You can think about it as a post office: when you put the mail that you want posting in a post box, you can be sure that the letter carrier will eventually deliver the mail to your recipient. In this analogy, RabbitMQ is a post box, a post office, and a letter carrier.RabbitMQ 是一个消息代理:它接受并转发消息。 你可以把它想象成一个邮局:当你把你想要邮寄的邮件放在邮箱里时, 您可以确定,邮递员最终会将邮件递送给您的收件人。 在这个类比中,RabbitMQ是一个邮政信箱,一个邮局和一个信件载体。

The major difference between RabbitMQ and the post office is that it doesn't deal with paper, instead it accepts, stores, and forwards binary blobs of data messages.RabbitMQ 和邮局的主要区别在于它不处理纸张, 相反,它接受、存储和转发数据的二进制 blob 消息。

rabbitmq 三大核心

producer 生产者:Producing means nothing more than sending. A program that sends messages is a producer.生产只意味着发送。发送消息的程序是生产者。

queue 队列:A queue is the name for the post box in RabbitMQ. Although messages flow through RabbitMQ and your applications, they can only be stored inside a queue. A queue is only bound by the host's memory & disk limits, it's essentially a large message buffer. Many producers can send messages that go to one queue, and many consumers can try to receive data from one queue. This is how we represent a queue.队列是 RabbitMQ 中邮箱的名称。 尽管消息流经 RabbitMQ 和您的应用程序,但它们只能存储在队列中。 队列仅受主机内存和磁盘限制的约束,它本质上是一个大型消息缓冲区。 许多生产者可以发送到一个队列的消息,许多使用者可以尝试从一个队列接收数据。 这就是我们表示队列的方式。

consumer 消费者:Consuming has a similar meaning to receiving. A consumer is a program that mostly waits to receive messages.消费与接受的含义相似。 使用者是一个主要等待接收消息的程序。

Note that the producer, consumer, and broker do not have to reside on the same host; indeed in most applications they don't. An application can be both a producer and consumer, too.请注意,生产者、使用者和代理不必驻留在同一个主机上;事实上,在大多数应用程序中,它们不会。 应用程序也可以是生产者和使用者。

In this part of the tutorial we'll write two programs in Java; a producer that sends a single message, and a consumer that receives messages and prints them out. We'll gloss over some of the detail in the Java API, concentrating on this very simple thing just to get started. It's a "Hello World" of messaging.在本教程的这一部分中,我们将用 Java 编写两个程序;一个发送单个消息的生产者和接收的使用者消息并打印出来。我们将在 Java API,专注于这个非常简单的事情只是为了得到开始。这是一个消息传递的“Hello World”。

In the diagram below, "P" is our producer and "C" is our consumer. The box in the middle is a queue - a message buffer that RabbitMQ keeps on behalf of the consumer.在下图中,“P”是我们的生产者,“C”是我们的消费者。这 中间的框是一个队列 - RabbitMQ 保留的消息缓冲区 代表消费者。

依赖:

 <dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.9.0</version>
</dependency>

案例:

import com.rabbitmq.client.*;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.concurrent.TimeoutException;


public class AppTest {
    public static final String QUEUE_NAME = "hello";
    ConnectionFactory factory;

    @Before
    public void init() {
        factory = new ConnectionFactory();
        factory.setHost("192.168.2.130");
        factory.setUsername("guest");
        factory.setPassword("guest");
    }

    @Test
    public void producer() throws IOException, TimeoutException {
        Connection connection = factory.newConnection();
        //获取信道
        Channel channel = connection.createChannel();
        /**
         * 生成一个队列
         * 1.队列名
         * 2.队列里面的消息是否持久化
         * 3.该队列是否被一个消费者消费,是否共享
         * 4.是否自动删除,最后一个消费者断开连接以后,该队列是否自动删除
         * 5.其他参数
         */
        //连接(创建)队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //发消息的消息内容
        String message = "hello world";
        /**
         * 发送消费
         * 1.发送到哪个交换机
         * 2.路由的Key值是哪个   本次队列的名称
         * 3.其他参数
         * 4.发送消息的消息体
         */
        //发送消息
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println("消息发送完毕");
    }

    @Test
    public void consumer() throws Exception {
        Connection connection = factory.newConnection();
        //获取信道
        Channel channel = connection.createChannel();

        /**
         * 消费者消费消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答,true(自动应答)false(手动应答)
         * 3. 当一个消息发送过来后的回调接口
         * 4.当一个消费者取消订阅时的回调接口;
         */
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println(new String(message.getBody()));
        };

        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("消费消息被中断");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
        Thread.sleep(10000000);
    }
}

执行 producer 方法可以看到 mq 里面已经有消息进入了。在 consumer 方法中通过 DeliverCallback 函数式接口获取消费消息,通过 CancelCallback 函数式接口获取消费消息被中断的回调。

We're about to tell the server to deliver us the messages from the queue. Since it will push us messages asynchronously, we provide a callback in the form of an object that will buffer the messages until we're ready to use them. That is what a DeliverCallback subclass does.我们即将告诉服务器将消息从队列。由于它将异步推送消息,因此我们提供了一个 对象形式的回调,将缓冲消息,直到 我们已准备好使用它们。这就是 DeliverCallback 子类的作用。    

点击队列可以查看队列里面的消息内容。

11.rabbitmq 工作队列

The main idea behind Work Queues (aka: Task Queues) is to avoid doing a resource-intensive task immediately and having to wait for it to complete. Instead we schedule the task to be done later. We encapsulate a task as a message and send it to a queue. A worker process running in the background will pop the tasks and eventually execute the job. When you run many workers the tasks will be shared between them.工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。 相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务。

This concept is especially useful in web applications where it's impossible to handle a complex task during a short HTTP request window.这个概念在 Web 应用程序中特别有用,因为它在短 HTTP 请求期间无法处理复杂任务窗格。

By default, RabbitMQ will send each message to the next consumer, in sequence. On average every consumer will get the same number of messages. This way of distributing messages is called round-robin. Try this out with three or more workers.默认情况下,RabbitMQ 会将每条消息发送给下一个消费者, 平均而言,每个消费者将获得相同数量的消息。这种分发消息的方式称为轮询。尝试 这需要两个或更多消费者。     

工作队列轮询的处理消息,两个线程轮询的处理队列中的消息。假设队列中有10 个消息,开启两个消费者 consumer1、consumer2 此时两个消费者平分 10 条消息,即一个消费者分到全是奇数的消息,一个分到的是偶数的消息。(注意:一定要先启动两个消费者,再启动生产者)

import com.rabbitmq.client.*;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.concurrent.TimeoutException;


public class WorkQueuesTest {
    public static final String QUEUE_NAME = "hello";
    ConnectionFactory factory;

    @Before
    public void init() {
        factory = new ConnectionFactory();
        factory.setHost("192.168.2.130");
        factory.setUsername("guest");
        factory.setPassword("guest");
    }

    @Test
    public void producer() throws IOException, TimeoutException {
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        for (int i = 0; i < 10; i++) {
            String message = i + "、hello world";
            System.out.println(message);
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        }
        System.out.println("消息发送完毕");
    }

    @Test
    public void consumer1() throws Exception {
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("consumer1:" + new String(message.getBody()));
        };
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("消费消息被中断");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
        Thread.sleep(10000000);
    }

    @Test
    public void consumer2() throws Exception {
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("consumer2" + new String(message.getBody()));
        };
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("消费消息被中断");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
        Thread.sleep(10000000);
    }
}

消息应答(Message acknowledgment)

Doing a task can take a few seconds, you may wonder what happens if a consumer starts a long task and it terminates before it completes. With our current code, once RabbitMQ delivers a message to the consumer, it immediately marks it for deletion. In this case, if you terminate a worker, the message it was just processing is lost. The messages that were dispatched to this particular worker but were not yet handled are also lost.执行任务可能需要几秒钟,您可能想知道如果消费者在启动一个长任务并在完成之前意外终止那会怎样。 使用我们当前的代码,一旦 RabbitMQ 向消费者传递消息,它立即将其标记为删除。在这种情况下,如果您终止消费者, 刚刚处理的消息丢失了。已发送的消息对于这个消费者来说尚未处理的消息也丢失了。   

 But we don't want to lose any tasks. If a worker dies, we'd like the task to be delivered to another worker.但我们不想失去任何任务。如果消费者挂掉,我们希望要交付给其他消费者该任务。

In order to make sure a message is never lost, RabbitMQ supports message acknowledgments. An acknowledgement is sent back by the consumer to tell RabbitMQ that a particular message has been received, processed and that RabbitMQ is free to delete it.为了确保消息永远不会丢失,RabbitMQ 支持消息确认。确认由消费者告诉 RabbitMQ 已收到特定消息, 处理然后 RabbitMQ 可以自由删除它。

If a consumer dies (its channel is closed, connection is closed, or TCP connection is lost) without sending an ack, RabbitMQ will understand that a message wasn't processed fully and will re-queue it. If there are other consumers online at the same time, it will then quickly redeliver it to another consumer. That way you can be sure that no message is lost, even if the workers occasionally die.如果消费者宕机(其通道关闭,连接关闭,或 TCP 连接丢失)如果不发送确认,RabbitMQ 将了解消息未完全处理,并将重新排队。 如果同时有其他消费者在线,它将迅速重新交付 给另一个消费者。这样您就可以确保不会丢失任何消息, 即使消费者偶尔会挂掉。

A timeout (30 minutes by default) is enforced on consumer delivery acknowledgement. This helps detect buggy (stuck) consumers that never acknowledge deliveries. You can increase this timeout as described in Delivery Acknowledgement Timeout.在消费者交付确认时强制实施超时(默认为 30 分钟)。 这有助于检测从不确认交付的错误(卡住)消费者。 您可以按照传递确认超时中所述增加此超时。

Manual message acknowledgments are turned on by default. In previous examples we explicitly turned them off via the autoAck=true flag. It's time to set this flag to false and send a proper acknowledgment from the worker, once we're done with a task.默认情况下,手动消息确认处于打开状态。在上一个示例,我们通过 autoAck=true 标志明确关闭了它们。从消费者那里将此标志设置为 false 并在我们完成了任务的情况下发送确认。

应答方式分为:自动应答和手动应答。

自动应答即消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡。因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢失了。当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制, 当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。

手动应答能够解决消费者在消费消息导致的消息丢失问题。 在消费者这边将消息处理完毕再给队列 channel 应答,防止消息的丢失。而不是自动应答一上来消息发送就认为应答成功。

手动应答需要如下步骤

1.设置 channel.basicConsume 第二个参数(true if the server should consider messages acknowledged once delivered; false if the server should expect explicit acknowledgements)为 false。

2.通过 message.getEnvelope().getDeliveryTag() 得到 long 类型的 deliveryTag。

3.Channel.basicAck(用于肯定确认) 该消息成功的处理,可以将其丢弃了。

4.Channel.basicNack(用于否定确认)不处理该消息了直接拒绝,可以将其丢弃了或者重新入队。

@Test
public void consumer() throws Exception {
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
    DeliverCallback deliverCallback = (consumerTag, message) -> {
        String string = new String(message.getBody());
        System.out.println(string);
        long deliveryTag = message.getEnvelope().getDeliveryTag();
        if (string.startsWith("3.")) {
            System.out.println("ack 返回 false,requeue:true 重新回到队列");
            channel.basicNack(deliveryTag, false, true);
        } else {
            System.out.println("ack 返回 true,第二个参数表示批量应答");
            channel.basicAck(deliveryTag, false);
        }
    };
    CancelCallback cancelCallback = consumerTag -> {
        System.out.println("消费消息被中断");
    };
    channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
    Thread.sleep(10000000);
}

批量应答

批量应答可以有效减少网络拥堵

multiple 的 true 和 false 代表不同意思 

true 代表批量应答 channel 上未应答的消息 比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是 8 那么此时 5-8 的这些还未应答的消息都会被确认收到消息应答 false 同上面相比 只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答。

消息自动重新入队

如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。

消息持久化(Message durability)

We have learned how to make sure that even if the consumer dies, the task isn't lost. But our tasks will still be lost if RabbitMQ server stops.我们已经学会了如何确保即使消费者死亡, 任务不会丢失。但是如果 RabbitMQ 服务器停止,我们的任务仍然会丢失。

When RabbitMQ quits or crashes it will forget the queues and messages unless you tell it not to. Two things are required to make sure that messages aren't lost: we need to mark both the queue and messages as durable.当 RabbitMQ 退出或崩溃时,它会忘记队列和消息除非你告诉它不要这样。需要做两件事来确保消息不会丢失:我们需要将队列和消息都标记为持久。

First, we need to make sure that the queue will survive a RabbitMQ node restart. In order to do so, we need to declare it as durable:首先,我们需要确保队列在 RabbitMQ 节点重新启动后能够幸存下来。 为此,我们需要将其声明为持久。

boolean durable = true;//队列持久化
channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null);

Although this command is correct by itself, it won't work in our present setup. That's because we've already defined a queue called hello which is not durable. RabbitMQ doesn't allow you to redefine an existing queue with different parameters and will return an error to any program that tries to do that. But there is a quick workaround - let's declare a queue with different name, for example task_queue:虽然这个命令本身是正确的,但它在我们目前不起作用设置。这是因为我们已经定义了一个名为 hello 的队列,它不持久。RabbitMQ 不允许您重新定义现有队列使用不同的参数,并将向任何程序返回错误那试图做到这一点。但是有一个快速的解决方法---让我们声明具有不同名称的队列,例如 task_queue:

boolean durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);

This queueDeclare change needs to be applied to both the producer and consumer code.此队列声明更改需要应用于两个生产者和消费者代码。

At this point we're sure that the task_queue queue won't be lost even if RabbitMQ restarts. Now we need to mark our messages as persistent - by setting MessageProperties (which implements BasicProperties) to the value PERSISTENT_TEXT_PLAIN.此时,我们确信 task_queue 队列不会丢失即使 RabbitMQ 重新启动。现在我们需要将我们的消息标记为持久 - 通过设置消息属性(实现基本属性) 到值PERSISTENT_TEXT_PLAIN。

import com.rabbitmq.client.MessageProperties;

channel.basicPublish("", "task_queue",
            MessageProperties.PERSISTENT_TEXT_PLAIN,
            message.getBytes());

公平调度(Fair dispatch)

You might have noticed that the dispatching still doesn't work exactly as we want. For example in a situation with two workers, when all odd messages are heavy and even messages are light, one worker will be constantly busy and the other one will do hardly any work. Well, RabbitMQ doesn't know anything about that and will still dispatch messages evenly.您可能已经注意到调度仍然无法正常工作正如我们所愿。例如,在有两个工人的情况下,当所有奇数消息很重,偶数消息很轻,一个工作人员将是经常忙碌,另一个几乎不会做任何工作。但 RabbitMQ 对此一无所知,仍然会调度消息均匀。

This happens because RabbitMQ just dispatches a message when the message enters the queue. It doesn't look at the number of unacknowledged messages for a consumer. It just blindly dispatches every n-th message to the n-th consumer.发生这种情况是因为 RabbitMQ 只是在消息时调度消息进入队列。它不看未确认的数量面向消费者的消息。它只是盲目地发送每 n 条消息给第 n 个消费者。

@Test
public void consumer1() throws IOException, TimeoutException, InterruptedException {
    Connection connection = factory.newConnection();
    //获取信道
    Channel channel = connection.createChannel();
    channel.basicQos(1);
    //收到信息的回调方法
    DeliverCallback deliverCallback = (consumerTag, message) -> {
        long deliveryTag = message.getEnvelope().getDeliveryTag();
        System.out.println("consumer1:" + new String(message.getBody()));
        channel.basicAck(deliveryTag, false);
    };
    channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> {
        System.out.println("消费消息被中断");
    });
    Thread.sleep(1000000);
}

@Test
public void consumer2() throws IOException, TimeoutException, InterruptedException {
    Connection connection = factory.newConnection();
    //获取信道
    Channel channel = connection.createChannel();
    channel.basicQos(2);
    //收到信息的回调方法
    DeliverCallback deliverCallback = (consumerTag, message) -> {
        long deliveryTag = message.getEnvelope().getDeliveryTag();
        String msg = new String(message.getBody());
        System.out.println("consumer2:" + msg);
        channel.basicAck(deliveryTag, false);
    };
    channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> {
        System.out.println("消费消息被中断");
    });
    Thread.sleep(1000000);
}

In order to defeat that we can use the basicQos method with the prefetchCount = 1 setting. This tells RabbitMQ not to give more than one message to a worker at a time. Or, in other words, don't dispatch a new message to a worker until it has processed and acknowledged the previous one. Instead, it will dispatch it to the next worker that is not still busy.为了解决这个问题,我们可以将 basicQos 方法与预取计数 = 1 设置一起使用。这告诉 RabbitMQ 不要付出更多一次给消费者一条消息。或者,换句话说,不要调度给消费者新的消息,直到它处理并确认前一个。相反,它会将其调度给下一个尚未繁忙的消费者。

int prefetchCount = 1;
channel.basicQos(prefetchCount);

Note about queue size

If all the workers are busy, your queue can fill up. You will want to keep an eye on that, and maybe add more workers, or have some other strategy.如果所有工作人员都忙,您的队列可能会填满。你会想要保持一个关注这一点,也许增加更多的工人或者有其他策略。

12.rabbitmq 发布确认

为了保证生产者发送给 MQ 的过程中消息不丢失,MQ 引入发布确认机制就是发送方确认模式。

参考文档:RabbitMQ tutorial - Reliable Publishing with Publisher Confirms | RabbitMQ

Publisher confirms are a RabbitMQ extension to implement reliable publishing. When publisher confirms are enabled on a channel, messages the client publishes are confirmed asynchronously by the broker, meaning they have been taken care of on the server side.发布者确认是 RabbitMQ 扩展以实现可靠的出版。当频道上启用了发布商确认时,客户端发布的消息是异步确认的由代理处理,这意味着它们已在服务器上得到处理边。

Publisher confirms are a RabbitMQ extension to the AMQP 0.9.1 protocol, so they are not enabled by default. Publisher confirms are enabled at the channel level with the confirmSelect method:发布者确认是 AMQP 0.9.1 协议的 RabbitMQ 扩展, 因此默认情况下不启用它们。发布者确认是使用 confirmSelect 方法在通道级别启用:

生产者在 Channel(通道)上调用 confirmSelect()方法,请求将 Broker 的 Channel(信道)设置成为 confirm 模式,一旦信道进入confirm 模式,所有在该信道上流通的消息都会被指派一个唯一的 ID(从1开始),并将消息被推送到匹配的队列中。

生产者可以在推送消息之后,通过信道调用waitForConfirms()方法,向RabbitMQ发送消息发布确认请求,RabbitMQ服务器会返回消息发布确认的状态给生产者(包括确认的消息及消息的ID信息),这样生产者就知道消息已经被正确推送到目的队列了。如果收到发布确认失败的消息,生产者可以进行后续的重新发送。 confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消 息,生产者应用程序同样可以在回调方法中处理该 nack 消息。

发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布确认,都需要在 channel 上调用该方法。

channel.confirmSelect();

Enables publisher acknowledgements on this channel.

发布确认分为单个发布确认、批量发布确认和异步发布确认。

1.单个发布确认(Publishing Messages Individually)

这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirms()这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。

This technique is very straightforward but also has a major drawback: it significantly slows down publishing, as the confirmation of a message blocks the publishing of all subsequent messages. This approach is not going to deliver throughput of more than a few hundreds of published messages per second. Nevertheless, this can be good enough for some applications. 这种技术非常简单,但也有一个主要缺点:它大大减慢了发布速度,因为消息的确认会阻止所有后续消息的发布。此方法不会提供每秒超过几百条已发布消息的吞吐量。尽管如此,这对于某些应用程序来说已经足够了。

@Test
public void producer() throws IOException, TimeoutException, InterruptedException {
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    //开启发布确认
    channel.confirmSelect();
    long begin = System.currentTimeMillis();
    for (int i = 0; i < 10; i++) {
        String message = i + ".hello world";
        System.out.println(message);
        channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
        //发送确认.单个发消息进行发布确认
        boolean b = channel.waitForConfirms();
        if (b) {
            System.out.println("消息发送成功" + (System.currentTimeMillis() - begin));
        }
    }
    System.out.println("消息发送完毕");
}

2.批量发布确认(Publishing Messages in Batches)

上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量。 当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。

@Test
public void producer() throws IOException, TimeoutException, InterruptedException {
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    //开启发布确认
    channel.confirmSelect();
    //指定批量的大小
    int batchSize = 100;
    long begin = System.currentTimeMillis();
    for (int i = 0; i < 100000; i++) {
        String message = i + ".hello world";
        System.out.println(message);
        channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
        //发送确认.单个发消息进行发布确认
        if (i % batchSize == 0) {
            boolean b = channel.waitForConfirms();
            if (b) {
                System.out.println("消息发送成功" + (System.currentTimeMillis() - begin));
            }
        }

    }
    System.out.println("消息发送完毕");
}

Waiting for a batch of messages to be confirmed improves throughput drastically over waiting for a confirm for individual message (up to 20-30 times with a remote RabbitMQ node). One drawback is that we do not know exactly what went wrong in case of failure, so we may have to keep a whole batch in memory to log something meaningful or to re-publish the messages. And this solution is still synchronous, so it blocks the publishing of messages. 等待一批消息被确认比等待单个消息的确认大大提高了吞吐量(使用远程 RabbitMQ 节点最多 20-30 倍)。一个缺点是,在发生故障时,我们不知道到底出了什么问题,因此我们可能不得不在内存中保留整个批次以记录有意义的内容或重新发布消息。并且此解决方案仍然是同步的,因此它会阻止消息的发布。

3.异步发布确认(Handling Publisher Confirms Asynchronously)

异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功。

首先在生产者发送消息之前,在生产者程序中设置一个确认发布监听addConfirmListener(ackCallback,nackCallback),监听哪些消息的成功和失败。用于给Borker端调用,Broker将确认发布信息推送给生产者确认发布监听器,设置好监听器之后生产者就可以向Broker端推送消息了。

确认发布监听器与消息推送是两个独立的线程,所以推送消息和监听状态是可以并行的,也就是所谓的异步操作,生产者可以一直给Broker端推送消息,Broker端会将确认状态通过监听器回调给生产者,Broker端会给确认发布监听器传递两种确认发布的状态,确认成功(Confirm Ack)和确认失败(Confirm Nack),生产者可以获取到成功或失败的消息,做其它处理,如将失败的消息重新发送。所以异步确认是性价比最好的。

@Test
public void producer() throws IOException, TimeoutException, InterruptedException {
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    //开启发布确认
    channel.confirmSelect();
    //指定批量的大小

    //消息确认成功回调函数        var1消息的标识      var2是否批量确认
    ConfirmCallback ackCallback = (var1, var2) -> {
        System.out.println("确认消息" + var1);
    };
    //消息确认失败回调函数
    ConfirmCallback nackCallback = (var1, var2) -> {
        System.out.println("未确认消息" + var1);
    };
    //准备消息监听器。监听哪些消息成功和失败
    channel.addConfirmListener(ackCallback, nackCallback);
    for (int i = 0; i < 100000; i++) {
        String message = i + ".hello world";
        System.out.println(message);
        channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
    }
    System.out.println("消息发送完毕");
}

处理异步未确认消息最好的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列, 比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。

There are 2 callbacks: one for confirmed messages and one for nack-ed messages (messages that can be considered lost by the broker). Each callback has 2 parameters: 有 2 个回调:一个用于已确认的消息,一个用于裸消息(代理可以认为丢失的消息)。每个回调有 2 个参数:

  • sequence number: a number that identifies the confirmed or nack-ed message. We will see shortly how to correlate it with the published message. 序列号:标识已确认或未加密邮件的数字。我们将很快看到如何将其与已发布的消息相关联。
  • multiple: this is a boolean value. If false, only one message is confirmed/nack-ed, if true, all messages with a lower or equal sequence number are confirmed/nack-ed. 倍数:这是一个布尔值。如果为 false,则仅确认/未编辑一条消息,如果为 true,则确认/未编辑序列号的所有消息。

The sequence number can be obtained with Channel#getNextPublishSeqNo() before publishing: 序列号可以在发布前使用 Channel#getNextPublishSeqNo() 获取:

int sequenceNumber = channel.getNextPublishSeqNo());
ch.basicPublish(exchange, queue, properties, body);

A simple way to correlate messages with sequence number consists in using a map. Let's assume we want to publish strings because they are easy to turn into an array of bytes for publishing. Here is a code sample that uses a map to correlate the publishing sequence number with the string body of the message: 将消息与序列号相关联的一种简单方法是使用映射。假设我们要发布字符串,因为它们很容易转换为字节数组进行发布。下面是一个代码示例,它使用映射将发布序列号与消息的字符串正文相关联:

ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
// ... code for confirm callbacks will come later
String body = "...";
outstandingConfirms.put(channel.getNextPublishSeqNo(), body);
channel.basicPublish(exchange, queue, properties, body.getBytes());

The publishing code now tracks outbound messages with a map. We need to clean this map when confirms arrive and do something like logging a warning when messages are nack-ed: 发布代码现在使用地图跟踪出站消息。我们需要在确认到达时清理此地图,并执行一些操作,例如在消息未显示时记录警告:

ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
ConfirmCallback cleanOutstandingConfirms = (sequenceNumber, multiple) -> {
    if (multiple) {
        ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(
          sequenceNumber, true
        );
        confirmed.clear();
    } else {
        outstandingConfirms.remove(sequenceNumber);
    }
};

channel.addConfirmListener(cleanOutstandingConfirms, (sequenceNumber, multiple) -> {
    String body = outstandingConfirms.get(sequenceNumber);
    System.err.format(
      "Message with body %s has been nack-ed. Sequence number: %d, multiple: %b%n",
      body, sequenceNumber, multiple
    );
    cleanOutstandingConfirms.handle(sequenceNumber, multiple);
});
// ... publishing code

The previous sample contains a callback that cleans the map when confirms arrive. Note this callback handles both single and multiple confirms. This callback is used when confirms arrive (as the first argument of Channel#addConfirmListener). The callback for nack-ed messages retrieves the message body and issues a warning. It then re-uses the previous callback to clean the map of outstanding confirms (whether messages are confirmed or nack-ed, their corresponding entries in the map must be removed.) 前面的示例包含一个回调,该回调在确认到达时清理地图。请注意,此回调同时处理单个和多个确认。此回调在确认到达时使用(作为 Channel#addConfirmListener 的第一个参数)。裸消息的回调将检索消息正文并发出警告。然后,它重用上一个回调来清理映射中的未完成确认(无论消息是确认还是裸报,都必须删除它们在映射中的相应条目。

How to Track Outstanding Confirms? 如何跟踪未完成确认? 

 Our samples use a ConcurrentNavigableMap to track outstanding confirms. This data structure is convenient for several reasons. It allows to easily correlate a sequence number with a message (whatever the message data is) and to easily clean the entries up to a given sequence id (to handle multiple confirms/nacks). At last, it supports concurrent access, because confirm callbacks are called in a thread owned by the client library, which should be kept different from the publishing thread. 我们的样本使用ConcurrentNavigableMap来跟踪未完成的确认。出于几个原因,此数据结构很方便。它允许轻松地将序列号与消息相关联(无论消息数据是什么),并轻松地将条目清理到给定的序列 ID(以处理多个确认/nack)。最后,它支持并发访问,因为确认回调是在客户端库拥有的线程中调用的,该线程应与发布线程保持不同。

There are other ways to track outstanding confirms than with a sophisticated map implementation, like using a simple concurrent hash map and a variable to track the lower bound of the publishing sequence, but they are usually more involved and do not belong to a tutorial. 除了复杂的映射实现之外,还有其他方法可以跟踪未完成的确认,例如使用简单的并发哈希映射和变量来跟踪发布序列的下限,但它们通常涉及更多,不属于教程。

To sum up, handling publisher confirms asynchronously usually requires the following steps: 总而言之,异步处理发布商确认通常需要以下步骤:

  • provide a way to correlate the publishing sequence number with a message. 提供一种将发布序列号与消息相关联的方法。
  • register a confirm listener on the channel to be notified when publisher acks/nacks arrive to perform the appropriate actions, like logging or re-publishing a nack-ed message. The sequence-number-to-message correlation mechanism may also require some cleaning during this step. 在通道上注册确认侦听器,以便在发布者 ACK/NACKS 到达时收到通知以执行适当的操作,例如记录或重新发布 NACK-ed 消息。在此步骤中,序列号到消息的关联机制可能还需要进行一些清理。
  • track the publishing sequence number before publishing a message. 在发布消息之前跟踪发布序列号。

13.rabbitmq 交换机

In the previous tutorial we created a work queue. The assumption behind a work queue is that each task is delivered to exactly one worker. In this part we'll do something completely different -- we'll deliver a message to multiple consumers. This pattern is known as "publish/subscribe".在前面的教程中,我们创建了一个工作队列。工作队列背后的假设是每个任务都是只交付给一名消费者。在这一部分中,我们将做一些事情完全不同的 - 我们将向多个消费者传递消息。此模式称为“发布/订阅”。

To illustrate the pattern, we're going to build a simple logging system. It will consist of two programs -- the first will emit log messages and the second will receive and print them.为了说明这种模式,我们将构建一个简单的日志记录系统。它将由两个程序组成 —第一个将发出日志消息,第二个将接收并打印它们。

In our logging system every running copy of the receiver program will get the messages. That way we'll be able to run one receiver and direct the logs to disk; and at the same time we'll be able to run another receiver and see the logs on the screen.在我们的日志记录系统中,接收器程序的每个运行副本都将获取消息。这样我们就能够运行一个接收器和将日志定向到磁盘;同时我们将能够运行另一个接收器,并在屏幕上查看日志。

Essentially, published log messages are going to be broadcast to all the receivers.本质上,已发布的日志消息将广播给所有人接收器。

Exchanges(交换机)

In previous parts of the tutorial we sent and received messages to and from a queue. Now it's time to introduce the full messaging model in Rabbit.在本教程的前几部分中,我们队列中发送和接收消息。现在是时候引入完整的消息传递模型了。

Let's quickly go over what we covered in the previous tutorials: 

  • A producer is a user application that sends messages.生产者是发送消息的用户应用程序。
  • A queue is a buffer that stores messages.队列是存储消息的缓冲区。
  • A consumer is a user application that receives messages.消费者是接收消息的用户应用程序。

The core idea in the messaging model in RabbitMQ is that the producer never sends any messages directly to a queue. Actually, quite often the producer doesn't even know if a message will be delivered to any queue at all.RabbitMQ 中消息传递模型的核心思想是生产者从不将任何消息直接发送到队列。实际上,很多时候生产者甚至不知道消息是否会传递给任何队列。

Instead, the producer can only send messages to an exchange. An exchange is a very simple thing. On one side it receives messages from producers and the other side it pushes them to queues. The exchange must know exactly what to do with a message it receives. Should it be appended to a particular queue? Should it be appended to many queues? Or should it get discarded. The rules for that are defined by the exchange type.相反,生产者只能向交换机发送消息。交换机作用是非常简单的。一方面,它接收来自生产者的消息然后把他们推到队列中。交换机必须确切地知道如何处理它收到的消息。应该是附加到特定队列?是否应该将其附加到许多队列中? 或者应该丢弃它。其规则由交换类型定义。

交换机类型主要包含以下四种:

  1. Fanout exchange(扇型交换机)
  2. Direct exchange(直连交换机)
  3. Topic exchange(主题交换机)
  4. Headers exchange(头交换机)

另外还有 RabbitMQ 的默认交换机: 默认交换机, 还有一类特殊的交换机 Dead Letter Exchange(死信交换机)

在前面部分我们对 exchange 一无所知,但仍然能够将消息发送到队列。之前能实现的原因是因为我们使用的是默认交换,我们通过空字符串(“”)进行标识。

channel.basicPublish("","hello",null,message.getBytes("UTF-8"));

第一个参数就是交换机的名字,空字符串表示默认或无名交换机。

(简单队列-->默认交换机)

消息能路由发送到队列中其实是由 routingKey(bindingkey)绑定 key 指定的。

1.扇型交换机(Fanout exchange)一般也叫工作队列、发布订阅模式或广播模式,这个模式即消息广播给所有订阅该消息的消费者。

(工作队列/发布订阅模式/广播模式-->扇形交换机)

使用 fanout 类型交换器,routingKey 忽略。每个消费者定义生成一个队列并绑定到同一个 Exchange,每个消费者都可以消费到完整的消息。 白话就是,消费者中的队列只要与模式fanout 的交换机进行了绑定 bingding,那么从这个交换机走的消息无视路由 routingKey,将消息全部发放到队列里,然后消费者消费。

import com.rabbitmq.client.*;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;

public class FanoutTest {
    public static final String EXCHANGE_NAME = "hello";
    ConnectionFactory factory;

    @Before
    public void init() {
        factory = new ConnectionFactory();
        factory.setHost("192.168.2.130");
        factory.setUsername("guest");
        factory.setPassword("guest");
    }

    @Test
    public void producer() throws IOException, TimeoutException, InterruptedException {
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        String message = "hello world";
        //fanout模式下:路由键无效不需要指示
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
    }

    @Test
    public void consumer1() throws Exception {
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        //声明队列   临时队列
        /**
         *生成一个临时队列、队列的名陈是随机的
         *当消费者断开队列连接的时候,队列自动删除
         */
        String queueName = channel.queueDeclare().getQueue();
        //绑定交换机与队列
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        System.out.println("等待接收消息,把接收的消息打印到屏幕上.......");


        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("消费者1消费的消息:" + new String(message.getBody()));
        };

        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("消费消息被中断");
        };
        //接收消息
        channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
        Thread.sleep(10000000);
    }

}

临时队列(Temporary queues):

As you may remember previously we were using queues that had specific names (remember hello and task_queue?). Being able to name a queue was crucial for us -- we needed to point the workers to the same queue. Giving a queue a name is important when you want to share the queue between producers and consumers.您可能还记得以前我们使用的队列具有具体名称(还记得 hello 和 task_queue 吗?能够命名 队列对我们来说至关重要 - 我们需要将消费者指向相同的队列。在以下情况下,为队列命名很重要希望在生产者和消费者之间共享队列。

But that's not the case for our logger. We want to hear about all log messages, not just a subset of them. We're also interested only in currently flowing messages not in the old ones. To solve that we need two things.但对于我们的记录器来说,情况并非如此。我们想听听所有日志消息,而不仅仅是其中的子集。我们只对当前流动的消息感兴趣,而不是旧的。为了解决这个问题,我们需要两件事。

Firstly, whenever we connect to Rabbit we need a fresh, empty queue. To do this we could create a queue with a random name, or, even better - let the server choose a random queue name for us.首先,每当我们连接到 Rabbit 时,我们都需要一个新的空队列。 为此,我们可以创建一个具有随机名称的队列,或者, 甚至更好 - 让服务器为我们选择一个随机队列名称。

Secondly, once we disconnect the consumer the queue should be automatically deleted.其次,一旦我们断开了消费者的连接,队列应该是自动删除。

In the Java client, when we supply no parameters to queueDeclare() we create a non-durable, exclusive, autodelete queue with a generated name:在 Java 客户端中,当我们不向 queueDeclare() 提供任何参数时,我们会创建一个非持久、独占的、自动删除的队列,并生成一个:

String queueName = channel.queueDeclare().getQueue();

At that point queueName contains a random queue name. For example it may look like amq.gen-JzTY20BRgKO-HjmUJj0wLg.此时,队列名称包含一个随机队列名称。例如 它可能看起来像amq.gen-JzTY20BRgKO-HjmUJj0wLg。

We've already created a fanout exchange and a queue. Now we need to tell the exchange to send messages to our queue. That relationship between exchange and a queue is called a binding.我们已经创建了一个扇形交换机和一个队列。现在我们需要告诉交换机将消息发送到我们的队列。这种关系交换和队列之间称为绑定。

channel.queueBind(queueName, "hello", "");

当然如果想要将多个消费者共享同一个队列,也可以收动指定队列名。

In this tutorial we're going to add a feature to it - we're going to make it possible to subscribe only to a subset of the messages. For example, we will be able to direct only critical error messages to the log file (to save disk space), while still being able to print all of the log messages on the console.在本教程中,我们将为其添加一个功能 - 我们将使仅订阅消息的子集成为可能。例如,我们将关键错误消息定向到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有的日志消息。

A binding is a relationship between an exchange and a queue. This can be simply read as: the queue is interested in messages from this exchange.绑定是交换和队列之间的关系。这可以简单地读作:队列对来自此交换机的消息感兴趣。

Bindings can take an extra routingKey parameter. To avoid the confusion with a basic_publish parameter we're going to call it a binding key. This is how we could create a binding with a key:绑定可以采用额外的 routingKey 参数。为了避免与 basic_publish 参数混淆,我们将它称为绑定键。这就是我们如何使用键创建绑定:

channel.queueBind(queueName, EXCHANGE_NAME, "black");

The meaning of a binding key depends on the exchange type. The fanout exchanges, which we used previously, simply ignored its value.绑定键的含义取决于交换类型。我们之前使用的扇形交换机只是忽略了它的值。

2.直连交换机(Direct exchange)

Our logging system from the previous tutorial broadcasts all messages to all consumers. We want to extend that to allow filtering messages based on their severity. For example we may want a program which writes log messages to the disk to only receive critical errors, and not waste disk space on warning or info log messages.上一教程中的日志记录系统广播所有消息给所有消费者。我们希望扩展它以允许过滤消息基于其严重程度。例如,我们可能想要一个程序将日志消息写入磁盘以仅接收严重错误,以及不要在警告或信息日志消息上浪费磁盘空间。

We were using a fanout exchange, which doesn't give us much flexibility - it's only capable of mindless broadcasting.我们使用的是扇形交换机,这并没有给我们太多灵活性 - 它只能进行盲目广播。

We will use a direct exchange instead. The routing algorithm behind a direct exchange is simple - a message goes to the queues whose binding key exactly matches the routing key of the message.我们将改用直连交换机。背后的路由算法直接交换很简单 - 消息转到绑定键与消息的路由键完全匹配的队列。

(路由模式-->直连交换机)

P 表示为生产者、 X 表示交换机、C1C2 表示为消费者,红色表示队列。

上图是一个结合日志消费级别的配图,在路由模式它会把消息路由到那些 binding key 与 routing key 完全匹配的 Queue 中,此模式也就是 Exchange 模式中的 direct 模式。 以上图的配置为例,我们以 routingKey="orange" 发送消息到 Exchange,则消息会路由到 Queue1(amqp.gen-S9b…,这是由 RabbitMQ 自动生成的Queue名称)和 Queue2(amqp.gen-Agl…)。如果我们以 routingKey="black" 或 routingKey="green" 来发送消息,则消息只会路由到 Queue2。如果我们以其他 routingKey 发送消息,则消息不会路由到这两个 Queue 中。

相对于发布订阅模式,我们可以看到不再是广播似的接收全部消息,而是有选择性的消费。

import com.rabbitmq.client.*;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class DirectTest {
    public static final String EXCHANGE_NAME = "direct_log";
    ConnectionFactory factory;

    @Before
    public void init() {
        factory = new ConnectionFactory();
        factory.setHost("192.168.2.130");
        factory.setUsername("guest");
        factory.setPassword("guest");
    }

    @Test
    public void producer() throws IOException, TimeoutException, InterruptedException {
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        String message = "", sendType = "";
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                sendType = "orange";
                message = "我是 orange 级别的消息类型:" + i;
            } else {
                sendType = "black";
                message = "我是 black 级别的消息类型:" + i;
            }
            System.out.println("[send]:" + message + "  " + sendType);
            channel.basicPublish(EXCHANGE_NAME, sendType, null, message.getBytes());
            Thread.sleep(5 * i);
        }
    }

    @Test
    public void consumer1() throws Exception {
        String queueName = "q1";
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(queueName, false, false, false, null);
        channel.queueBind(queueName, EXCHANGE_NAME, "orange");
        System.out.println("等待接收消息,把接收的消息打印到屏幕上.......");
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("消费者1消费的消息:" + new String(message.getBody()));
        };
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("消费消息被中断");
        };
        channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
        Thread.sleep(10000000);
    }

    @Test
    public void consumer2() throws Exception {
        String queueName = "q2";
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(queueName, false, false, false, null);

        channel.queueBind(queueName, EXCHANGE_NAME, "black");
        channel.queueBind(queueName, EXCHANGE_NAME, "green");
        System.out.println("等待接收消息,把接收的消息打印到屏幕上.......");
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("消费者2消费的消息:" + new String(message.getBody()));
        };
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("消费消息被中断");
        };
        channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
        Thread.sleep(10000000);
    }
}

最终结果就是,在生产者里面指定哪个 routingKey,消息就会发送到哪个对应相同的 routingKey 的队列当中,被消费者消费 。

路由模式下的交换机只能根据 routingKey 指定一个队列(前提此时不同队列的 routingKey 是不同的),如果我们有一条消息想要同时通过交换机发送给两个不同的队列,路由模式是不行的,它不能同时指定两个队列。下面的主题模式(Topic exchange)可以解决。

3.主题交换机(Topic exchange)

In our logging system we might want to subscribe to not only logs based on severity, but also based on the source which emitted the log. You might know this concept from the syslog unix tool, which routes logs based on both severity (info/warn/crit...) and facility (auth/cron/kern...).在我们的日志系统中,我们可能不仅要基于严重性订阅日志,也要基于发出日志的来源。 您可能从 syslog unix 工具中知道这个概念,该工具根据严重性(信息/警告/暴击...)和设施路由日志 (auth/cron/kern...)。

That would give us a lot of flexibility - we may want to listen to just critical errors coming from 'cron' but also all logs from 'kern'.这将给我们很大的灵活性 - 我们可能想听只有来自“cron”的严重错误,还有来自“kern”的所有日志。

To implement that in our logging system we need to learn about a more complex topic exchange.为了在我们的日志记录系统中实现这一点,我们需要了解更多复杂的话题交流。

Messages sent to a topic exchange can't have an arbitrary routing_key - it must be a list of words, delimited by dots. The words can be anything, but usually they specify some features connected to the message. A few valid routing key examples: "stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit". There can be as many words in the routing key as you like, up to the limit of 255 bytes.发送到主题交换的消息不能有任意routing_key - 它必须是单词列表,由点分隔。这些单词可以是任何东西,但通常它们指定了一些已连接到的特征消息。一些有效的路由密钥示例: “stock.usd.nyse”, “nyse.vmw”, “quick.orange.rabbit”。可以有路由密钥中随心所欲地包含多个单词,最多可达 255 个字节。

The binding key must also be in the same form. The logic behind the topic exchange is similar to a direct one - a message sent with a particular routing key will be delivered to all the queues that are bound with a matching binding key. However there are two important special cases for binding keys:绑定密钥也必须采用相同的形式。主题交换背后的逻辑类似于直接交换 - 发送的消息带有特定的路由密钥将被传递到所有队列绑定匹配的绑定键。但是有两个重要的绑定键的特殊情况:

  • * (star) can substitute for exactly one word. 
  • # (hash) can substitute for zero or more words.

topics 主题模式跟路由模式类似,只不过路由模式是指定固定的路由键 routingKey,而主题模式是可以模糊匹配路由键 routingKey,类似于SQL中 = 和 like 的关系。

从前面的几篇我们依次经历了 exchange 模式从 fanout > direct 的转变过程,在 fanout 时,我们只能进行简单的广播,对应类型比较单一,使用 direct 后,消费者则可以进行一定程度的选择,但是,direct 还是有局限性,路由不支持多个条件。

direct 不支持匹配 routingKey,一但绑定了就是绑定了,而 topic 主题模式支持规则匹配,只要符合 routingKey 就能发送到绑定的队列上。

(主题模式-->主题交换机)

P 表示为生产者、 X 表示交换机、C1C2 表示为消费者,红色表示队列。

topics 模式与 routing 模式比较相近,topics 模式不能具有任意的 routingKey,必须由 一个英文句点号“.”分隔的字符串(我们将被句点号“.”分隔开的每一段独立的字符串称为一个单词),比如 "lazy.orange.fox"。topics routingKey 中可以存在两种特殊字符“*”与“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。

以上图中的配置为例:

如果一个消息的 routingKey 设置为 “xxx.orange.rabbit”,那么该消息会同时路由到 Q1 与 Q2,routingKey="lazy.orange.fox”的消息会路由到Q1与Q2;

routingKey="lazy.brown.fox”的消息会路由到 Q2;

routingKey="lazy.pink.rabbit”的消息会路由到 Q2(只会投递给Q2一次,虽然这个routingKey 与 Q2 的两个 bindingKey 都匹配);

routingKey="quick.brown.fox”、routingKey="orange”、routingKey="quick.orange.male.rabbit”的消息将会被丢弃,因为它们没有匹配任何 bindingKey。

import com.rabbitmq.client.*;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class TopicExchange {
    public static final String EXCHANGE_NAME = "my_topic_exchange";
    ConnectionFactory factory;

    @Before
    public void init() {
        factory = new ConnectionFactory();
        factory.setHost("192.168.196.129");
        factory.setUsername("guest");
        factory.setPassword("guest");

    }

    @Test
    public void producer1() throws IOException, TimeoutException {
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
        String routingKey = "aaa.orange.ccc";
        String msg = "topic_exchange_msg: " + routingKey;
        System.out.println("[send] = " + msg);
        channel.basicPublish(EXCHANGE_NAME, routingKey, null, msg.getBytes());
        System.out.println("消息发送完毕");
    }

    @Test
    public void consumer1() throws IOException, TimeoutException, InterruptedException {
        String Q1 = "topic_log_disk";
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(Q1, false, false, false, null);
        channel.queueBind(Q1, EXCHANGE_NAME, "*.orange.*");
        System.out.println("等待接收消息,把接收的消息打印到屏幕上......");
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("C1:" + new String(message.getBody()));
        };
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("消息被中断");
        };
        channel.basicConsume(Q1, true, deliverCallback, cancelCallback);
        Thread.sleep(1000000);
    }

    @Test
    public void consumer2() throws IOException, TimeoutException, InterruptedException {
        String Q2 = "topic_log_info";
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(Q2, false, false, false, null);
        channel.queueBind(Q2, EXCHANGE_NAME, "*.*.rabbit");
        channel.queueBind(Q2, EXCHANGE_NAME, "lazy.#");
        System.out.println("等待接收消息,把接收的消息打印到屏幕上......");
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("Q2 :" + new String(message.getBody()));
        };
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("消息被中断");
        };
        channel.basicConsume(Q2, true, deliverCallback, cancelCallback);
        Thread.sleep(1000000);
    }

}

1、topic 相对于之前几种算是比较复杂了,简单来说,就是每个队列都有其关心的主题,所有的消息都带有一个“标题”(RouteKey),exchange 会将消息转发到所有关注主题能与 routeKey 模糊匹配的队列。

2、在进行绑定时,要提供一个该队列关心的主题,如“#.sscai.#”表示该队列关心所有涉及 sscai 的消息(一个 routeKey 为 "club.sscai.tmax”的消息会被转发到该队列)。

3、"#”表示0个或若干个关键字,“*”表示一个关键字。如“club.*”能与“club.sscai”匹配,无法与“club.sscai.xxx”匹配;但是“club.#”能与上述两者匹配。

4、同样,如果 exchange 没有发现能够与 routeKey 匹配的 Queue,则会抛弃此消息。

练习:

练习:按照下图设计直连交换机为临时队列。

14.rabbitmq 死信队列

死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。

应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。还有比如说:用户在商城下单成功并点击去支付后在指定时间未支付时自动失效。

死信原因

  • 消息 TTL 过期
  • 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
  • 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.
import com.rabbitmq.client.*;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;

public class DeadLetterTest {
    /**
     * 造成消息进入死信队里的原因三点:
     * 消费者拒绝消费
     * 消息时间过期
     * 队列长度已满,消息无法进入
     * <p>
     * 死信队列的消息不是直接由生产者将消息发送进入队列的,而是满足以上三种条件后,
     * 被普通队列将对应的消息传到死信交换机,然后死信交换机又将消息传入死信队列然后消息被消费者消费。
     */
    //普通交换机的名字
    public static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机的名字
    public static final String DEAD_EXCHANGE = "dead_exchange";
    //普通队列的名字
    public static final String NORMAL_NAME = "normal_name";
    //死信队列的名字
    public static final String DEAD_NAME = "dead_name";
    ConnectionFactory factory;

    @Before
    public void init() {
        factory = new ConnectionFactory();
        factory.setHost("192.168.2.131");
        factory.setUsername("guest");
        factory.setPassword("guest");
    }

    @Test
    public void producer() throws IOException, TimeoutException {
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        /**
         * 这个参数主要用来修改消息的
         */
        //1.模拟消息时间过期条件  消息过期时间设置10s
        // AMQP.BasicProperties properties=new AMQP.BasicProperties().builder().expiration("10000").build();
        //发送10条消息
        for (int i = 0; i < 10; i++) {
            String message = "info" + i;
            channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", null, message.getBytes());
        }

    }

    @Test
    public void consumer1() throws Exception {
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //声明普通交换机  类型为direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        //声明死信交换机  类型direct
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        /**
         * 由于死信交换机的消息来源于普通队列当中,所以声明普通队列时,要传入对应的参数
         这个参数主要是用来修改队列的
         * 这两个就相当于将普通队列触发条件后的消息可以转发到死信交换机中
         * var5.put("x-dead-letter-exchange",NORMAL_EXCHANGE);
         *  var5.put("x-dead-letter-routing-key","lisi");
         */
        Map<String, Object> var5 = new HashMap<>();
        //正常队列设置要转发的死信交换机  key都是固定的
        var5.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        //正常队列设置死信队列的routingKey
        var5.put("x-dead-letter-routing-key", "lisi");
        //1.设置消息过期时间
        //var5.put("x-message-ttl",10000);
        //2.设置正常队列长度 模拟队列满了的情况
        //var5.put("x-max-length",6);
        //声明普通队列
        channel.queueDeclare(NORMAL_NAME, false, false, false, var5);

        //声明死信队列
        channel.queueDeclare(DEAD_NAME, false, false, false, null);

        //普通队列与普通交换机绑定
        channel.queueBind(NORMAL_NAME, NORMAL_EXCHANGE, "zhangsan");
        //死信队列与死信交换机的绑定
        channel.queueBind(DEAD_NAME, DEAD_EXCHANGE, "lisi");


        System.out.println("消费者等待消息接收.........");
        //消息发送过来回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            String msg = new String(message.getBody());
            if (msg.equals("info5")) {
                System.out.println("被消费者1拒绝的消息" + msg);
                channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
            } else {
                System.out.println("消费者1消费的普通队列消息是:" + msg);
                channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            }
        };

        //消费者消费消息
        channel.basicConsume(NORMAL_NAME, false, deliverCallback, CancelCallback -> {
        });
        Thread.sleep(10000000);
    }

    @Test
    public void consumer2() throws Exception {
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        System.out.println("消费者等待消息接收.........");
        //消费者消费消息
        //消息发送过来回调函数
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println("消费者2消费的死信队列消息是:" + new String(message.getBody()));
        };

        //消费者消费消息
        channel.basicConsume(DEAD_NAME, true, deliverCallback, CancelCallback -> {
        });

        Thread.sleep(10000000);
    }

}

注意: 每次参数的修改,应该从新在Web管理界面将对应的队列或者交换机删除,否则重新启动就会报错。

Web界面管理图:

队列长度满了,此时消费者1无法消费队列中的消息,但普通对列的长度设置了6,所以生产者发送的消息会有4条会被送入死信队列当中。

消息时间过期,消费者1无法消费导致消息堆积在普通队列当中,然后消息设置了过期时间,时间一到就会被发送到死信队列当中。

消息被拒掉,这里指定哪一条消息被消费者1拒绝接收,消费者1要修改为手动应答,并且设置死信对应的参数,这条被拒绝的消息将会被死信交换机接收,然后发送给死信队列被另外一个消费者消费。

TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。

换句话说,如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这 条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的 TTL 和消息的 TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。

直接在生产者这边设置消息的过期时间 TTL

AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", properties, message.getBytes());

在创建队列的时候设置消息的过期时间,作为普通队列参数存在。

Map<String, Object> var5 = new HashMap<>();
var5.put("x-message-ttl",10000);
channel.queueDeclare(NORMAL_NAME, false, false, false, var5);

二者区别:

如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;另外,还需要注意的一点是,如果不设置 TTL,表示消息永远不会过期,如果将 TTL 设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。

在RabbitMQ中,basic.rejectbasic.nack都是用于拒绝(reject)一条消息的方法,但它们在功能和用法上有一些区别。

basic.reject方法是AMQP 0-9-1协议中的方法,它允许消费者拒绝一条消息,并将其从队列中删除。它的用法如下:

basic.reject(deliveryTag, requeue)
  • deliveryTag是消息的唯一标识符,用于确认消息。
  • requeue是一个布尔值,用于指定是否将消息重新放回队列中。如果设置为true,则消息将重新排队等待消费;如果设置为false,则消息将被丢弃。

basic.nack方法是AMQP 0-9-1协议中的方法,它允许消费者拒绝一条或多条消息,并可以选择将它们重新放回队列中。它的用法如下:

basic.nack(deliveryTag, multiple, requeue)
  • deliveryTag是消息的唯一标识符,用于确认消息。
  • multiple是一个布尔值,用于指定是否拒绝多个消息。如果设置为true,则拒绝所有比deliveryTag小或等于deliveryTag的消息;如果设置为false,则只拒绝一条消息。
  • requeue是一个布尔值,用于指定是否将消息重新放回队列中。如果设置为true,则消息将重新排队等待消费;如果设置为false,则消息将被丢弃。

因此,basic.nack相比于basic.reject具有更多的灵活性,可以选择拒绝多条消息并选择是否将它们重新放回队列中。但需要注意的是,basic.nack方法只在RabbitMQ的AMQP 0-9-1协议中可用,而不是在所有AMQP协议版本中都可用。

15.elasticsearch(一)

Elasticsearch 是一个分布式、高扩展、高实时、RESTful 风格的搜索和数据分析引擎。它能很方便的使大量数据具有搜索、分析和探索的能力。充分利用 Elasticsearch 的水平伸缩性,能使数据在生产环境变得更有价值。Elasticsearch 的实现原理主要分为以下几个步骤,首先用户将数据提交到 Elasticsearch 数据库中,再通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据,当用户搜索数据时候,再根据权重将结果排名,打分,再将返回结果呈现给用户。

官网:What is Elasticsearch? | Elasticsearch Guide [7.17] | Elastic

Solr 是第一个基于 Lucene 核心库功能完备的搜索引擎产品,诞生远早于 Elasticsearch。 当单纯的对已有数据进行搜索时,Solr 更快。当实时建立索引时, Solr 会产生 io 阻塞,查询性能较差,Elasticsearch 具有明显的优势。

Solr 利用 Zookeeper 进行分布式管理,而Elasticsearch 自身带有分布式协调管理功能。

Solr 支持更多格式的数据,比如JSON、XML、CSV,而 Elasticsearch 仅支持json文件格式。

Solr 在传统的搜索应用中表现好于 Elasticsearch,但在处理实时搜索应用时效率明显低于 Elasticsearch。

Solr 是传统搜索应用的有力解决方案,但 Elasticsearch 更适用于新兴的实时搜索应用。

ElasticSearch 应用场景

  • 站内搜索
  • 日志管理与分析
  • 大数据分析
  • 应用性能监控
  • 机器学习

除了搜索之外,结合 Kibana、Logstash、Beats,Elasticsearch 还被广泛运用在大数据近实时分析领域,包括日志分析、指标监控、信息安全等多个领域。它可以帮助你探索海量结构化、非结构化数据,按需创建可视化报表,对监控数据设置报警阈值,甚至通过使用机器学习技术,自动识别异常状况。

拉取镜像

docker pull elasticsearch:7.17.6

启动容器

docker run -d \
    --name elasticsearch \
    -e ES_JAVA_OPTS="-Xms512m -Xmx512m" \
    -e discovery.type="single-node" \
    --privileged \
    -p 9200:9200 \
    -p 9300:9300 \
elasticsearch:7.17.6

浏览器打开网址:http://124.70.51.123:9200/ 显示如下内容:

{
  "name" : "244ccfd9f865",
  "cluster_name" : "docker-cluster",
  "cluster_uuid" : "CmPmQPpFRjCJOWL7Tiu1fQ",
  "version" : {
    "number" : "7.17.6",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "fecd68e3150eda0c307ab9a9d7557f5d5fd71349",
    "build_date" : "2023-04-23T05:33:18.138275597Z",
    "build_snapshot" : false,
    "lucene_version" : "8.11.1",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

进入容器

docker exec -it elasticsearch /bin/bash

进入 bin 目录后执行下载安装 ik 分词器命令

elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.6/elasticsearch-analysis-ik-7.17.6.zip

成功标志:-> Installed analysis-ik

离线安装:本地下载相应的插件,解压,然后手动上传到 elasticsearch 的 plugins 目录,然后重启 ES 实例就可以了。

配置跨域需要安装 vim 编辑器,并通过 vim 更改配置文件,如已安装可以忽略。

apt-get update
apt-get install vim

进入到 /config/elasticsearch.yml 配置文件,添加以下配置代码(删除注释才能启动):

# 因为elasticsearch与elasticsearch-head工具是前后端分离项目,所以需要处理跨域问题
http.cors.enabled: true
http.cors.allow-origin: "*"

# 开启账户密码验证
http.cors.allow-headers: Authorization,X-Requested-With,Content-Length,Content-Type
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true

退出容器:exit

重启容器:

docker restart elasticsearch

设置用户的密码:

[root@hecs-16743 ~]# docker exec -it elasticsearch /bin/bash
root@e959cbb6a93e:/usr/share/elasticsearch# bin/elasticsearch-setup-passwords interactive
Initiating the setup of passwords for reserved users elastic,apm_system,kibana,kibana_system,logstash_system,beats_system,remote_monitoring_user.
You will be prompted to enter passwords as the process progresses.
Please confirm that you would like to continue [y/N]y


Enter password for [elastic]: 
Reenter password for [elastic]: 
Enter password for [apm_system]: 
Reenter password for [apm_system]: 
Enter password for [kibana_system]: 
Reenter password for [kibana_system]: 
Enter password for [logstash_system]: 
Reenter password for [logstash_system]: 
Enter password for [beats_system]: 
Reenter password for [beats_system]: 
Enter password for [remote_monitoring_user]: 
Reenter password for [remote_monitoring_user]: 
Changed password for user [apm_system]
Changed password for user [kibana_system]
Changed password for user [kibana]
Changed password for user [logstash_system]
Changed password for user [beats_system]
Changed password for user [remote_monitoring_user]
Changed password for user [elastic]

使用 idea 安装插件 Elasticsearch Query - EDQL 操作 elasticsearch 。

使用 JavaScript 调用 

<script>
    const username = 'elastic';
    const password = '123456';
    const credentials = `${username}:${password}`;
    const encodedCredentials = btoa(credentials);
    fetch('http://124.70.51.123:9200/phone/_search?size=1000&sort=date', {
        headers: {
            'Authorization': `Basic ${encodedCredentials}`
        }
    })
        .then(response => response.json())
        .then(data => console.log(data))
        .catch(error => console.error(error));
</script>

btoa() 方法可以将一个二进制字符串(例如,将字符串中的每一个字节都视为一个二进制数据字节)编码为 Base64 编码的 ASCII 字符串。

发送请求测试分词效果。

GET /_analyze

{
  "analyzer": "ik_smart",
  "text": "河南科技大学"
}

数据:ik_smart 最少切分;ik_max_word 最细粒度划分!穷尽词库所有可能

返回:

{
  "tokens": [
    {
      "token": "河南",
      "start_offset": 0,
      "end_offset": 2,
      "type": "CN_WORD",
      "position": 0
    },
    {
      "token": "科技大学",
      "start_offset": 2,
      "end_offset": 6,
      "type": "CN_WORD",
      "position": 1
    }
  ]
}

全文检索:

通过一个程序扫描文本中的每一个单词,针对单词建立索引,并保存该单词在文本中的位置、以及出现的次数。

用户查询时,通过之前建立好的索引来查询,将索引中单词对应的文本位置、出现的次数返回给用户,因为有了具体文本的位置,所以就可以将具体内容读取出来了。

Elasticsearch 与 MySQL对比

MySQLElasticsearch说明
TableIndex索引(index),就是文档的集合,类似数据库的表(table)
RowDocument文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
ColumnField字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
SchemaMappingMapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQLDSLDSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD

ElasticSearch 索引基本操作,官方文档:Quick start | Elasticsearch Guide [7.17] | Elastic

倒排索引:

索引就类似于目录,平时我们使用的都是索引,都是通过主键定位到某条数据,那么倒排索引呢,刚好相反,数据对应到主键。

简单理解,正向索引是通过key找value,反向索引则是通过value找key。ES底层在检索时底层使用的就是倒排索引。

Elasticsearch 的JSON文档中的每个字段,都有自己的倒排索引。 可以指定对某些字段不做索引这样可以节省存储空间,但是该字段无法被搜索。

You send data and other requests to Elasticsearch using REST APIs. This lets you interact with Elasticsearch using any client that sends HTTP requests, such as curl. You can also use Kibana’s console to send requests to Elasticsearch. 您可以使用 REST API 向 Elasticsearch 发送数据和其他请求。这允许您使用任何发送HTTP请求的客户端(例如curl)与 Elasticsearch 进行交互。您还可以使用 Kibana 的控制台向 Elasticsearch 发送请求。

ElasticSearch 基础操作

索引操作:索引命名必须小写,不能以下划线开头

#创建索引
put /user_index
{
    "settings" : {
        "index" : {
            "analysis.analyzer.default.type": "ik_max_word",
            "number_of_shards" : 3,
            "number_of_replicas" : 2
        }
    }
}
#查询索引
GET /user_index

#删除索引
DELETE /user_index

文档操作:POST 和 PUT 都能起到创建/更新的作用,PUT 需要对一个具体的资源进行操作也就是要确定 id 才能进行更新/创建,而 POST 是可以针对整个资源集合进行操作的,如果不写 id 就由 ES 生成一个唯一 id 进行创建新文档,如果填了 id 那就针对这个 id 的文档进行创建/更新

# 创建文档,指定id
# 如果id不存在,创建新的文档,否则先删除现有文档,再创建新的文档,版本会增加
PUT /user_index/_doc/1
{
  "name":"草帽路飞",
  "age":22,
  "description":"海贼王,我当定了!",
  "tags":["吃货","船长","未来的海贼王","橡胶果实能力者"]
}
#创建文档,ES生成id
POST /user_index/_doc
{
  "name":"海贼猎人索隆1",
  "age":22,
  "description":"背后的伤疤,是剑士的耻辱!",
  "tags":["路痴","副船长","未来的世界第一大剑豪","三刀流剑客"]
}


#根据id查询文档,格式: GET /索引名称/_doc/id
GET /user_index/_doc/1


#查询前10条文档
GET /user_index/_search
#删除文档 格式: DELETE /索引名称/_doc/id
DELETE /user_index/_doc/1

# 全量更新,替换整个json
PUT /user_index/_doc/1
{
"name": "张三",
"sex": 1,
"age": 25
}

# 部分更新:在原有文档上更新
# Update -文档必须已经存在,更新只会对相应字段做增量修改
POST /user_index/_update/1
{
  "doc": {
    "age": 28
  }
}

16.elasticsearch(二)

并发场景下修改文档

_seq_no 和 _primary_term 是对 _version 的优化,7.X 版本的 ES 默认使用这种方式控制版本,所以当在高并发环境下使用乐观锁机制修改文档时,要带上当前文档的_seq_no 和 _primary_term 进行更新:

POST /es_db/_doc/1?if_seq_no=6&if_primary_term=1
{
    "name": "李四xxx"
}

如果版本号不对,会抛出版本冲突异常。

文档映射 Mapping

Mapping 类似数据库中的 schema 的定义,作用如下:

  • 定义索引中的字段的名称
  • 定义字段的数据类型,例如字符串,数字,布尔等
  • 字段,倒排索引的相关配置(Analyzer)

ES 中 Mapping 映射可以分为动态映射和静态映射。

动态映射: 在关系数据库中,需要事先创建数据库,然后在该数据库下创建数据表,并创建表字段、类 型、长度、主键等,最后才能基于表插入数据。而 Elasticsearch 中不需要定义 Mapping 映射(即关系型数据库的表、字段等),在文档写入 Elasticsearch 时,会根据文档字段自动识别类型,这种机制称之为动态映射。

静态映射: 静态映射是在 Elasticsearch 中也可以事先定义好映射,包含文档的各字段类型、分词器 等,这种方式称之为静态映射。

动态映射一般查询时不显示 dynamic 属性,该值为 true。实际开发时我们需要设置 dynamic 为严格模式或关闭。

dynamic 设为 true 时,一旦有新增字段的文档写入,Mapping 也同时被更新。

dynamic 设为 false,Mapping 不会被更新,新增字段的数据无法被索引,但是信息会出现在_source 中。

dynamic 设置成 strict(严格控制策略),文档写入失败,抛出异常。

PUT /user
{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "address": {
        "type": "text"
      },
      "age": {
        "type": "long"
      },
      "name": {
        "type": "text"
      }
    }
  }
}

GET /user

PUT /user/_doc/1
{
  "name": "张三",
  "age": 18,
  "address": "中国"
}

# 如果"dynamic"为 true,province 会更新到 mapping 中,可以被搜索
# 如果"dynamic"为 false,province不会更新到mapping中,无法被搜索,但会在_source中展示
# 如果"dynamic"为 strict,执行 PUT /user/_doc/2 会报错
PUT /user/_doc/2
{
  "name": "李四",
  "age": 18,
  "address": "中国福建厦门",
  "province": "厦门"
}

一旦已经有数据写入,就不再支持修改字段定义。Lucene 实现的倒排索引,一旦生成后,就不允许修改如果希望改变字段类型,可以利用 reindex API,重建索引。因为:如果修改了字段的数据类型,会导致已被索引的数据无法被搜索。但是如果是增加新的字段,就不会有这样的影响。

修改已有的字段,具体步骤如下:

1)如果要推倒现有的映射,你得重新建立一个静态索引。

2)然后把之前索引里的数据导入到新的索引里。

3)删除原创建的索引。

4)为新索引起个别名, 为原索引名。

#新建索引
PUT /user2
{
  "mappings": {
    "properties": {
      "address": {
        "type": "keyword"
      },
      "age": {
        "type": "long"
      },
      "name": {
        "type": "text",
        "analyzer": "ik_max_word"
      }
    }
  }
}

#将旧索引的数据复制到新索引中
POST _reindex
{
  "source": {
    "index": "user"
  },
  "dest": {
    "index": "user2"
  }
}

#删除旧索引
DELETE /user

# 给新索引设置别名
PUT /user2/_alias/user

通过这几个步骤就实现了索引的平滑过渡,并且是零停机。

常用 Mapping 参数配置

index:控制当前字段是否被索引,默认为 true。如果设置为 false,该字段不创建索引,不可被搜索。

index_options 配置:有四种不同 index options 配置,控制倒排索引记录的内容:

  • docs : 记录 doc id
  • freqs:记录 doc id 和 term frequencies(词频)
  • positions: 记录 doc id / term frequencies / term position
  • offsets: 记录 doc id / term frequencies / term posistion / character offsets

text 类型默认记录 postions,其他默认为 docs。记录内容越多,占用存储空间越大

null_value:需要对 Null 值进行搜索,只有 keyword 类型支持设计 Null_Value

copy_to 设置:将字段的数值拷贝到目标字段,满足一些特定的搜索需求。copy_to 的目标字段不出现在 _source 中。

PUT /user2
{
  "mappings": {
    "properties": {
      "address": {
        "type": "keyword",
        "index": false,
        "null_value": "NULL"
      },
      "age": {
        "type": "long"
      },
      "name": {
        "type": "text",
        "analyzer": "ik_max_word",
        "index_options": "docs"
      },
      "province": {
        "type": "keyword",
        "copy_to": "full_address"
      },
      "city": {
        "type": "text",
        "copy_to": "full_address"
      }
    }
  },
  "settings": {
    "index": {
      "analysis.analyzer.default.type": "ik_max_word"
    }
  }
}

批量操作

批量操作可以减少网络连接所产生的开销,提升性能。支持在一次 API 调用中,对不同的索引进行操作。可以再 URI 中指定 Index,也可以在请求的 Payload 中进行,操作中单条操作失败,并不会影响其他操作。返回结果包括了每一条操作执行的结果。

通过 _bulk 操作文档,一般至少有两行参数(或偶数行参数),第一行参数为指定操作的类型及操作的对象(index,type和id) 第二行参数才是操作的数据。参数类似于:

{"actionName":{"_index":"indexName", "_type":"typeName","_id":"id"}}
{"field1":"value1", "field2":"value2"}

actionName:表示操作类型,主要有 create,index,delete 和 update。

批量创建文档 create 批量对文档进行写操作是通过 _bulk 的 API 来实现的。

POST _bulk
{"create":{"_index":"user","_id":1}}
{"id":1,"name":"张三","content":"张三的名称","tags":["java","面向对象"],"create_time":1554015482530}
{"create":{"_index":"user","_id":2}}
{"id":2,"name":"李四","content":"李四的名称","tags":["java","面向对象"],"create_time":1554015482530}

普通创建或全量替换 index,如果原文档不存在,则是创建。如果原文档存在,则是替换(全量修改原文档)

POST _bulk
{"index":{"_index":"user","_id":1}}
{"id":1,"name":"张三","content":"张三的名称","tags":["java","面向对象"],"create_time":1554015482530}
{"index":{"_index":"user","_id":2}}
{"id":2,"name":"李四","content":"李四的名称","tags":["java","面向对象"],"create_time":1554015482530}

批量删除 delete

POST _bulk
{"delete":{"_index":"user","_id":1}}
{"delete":{"_index":"user","_id":2}}

批量修改 update

POST _bulk
{"update":{"_index":"user", "_id":1}}
{"doc":{"name":"张三111"}}
{"update":{"_index":"user", "_id":2}}
{"doc":{"name":"李四111"}}

ES 高级查询 Query DSL

ES 中提供了一种强大的检索数据方式,这种检索方式称之为 Query DSL(Domain Specified Language) , Query DSL 是利用 Rest API 传递 JSON 格式的请求体(RequestBody)数据与 ES 进行交互,这种方式的丰富查询语法让 ES 检索变得更强大,更简洁。语法:

GET /es_db/_search
{json请求体数据}

查询所有 match_all

使用 match_all,默认只会返回 10 条数据。_search 查询默认采用的是分页查询,每页记录数 size 的默认值为 10。如果想显示更多数据,指定 size。

GET /user/_search

#等同于
GET /user/_search
{
  "query": {
    "match_all": {}
  }
}


GET user/_search
{
  "size": 10001
}

#size 默认最大 1 万条,超过会报错 可以通过参数配置
#设置查询结果的窗口的限制
PUT user/_settings
{
  "index.max_result_window": "20000"
}

注意:参数 index.max_result_window 主要用来限制单次查询满足查询条件的结果窗口的大小,窗口大小由 from + size 共同决定。不能简单理解成查询返回给调用方的数据量。这样做主要是为了限制内存的消耗。

分页查询

from 关键字:用来指定起始返回位置,和 size 关键字连用可实现分页效果。

GET user/_search
{
  "size": 2,
  "from": 0
}

指定字段排序和指定字段返回

GET user/_search
{
  "from": 0,
  "size": 10,
  "sort": [
    {
      "id": "desc"
    }
  ],
  "_source": [
    "name",
    "content"
  ]
}

使用可视化界面完成搜索:

注意:指定排序会让得分失效。

match

match 在匹配时会对所查找的关键词进行分词,然后按分词匹配查找。match 支持以下参数:

  • query : 指定匹配的值
  • operator : 匹配条件类型
  • and : 条件分词后都要匹配
  • or : 条件分词后有一个匹配即可(默认)
  • minmum_should_match : 最低匹配度,即条件在倒排索引中最低的匹配度

测试数据:

注:ES 6.0 后字符串不用 string;改成 text 和 keyword 两种了,keyword 是默认不分词,text 是要分词。默认 mapping 结构一般是: 如果不设置 mapping,ES 默认把字符串设为 text 类型,并包含一个 keyword 子类型。

"name": {
  "type": "text",
  "fields": {
    "keyword": {
    "type": "keyword",
    "ignore_above": 256
    }
  }
}

这种结构保存字段会存两份索引(个人理解),首先第一个 type text 这个会进行分词建索引保存,再后面 fields keyword 会进行保存完整字符串附加。所以一条数据过来的时候,会建立两次索引。一次是自己本身,是要分词的,分词后放入倒排索引;另一次是基于 XXX.keyword,不分词,最多保留 256 字符,直接一个完整的字符串放入倒排索引中。

PUT /es_db
{
  "mappings": {
    "properties": {
      "address": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "remark": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "age": {
        "type": "long"
      },
      "sex": {
        "type": "long"
      },
      "name": {
        "type": "keyword"
      }
    }
  }
}

POST _bulk
{"create":{"_index":"es_db","_id":1}}
{"name": "张三","sex": 1,"age": 25,"address": "广州天河公园","remark": "java developer"}
{"create":{"_index":"es_db","_id":2}}
{"name": "李四","sex": 1,"age": 28,"address": "广州荔湾大厦","remark": "java assistant"}
{"create":{"_index":"es_db","_id":3}}
{"name": "王五","sex": 0,"age": 26,"address": "广州白云山公园","remark": "php developer"}
{"create":{"_index":"es_db","_id":4}}
{"name": "赵六","sex": 0,"age": 22,"address": "长沙橘子洲","remark": "python assistant"}
{"create":{"_index":"es_db","_id":5}}
{"name": "张龙","sex": 0,"age": 19,"address": "长沙麓谷企业广场","remark":"java architect assistant"}
{"create":{"_index":"es_db","_id":6}}
{"name": "赵虎","sex": 1,"age": 32,"address": "长沙麓谷兴工国际产业园","remark":"java architect"}
{"create":{"_index":"es_db","_id":7}}
{"name": "小小","sex": 1,"age": 32,"address": "广州世界公园","remark":"java architect"}

查看某个条数据的分词

GET /es_db/_termvectors/1
{
  "fields": ["address"]
}

查询

#分词后or的效果
GET es_db/_search
{
  "query": {
    "match": {
      "address": "广州白云山公园"
    }
  }
}
#可视化界面生成的
GET es_db/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "address": {
              "query": "广州白云山公园"
            }
          }
        }
      ]
    }
  }
}

#分词后and的效果
GET es_db/_search
{
  "query": {
    "match": {
      "address": {
        "query": "广州白云山公园",
        "operator": "and"
      }
    }
  }
}
#可视化界面生成
GET es_db/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match_phrase": {
            "address": {
              "query": "广州白云山公园"
            }
          }
        }
      ]
    }
  }
}

多字段查询 multi_match 可以根据字段类型,决定是否使用分词查询,得分最高的在前面。

GET es_db/_search
{
  "query": {
    "multi_match": {
      "query": "长沙王五",
      "fields": [
        "name",
        "address"
      ]
    }
  }
}
# 可视化界面生成
GET es_db/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "name": {
              "query": "长沙王五"
            }
          }
        },
        {
          "match": {
            "address": {
              "query": "长沙王五"
            }
          }
        }
      ]
    }
  }
}

注意:字段类型分词,将查询条件分词之后进行查询,如果该字段不分词就会将查询条件作为整体进行查询。

多字段查询

query_string 允许我们在单个查询字符串中指定 AND | OR | NOT 条件,同时也和 multi_match query 一样,支持多字段搜索。和 match 类似,但是 match 需要指定字段名,query_string 是在所有字段中搜索,范围更广泛。 注意:查询字段分词就将查询条件分词查询,查询字段不分词将查询条件不分词查询。

#指定字段查询
GET /es_db/_search
{
  "query": {
    "query_string": {
      "default_field": "address",
      "query": "白云山 OR 橘子洲"
    }
  }
}
#可视化界面生成
GET /es_db/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "query_string": {
            "query": "白云山 OR 橘子洲",
            "default_field": "address"
          }
        }
      ]
    }
  }
}
#未指定字段查询
GET es_db/_search
{
  "query": {
    "query_string": {
      "query": "张三 OR 橘子洲"
    }
  }
}
#可视化界面生成
GET es_db/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "query_string": {
            "query": "张三 OR 橘子洲",
            "default_field": "*"
          }
        }
      ]
    }
  }
}

#指定多字段查询
GET /es_db/_search
{
  "query": {
    "query_string": {
      "fields": [
        "name",
        "address"
      ],
      "query": "张三 OR 橘子洲"
    }
  }
}
#可视化界面生成
GET /es_db/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "query_string": {
            "query": "张三 OR 橘子洲",
            "default_field": "name"
          }
        },
        {
          "query_string": {
            "query": "张三 OR 橘子洲",
            "default_field": "address"
          }
        }
      ]
    }
  }
}

关键词查询 Term

Term 的 keyword 查询要求 mapping 创建时使用 keyword

PUT /es_db
{
  "mappings": {
    "properties": {
      "address": {
        "type": "text",
        "analyzer": "ik_max_word",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "remark": {
        "type": "text",
        "analyzer": "ik_max_word",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "age": {
        "type": "long"
      },
      "sex": {
        "type": "long"
      },
      "name": {
        "type": "keyword"
      }
    }
  }
}

Term 用来使用关键词查询(精确匹配),还可以用来查询没有被进行分词的数据类型。Term 是表达语意的最小单位,搜索和利用统计语言模型进行自然语言处理都需要处理 Term。match 在匹配时会对所查找的关键词进行分词,然后按分词匹配查找,而 term 会直接对关键词进行查找。一般模糊查找的时候,多用 match,而精确查找时可以使用 term 在 ES 的 Mapping Type 中 keyword , date ,integer, long , double , boolean or ip 这些类型不分词,只有 text 类型分词。

# 查不到数据 因为倒排索引中没有广白云山公园
GET es_db/_search
{
  "query": {
    "term": {
      "address": {
        "value": "广州白云山公园"
      }
    }
  }
}
#可视化界面
GET es_db/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "address": "广州白云山公园"
          }
        }
      ]
    }
  }
}
# 可以查到数据 通过keyword 映射到address字段不进行分词
# 相当于mapping中的address的类型是keyword
GET /es_db/_search
{
  "query": {
    "term": {
      "address.keyword": {
        "value": "广州白云山公园"
      }
    }
  }
}
#可视化界面
GET /es_db/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "address.keyword": "广州白云山公园"
          }
        }
      ]
    }
  }
}

在 ES 中,Term 查询数组类型的字段时,是包含而不是等于。

POST /employee/_bulk
{"index":{"_id":1}}
{"name":"小明","interest":["跑步","篮球"]}
{"index":{"_id":2}}
{"name":"小红","interest":["跳舞","画画"]}
{"index":{"_id":3}}
{"name":"小丽","interest":["跳舞","唱歌","跑步"]}
#term处理多值字段,term查询是包含,不是等于
POST /employee/_search
{
  "query": {
    "term": {
      "interest.keyword": {
        "value": "跑步"
      }
    }
  }
}

前缀查询

它会对分词后的倒排索引进行前缀搜索。 它不会分析要搜索字符串,传入的前缀就是想要查找的前缀。默认状态下,前缀查询不做相关度分数计算,它只是将所有匹配的文档返回,然后赋予所有相关分数值为 1。它的行为更像是一个过滤器而不是查询。两者实际的区别就是过滤器是可以被缓存的,而前缀查询不行。 prefix 的原理:需要遍历所有倒排索引,并比较每个 term 是否已所指定的前缀开头。

GET /es_db/_search
{
  "query": {
    "prefix": {
      "address": {
        "value": "广州"
      }
    }
  }
}
#可视化界面生成
GET /es_db/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "prefix": {
            "address": {
              "value": "广州"
            }
          }
        }
      ]
    }
  }
}

通配符查询

工作原理和 prefix 相同,只不过它不是只比较开头,它能支持更为复杂的匹配模式。

GET /es_db/_search
{
  "query": {
    "wildcard": {
      "address": {
        "value": "*白*"
      }
    }
  }
}
#可视化界面生成
GET /es_db/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "wildcard": {
            "address": {
              "value": "*白*"
            }
          }
        }
      ]
    }
  }
}

范围查询

range:范围关键字

  • gte 大于等于
  • lte 小于等于
  • gt 大于
  • lt 小于
  • now 当前时间
POST /es_db/_search
{
  "query": {
    "range": {
      "age": {
        "gte": 25,
        "lte": 28
      }
    }
  }
}
#可视化界面生成
POST es_db/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "range": {
            "age": {
              "gt": "22",
              "lte": "28"
            }
          }
        }
      ]
    }
  }
}
#多id查询 
GET /es_db/_search 
{ 
  "query": { 
    "ids": { 
      "values": [1,2] 
    } 
  } 
}

第二个案例暂时无法用可视化界面完成。

POST /product/_bulk
{"index":{"_id":1}}
{"price":100,"date":"2021-01-01","productId":"XHDK-1293"}
{"index":{"_id":2}}
{"price":200,"date":"2022-01-01","productId":"KDKE-5421"}
GET /product/_search
{
  "query": {
    "range": {
      "date": {
        "gte": "now-3y"
      }
    }
  }
}
#可视化生成
GET /product/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "range": {
            "date": {
              "gte": "now-2y"
            }
          }
        }
      ]
    }
  }
}

17.elasticsearch(三)

模糊查询 fuzzy(不清楚的)  在实际的搜索中,我们有时候会打错字,从而导致搜索不到。在 Elasticsearch 中,我们可以使用 fuzziness 属性来进行模糊查询,从而达到搜索有错别字的情形。 fuzzy 查询会用到两个很重要的参数,fuzziness,prefix_length。

fuzziness:表示输入的关键字通过几次操作可以转变成为 ES 库里面的对应 field 的字段。

操作是指:新增一个字符,删除一个字符,修改一个字符,每次操作可以记做编辑距离为 1,如中文集团到中威集团编辑距离就是 1,只需要修改一个字符;该参数默认值为0,即不开启模糊查询。 如果 fuzziness 值在这里设置成 2,会把编辑距离为 2 的东东集团也查出来。 

prefix_length:表示限制输入关键字和 ES 对应查询 field 的内容开头的第 n 个字符必须完全匹配,不允许错别字匹配,如这里等于 1,则表示开头的字必须匹配,不匹配则不返回 默认值也是 0 加大 prefix_length 的值可以提高效率和准确率。

GET /es_db/_search
{
  "query": {
    "match": {
      "address": {
        "query": "广洲",
        "fuzziness": 1,
        "prefix_length": 5,
      }
    }
  }
}

暂时无法使用可视化界面完成查询。

高亮 highlight

highlight 关键字: 可以让符合条件的文档中的关键词高亮。 highlight 相关属性: pre_tags 前缀标签 post_tags 后缀标签 tags_schema 设置为 styled 可以使用内置高亮样式 require_field_match 多字段高亮需要设置为 false。

数据:

#指定ik分词器
PUT /products
{
  "settings" : {
    "index" : {
      "analysis.analyzer.default.type": "ik_max_word"
    }
  }
}
PUT /products/_doc/1
{
  "proId" : "2",
  "name" : "牛仔男外套",
  "desc" : "牛仔外套男装春季衣服男春装夹克修身休闲男生潮牌工装潮流头号青年春秋棒球服男 7705浅蓝常规 XL",
  "createTime" : "2023-12-13 12:56:56"
}
PUT /products/_doc/2
{
  "proId" : "6",
  "name" : "HLA海澜之家牛仔裤男",
  "desc" : "HLA海澜之家牛仔裤男2019时尚有型舒适HKNAD3E109A 牛仔蓝(A9)175/82A(32)",
  "createTime" : "2023-12-18 15:56:56"
}

测试:

GET /products/_search
{
  "query": {
    "term": {
      "name": {
        "value": "牛仔"
      }
    }
  },
  "highlight": {
    "fields": {
      "*":{}
    }
  }
}

自定义高亮 html 标签 可以在 highlight 中使用 pre_tags 和 post_tags。

GET /products/_search
{
  "query": {
    "term": {
      "name": {
        "value": "牛仔"
      }
    }
  },
  "highlight": {
    "post_tags": ["</span>"], 
    "pre_tags": ["<span style='color:red'>"],
    "fields": {
      "*":{}
    }
  }
}

多字段高亮

GET /products/_search
{
  "query": {
    "term": {
      "name": {
        "value": "牛仔"
      }
    }
  },
  "highlight": {
    "pre_tags": ["<font color='red'>"],
    "post_tags": ["<font/>"],
    "require_field_match": "false",
    "fields": {
      "name": {},
      "desc": {}
    }
  }
}

上面案例无法使用可视化界面完成。

聚合查询

聚合查询是 Elasticsearch 中一种用于对数据进行分析和统计的强大工具。它可以对数据进行各种聚合操作,如计算平均值、最大值、最小值、总和等,还可以按照字段进行分组、按日期范围进行分组、按地理位置进行分组等。

语法:

"aggs" : {  #和query同级的关键词
    "<aggregation_name>" : { #自定义的聚合名字
        "<aggregation_type>" : { #聚合的定义: 不同的type+body
            <aggregation_body>
        }
        [,"meta" : {  [<meta_data_body>] } ]?
        [,"aggregations" : { [<sub_aggregation>]+ } ]?  #子聚合查询
    }
    [,"<aggregation_name_2>" : { ... } ]*  #可以包含多个同级的聚合查询
}

测试数据:

PUT /my_index/_doc/1
{
  "name": "John",
  "age": 25,
  "salary": 5000,
  "department": "HR",
  "date": "2021-01-01",
  "location": {
    "lat": 40.7128,
    "lon": -74.0060
  }
}

PUT /my_index/_doc/2
{
  "name": "Alice",
  "age": 30,
  "salary": 6000,
  "department": "IT",
  "date": "2021-01-02",
  "location": {
    "lat": 41.8781,
    "lon": -87.6298
  }
}

PUT /my_index/_doc/3
{
  "name": "Bob",
  "age": 35,
  "salary": 7000,
  "department": "IT",
  "date": "2021-01-03",
  "location": {
    "lat": 34.0522,
    "lon": -118.2437
  }
}

PUT /my_index/_doc/4
{
  "name": "Jane",
  "age": 40,
  "salary": 8000,
  "department": "HR",
  "date": "2021-01-04",
  "location": {
    "lat": 51.5074,
    "lon": -0.1278
  }
}

请将上述数据插入到Elasticsearch中,确保数据插入成功。

案例1:计算平均值

计算所有员工的薪水平均值:

GET /my_index/_search
{
  "size": 0,
  "aggs": {
    "avg_salary": {
      "avg": {
        "field": "salary"
      }
    }
  }
}

如果没有参数:"size":0,查询结果会带有文档信息返回。

案例2:计算最大值和最小值

计算所有员工的薪水最大值和最小值:

GET /my_index/_search
{
  "size": 0,
  "aggs": {
    "max_salary": {
      "max": {
        "field": "salary"
      }
    },
    "min_salary": {
      "min": {
        "field": "salary"
      }
    }
  }
}

案例3:计算总和

计算所有员工的薪水总和:

GET /my_index/_search
{
  "size": 0,
  "aggs": {
    "sum_salary": {
      "sum": {
        "field": "salary"
      }
    }
  }
}

案例4:计算统计信息

计算所有员工的薪水的统计信息,包括平均值、最大值、最小值、总和和标准差:

GET /my_index/_search
{
  "size": 0,
  "aggs": {
    "salary_stats": {
      "stats": {
        "field": "salary"
      }
    }
  }
}

案例5:按字段分组

按部门对员工进行分组,并计算每个部门的平均薪水:

GET /my_index/_search
{
  "size": 0,
  "aggs": {
    "department_group": {
      "terms": {
        "field": "department.keyword"
      },
      "aggs": {
        "avg_salary": {
          "avg": {
            "field": "salary"
          }
        }
      }
    }
  }
}

案例6:按日期范围分组

按日期范围对员工进行分组,并计算每个日期范围内的平均薪水:

GET /my_index/_search
{
  "size": 0,
  "aggs": {
    "date_range_group": {
      "date_range": {
        "field": "date",
        "ranges": [
          {
            "from": "2021-01-01",
            "to": "2021-01-03"
          },
          {
            "from": "2021-01-04",
            "to": "2021-01-05"
          }
        ]
      },
      "aggs": {
        "avg_salary": {
          "avg": {
            "field": "salary"
          }
        }
      }
    }
  }
}

案例7:按日期直方图分组

按日期进行直方图分组,并计算每个日期范围内的员工数量:

GET /my_index/_search
{
  "size": 0,
  "aggs": {
    "date_histogram_group": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "1d"
      },
      "aggs": {
        "employee_count": {
          "value_count": {
            "field": "name"
          }
        }
      }
    }
  }
}

案例8:按地理位置分组

按地理位置进行分组,并计算每个位置的员工数量:

GET /my_index/_search
{
  "size": 0,
  "aggs": {
    "geo_distance_group": {
      "geo_distance": {
        "field": "location",
        "origin": {
          "lat": 40.7128,
          "lon": -74.0060
        },
        "unit": "km",
        "ranges": [
          {
            "to": 100
          },
          {
            "from": 100,
            "to": 500
          },
          {
            "from": 500
          }
        ]
      },
      "aggs": {
        "employee_count": {
          "value_count": {
            "field": "name.keyword"
          }
        }
      }
    }
  }
}

18.Docker Compose

Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。它使用一个 YAML 文件来配置应用程序的服务、网络和卷等方面的设置,并可以使用单个命令来启动、停止和管理整个应用程序。使用 Docker Compose,可以将多个容器组合成一个应用程序,并定义它们之间的依赖关系、网络连接和数据卷等。通过在一个文件中定义所有这些配置,可以轻松地管理和部署整个应用程序,而不需要手动执行多个 Docker 命令。

Docker Compose 的配置文件使用 YAML 语法,其中包含了服务的名称、镜像、端口映射、环境变量、数据卷等信息。您可以根据需要定义多个服务,并在配置文件中指定它们之间的依赖关系。Docker Compose 会根据配置文件中的定义,自动创建和管理这些容器,并确保它们之间的通信和数据共享正常工作。使用 Docker Compose 可以极大地简化多容器应用程序的管理和部署过程。您可以使用一个命令启动整个应用程序,并通过查看日志和状态来监控它们的运行情况。此外,Docker Compose 还支持扩展和缩减应用程序的容器数量,以适应不同的负载需求。

安装 Docker Compose,按照以下步骤进行操作:

1.首先,确保已经安装了 Docker。可以通过运行以下命令来检查 Docker 是否已经安装: docker --version 如果看到了 Docker 的版本信息,则表示已经安装成功。

2.安装 docker-compose

yum install -y docker-compose 

查看 docker-compose 版本

docker-compose --version

docker-compose 将所管理的容器分为三层, 分别是工程(project),服务(service)以及容器(containner)

  • docker-compose运行目录下的所有文件(docker-compose.yml 文件、extends 文件或环境变量等)组成一个工程,如无特殊指定,工程名即为当前目录名。
  • 一个工程当中,可以包含多个服务,每个服务中定义了容器运行的镜像、参数、依赖。
  • 一个服务中可以包括多个容器实例,docker-compose 并没有解决负载均衡的问题。因此需要借助其他工具实现服务发现及负载均衡,比如 consul。 

Docker Compose 配置常用字段

字段描述
build指定Dockerfile文件名(要指定的Dockerfile文件需要在build标签的子级标签中用dockefile标签指定)
dockerfile构建镜像上下文路径
context可以是dockerfile路径,或者时执行git仓库的url地址
images指定镜像(已存在)
command执行命令,会覆盖容器启动后默认执行的命令(会覆盖dockefile中的CMD指令)
container_name指定容器名称,由于容器名称是唯一的,如果指定自定义名称,则无法scale指定容器数量。
deploy指定部署和运行服务相关配置,只能在swarm模式使用
environment添加环境变量
networks加入网络,引用顶级networks下条目
network-mode设置容器的网络模式
ports暴露容器端口,与-p相同,但是端口不能低于60
volumes挂载一个宿主机目录或命令卷到容器,命令卷要在顶级volumes定义卷名称
volumes_from从另一个服务或容器挂载卷,可选参数:ro和rw(仅版本‘2’支持)
hostname在容器内设置内核参数
links连接诶到另一个容器,- 服务名称[ : ]
privileged用来给容器root权限,注意是不安全的,true
restart重启策略,定义是否重启容器1、no,默认策略,在容器退出时不重启容器2、on-failure,在容器非正常退出时(退出状态非0),才会重启容器3、on-failure:3 在容器非正常退出时,重启容器,最多重启3次4、always,在容器退出时总是重启容器,5、unless-stopped,在容器退出时总是重启容器,但是不考虑在Docker守护进程启动时就已经停止了的容器。
depends_on此标签用于解决容器的依赖,启动先后问题。如启动应用容器,需要先启动数据库容器。php:depends_on:- apache- mysql

Docker-compose 常用命令

运行这些命令需要结合 docker-compose 一起使用。且必须要在含有 docker-compose.yml 文件的目录中才可以使用,不然报错。

命令描述
build重新构建服务
ps列出容器
up创建和启动容器
exec在容器里面执行命令
scale指定一个服务容器启动数量
top显示正在运行的容器进程
logs查看服务容器的输出
down删除容器、网络、数据卷和镜像
stop/start/restart停止/启动/重启服务

在 Linux 上创建 docker-compose.yml 文件,内容如下:

version: '3'
services:
  db:
    image: mysql:5.7
    volumes:
      - db_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: somewordpress
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD: wordpress
  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpress
volumes:
  db_data: {}

在命令行使用 docker-compose up -d 命令运行。

19.nginx 相关

一、nginx 安装与部署普通 web 项目。

下载 nginx 镜像

docker pull nginx:1.22.0

参数详解 --name nginx 指定容器的名称 -p 80:80 映射端口 -d 守护进程运行

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

1、从 nginx 容器中映射核心文件

mkdir -p /opt/docker/nginx/conf
mkdir -p /opt/docker/nginx/log
mkdir -p /opt/docker/nginx/html

2、拷贝nginx容器对应的文件默认配置

docker cp nginx:/etc/nginx/nginx.conf /opt/docker/nginx/conf/nginx.conf
docker cp nginx:/etc/nginx/conf.d /opt/docker/nginx/conf/conf.d
docker cp nginx:/usr/share/nginx/html /opt/docker/nginx/

3、停止并删除 nginx 容器

docker stop nginx
docker rm nginx

重新启动nginx镜像重新新容器

docker run  -p 80:80 --name nginx --restart=always \
-v /opt/docker/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \
-v /opt/docker/nginx/conf/conf.d:/etc/nginx/conf.d \
-v /opt/docker/nginx/log:/var/log/nginx \
-v /opt/docker/nginx/html:/usr/share/nginx/html \
-d  nginx:1.22.0

二、nginx 基本配置。

参考:https://juejin.cn/post/7049252936391589918

三、nginx+rtmp 搭建直播服务器。

1. 在 docker 中安装 nginx+rtmp 拉取镜像 

docker pull alfg/nginx-rtmp

1 创建并运行容器,映射出两个端口1935、80

docker run -itd -p 1935:1935 -p 80:80 --name nginx-rtmp-test alfg/nginx-rtmp

流默认地址为:rtmp://ip:port/stream/自定义名称

2. ffmpeg 推流将视频文件推流至 rtmp 服务器 

ffmpeg -re -i video.mp4 -f flv rtmp://127.0.0.1:1935/stream/123

3. 使用 ffplay 播放 rtmp流

ffplay rtmp://127.0.0.1:1935/stream/123

使用 ffmpeg 完成摄像头推流

ffmpeg 查看电脑设备 输入下面的语句即可列出电脑的设备

ffmpeg -list_devices true -f dshow -i dummy

如:

[dshow @ 0000023d33efe940] "Rapoo camera" (video)
[dshow @ 0000023d33efe940]   Alternative name "@device_pnp_\\?\usb#vid_0c45&pid_6367&mi_00#6&2bd3af6b&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global"
[dshow @ 0000023d33efe940] "麦克风 (Rapoo camera)" (audio)
[dshow @ 0000023d33efe940]   Alternative name "@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{8D28FC3A-737E-4D57-B476-3B28EAFA8660}"
[dshow @ 0000023d33efe940] "麦克风 (Thanks your choice INVONS!)" (audio)
[dshow @ 0000023d33efe940]   Alternative name "@device_cm_{33D9A762-90C8-11D0-BD43-00A0C911CE86}\wave_{2FA9ECFF-9B26-424C-ADB8-44EF8CE2FEE9}"

测试摄像头是否可用

ffplay -f dshow -i video="Rapoo camera"

或者

ffplay -f vfwcap -i 0

摄像头&麦克风推流

ffmpeg -f dshow -i video="Rapoo camera":audio="麦克风 (Rapoo camera)" -vcodec libx264 -r 25 -preset:v ultrafast -tune:v zerolatency -f flv rtmp://124.70.51.123:1935/stream/123

20.部署PHP项目

1 先简单解释下原理,nginx 是一个 web 服务器,它只能处理静态文件,无法处理 PHP Python 等具体程序语言的请求。所以,原理是这样,用户统一先请求到 nginx,nginx 会再把请求转发给 php-fpm。

2 php-fpm是处理 PHP 请求的一个东西,实现了 FastCGI 协议的一个东西,它叫PHP FastCGI 管理器。

3 FastCGI 是什么?是一种与 Web 服务器通信的协议,规定了要传什么数据,具体什么格式。

拉取 php:7.3-fpm 并运行

docker run --name php7.3 -p 9000:9000 -v /home/test/www:/www -d php:7.3-fpm

--name php7.3 给容器取个名字

-p 9000:9000 php容器的端口默认是9000,映射到宿主机的9000端口

-v /home/test/www:/www 把宿主机的PHP源代码目录 /home/test/www 挂载到容器内的 /www。未来在容器内访问 /www 就相当于访问宿主机的 /home/test/www

-d 后台静默运行 php:7.4-fpm 镜像名

nginx

创建一个存放配置文件的目录。这个目录等下要挂载到容器里。

mkdir -p /opt/docker/nginx/conf.d

2 然后创建一个空配置文件,并在里面填入如下内容。比如 vim youke.conf,建议一个站点一个文件。

# 服务端配置节点
server {
    # 监听端口。此端口不能被占用了
    listen       80;
    # 此站点的域名。直接在宿主机配置一个host域名,或者在阿里云等云服务商那里解析过来。
    server_name  127.0.0.1;
    # 此站点的入口目录。这里要注意,这是当前容器内的路径。因为我等下会把宿主机的项目路径挂载到容器内的 /www 目录。
    root  /www/youke;
    # 入口目录里可识别的入口文件
    index index.html index.htm index.php;
    
    # 配置伪静态设置
    location / {
        #访问路径的文件不存在则重写URL转交给ThinkPHP处理
        if (!-e $request_filename) {
            rewrite  ^(.*)$  /index.php?s=$1  last;   break;
        }
    }

    # 配置url,处理及转发PHP请求
    location ~ \.php(/|$) {
        # 入口文件
        fastcgi_index index.php;
        # PHP项目的IP和端口。这是php-fpm的地址。由于nginx处理不了PHP代码,所以需要把请求转发给php-fpm进行处理。
        fastcgi_pass  192.168.8.234:9000;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include       fastcgi_params;
    }
    # 用户的访问日志。注意,这目录必须存在,否则nginx将启动不了。
    access_log  /www/youke.log;
    # 错误日志
    error_log  /www/youke.error.log;
}

创建并启动 nginx

docker run --name nginx -p 80:80 -d -v /home/test/www:/www -v /opt/docker/nginx/conf.d:/etc/nginx/conf.d nginx

-v /home/tian/www:/www 把宿主机的源码目录 /home/test/www 挂载到容器内的 /www 目录。容器内访问 /www 就相当于访问 /home/test/www

-v /opt/docker/nginx/conf.d:/etc/nginx/conf.d 把宿主机的配置目录,挂载到容器内nginx的配置目录。nginx 会自动去加载这目录内所有的配置文件。/opt/docker/nginx/conf.d 里建议每个站点对应一个配置文件。

建议开启 php 项目的调试模式,不同项目配置不尽相同。

报错:

Uncaught Exception: MySQL Error: could not find driver in xxx.php

#进入启动的php:5.6-fpm  23f25c24d6e8 为容器id
docker exec -it 23f25c24d6e8 bash

#安装扩展pdo_mysql
docker-php-ext-install pdo_mysql
#安装扩展mysql
docker-php-ext-install mysql
#安装扩展mysqli
docker-php-ext-install mysqli

报错:

MySQL Error: SQLSTATE[HY000] [2054] The server requested authentication method unknown to the client

错误原因是对 MySQL 进行的版本升级,MySQL8中用户的认证类型(Authentication type)默认为 caching_sha2_password 导致的错误,需要修改用户权限认证方式为 mysql_native_password。

mysql> alter user 'root'@'%' identified with mysql_native_password by '123456';
mysql> flush privileges;

修改后查看

mysql> use mysql;
mysql> select host,user,plugin from user;
+-----------+------------------+-----------------------+
| host      | user             | plugin                |
+-----------+------------------+-----------------------+
| %         | root             | mysql_native_password |
| localhost | mysql.infoschema | caching_sha2_password |
| localhost | mysql.session    | caching_sha2_password |
| localhost | mysql.sys        | caching_sha2_password |
+-----------+------------------+-----------------------+
4 rows in set (0.00 sec)

报错:

Call to undefined function imagecreatetruecolor()

如果 gd 库中没有 freeType 会产生这个问题,则按照以下步骤进行:

进入容器

apt-get update
apt-get install -y libfreetype6 libfreetype6-dev

然后,使用 docker-php-ext-configure 和 docker-php-ext-install 来启用 Freetype 扩展,例如:

docker-php-ext-configure gd --with-freetype --with-jpeg
docker-php-ext-install -j$(nproc) gd

这样Docker容器中就可以安装并启用Freetype库支持了。

FreeType 是一个开源的字体渲染引擎,提供了从字体文件中加载字形并将其呈现到屏幕或其他设备上的功能。它支持多种字体格式,包括 TrueType、OpenType、Type 1、CID-keyed、CFF 和其他格式。

FreeType 可以用于在各种应用程序中实现高质量的字体渲染,包括操作系统、图形编辑软件、桌面出版工具、Web浏览器等。

如果需要部署多个项目需要在 /opt/docker/nginx/conf.d 目录下创建多个配置文件。每个应用启动的端口不能相同。同时容器应该多个端口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值