Compare commits
4 Commits
25c582093d
...
d0fcac5455
Author | SHA1 | Date |
---|---|---|
|
d0fcac5455 | |
|
84c2d06b0c | |
|
6fcf7a4b75 | |
|
f1e7394450 |
|
@ -5,14 +5,29 @@
|
|||
```bash
|
||||
docker run -d --name rabbitmq_master --restart=always \
|
||||
-p 5672:5672 -p 15672:15672 \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq_master/data:/var/lib/rabbitmq_master \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq_master/conf:/etc/rabbitmq_master \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq_master/log:/var/log/rabbitmq_master \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq_master/data:/var/lib/rabbitmq \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq_master/conf:/etc/rabbitmq \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq_master/log:/var/log/rabbitmq \
|
||||
-e RABBITMQ_DEFAULT_VHOST=rabbitmq_master \
|
||||
-e RABBITMQ_DEFAULT_USER=admin \
|
||||
-e RABBITMQ_DEFAULT_PASS=admin rabbitmq:3.13.7-management
|
||||
```
|
||||
|
||||
**后续测试联邦队列**
|
||||
|
||||
如果后续需要学习联邦队列可以再复制下面的内容,根据自己需求修改下端口号
|
||||
|
||||
```bash
|
||||
docker run -d --name rabbitmq_2 --restart=always \
|
||||
-p 5673:5672 -p 15673:15672 \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq2/data:/var/lib/rabbitmq \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq2/conf:/etc/rabbitmq \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq2/log:/var/log/rabbitmq \
|
||||
-e RABBITMQ_DEFAULT_VHOST=rabbitmq2 \
|
||||
-e RABBITMQ_DEFAULT_USER=admin \
|
||||
-e RABBITMQ_DEFAULT_PASS=admin rabbitmq:3.13.7-management
|
||||
```
|
||||
|
||||
## 基本示例
|
||||
|
||||
**生产者**
|
||||
|
@ -615,6 +630,8 @@ void buildExchangeOverflowTest() {
|
|||
>
|
||||
> - 最大延迟时间:**2天(48小时)**
|
||||
> - 必须匹配RabbitMQ版本
|
||||
>
|
||||
> **插件版本必须与RabbitMQ严格匹配(如3.13.x需使用v3.13.x插件)。**
|
||||
|
||||
#### 1、确认docker数据卷
|
||||
|
||||
|
@ -746,8 +763,8 @@ try {
|
|||
|
||||
---
|
||||
|
||||
### **2. 生产环境推荐方案**
|
||||
#### **(1)Confirm 模式(轻量级确认)**
|
||||
### 生产环境推荐方案
|
||||
#### (1)Confirm 模式(轻量级确认)
|
||||
- **原理**:异步确认消息是否成功到达 Broker。
|
||||
- **配置方式**:
|
||||
```java
|
||||
|
@ -763,14 +780,14 @@ try {
|
|||
```
|
||||
- **优点**:性能接近非事务模式,可靠性高。
|
||||
|
||||
#### **(2)消息补偿 + 幂等设计**
|
||||
#### (2)消息补偿 + 幂等设计
|
||||
- **步骤**:
|
||||
1. 消息表记录发送状态(如 `status: sending/success/fail`)。
|
||||
2. 定时任务补偿失败消息。
|
||||
3. 消费者端做幂等处理(如唯一 ID + 去重表)。
|
||||
- **适用场景**:订单支付、库存扣减等关键业务。
|
||||
|
||||
#### **(3)本地消息表(最终一致性)**
|
||||
#### (3)本地消息表(最终一致性)
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App
|
||||
|
@ -852,6 +869,8 @@ channel.queueDeclare("myLazyQueue", true, false, false, args);
|
|||
|
||||
## 优先级队列
|
||||
|
||||
**优先级仅在消息堆积时生效(空队列时无意义)**
|
||||
|
||||
**优先级范围**
|
||||
|
||||
- 通过 `x-max-priority` 参数定义队列支持的最大优先级(默认0=无优先级)
|
||||
|
@ -930,4 +949,900 @@ void priorityTest() {
|
|||
public void processMessagePriority(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
log.info("<<优先级队列>>----<priority>{}", dataString);
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
## 集群搭建
|
||||
|
||||
完成示例参考
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> 在操作之前可以拍摄虚拟机快照,方便回滚,要保证这个环境没有RabbitMQ或妨碍搭建的内容。
|
||||
|
||||
### 修改Ubuntu网络(可选)
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 如果需要修改,记得修改网关和IP地址。
|
||||
>
|
||||
> 节点间端口要求(如4369/25672需互通),避免防火墙问题.。
|
||||
|
||||
看自己是否有这个需求。
|
||||
|
||||
```yaml
|
||||
network:
|
||||
ethernets:
|
||||
ens33:
|
||||
dhcp4: false
|
||||
addresses: [192.168.3.144/24]
|
||||
optional: true
|
||||
routes:
|
||||
- to: default
|
||||
via: 192.168.3.1
|
||||
nameservers:
|
||||
addresses: [8.8.8.8]
|
||||
version: 2
|
||||
```
|
||||
|
||||
应用修改
|
||||
|
||||
```bash
|
||||
sudo netplan apply
|
||||
# or
|
||||
sudo reboot
|
||||
```
|
||||
|
||||
### Docker方式
|
||||
|
||||
集群环境搭建版本是:
|
||||
|
||||
#### 1、配置docker镜像源
|
||||
|
||||
```bash
|
||||
# 创建目录
|
||||
sudo mkdir -p /etc/docker
|
||||
# 写入配置文件
|
||||
sudo tee /etc/docker/daemon.json <<-'EOF'
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://docker-0.unsee.tech",
|
||||
"https://docker-cf.registry.cyou",
|
||||
"https://docker.1panel.live"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
# 重启docker服务
|
||||
sudo systemctl daemon-reload && sudo systemctl restart docker
|
||||
```
|
||||
|
||||
#### 2、docker-compose.yaml
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 执行前确保有权限!!!
|
||||
>
|
||||
> ```bash
|
||||
> # 1. 停止容器
|
||||
> docker compose stop
|
||||
>
|
||||
> # 2. 清理旧目录(可选)
|
||||
> sudo rm -rf ~/docker/docker_data/rabbitmq
|
||||
>
|
||||
> # 3. 重建目录并设置权限
|
||||
> mkdir -p ~/docker/docker_data/rabbitmq/{rabbit1,rabbit2,rabbit3}/{data,conf,log}
|
||||
> sudo chown -R 999:999 ~/docker/docker_data/rabbitmq
|
||||
>
|
||||
> # 4. 重新启动
|
||||
> docker compose up -d
|
||||
> ```
|
||||
|
||||
创建文件:`docker-compose.yaml`。
|
||||
|
||||
```yaml
|
||||
name: 'rabbitmq-cluster'
|
||||
|
||||
services:
|
||||
rabbit1:
|
||||
image: rabbitmq:3.13.7-management
|
||||
container_name: rabbit1
|
||||
hostname: rabbit1
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
environment:
|
||||
- RABBITMQ_ERLANG_COOKIE=SECRETCOOKIE
|
||||
- RABBITMQ_NODENAME=rabbit@rabbit1
|
||||
- RABBITMQ_DEFAULT_USER=admin
|
||||
- RABBITMQ_DEFAULT_PASS=admin
|
||||
volumes:
|
||||
- ~/docker/docker_data/rabbitmq/rabbit1/data:/var/lib/rabbitmq
|
||||
- ~/docker/docker_data/rabbitmq/rabbit1/conf:/etc/rabbitmq
|
||||
- ~/docker/docker_data/rabbitmq/rabbit1/log:/var/log/rabbitmq
|
||||
networks:
|
||||
- rabbitmq-cluster
|
||||
|
||||
rabbit2:
|
||||
image: rabbitmq:3.13.7-management
|
||||
container_name: rabbit2
|
||||
hostname: rabbit2
|
||||
ports:
|
||||
- "5673:5672"
|
||||
- "15673:15672"
|
||||
environment:
|
||||
- RABBITMQ_ERLANG_COOKIE=SECRETCOOKIE
|
||||
- RABBITMQ_NODENAME=rabbit@rabbit2
|
||||
- RABBITMQ_DEFAULT_USER=admin
|
||||
- RABBITMQ_DEFAULT_PASS=admin
|
||||
volumes:
|
||||
- ~/docker/docker_data/rabbitmq/rabbit2/data:/var/lib/rabbitmq
|
||||
- ~/docker/docker_data/rabbitmq/rabbit2/conf:/etc/rabbitmq
|
||||
- ~/docker/docker_data/rabbitmq/rabbit2/log:/var/log/rabbitmq
|
||||
networks:
|
||||
- rabbitmq-cluster
|
||||
depends_on:
|
||||
- rabbit1
|
||||
|
||||
rabbit3:
|
||||
image: rabbitmq:3.13.7-management
|
||||
container_name: rabbit3
|
||||
hostname: rabbit3
|
||||
ports:
|
||||
- "5674:5672"
|
||||
- "15674:15672"
|
||||
environment:
|
||||
- RABBITMQ_ERLANG_COOKIE=SECRETCOOKIE
|
||||
- RABBITMQ_NODENAME=rabbit@rabbit3
|
||||
- RABBITMQ_DEFAULT_USER=admin
|
||||
- RABBITMQ_DEFAULT_PASS=admin
|
||||
volumes:
|
||||
- ~/docker/docker_data/rabbitmq/rabbit3/data:/var/lib/rabbitmq
|
||||
- ~/docker/docker_data/rabbitmq/rabbit3/conf:/etc/rabbitmq
|
||||
- ~/docker/docker_data/rabbitmq/rabbit3/log:/var/log/rabbitmq
|
||||
networks:
|
||||
- rabbitmq-cluster
|
||||
depends_on:
|
||||
- rabbit1
|
||||
- rabbit2
|
||||
|
||||
networks:
|
||||
rabbitmq-cluster:
|
||||
external: true
|
||||
```
|
||||
|
||||
#### 3、启动docker-compose
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### 4、加入集群
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 注意你的名称是否和我的一样,直接复制忽略。
|
||||
|
||||
```bash
|
||||
# 将rabbit2加入集群
|
||||
docker exec -it rabbit2 rabbitmqctl stop_app
|
||||
docker exec -it rabbit2 rabbitmqctl reset
|
||||
docker exec -it rabbit2 rabbitmqctl join_cluster rabbit@rabbit1
|
||||
docker exec -it rabbit2 rabbitmqctl start_app
|
||||
|
||||
# 将rabbit3加入集群
|
||||
docker exec -it rabbit3 rabbitmqctl stop_app
|
||||
docker exec -it rabbit3 rabbitmqctl reset
|
||||
docker exec -it rabbit3 rabbitmqctl join_cluster rabbit@rabbit1
|
||||
docker exec -it rabbit3 rabbitmqctl start_app
|
||||
```
|
||||
|
||||
> 成功的输出:
|
||||
>
|
||||
> ```bash
|
||||
> bunny@bunny:~$ # 将rabbit2加入集群
|
||||
> docker exec -it rabbit2 rabbitmqctl stop_app
|
||||
> docker exec -it rabbit2 rabbitmqctl reset
|
||||
> docker exec -it rabbit2 rabbitmqctl join_cluster rabbit@rabbit1
|
||||
> docker exec -it rabbit2 rabbitmqctl start_app
|
||||
>
|
||||
> # 将rabbit3加入集群
|
||||
> docker exec -it rabbit3 rabbitmqctl stop_app
|
||||
> docker exec -it rabbit3 rabbitmqctl reset
|
||||
> docker exec -it rabbit3 rabbitmqctl join_cluster rabbit@rabbit1
|
||||
> docker exec -it rabbit3 rabbitmqctl start_app
|
||||
> Stopping rabbit application on node rabbit@rabbit2 ...
|
||||
> Resetting node rabbit@rabbit2 ...
|
||||
> Clustering node rabbit@rabbit2 with rabbit@rabbit1
|
||||
> Starting node rabbit@rabbit2 ...
|
||||
> Stopping rabbit application on node rabbit@rabbit3 ...
|
||||
> Resetting node rabbit@rabbit3 ...
|
||||
> Clustering node rabbit@rabbit3 with rabbit@rabbit1
|
||||
> Starting node rabbit@rabbit3 ...
|
||||
> ```
|
||||
|
||||
### 普通方式
|
||||
|
||||
#### 先决条件
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> 所有节点使用相同版本的Erlang(可通过`erl -version`检查)
|
||||
|
||||
- 至少两台Ubuntu服务器(建议18.04 LTS或更高版本)
|
||||
- 所有服务器之间网络互通
|
||||
- 每台服务器上已安装相同版本的RabbitMQ
|
||||
- 所有节点使用相同版本的Erlang
|
||||
|
||||
#### 1、安装RabbitMQ
|
||||
|
||||
在所有节点上执行以下步骤:
|
||||
|
||||
```bash
|
||||
# 更新软件包列表
|
||||
sudo apt update
|
||||
|
||||
# 安装必要的依赖
|
||||
sudo apt install -y curl gnupg
|
||||
|
||||
# 移除之前添加的 packagecloud.io 仓库
|
||||
sudo rm /etc/apt/sources.list.d/rabbitmq.list
|
||||
|
||||
# 添加 RabbitMQ 官方仓库密钥
|
||||
sudo apt-get install curl gnupg apt-transport-https -y
|
||||
|
||||
## Team RabbitMQ's main signing key
|
||||
curl -1sLf "https://keys.openpgp.org/vks/v1/by-fingerprint/0A9AF2115F4687BD29803A206B73A36E6026DFCA" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/com.rabbitmq.team.gpg > /dev/null
|
||||
|
||||
## Cloudsmith: modern Erlang repository
|
||||
curl -1sLf "https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-erlang/gpg.E495BB49CC4BBE5B.key" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/io.cloudsmith.rabbitmq.E495BB49CC4BBE5B.gpg > /dev/null
|
||||
|
||||
## Cloudsmith: RabbitMQ repository
|
||||
curl -1sLf "https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-server/gpg.9F4587F226208342.key" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/io.cloudsmith.rabbitmq.9F4587F226208342.gpg > /dev/null
|
||||
|
||||
# 添加仓库源
|
||||
sudo tee /etc/apt/sources.list.d/rabbitmq.list <<EOF
|
||||
## Provides modern Erlang/OTP releases
|
||||
deb [signed-by=/usr/share/keyrings/io.cloudsmith.rabbitmq.E495BB49CC4BBE5B.gpg] https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-erlang/deb/ubuntu $(lsb_release -cs) main
|
||||
deb-src [signed-by=/usr/share/keyrings/io.cloudsmith.rabbitmq.E495BB49CC4BBE5B.gpg] https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-erlang/deb/ubuntu $(lsb_release -cs) main
|
||||
|
||||
## Provides RabbitMQ
|
||||
deb [signed-by=/usr/share/keyrings/io.cloudsmith.rabbitmq.9F4587F226208342.gpg] https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-server/deb/ubuntu $(lsb_release -cs) main
|
||||
deb-src [signed-by=/usr/share/keyrings/io.cloudsmith.rabbitmq.9F4587F226208342.gpg] https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-server/deb/ubuntu $(lsb_release -cs) main
|
||||
EOF
|
||||
|
||||
# 更新软件包列表
|
||||
sudo apt-get update -y
|
||||
|
||||
# 安装 Erlang 和 RabbitMQ
|
||||
sudo apt-get install -y erlang-base \
|
||||
erlang-asn1 erlang-crypto erlang-eldap erlang-ftp erlang-inets \
|
||||
erlang-mnesia erlang-os-mon erlang-parsetools erlang-public-key \
|
||||
erlang-runtime-tools erlang-snmp erlang-ssl erlang-syntax-tools \
|
||||
erlang-tftp erlang-tools erlang-xmerl
|
||||
|
||||
sudo apt-get install rabbitmq-server -y --fix-missing
|
||||
|
||||
# 启动RabbitMQ服务
|
||||
sudo systemctl start rabbitmq-server
|
||||
sudo systemctl enable rabbitmq-server
|
||||
|
||||
# 检查状态
|
||||
sudo systemctl status rabbitmq-server
|
||||
```
|
||||
|
||||
#### 2、配置主机名和hosts文件
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> 所有主机上的`hostname`都必须和配置的一样,否则无法加入集群还会报错。
|
||||
>
|
||||
> 查看本机的hostname;如果输出的和配置的hosts的不一致,需要修改hostname。
|
||||
>
|
||||
> ```bash
|
||||
> # 查看本机的hostname
|
||||
> hostname
|
||||
>
|
||||
> # 永久修改hostname(以rabbit1为例)
|
||||
> sudo hostnamectl set-hostname rabbit1
|
||||
> ```
|
||||
>
|
||||
> **例如:**
|
||||
>
|
||||
> - 主节点hostname为rabbit1
|
||||
> - 192.168.3.145节点hostname为rabbit2
|
||||
> - 192.168.3.146节点hostname为rabbit3
|
||||
> - 以此类推
|
||||
|
||||
在所有节点上编辑`/etc/hosts`文件,确保包含所有集群节点的IP和主机名:
|
||||
|
||||
```bash
|
||||
sudo vim /etc/hosts
|
||||
```
|
||||
|
||||
添加类似以下内容(根据您的实际IP和主机名修改):
|
||||
|
||||
```bash
|
||||
192.168.3.144 rabbit1
|
||||
192.168.3.145 rabbit2
|
||||
192.168.3.146 rabbit3
|
||||
192.168.3.147 rabbit4
|
||||
```
|
||||
|
||||
#### 3、设置Erlang Cookie
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 普通方式搭建集群时,`.erlang.cookie`的权限必须为`400`
|
||||
>
|
||||
> ```bash
|
||||
> sudo chmod 400 /var/lib/rabbitmq/.erlang.cookie
|
||||
> ```
|
||||
|
||||
RabbitMQ节点使用Erlang cookie进行认证,所有节点必须使用相同的cookie。
|
||||
|
||||
如果是克隆的虚拟机,可以不检查(最好还是检查下),因为克隆的基本上是一样的。
|
||||
|
||||
选择一个节点作为主节点,将其cookie复制到其他节点:
|
||||
|
||||
```bash
|
||||
# 在主节点上查看cookie
|
||||
sudo cat /var/lib/rabbitmq/.erlang.cookie
|
||||
|
||||
# 在其他节点上停止RabbitMQ服务
|
||||
sudo systemctl stop rabbitmq-server
|
||||
|
||||
# 编辑或创建cookie文件
|
||||
sudo nano /var/lib/rabbitmq/.erlang.cookie
|
||||
|
||||
# 粘贴主节点的cookie内容
|
||||
sudo chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
|
||||
# 然后设置正确的权限
|
||||
sudo chmod 400 /var/lib/rabbitmq/.erlang.cookie
|
||||
|
||||
# 重新启动RabbitMQ服务
|
||||
sudo systemctl start rabbitmq-server
|
||||
```
|
||||
|
||||
#### 4、加入集群
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 加入集群时,必须要是这样的格式`sudo rabbitmqctl join_cluster rabbit@主节点名称`
|
||||
|
||||
在非主节点上执行以下命令加入集群:
|
||||
|
||||
> [!WARNING]
|
||||
> `reset`命令会清除该节点的所有数据和配置,生产环境慎用
|
||||
|
||||
```bash
|
||||
# 停止RabbitMQ应用(不停止服务)
|
||||
sudo rabbitmqctl stop_app
|
||||
|
||||
# 重置节点(如果是新安装可以跳过,生产环境谨慎使用)
|
||||
sudo rabbitmqctl reset
|
||||
|
||||
# 加入集群(将rabbit1替换为您的主节点主机名)
|
||||
sudo rabbitmqctl join_cluster rabbit@rabbit1
|
||||
|
||||
# 启动RabbitMQ应用
|
||||
sudo rabbitmqctl start_app
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> 如果因为别的原因弄乱了,可以删除原来数据
|
||||
>
|
||||
> 仔细甄别下面是否有你需要的!!!
|
||||
>
|
||||
> ```bash
|
||||
> # 停止 RabbitMQ
|
||||
> sudo systemctl stop rabbitmq-server
|
||||
>
|
||||
> # 删除旧数据
|
||||
> sudo rm -rf /var/lib/rabbitmq/mnesia/
|
||||
>
|
||||
> # 确保 cookie 与其他节点一致
|
||||
> sudo cat /var/lib/rabbitmq/.erlang.cookie # 必须与其他节点相同
|
||||
>
|
||||
> # 启动服务
|
||||
> sudo systemctl start rabbitmq-server
|
||||
>
|
||||
> # 加入集群(使用新主机名)
|
||||
> sudo rabbitmqctl stop_app
|
||||
> sudo rabbitmqctl reset
|
||||
> sudo rabbitmqctl join_cluster rabbit@rabbit1 # 假设 rabbit1 是主节点
|
||||
> sudo rabbitmqctl start_app
|
||||
> ```
|
||||
|
||||
#### 5、验证集群状态
|
||||
|
||||
在任何节点上执行以下命令检查集群状态:
|
||||
|
||||
```bash
|
||||
sudo rabbitmqctl cluster_status
|
||||
|
||||
# OR 查看集群节点列表
|
||||
sudo rabbitmqctl cluster_status | grep -A10 'running_nodes'
|
||||
|
||||
# 检查服务状态
|
||||
sudo rabbitmqctl status
|
||||
```
|
||||
|
||||
> 为了提高可用性,可以设置镜像队列策略:
|
||||
>
|
||||
> ```bash
|
||||
> sudo rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'
|
||||
> ```
|
||||
|
||||
#### 6、启用管理插件(可选)
|
||||
|
||||
- 如果需要Web管理界面:
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 如果使用Web界面,需要在每个集群上都执行一下,否则情况会出现下面情况,举个例子。
|
||||
>
|
||||
> 如果在主节点上执行,那么主节点可以访问,之后输入账户密码登录之后可以看到主节点的情况,但是其余节点会出现【Node statistics not available】。
|
||||
>
|
||||
> 为了避免这种情况,要在每个节点上都执行一下下面的命令。
|
||||
|
||||
```bash
|
||||
sudo rabbitmq-plugins enable rabbitmq_management
|
||||
```
|
||||
|
||||
然后可以通过`http://<node-ip>:15672`访问,默认用户名/密码为`guest/guest`(建议更改)。
|
||||
|
||||
如果需要开启防火墙
|
||||
|
||||
```bash
|
||||
# 开放15672端口(如果使用防火墙)
|
||||
sudo ufw allow 15672/tcp
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> 如果到页面中查看,没有用户或者没有权限可以通过下面方式进行更改或添加。
|
||||
|
||||
如果需要添加用户,在你安装管理界面的机器上运行。
|
||||
|
||||
```bash
|
||||
# 如果存在可以先删除
|
||||
sudo rabbitmqctl delete_user 用户名
|
||||
|
||||
# 添加用户
|
||||
sudo rabbitmqctl add_user 用户名 密码
|
||||
|
||||
#设置用户操作权限
|
||||
sudo rabbitmqctl set_user_tags 用户名 administrator
|
||||
```
|
||||
|
||||
**示例**
|
||||
|
||||
```bash
|
||||
# 如果存在可以先删除
|
||||
sudo rabbitmqctl delete_user admin
|
||||
|
||||
# 添加用户
|
||||
sudo rabbitmqctl add_user admin admin
|
||||
|
||||
#设置用户操作权限
|
||||
sudo rabbitmqctl set_user_tags admin administrator
|
||||
```
|
||||
|
||||
- **修改配置文件**(如 `/etc/hosts`、防火墙规则等)需在所有节点操作。
|
||||
- **重启 RabbitMQ 服务**(如果修改了核心配置):
|
||||
|
||||
```bash
|
||||
sudo systemctl restart rabbitmq-server
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
Q:打开主界面没有用户可以登录。
|
||||
|
||||
A:添加或者创建新用户,参考【普通方式->第六点】
|
||||
|
||||
Q:打开之后显示【⚠ All stable feature flags must be enabled after completing an upgrade. 】
|
||||
|
||||
```bash
|
||||
# 查看当前 Feature Flags 状态
|
||||
sudo rabbitmqctl list_feature_flags
|
||||
|
||||
# 启用所有稳定的 Feature Flags,这会启用所有 稳定(stable) 的功能标志(不包括实验性功能)。
|
||||
sudo rabbitmqctl enable_feature_flag all
|
||||
|
||||
# 检查是否成功启用
|
||||
sudo rabbitmqctl list_feature_flags
|
||||
|
||||
# 重启 RabbitMQ(可选)
|
||||
sudo systemctl restart rabbitmq-server
|
||||
```
|
||||
|
||||
Q:出现下面情况【Node statistics not available】
|
||||
|
||||

|
||||
|
||||
A:解决方法
|
||||
|
||||
**情况一 管理插件未在所有节点启用**
|
||||
|
||||
```bash
|
||||
sudo rabbitmq-plugins enable rabbitmq_management
|
||||
sudo systemctl restart rabbitmq-server
|
||||
```
|
||||
|
||||
**情况二 **
|
||||
|
||||
如果节点之间无法正常通信(如防火墙、网络策略阻止),管理界面无法获取其他节点的统计信息。
|
||||
|
||||
**检查方法**
|
||||
|
||||
在任意节点执行:
|
||||
|
||||
```bash
|
||||
sudo rabbitmqctl cluster_status
|
||||
```
|
||||
|
||||
**检查防火墙**(确保 `4369`、`25672`、`5672`、`15672` 端口开放):
|
||||
|
||||
```bash
|
||||
sudo ufw allow 4369/tcp # Erlang 分布式通信端口
|
||||
sudo ufw allow 25672/tcp # RabbitMQ 集群通信端口
|
||||
sudo ufw allow 5672/tcp # AMQP 协议端口
|
||||
sudo ufw allow 15672/tcp # 管理界面端口
|
||||
sudo ufw reload
|
||||
```
|
||||
|
||||
**情况三 Cookie 不一致**
|
||||
|
||||
在所有节点执行:
|
||||
|
||||
```bash
|
||||
sudo cat /var/lib/rabbitmq/.erlang.cookie
|
||||
```
|
||||
|
||||
1. 停止 RabbitMQ:
|
||||
|
||||
```bash
|
||||
sudo systemctl stop rabbitmq-server
|
||||
```
|
||||
|
||||
2. 修改 Cookie(复制主节点的 Cookie 到其他节点):
|
||||
|
||||
```bash
|
||||
echo "YOUR_MASTER_NODE_COOKIE" | sudo tee /var/lib/rabbitmq/.erlang.cookie
|
||||
```
|
||||
|
||||
3. 设置权限:
|
||||
|
||||
```bash
|
||||
sudo chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
|
||||
sudo chmod 400 /var/lib/rabbitmq/.erlang.cookie
|
||||
```
|
||||
|
||||
4. 重启 RabbitMQ:
|
||||
|
||||
```bash
|
||||
sudo systemctl start rabbitmq-server
|
||||
```
|
||||
|
||||
**情况四 管理界面用户权限问题**
|
||||
|
||||
如果用户没有 **跨节点访问权限**,管理界面可能无法获取其他节点的数据。
|
||||
|
||||
**解决方案**
|
||||
|
||||
确保用户在所有节点都有 **管理员权限**:
|
||||
|
||||
```bash
|
||||
sudo rabbitmqctl set_user_tags <username> administrator
|
||||
sudo rabbitmqctl set_permissions -p / <username> ".*" ".*" ".*"
|
||||
```
|
||||
|
||||
例如:
|
||||
|
||||
```bash
|
||||
sudo rabbitmqctl set_user_tags admin administrator
|
||||
sudo rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
|
||||
```
|
||||
|
||||
**情况五 管理界面缓存问题**
|
||||
|
||||
浏览器可能缓存了旧数据,导致显示异常。
|
||||
|
||||
### 集群环境-负载均衡
|
||||
|
||||
#### 1. 安装 HAProxy
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install haproxy -y
|
||||
```
|
||||
|
||||
#### 2. 配置 HAProxy
|
||||
编辑 HAProxy 的配置文件 `/etc/haproxy/haproxy.cfg`:
|
||||
```bash
|
||||
sudo vim /etc/haproxy/haproxy.cfg
|
||||
```
|
||||
添加以下内容(假设 RabbitMQ 节点为 `rabbit1:5672` 和 `rabbit2:5672`):
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 绑定的端口号(`bind *:`)不能重复!!!
|
||||
|
||||
```ini
|
||||
frontend rabbitmq_front
|
||||
bind *:22222
|
||||
mode tcp
|
||||
default_backend rabbitmq_back
|
||||
|
||||
backend rabbitmq_back
|
||||
mode tcp
|
||||
balance roundrobin
|
||||
# 在之前设置了hosts文件,所以这里没有写端口号 == server rabbit1 192.168.3.144:5672 check
|
||||
server rabbit1 rabbit1:5672 check
|
||||
server rabbit2 rabbit2:5672 check
|
||||
server rabbit3 rabbit3:5672 check
|
||||
server rabbit4 rabbit4:5672 check
|
||||
|
||||
# 可选:启用 RabbitMQ 管理界面的负载均衡(默认端口 15672)
|
||||
frontend rabbitmq_admin
|
||||
bind *:12222
|
||||
mode http
|
||||
default_backend rabbitmq_admin_back
|
||||
|
||||
backend rabbitmq_admin_back
|
||||
mode http
|
||||
balance roundrobin
|
||||
# 在之前设置了hosts文件,所以这里没有写端口号 == server rabbit1 192.168.3.144:15672 check
|
||||
server rabbit1 rabbit1:15672 check
|
||||
server rabbit2 rabbit2:15672 check
|
||||
server rabbit3 rabbit3:15672 check
|
||||
server rabbit4 rabbit4:15672 check
|
||||
```
|
||||
|
||||
#### 3. 重启 HAProxy
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> 重启报错可以看下报错内容: journalctl -xeu haproxy.service
|
||||
>
|
||||
> 如果使用一些远程软件,修改时可能会出现,多行配置变一行问题,需要手动通过vim查看。
|
||||
|
||||
```bash
|
||||
sudo systemctl restart haproxy
|
||||
```
|
||||
### 集群环境的连接
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 如果是新建环境,需要将之前写的监听内容注释,否则启动容易报错。
|
||||
|
||||
使用Java程序进行连接。如果搭建了集群环境还配置了负载均衡,就是用负载均衡的方式进行连接。
|
||||
|
||||
负载均衡配置的是端口是:`5672`对应上面的文件是`22222`。所以在连接时候是`22222`这个 端口(参考【2. 配置 HAProxy】)
|
||||
|
||||
#### Java配置
|
||||
|
||||
```yaml
|
||||
# application.yaml
|
||||
rabbitmq:
|
||||
host: ${bunny.rabbitmq.host}
|
||||
port: ${bunny.rabbitmq.port}
|
||||
username: ${bunny.rabbitmq.username}
|
||||
password: ${bunny.rabbitmq.password}
|
||||
virtual-host: ${bunny.rabbitmq.virtual-host}
|
||||
|
||||
# application-dev.yaml
|
||||
bunny:
|
||||
rabbitmq:
|
||||
host: 192.168.3.144
|
||||
# port: 5672
|
||||
# 集群环境的端口号
|
||||
port: 22222
|
||||
virtual-host: /
|
||||
username: admin
|
||||
password: admin
|
||||
```
|
||||
|
||||
#### 创建环境
|
||||
|
||||
**交换机:**exchange.cluster.test
|
||||
|
||||

|
||||
|
||||
**队列:**queue.cluster.test
|
||||
|
||||

|
||||
|
||||
**路由键:**routing.key.cluster.test
|
||||
|
||||

|
||||
|
||||
## 集群中的队列
|
||||
|
||||
### 镜像队列
|
||||
|
||||
最新版本中已经退出历史舞台了,就不说了。
|
||||
|
||||
### 仲裁队列
|
||||
|
||||
当我们在集群中创建队列时,如【集群环境的连接】那一节,可以看到当前的队列是位于,第一个节点上的。如果这时候服务器宕机或者其他情况,这时这个节点上数据没了,其它j节点时访问不到的;我们理想的情况是,如果一台服务器宕机,这台服务器上的所有内容在别的节点上都是可以看到的。
|
||||
|
||||

|
||||
|
||||
#### 创建仲裁队列
|
||||
|
||||
**交换机:**exchange.quorum.test
|
||||
|
||||
**队列:**queue.quorum.test
|
||||
|
||||
**l路由键:**routing.key.quorum.test
|
||||
|
||||

|
||||
|
||||
#### 测试Code
|
||||
|
||||
**生产者**
|
||||
|
||||
```java
|
||||
/* 仲裁队列发送消息 */
|
||||
@Test
|
||||
void clusterQuorumTest2() {
|
||||
final String EXCHANGE = "exchange.quorum.test";
|
||||
final String ROUTING_KEY = "routing.key.quorum.test";
|
||||
|
||||
for (int i = 0; i <= 10; i++) {
|
||||
rabbitTemplate.convertAndSend(EXCHANGE, ROUTING_KEY, "仲裁队列发送消息-" + i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**消费者**
|
||||
|
||||
```java
|
||||
/* 仲裁队列监听 */
|
||||
@RabbitListener(queues = "queue.quorum.test")
|
||||
public void processMessage(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
log.info("<<仲裁队列监听下消息>>----{}", dataString);
|
||||
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
|
||||
}
|
||||
```
|
||||
|
||||
### 联邦队列
|
||||
|
||||
Federation插件的设计目标是使RabbitMQ在不同的Broker节点之间进行消息传递而无须建立集群。
|
||||
|
||||
它可以在不同的管理域中的Broker或集群间传递消息,这些管理域可能设置了不同的用户和vhost,也可能运行在不同版本的RabbitMQ和Erlang上。Federation基于AMQP0-9-1协议在不同的Broker之间进行通信,并且设计成能够容忍不稳定的网络连接情况。
|
||||
|
||||
- 各节点操作:启用联邦插件
|
||||
- 下游操作:
|
||||
- 添加上游连接端点
|
||||
- 创建控制策略
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 因为联邦队列不是集群,需要两个不同rabbit,比如说,一个服务器在上海,一个在杭州,两个不同的服务器且不是集群环境。
|
||||
>
|
||||
> 所以在创建时候不要加入集群。
|
||||
|
||||
#### 创建docker容器
|
||||
|
||||
**设置权限**
|
||||
|
||||
```bash
|
||||
sudo chown -R 999:999 ~/docker/docker_data/rabbitmq/rabbitmq_master
|
||||
sudo chown -R 999:999 ~/docker/docker_data/rabbitmq/rabbitmq_2
|
||||
```
|
||||
|
||||
**创建容器**
|
||||
|
||||
```bash
|
||||
# 第一个
|
||||
docker run -d --name rabbitmq_master --restart=always \
|
||||
-p 5672:5672 -p 15672:15672 \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq_master/data:/var/lib/rabbitmq \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq_master/conf:/etc/rabbitmq \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq_master/log:/var/log/rabbitmq \
|
||||
-e RABBITMQ_DEFAULT_VHOST=rabbitmq_master \
|
||||
-e RABBITMQ_DEFAULT_USER=admin \
|
||||
-e RABBITMQ_DEFAULT_PASS=admin rabbitmq:3.13.7-management
|
||||
|
||||
# 第二个
|
||||
docker run -d --name rabbitmq_2 --restart=always \
|
||||
-p 5673:5672 -p 15673:15672 \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq_2/data:/var/lib/rabbitmq \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq_2/conf:/etc/rabbitmq \
|
||||
-v ~/docker/docker_data/rabbitmq/rabbitmq_2/log:/var/log/rabbitmq \
|
||||
-e RABBITMQ_DEFAULT_VHOST=rabbitmq_2 \
|
||||
-e RABBITMQ_DEFAULT_USER=admin \
|
||||
-e RABBITMQ_DEFAULT_PASS=admin rabbitmq:3.13.7-management
|
||||
```
|
||||
|
||||
#### 启动联邦插件
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 联邦插件并不需要下载启动即可。
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 创建时候需要重新设置Virtual Hosts否则设置了内容会显示`not_allow`,不能使用默认的Virtual Hosts。
|
||||
|
||||
```bash
|
||||
# 执行下面命令进行开启
|
||||
rabbitmq-plugins enable rabbitmq_federation
|
||||
rabbitmq-plugins enable rabbitmq_federation_mangement
|
||||
```
|
||||
|
||||
不进入docker容器直接运行方式
|
||||
|
||||
```bash
|
||||
# 开启全部插件防止警告
|
||||
docker exec -it rabbitmq_master rabbitmqctl enable_feature_flag all
|
||||
docker exec -it rabbitmq_2 rabbitmqctl enable_feature_flag all
|
||||
|
||||
docker exec -it rabbitmq_master rabbitmq-plugins enable rabbitmq_federation
|
||||
docker exec -it rabbitmq_master rabbitmq-plugins enable rabbitmq_federation_management
|
||||
|
||||
docker exec -it rabbitmq_2 rabbitmq-plugins enable rabbitmq_federation
|
||||
docker exec -it rabbitmq_2 rabbitmq-plugins enable rabbitmq_federation_management
|
||||
```
|
||||
|
||||

|
||||
|
||||
启用成功后会有下面两个选项
|
||||
|
||||

|
||||
|
||||
在此页面中添加
|
||||
|
||||

|
||||
|
||||
上图输入框中的内容
|
||||
|
||||
```bash
|
||||
# 连接名称
|
||||
bunny.upstream
|
||||
|
||||
# 连接地址
|
||||
amqp://admin:admin@172.17.0.1:5672
|
||||
```
|
||||
|
||||
创建Policies
|
||||
|
||||

|
||||
|
||||
上图输入框内容
|
||||
|
||||
```bash
|
||||
name: policy.federation.exchange
|
||||
Pattern: ^federation\.
|
||||
Apply to: Exchanges
|
||||
Priority: 10
|
||||
|
||||
# 这里的federation-upstream 是我们之前创建的联邦名称,详细看上面【上图的,输入框中的内容】
|
||||
federation-upstream bunny.upstream
|
||||
```
|
||||
|
||||
#### 创建交换机
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 创建联邦交换机时,需要匹配上面写的`Pattern`,如果按照文档创建,那么名称需要是:`federation.xxx.xxxxx`这种格式,反正肯定是以`federation`开头的。
|
||||
>
|
||||
> 其次路由键要一致,队列名称可以不一样。
|
||||
|
||||
| 所在机房 | 交换机名称 | 路由键 | 队列名称 |
|
||||
| ----------------------------- | ------------------------ | --------------------- | ---------------------- |
|
||||
| rabbitmq_master(你的上游MQ) | federation.exchange.demo | routing.key.demo.test | queue.normal.master |
|
||||
| rabbitmq2(你的下游MQ) | federation.exchange.demo | routing.key.demo.test | queue.normal.rabbitmq2 |
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 但此时发现下游节点中联邦队列并没有接收到消息,这是为什么呢?这里就体现出了联邦队列和联邦交换机工作逻辑的区别。
|
||||
> 对联邦队列来说,如果没有监听联邦队列的消费端程序,它是不会到上游去拉取消窶的!
|
||||
> 如果有消费端监听联邦队列,那么首先消费联邦队列自身的消息;如果联邦队列为空,这时候才会到上游队列节点中拉取消息。
|
||||
> 所以现在的测试效果需要消费端程序配合才能看到:
|
||||
|
|
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 174 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 7.2 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 64 KiB |
|
@ -0,0 +1,20 @@
|
|||
package cn.bunny.mq.mqdemo.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@Controller
|
||||
@RequestMapping
|
||||
public class IndexController {
|
||||
|
||||
@GetMapping("")
|
||||
public String index() {
|
||||
return "index";
|
||||
}
|
||||
|
||||
@GetMapping("dialog")
|
||||
public String dialog() {
|
||||
return "dialog";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package cn.bunny.mq.mqdemo.mq.listener;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class ClusterEnvironmentListener {
|
||||
|
||||
// /* 测试优先级队列 */
|
||||
// @RabbitListener(queues = "queue.cluster.test")
|
||||
// public void processMessagePriority(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
// log.info("<<集群环境下消息>>----<cluster>{}", dataString);
|
||||
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
|
||||
// }
|
||||
//
|
||||
// /* 仲裁队列监听 */
|
||||
// @RabbitListener(queues = "queue.quorum.test")
|
||||
// public void processMessage(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
// log.info("<<仲裁队列监听下消息>>----{}", dataString);
|
||||
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
|
||||
// }
|
||||
}
|
|
@ -7,8 +7,6 @@ import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
|||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
|
@ -81,17 +79,24 @@ public class MessageListenerOrder {
|
|||
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
|
||||
// }
|
||||
|
||||
/* 测试延迟消息 */
|
||||
@RabbitListener(queues = "queue.test.delay")
|
||||
public void processMessageDelay(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
log.info("<延迟消息>----消息本身{}", dataString);
|
||||
log.info("<延迟消息>----当前时间{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
||||
}
|
||||
// /* 测试延迟消息 */
|
||||
// @RabbitListener(queues = "queue.test.delay")
|
||||
// public void processMessageDelay(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
// log.info("<延迟消息>----消息本身{}", dataString);
|
||||
// log.info("<延迟消息>----当前时间{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
||||
// }
|
||||
//
|
||||
// /* 测试优先级队列 */
|
||||
// @RabbitListener(queues = "queue.test.priority")
|
||||
// public void processMessagePriority(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
// log.info("<<优先级队列>>----<priority>{}", dataString);
|
||||
// }
|
||||
|
||||
/* 测试优先级队列 */
|
||||
@RabbitListener(queues = "queue.test.priority")
|
||||
|
||||
/* 测试联邦队列消息 */
|
||||
@RabbitListener(queues = "queue.normal.rabbitmq2")
|
||||
public void processMessagePriority(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||
log.info("<<优先级队列>>----<priority>{}", dataString);
|
||||
log.info("<<测试联邦队列消息>>----<priority>{}", dataString);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -3,8 +3,14 @@ server:
|
|||
|
||||
bunny:
|
||||
rabbitmq:
|
||||
host: 192.168.3.144
|
||||
port: 5672
|
||||
# # 集群环境
|
||||
# host: 192.168.3.144
|
||||
# port: 22222 # 集群环境的端口号
|
||||
|
||||
# 联邦队列---需要下游的
|
||||
host: 192.168.3.148
|
||||
port: 5673
|
||||
|
||||
virtual-host: /
|
||||
username: admin
|
||||
password: admin
|
||||
|
|
|
@ -0,0 +1,480 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<title>优雅的Bootstrap弹窗表单</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #4e73df;
|
||||
--secondary-color: #2e59d9;
|
||||
--success-color: #1cc88a;
|
||||
--danger-color: #e74a3b;
|
||||
--light-color: #f8f9fc;
|
||||
--dark-color: #5a5c69;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: #f8f9fc;
|
||||
}
|
||||
|
||||
/* 自定义弹窗样式 */
|
||||
.custom-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1050;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.custom-modal-dialog {
|
||||
max-width: 500px;
|
||||
margin: 1.75rem auto;
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
.custom-modal-content {
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.custom-modal-header {
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.custom-modal-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.btn-close-custom {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
font-size: 1.5rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.btn-close-custom:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.custom-modal-body {
|
||||
padding: 1.5rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.custom-modal-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
background-color: var(--light-color);
|
||||
border-top: 1px solid #e3e6f0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-control-custom {
|
||||
border: 1px solid #d1d3e2;
|
||||
border-radius: 0.35rem;
|
||||
padding: 0.75rem 1rem;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.form-control-custom:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(78, 115, 223, 0.25);
|
||||
}
|
||||
|
||||
/* 验证样式 */
|
||||
.is-invalid {
|
||||
border-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.is-valid {
|
||||
border-color: var(--success-color);
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: var(--danger-color);
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.valid-feedback {
|
||||
color: var(--success-color);
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn-primary-custom {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary-custom:hover {
|
||||
background-color: var(--secondary-color);
|
||||
border-color: var(--secondary-color);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-outline-secondary-custom {
|
||||
border-color: #d1d3e2;
|
||||
color: var(--dark-color);
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-outline-secondary-custom:hover {
|
||||
background-color: #f8f9fc;
|
||||
border-color: #d1d3e2;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
transform: translateY(-50px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-show {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
.modal-show .custom-modal-dialog {
|
||||
animation: slideInDown 0.3s;
|
||||
}
|
||||
|
||||
/* 浮动标签效果 */
|
||||
.form-floating-custom {
|
||||
position: relative;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-floating-custom label {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
left: 1rem;
|
||||
color: #6e707e;
|
||||
transition: all 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-floating-custom input:focus ~ label,
|
||||
.form-floating-custom input:not(:placeholder-shown) ~ label {
|
||||
top: -0.75rem;
|
||||
left: 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
background-color: white;
|
||||
padding: 0 0.25rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 密码强度指示器 */
|
||||
.password-strength {
|
||||
height: 4px;
|
||||
background-color: #e3e6f0;
|
||||
margin-top: 0.5rem;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.password-strength-bar {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
transition: width 0.3s ease, background-color 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-8 text-center">
|
||||
<h1 class="display-4 mb-4">优雅的Bootstrap弹窗示例</h1>
|
||||
<p class="lead mb-5">点击下方按钮查看带有表单验证的现代化弹窗</p>
|
||||
<button class="btn btn-primary btn-lg px-4 py-2" id="openModalBtn">
|
||||
<i class="bi bi-box-arrow-in-down-right me-2"></i>打开注册弹窗
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自定义弹窗 -->
|
||||
<div class="custom-modal" id="customModal">
|
||||
<div class="custom-modal-dialog">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">
|
||||
<i class="bi bi-person-plus-fill me-2"></i>用户注册
|
||||
</h5>
|
||||
<button class="btn-close-custom" id="closeModalBtn" type="button">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<form id="registrationForm" novalidate>
|
||||
<div class="form-floating-custom mb-4">
|
||||
<input class="form-control form-control-custom" id="username" placeholder="用户名" required
|
||||
type="text">
|
||||
<label for="username">用户名</label>
|
||||
<div class="invalid-feedback">请输入有效的用户名(4-20个字符)</div>
|
||||
<div class="valid-feedback">用户名可用</div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating-custom mb-4">
|
||||
<input class="form-control form-control-custom" id="email" placeholder="电子邮箱" required
|
||||
type="email">
|
||||
<label for="email">电子邮箱</label>
|
||||
<div class="invalid-feedback">请输入有效的电子邮箱地址</div>
|
||||
<div class="valid-feedback">邮箱格式正确</div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating-custom mb-4">
|
||||
<input class="form-control form-control-custom" id="password" minlength="8" placeholder="密码"
|
||||
required type="password">
|
||||
<label for="password">密码</label>
|
||||
<div class="invalid-feedback">密码至少需要8个字符</div>
|
||||
<div class="password-strength">
|
||||
<div class="password-strength-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-floating-custom mb-4">
|
||||
<input class="form-control form-control-custom" id="confirmPassword" placeholder="确认密码"
|
||||
required type="password">
|
||||
<label for="confirmPassword">确认密码</label>
|
||||
<div class="invalid-feedback">密码不匹配</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" id="terms" required type="checkbox">
|
||||
<label class="form-check-label" for="terms">
|
||||
我已阅读并同意 <a class="text-primary" href="#">服务条款</a> 和 <a class="text-primary"
|
||||
href="#">隐私政策</a>
|
||||
</label>
|
||||
<div class="invalid-feedback">必须同意条款才能继续</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button class="btn btn-outline-secondary-custom me-2" id="cancelBtn" type="button">
|
||||
取消
|
||||
</button>
|
||||
<button class="btn btn-primary-custom" id="submitBtn" type="button">
|
||||
<i class="bi bi-check-lg me-1"></i> 提交注册
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 弹窗控制
|
||||
const modal = document.getElementById('customModal');
|
||||
const openModalBtn = document.getElementById('openModalBtn');
|
||||
const closeModalBtn = document.getElementById('closeModalBtn');
|
||||
const cancelBtn = document.getElementById('cancelBtn');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const form = document.getElementById('registrationForm');
|
||||
|
||||
// 打开弹窗
|
||||
openModalBtn.addEventListener('click', () => {
|
||||
modal.classList.add('modal-show');
|
||||
document.body.style.overflow = 'hidden';
|
||||
});
|
||||
|
||||
// 关闭弹窗
|
||||
function closeModal() {
|
||||
modal.classList.remove('modal-show');
|
||||
document.body.style.overflow = 'auto';
|
||||
form.reset();
|
||||
resetValidation();
|
||||
}
|
||||
|
||||
closeModalBtn.addEventListener('click', closeModal);
|
||||
cancelBtn.addEventListener('click', closeModal);
|
||||
|
||||
// 点击弹窗外部关闭
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 表单验证
|
||||
function resetValidation() {
|
||||
const inputs = form.querySelectorAll('input');
|
||||
inputs.forEach(input => {
|
||||
input.classList.remove('is-invalid', 'is-valid');
|
||||
});
|
||||
}
|
||||
|
||||
// 实时验证
|
||||
form.addEventListener('input', (e) => {
|
||||
const input = e.target;
|
||||
validateInput(input);
|
||||
|
||||
// 密码强度检测
|
||||
if (input.id === 'password') {
|
||||
updatePasswordStrength(input.value);
|
||||
}
|
||||
|
||||
// 确认密码验证
|
||||
if (input.id === 'confirmPassword' || input.id === 'password') {
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (confirmPassword && password !== confirmPassword) {
|
||||
document.getElementById('confirmPassword').classList.add('is-invalid');
|
||||
document.getElementById('confirmPassword').classList.remove('is-valid');
|
||||
} else if (confirmPassword) {
|
||||
document.getElementById('confirmPassword').classList.add('is-valid');
|
||||
document.getElementById('confirmPassword').classList.remove('is-invalid');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function validateInput(input) {
|
||||
if (input.id === 'username') {
|
||||
const isValid = input.value.length >= 4 && input.value.length <= 20;
|
||||
updateValidationState(input, isValid);
|
||||
} else if (input.id === 'email') {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
const isValid = emailRegex.test(input.value);
|
||||
updateValidationState(input, isValid);
|
||||
} else if (input.id === 'password') {
|
||||
const isValid = input.value.length >= 8;
|
||||
updateValidationState(input, isValid);
|
||||
} else if (input.id === 'terms') {
|
||||
updateValidationState(input, input.checked);
|
||||
}
|
||||
}
|
||||
|
||||
function updateValidationState(input, isValid) {
|
||||
if (input.value === '' && input.required) {
|
||||
input.classList.remove('is-invalid', 'is-valid');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
input.classList.add('is-valid');
|
||||
input.classList.remove('is-invalid');
|
||||
} else {
|
||||
input.classList.add('is-invalid');
|
||||
input.classList.remove('is-valid');
|
||||
}
|
||||
}
|
||||
|
||||
// 密码强度检测
|
||||
function updatePasswordStrength(password) {
|
||||
const strengthBar = document.querySelector('.password-strength-bar');
|
||||
let strength = 0;
|
||||
|
||||
// 长度检测
|
||||
if (password.length >= 8) strength += 20;
|
||||
if (password.length >= 12) strength += 20;
|
||||
|
||||
// 复杂度检测
|
||||
if (/[A-Z]/.test(password)) strength += 20;
|
||||
if (/[0-9]/.test(password)) strength += 20;
|
||||
if (/[^A-Za-z0-9]/.test(password)) strength += 20;
|
||||
|
||||
// 更新UI
|
||||
strengthBar.style.width = `${strength}%`;
|
||||
|
||||
if (strength < 40) {
|
||||
strengthBar.style.backgroundColor = '#e74a3b'; // 红色
|
||||
} else if (strength < 80) {
|
||||
strengthBar.style.backgroundColor = '#f6c23e'; // 黄色
|
||||
} else {
|
||||
strengthBar.style.backgroundColor = '#1cc88a'; // 绿色
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
submitBtn.addEventListener('click', () => {
|
||||
let isValid = true;
|
||||
const inputs = form.querySelectorAll('input[required]');
|
||||
|
||||
inputs.forEach(input => {
|
||||
validateInput(input);
|
||||
if (input.classList.contains('is-invalid')) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 检查密码匹配
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
document.getElementById('confirmPassword').classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
// 模拟提交
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span> 提交中...';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
setTimeout(() => {
|
||||
alert('注册成功!');
|
||||
closeModal();
|
||||
submitBtn.innerHTML = '<i class="bi bi-check-lg me-1"></i> 提交注册';
|
||||
submitBtn.disabled = false;
|
||||
}, 1500);
|
||||
} else {
|
||||
// 滚动到第一个错误
|
||||
const firstInvalid = form.querySelector('.is-invalid');
|
||||
if (firstInvalid) {
|
||||
firstInvalid.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,34 @@
|
|||
package cn.bunny.mq.mqdemo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
public class ClusterEnvironmentTest {
|
||||
|
||||
@Autowired
|
||||
private RabbitTemplate rabbitTemplate;
|
||||
|
||||
/* 集群环境下消息 */
|
||||
@Test
|
||||
void clusterTest() {
|
||||
for (int i = 0; i <= 10; i++) {
|
||||
rabbitTemplate.convertAndSend("exchange.cluster.test",
|
||||
"routing.key.cluster.test",
|
||||
"集群环境下消息-" + i);
|
||||
}
|
||||
}
|
||||
|
||||
/* 仲裁队列发送消息 */
|
||||
@Test
|
||||
void clusterQuorumTest2() {
|
||||
final String EXCHANGE = "exchange.quorum.test";
|
||||
final String ROUTING_KEY = "routing.key.quorum.test";
|
||||
|
||||
for (int i = 0; i <= 10; i++) {
|
||||
rabbitTemplate.convertAndSend(EXCHANGE, ROUTING_KEY, "仲裁队列发送消息-" + i);
|
||||
}
|
||||
}
|
||||
}
|