1项目启动

1.1 配置文件
1.1.1 添加RabbitMQ的用户
windows系统
打开RabbitMQ的sbin目录的cmd终端,执行以下命令,添加账号密码(均为admin),并赋予最高权限
cpp
## 添加账号
rabbitmqctl add_user admin admin
## 添加访问权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
## 设置超级权限
rabbitmqctl set_user_tags admin administrator
1.1.2 pom.xml依赖引入
cpp
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.7.1</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--RabbitMQ-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.5.1</version>
</dependency>
<!-- SLF4J -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<!--以下为基础依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
1.1.3 application.yml
cpp
rabbitmq:
host: 127.0.0.1 #主机IP地址
port: 5672 #端口号(注意15672为UI管理台页面)
username: admin #用户名
passport: admin #密码
virtualhost: / #虚拟主机(我是用的根主机)
pool_size: 5
1.1.4 配置类读取
cpp
@Setter
@Getter
@ConfigurationProperties(prefix = "rabbitmq")
@Component
public class RabbitmqProperties {
/**
* 主机
*/
private String host;
/**
* 端口
*/
private Integer port;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String passport;
/**
* 路径
*/
private String virtualhost;
/**
* 连接池大小
*/
private Integer poolSize;
}
1.2 RabbitMQ连接池
RabbitmqConnection 类
java
public class RabbitmqConnection {
private Connection connection;
public RabbitmqConnection(String host, int port, String userName, String password, String virtualhost) {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(host);
connectionFactory.setPort(port);
connectionFactory.setUsername(userName);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost(virtualhost);
try {
connection = connectionFactory.newConnection();
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
/**
* 获取链接
*
* @return
*/
public Connection getConnection() {
return connection;
}
/**
* 关闭链接
*
*/
public void close() {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
RabbitmqConnectionPool 类
java
public class RabbitmqConnectionPool {
private static BlockingQueue<RabbitmqConnection> pool;
public static void initRabbitmqConnectionPool(String host, int port, String userName, String password,
String virtualhost,
Integer poolSize) {
pool = new LinkedBlockingQueue<>(poolSize);
for (int i = 0; i < poolSize; i++) {
pool.add(new RabbitmqConnection(host, port, userName, password, virtualhost));
}
}
public static RabbitmqConnection getConnection() throws InterruptedException {
return pool.take();
}
public static void returnConnection(RabbitmqConnection connection) {
pool.add(connection);
}
public static void close() {
pool.forEach(RabbitmqConnection::close);
}
}
1.3 核心代码
RabbitmqService 接口
java
public interface RabbitmqService {
void publishMsg(String exchange,
BuiltinExchangeType exchangeType,
String toutingKey,
String message);
void consumerMsg(String exchange,
String queue,
String routingKey) throws IOException, TimeoutException;
}
RabbitmqServiceImpl 实现类
核心代码(发送消息逻辑)(接收消息逻辑)
java
@Slf4j
@Service
public class RabbitmqServiceImpl implements RabbitmqService {
/**
* 向RabbitMQ发送消息
* @param exchange:交换机名称
* @param exchangeType:交换机类型
* DIRECT直接交换机(适合一对一通知),
* FANOUT扇形交换机(广播模式),
* TOPIC主题交换机(支持通配符匹配路由键),
* HEADERS头交换机(根据header内容匹配)
* @param toutingKey:路由键,决定消息发往哪个队列
* @param message:消息体内容
*/
@Override
public void publishMsg(String exchange,
BuiltinExchangeType exchangeType,
String toutingKey,
String message) {
try {
//创建连接(快递车出发)
RabbitmqConnection rabbitmqConnection = RabbitmqConnectionPool.getConnection();
Connection connection = rabbitmqConnection.getConnection();
//创建消息通道(确定快递的运输路线)
Channel channel = connection.createChannel();
// 声明exchange中的消息为可持久化,不自动删除(设置快递分拣规则)
/**
* exchange:声明的交换机名称
* BuiltinExchangeType.DIRECT:指定交换机类型为DIRECT
* boolean durable:是否持久化,true表示持久化,false表示非持久化
* boolean deleteWhenUnused:是否自动删除,true表示没有队列绑定到交换机时自动删除,false表示不自动删除
* Map<String, Object> arguments:其他参数
*/
channel.exchangeDeclare(exchange, exchangeType, true, false, null);
// 发布消息(把包裹交给分拣中心)
/**
* exchange:交换机名称
* toutingKey:消息的路由键
* BasicProperties:消息属性
* body:消息内容
*/
channel.basicPublish(exchange, toutingKey, null, message.getBytes());
log.info("Publish msg: {}", message);
//快递车返回公司
channel.close();
//车辆入库
RabbitmqConnectionPool.returnConnection(rabbitmqConnection);
} catch (InterruptedException | IOException | TimeoutException e) {
log.error("rabbitMq消息发送异常: exchange: {}, msg: {}", exchange, message, e);
}
}
/**
* 消费消息
* @param exchange : 交换机名称
* @param queueName : 队列名称
* @param routingKey : 路由键
*/
@Override
public void consumerMsg(String exchange,
String queueName,
String routingKey) {
try {
//创建连接
RabbitmqConnection rabbitmqConnection = RabbitmqConnectionPool.getConnection();
Connection connection = rabbitmqConnection.getConnection();
//创建消息信道
final Channel channel = connection.createChannel();
//消息队列(声明了一个持久化队列,即使RabbitMQ重启也不会丢失数据)
//channel.exchangeDeclare(exchange, BuiltinExchangeType.DIRECT, true, false, null);
/**
* 这里属于重复交换机声明,否则没有时会报错,第一次执行后,去掉也不会报错
* queue:队列名称
* boolean durable:是否持久化,true表示持久化,false表示非持久化
* boolean exclusive:是否独占队列,true表示独占队列,false表示非独占队列
* boolean autoDelete:是否自动删除,true表示自动删除,false表示不自动删除
* Map<String, Object> arguments:其他参数
*/
channel.queueDeclare(queueName, true, false, false, null);
//绑定队列到交换机
/**
* queueName:队列名称
* exchange:交换机名称
* routingKey:队列的路由键
*/
channel.queueBind(queueName, exchange, routingKey);
/**
* 创建消费者
* channel:消息通道
*/
Consumer consumer = new DefaultConsumer(channel) {
/**
* 当收到消息时,触发的方法
* @param consumerTag:消费者标签(唯一标识)
* @param envelope:消息的元信息
* @param properties:消息属性
* @param body:实际的消息体
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
String message = new String(body, "UTF-8");
log.info("Consumer msg: {}", message);
// 获取Rabbitmq消息,并保存到DB
// 说明:这里仅作为示例,如果有多种类型的消息,可以根据消息判定,简单的用 if...else 处理,复杂的用工厂 + 策略模式
log.error("正在消费message: {}", message);
//手动确认消息,告诉RabbitMQ这条消息我已经成功处理了,请从队列中删除
/**
* deliveryTag:每条消息的唯一标识
* multiple:是否批量确认,true表示批量确认,false表示单个确认
*/
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 取消自动ack
/**
* 监听队列,该方法内部使用RabbitMQ内部的线程池异步执行,不会阻塞后续消息
* queue:拿取消息的队列名称
* autoAck:是否自动ack,true表示自动ack,false表示手动ack
* consumer:消费者
*/
channel.basicConsume(queueName, false, consumer);
} catch (InterruptedException | IOException e) {
e.printStackTrace();
}
}
}
1.4 消费逻辑的入口代码
负责初始化连接池 ,初始化消费逻辑的入口
java
@Slf4j
@Configuration
@EnableConfigurationProperties(RabbitmqProperties.class)
public class RabbitMqAutoConfig implements ApplicationRunner {
@Resource
private RabbitmqService rabbitmqService;
@Autowired
private RabbitmqProperties rabbitmqProperties;
@Override
public void run(ApplicationArguments args) throws Exception {
String host = rabbitmqProperties.getHost();
Integer port = rabbitmqProperties.getPort();
String userName = rabbitmqProperties.getUsername();
String password = rabbitmqProperties.getPassport();
String virtualhost = rabbitmqProperties.getVirtualhost();
Integer poolSize = rabbitmqProperties.getPoolSize();
RabbitmqConnectionPool.initRabbitmqConnectionPool(host, port, userName, password, virtualhost, poolSize);
Thread thread=new Thread(() -> {
try {
log.info("手动线程开始:{}", Thread.currentThread().getName());
rabbitmqService.consumerMsg("direct.exchange", "quere.praise","praise");
log.info("手动线程结束:{}", Thread.currentThread().getName());
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
});
/**
*启动消费程序的入口
*/
thread.start();
}
}
1.5发送逻辑的入口代码
我这里是用了controller层,用浏览器控制消息的发送
java
@RestController
public class RabbitmqTest{
@Autowired
private RabbitmqService rabbitmqService;
@GetMapping("/test")
public void testProductRabbitmq(String msg) {
try {
rabbitmqService.publishMsg(
"direct.exchange",
BuiltinExchangeType.DIRECT,
"praise",
msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
1.6运行结果
项目启动后
在浏览器输入 http://localhost:8080/test?msg=看看项目运行了没
控制台输出如下:
1.7疑问解答
为什么控制台打印时有很多线程呢?
- 你在 run 方法中手动启动的 Thread,核心任务是初始化消费逻辑的 "入口";
- 手动线程调用 channel.basicConsume 后,客户端会与 RabbitMQ 建立 长连接,并在后台维护一个 "消息监听循环",这个手动线程的任务就结束了(它不会一直阻塞等待消息),真正的消息消费逻辑由 RabbitMQ 客户端的内部线程池处理。
- 当 RabbitMQ 有消息推送给消费者时,客户端不会在你手动启动的线程中处理,而是从 内部线程池 中取出一个空闲线程,执行 DefaultConsumer 的 handleDelivery 方法(即你的消费逻辑);
- 控制台日志中 pool-1-thread-4、pool-1-thread-5... 就是这个内部线程池的线程,每处理一条消息可能复用或新建线程(取决于线程池配置);
生产者和消费者是如何实现沟通的呢?
发送消息:拿到连接--->创建通道-->声明交换机-->发送消息-->关闭连接
消费消息:拿到连接-->创建通道-->声明队列-->绑定队列到交换机-->接收并消费消息
java
//生产者方法
①channel.exchangeDeclare(exchange, exchangeType, true, false, null);
②channel.queueDeclare(queueName, true, false, false, null);
//消费者方法
③channel.queueBind(queueName, exchange, routingKey);
④channel.basicPublish(exchange, toutingKey, null, message.getBytes());
⑤channel.basicConsume(queueName, false, consumer);
- 绑定队列到交换机(queueBind)时,指定的交换机名称相同,即为同一个,由方法①②③实现(③的queueName与②的queueName保持一致,③的exchange与①的exchange保持一致);
- 发布消息时,指定发往的交换机名称和路由键 ,由方法④实现(toutingKey);
绑定队列到交换机时,指定队列的路由键 ,由方法③实现(routingKey);
根据交换机的类型,若为direct 类型,必须routingKey 和toutingKey完全一致时才能收到消息; - 接收消息时,指定从哪个队列接收消息,由方法⑤实现(queueName);
消费者是怎么实现一直监听的呢?
首先 要知道,生产者 一发送完毕消息,立马执行channel.close();关闭通道
和RabbitmqConnectionPool.returnConnection(rabbitmqConnection);归还连接
而 消费者不关闭通道,也不归还连接,相当于是一个长连接,一直监听着队列内是否有消息,只要有消息就消费消息。
1.8 如何优化
1.8.1 重连机制
问题 :如果因为一些异常,导致消费方法终止,必须有重新启动机制
在consumerMsg方法中增加以下代码:
java
// 设置连接关闭回调
connection.addShutdownListener(cause -> {
log.warn("RabbitMQ连接意外关闭,准备重连: {}", cause.getMessage());
// 启动重连任务
new Thread(() -> {
try {
Thread.sleep(5000); // 延迟5秒重试
consumerMsg(exchange, queueName, routingKey); // 重新调用消费方法
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
});
1.8.2 异步处理消息
问题 :如果我们在消息处理中直接执行耗时的业务逻辑(如 DB 操作、远程调用),那么客户端的这个线程就会被阻塞,无法处理新的消息,导致整体消费速率下降。
解决方案:RabbitMQ 客户端的内部线程池负责 "接收消息",而我们自己添加的线程池负责 "处理消息"
java
//这里的线程池只是模拟演示
ExecutorService executor = Executors.newFixedThreadPool(10);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
executor.submit(() -> {
try {
// 业务逻辑处理
//...
//手动确认
channel.basicAck(...);
} catch (Exception e) {
//发生异常时,不确认,消息重新回到队列中,保证消息不丢失
channel.basicNack(...);
}
});
}
};
1.8.3 优雅的关闭
问题 :JVM 退出时会 "回收" 资源,但不会 "优雅关闭" 资源。
在consumeMsg方法中添加以下代码:
java
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
channel.close();
connection.close();
} catch (Exception e) {
// 处理关闭异常
}
}));