✨ 集群环境-负载均衡完成;
This commit is contained in:
parent
f1e7394450
commit
6fcf7a4b75
|
@ -615,6 +615,8 @@ void buildExchangeOverflowTest() {
|
||||||
>
|
>
|
||||||
> - 最大延迟时间:**2天(48小时)**
|
> - 最大延迟时间:**2天(48小时)**
|
||||||
> - 必须匹配RabbitMQ版本
|
> - 必须匹配RabbitMQ版本
|
||||||
|
>
|
||||||
|
> **插件版本必须与RabbitMQ严格匹配(如3.13.x需使用v3.13.x插件)。**
|
||||||
|
|
||||||
#### 1、确认docker数据卷
|
#### 1、确认docker数据卷
|
||||||
|
|
||||||
|
@ -746,8 +748,8 @@ try {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### **2. 生产环境推荐方案**
|
### 生产环境推荐方案
|
||||||
#### **(1)Confirm 模式(轻量级确认)**
|
#### (1)Confirm 模式(轻量级确认)
|
||||||
- **原理**:异步确认消息是否成功到达 Broker。
|
- **原理**:异步确认消息是否成功到达 Broker。
|
||||||
- **配置方式**:
|
- **配置方式**:
|
||||||
```java
|
```java
|
||||||
|
@ -763,14 +765,14 @@ try {
|
||||||
```
|
```
|
||||||
- **优点**:性能接近非事务模式,可靠性高。
|
- **优点**:性能接近非事务模式,可靠性高。
|
||||||
|
|
||||||
#### **(2)消息补偿 + 幂等设计**
|
#### (2)消息补偿 + 幂等设计
|
||||||
- **步骤**:
|
- **步骤**:
|
||||||
1. 消息表记录发送状态(如 `status: sending/success/fail`)。
|
1. 消息表记录发送状态(如 `status: sending/success/fail`)。
|
||||||
2. 定时任务补偿失败消息。
|
2. 定时任务补偿失败消息。
|
||||||
3. 消费者端做幂等处理(如唯一 ID + 去重表)。
|
3. 消费者端做幂等处理(如唯一 ID + 去重表)。
|
||||||
- **适用场景**:订单支付、库存扣减等关键业务。
|
- **适用场景**:订单支付、库存扣减等关键业务。
|
||||||
|
|
||||||
#### **(3)本地消息表(最终一致性)**
|
#### (3)本地消息表(最终一致性)
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant App
|
participant App
|
||||||
|
@ -852,6 +854,8 @@ channel.queueDeclare("myLazyQueue", true, false, false, args);
|
||||||
|
|
||||||
## 优先级队列
|
## 优先级队列
|
||||||
|
|
||||||
|
**优先级仅在消息堆积时生效(空队列时无意义)**
|
||||||
|
|
||||||
**优先级范围**
|
**优先级范围**
|
||||||
|
|
||||||
- 通过 `x-max-priority` 参数定义队列支持的最大优先级(默认0=无优先级)
|
- 通过 `x-max-priority` 参数定义队列支持的最大优先级(默认0=无优先级)
|
||||||
|
@ -949,6 +953,8 @@ public void processMessagePriority(String dataString, Channel channel, Message m
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
>
|
>
|
||||||
> 如果需要修改,记得修改网关和IP地址。
|
> 如果需要修改,记得修改网关和IP地址。
|
||||||
|
>
|
||||||
|
> 节点间端口要求(如4369/25672需互通),避免防火墙问题.。
|
||||||
|
|
||||||
看自己是否有这个需求。
|
看自己是否有这个需求。
|
||||||
|
|
||||||
|
@ -1014,7 +1020,6 @@ sudo systemctl daemon-reload && sudo systemctl restart docker
|
||||||
> # 3. 重建目录并设置权限
|
> # 3. 重建目录并设置权限
|
||||||
> mkdir -p ~/docker/docker_data/rabbitmq/{rabbit1,rabbit2,rabbit3}/{data,conf,log}
|
> mkdir -p ~/docker/docker_data/rabbitmq/{rabbit1,rabbit2,rabbit3}/{data,conf,log}
|
||||||
> sudo chown -R 999:999 ~/docker/docker_data/rabbitmq
|
> sudo chown -R 999:999 ~/docker/docker_data/rabbitmq
|
||||||
> sudo chmod -R 775 ~/docker/docker_data/rabbitmq
|
|
||||||
>
|
>
|
||||||
> # 4. 重新启动
|
> # 4. 重新启动
|
||||||
> docker compose up -d
|
> docker compose up -d
|
||||||
|
@ -1253,6 +1258,14 @@ sudo vim /etc/hosts
|
||||||
|
|
||||||
#### 3、设置Erlang Cookie
|
#### 3、设置Erlang Cookie
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
>
|
||||||
|
> 普通方式搭建集群时,`.erlang.cookie`的权限必须为`400`
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> sudo chmod 400 /var/lib/rabbitmq/.erlang.cookie
|
||||||
|
> ```
|
||||||
|
|
||||||
RabbitMQ节点使用Erlang cookie进行认证,所有节点必须使用相同的cookie。
|
RabbitMQ节点使用Erlang cookie进行认证,所有节点必须使用相同的cookie。
|
||||||
|
|
||||||
如果是克隆的虚拟机,可以不检查(最好还是检查下),因为克隆的基本上是一样的。
|
如果是克隆的虚拟机,可以不检查(最好还是检查下),因为克隆的基本上是一样的。
|
||||||
|
@ -1270,8 +1283,8 @@ sudo systemctl stop rabbitmq-server
|
||||||
sudo nano /var/lib/rabbitmq/.erlang.cookie
|
sudo nano /var/lib/rabbitmq/.erlang.cookie
|
||||||
|
|
||||||
# 粘贴主节点的cookie内容
|
# 粘贴主节点的cookie内容
|
||||||
# 然后设置正确的权限
|
|
||||||
sudo chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
|
sudo chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
|
||||||
|
# 然后设置正确的权限
|
||||||
sudo chmod 400 /var/lib/rabbitmq/.erlang.cookie
|
sudo chmod 400 /var/lib/rabbitmq/.erlang.cookie
|
||||||
|
|
||||||
# 重新启动RabbitMQ服务
|
# 重新启动RabbitMQ服务
|
||||||
|
@ -1353,6 +1366,14 @@ sudo rabbitmqctl status
|
||||||
|
|
||||||
- 如果需要Web管理界面:
|
- 如果需要Web管理界面:
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
>
|
||||||
|
> 如果使用Web界面,需要在每个集群上都执行一下,否则情况会出现下面情况,举个例子。
|
||||||
|
>
|
||||||
|
> 如果在主节点上执行,那么主节点可以访问,之后输入账户密码登录之后可以看到主节点的情况,但是其余节点会出现【Node statistics not available】。
|
||||||
|
>
|
||||||
|
> 为了避免这种情况,要在每个节点上都执行一下下面的命令。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo rabbitmq-plugins enable rabbitmq_management
|
sudo rabbitmq-plugins enable rabbitmq_management
|
||||||
```
|
```
|
||||||
|
@ -1470,26 +1491,26 @@ sudo cat /var/lib/rabbitmq/.erlang.cookie
|
||||||
|
|
||||||
1. 停止 RabbitMQ:
|
1. 停止 RabbitMQ:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
sudo systemctl stop rabbitmq-server
|
sudo systemctl stop rabbitmq-server
|
||||||
```
|
```
|
||||||
|
|
||||||
2. 修改 Cookie(复制主节点的 Cookie 到其他节点):
|
2. 修改 Cookie(复制主节点的 Cookie 到其他节点):
|
||||||
|
|
||||||
```
|
```bash
|
||||||
echo "YOUR_MASTER_NODE_COOKIE" | sudo tee /var/lib/rabbitmq/.erlang.cookie
|
echo "YOUR_MASTER_NODE_COOKIE" | sudo tee /var/lib/rabbitmq/.erlang.cookie
|
||||||
```
|
```
|
||||||
|
|
||||||
3. 设置权限:
|
3. 设置权限:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
sudo chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
|
sudo chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie
|
||||||
sudo chmod 400 /var/lib/rabbitmq/.erlang.cookie
|
sudo chmod 400 /var/lib/rabbitmq/.erlang.cookie
|
||||||
```
|
```
|
||||||
|
|
||||||
4. 重启 RabbitMQ:
|
4. 重启 RabbitMQ:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
sudo systemctl start rabbitmq-server
|
sudo systemctl start rabbitmq-server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1501,14 +1522,14 @@ sudo cat /var/lib/rabbitmq/.erlang.cookie
|
||||||
|
|
||||||
确保用户在所有节点都有 **管理员权限**:
|
确保用户在所有节点都有 **管理员权限**:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
sudo rabbitmqctl set_user_tags <username> administrator
|
sudo rabbitmqctl set_user_tags <username> administrator
|
||||||
sudo rabbitmqctl set_permissions -p / <username> ".*" ".*" ".*"
|
sudo rabbitmqctl set_permissions -p / <username> ".*" ".*" ".*"
|
||||||
```
|
```
|
||||||
|
|
||||||
例如:
|
例如:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
sudo rabbitmqctl set_user_tags admin administrator
|
sudo rabbitmqctl set_user_tags admin administrator
|
||||||
sudo rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
|
sudo rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
|
||||||
```
|
```
|
||||||
|
@ -1516,3 +1537,111 @@ 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
|
||||||
|
|
||||||
|

|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.2 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,25 @@
|
||||||
|
package cn.bunny.mq.mqdemo.mq.listener;
|
||||||
|
|
||||||
|
import com.rabbitmq.client.Channel;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.amqp.core.Message;
|
||||||
|
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||||
|
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class ClusterEnvironmentListener {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RabbitTemplate rabbitTemplate;
|
||||||
|
|
||||||
|
/* 测试优先级队列 */
|
||||||
|
@RabbitListener(queues = "queue.cluster.test")
|
||||||
|
public void processMessagePriority(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||||
|
log.info("<<集群环境下消息>>----<cluster>{}", dataString);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,8 @@
|
||||||
package cn.bunny.mq.mqdemo.mq.listener;
|
package cn.bunny.mq.mqdemo.mq.listener;
|
||||||
|
|
||||||
import com.rabbitmq.client.Channel;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.amqp.core.Message;
|
|
||||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class MessageListenerOrder {
|
public class MessageListenerOrder {
|
||||||
|
@ -81,17 +74,17 @@ public class MessageListenerOrder {
|
||||||
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
|
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
/* 测试延迟消息 */
|
// /* 测试延迟消息 */
|
||||||
@RabbitListener(queues = "queue.test.delay")
|
// @RabbitListener(queues = "queue.test.delay")
|
||||||
public void processMessageDelay(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
// public void processMessageDelay(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||||
log.info("<延迟消息>----消息本身{}", dataString);
|
// log.info("<延迟消息>----消息本身{}", dataString);
|
||||||
log.info("<延迟消息>----当前时间{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
// log.info("<延迟消息>----当前时间{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
/* 测试优先级队列 */
|
// /* 测试优先级队列 */
|
||||||
@RabbitListener(queues = "queue.test.priority")
|
// @RabbitListener(queues = "queue.test.priority")
|
||||||
public void processMessagePriority(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
// public void processMessagePriority(String dataString, Channel channel, Message message) throws IOException, InterruptedException {
|
||||||
log.info("<<优先级队列>>----<priority>{}", dataString);
|
// log.info("<<优先级队列>>----<priority>{}", dataString);
|
||||||
}
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,9 @@ server:
|
||||||
bunny:
|
bunny:
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
host: 192.168.3.144
|
host: 192.168.3.144
|
||||||
port: 5672
|
# port: 5672
|
||||||
|
# 集群环境的端口号
|
||||||
|
port: 22222
|
||||||
virtual-host: /
|
virtual-host: /
|
||||||
username: admin
|
username: admin
|
||||||
password: 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,23 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue