遇到的坑:
1.connection error; protocol method: #method(reply-code=530, reply-text=NOT_ALLOWED - access to vhost '/' refused for user 'root', class-id=10, method-id=40)
去页面设置

点击即可
2.在使用队列的时候,其中在使用publish/subscribe和Routing中,俩个消费者使用的队列都是一样的的时候,这样都是同一个队列,无论怎么样都是对这来个对立进行轮询操作没有什么效果!!!!!
RabbitMQ(免费!!!)
它是什么?
是一个第三方的软件,他兼容各种计算机语言(包括java),有各种语言的客户端,他是一个功能非常完整的队列(先进先出),主要有发送者,队列,消费者 三种角色
生产者:制造消息的角色(将消息放入队列)
队列:存放信息的的地方(就像一个邮箱)
消费者:去队列中拿消息的角色
它能干什么?
1.最明显的是它能够存储生产者制造出来的消息,可以让消费者到特定的队列去取(这个消息可以存储很久,不那么容易丢失,有各种对策)
2.可以做流量的消峰,一个时间段内,如果用户登录的太多了,可以做一定限制,让这些消息,一个个放入队列,然后一个一个慢慢的处理,(让他们再里面排队,一个一个处理)保证我们服务器的健壮性
3.可以做异步处理,比如俩个操作可以同步进行最常见的就是 数据库中生成下单的信息,向用户与服务商发送下单消息,都是可以同时进行的,我们将这个下单的信息存储到rabbitmq中,然后这俩个模块都可以接收到这个消息就可以同时做处理了。
4.有助于系统解耦,我们可以看出上面这些操作,获得数据都是从rabbitmq中获取的,并没有用到其他模块的类就可以执行;可以说俩个模块之间都不需要,对方做了什么,一个只需要知道发送小心进队列,一个是有消息了从队列里面拿出来干活就行
RabbitMQ使用中的一些操作
首先引入包
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.7.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.17.0</version>
</dependency>
第二,创建连接工具类
java
public class RabbitUtils {
public static Channel getRabbitMqChannel(){
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("root");
factory.setPassword("root");
Channel channel = null;
try {
Connection connection = factory.newConnection();
channel = connection.createChannel();
} catch (IOException | TimeoutException e) {
throw new RuntimeException(e);
}
return channel;
}
}
简单的发送队列hello word
作为生产者我们需要什么?
1.首先肯定是要发出去消息 (所以消息放在哪里呢?)
2.其次要有放的队列 channel.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments)
3.最后是发送消息channel.basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
介绍一下发送的参数:
我们先从channel.queueDeclare 开始
queue:队列的名称
durable:是否让这个队列永远存在
exclusive:只允许一个连接(消费者)独占队列
autoDelete:当没有消费者使用这条消息的时候直接自动删除
arguments:构造队列的时候设置的参数(队列属性)
发布消息channel.basicPublish
exchange:指定交换机的名字 这个消息发布到哪一个交换机
routingKey:路由key,可以根据key找到对应的路径
props:其他的路由的配置
body:发送的消息
生产者:
java
public class HelloWordProduce {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException {
Channel channel = RabbitUtils.getRabbitMqChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
消费者的一些参数设置
1.首先我们消费者肯定是也要声明一个队列的,queueDeclare()这个一定要和上面的生产者的一致,这样才能是同一条队列里面取东西。
2.我们收到消息是不是要获取,那么我们怎么获取呢?
使用basicConsume(String queue, boolean autoAck, DeliverCallback deliverCallback, CancelCallback cancelCallback)方法
queue:接收消息的队列的名称
autoAck:是否自动接收消息(自动应答,服务器自动应答)
deliverCallback: 接收到消息的方法,里面可以获取得到的数据
cancelCallback: 拒绝接收方法的操作
消费者
java
public class HelloWordConsumer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitUtils.getRabbitMqChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
}
}
工作队列分发任务Work Queue
其实就是上面的demo再多添加一个消费者即可
工作队列:rabbitmq默认就是会形象一个轮询的负载均衡操作,每个消费者轮着去队列里面的消息
使用场景(10/2):就好像你部署了多台的服务器,上面都用了rabbitmq,然后都用者一个队列,他们就可以做到负载均衡,根据情况按顺序的转发每一个队列(如果一个消息还没有处理完,时间太久了,可以给其他的消费者进行取,然后使用方法)
消息丢失?
这个时候我们应该考虑一个问题,有没有可能消息会丢失呢?
其实这就是rabbitmq这种专门做消息的软件,是肯定有这些预防措施的,这种一般都是在消费者端可以设置一个返回值,让队列知道,消息队列一旦收到了确认消息的提示,就会删除这条消息消息,如果很久都没有返回消息,那就会重新将消息入队;如果此时那个消费者宕机了,如果还有其他的对应队列的消费者存在就会发送到那个另外存活的消费者 ,如果只是你忘记在消费者端口标记确认收到消息,那么就会发生,消费者没有收到,然后其他队列也没有收到的情况,并且有一个超时时间30分钟,30分钟后会消息重新入队
那么这个参数又是什么呢?
这个参数就是在消费者这边确认消息时候的autoAck,我们之前都是将他设置成true,我们就会导致消息队列如果还没有处理完消息,就崩溃了,就会导致这条消息直接消失,但是我们设置成false就可以变为手动应答,就可以有效的阻止这个情况发送
如下
java
public static void main(String[] args) throws IOException {
Connection connection = RabbitUtils.getRabbitMqChannel();
Channel channel = connection.createChannel();
channel.queueDeclare(WORK_QUEUE_NAME, false, false, false, null);
channel.basicQos(1);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(" 接收的消息WORK0 '" + message + "'");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
channel.basicConsume(WORK_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
}
其中我们发现在我们设置成功手动应答之后,在接收消息的方法中多了一个方法
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); (如果这个忘记设置,那么会导致rabbitmq内存越吃越多,因为rabbitmq不能释放没有确认的消息)
这个就是设置手动应答,如果消息接收到了我们才进行确认消息,他参数如下
java
void basicAck(long deliveryTag, boolean multiple) throws IOException;
deliveryTag: 一个rabbitmq收到消息的一个标记
multiple:如果是true,那就是发送过来的消息,直接全部都确认(还是可能造成消息丢失,因为可能确认还没有真正确认的消息),false代表消息一条一条慢慢确认
还有一个在消费者端口要设置的每次都只接收一条消息 如果不需要限制一条一条收可以设置为0
ini
channel.basicQos(1);
接下来又有一个问题:如果服务器宕机了,那队列是不是就消失了?那我们该如何让这个队列也持久化呢?
我们可以看到我们队列创建的时候有一个参数 durable 他的作用就是让队列永远存在,如果设置为true就可以让他永远存在,就算重启也会存在这个队列,但是如果一个已经存在的队列,已经设置好了不是true,如果你要对这个队列进行修改为true那么这个将会报错(这是一个值得注意的事项)这个时候切记,设置队列的时候俩端(生产者,消费者)都要设置为true不然也会报错(注意事项二)
最后我们的消息会不会丢失呢?其实还是有可以能的
因为我们发送的消息,如果在发送的过程中到队列的时候,突然宕机了,那这个消息就会丢失,虽然队列持久化了,消费者那边有是否确认消息,但是消息他没有持久化啊,还是会丢失消息的,所以我们也需要将发送的消息设置为持久化这个时候我们需要设置发送的消息也为持久化!!!
这个怎么设置呢,肯定是要从发送消息那边设置的就是这个方法中channel.basicPublish()从上面我们可以看到它还有一个参数就是BasicProperties props这个可以做一些设置,需要写入MessageProperties.PERSISTENT_TEXT_PLAIN,就可以保证消息也持久化了
这里面设置了好了消息持久化如下
java
public static final BasicProperties PERSISTENT_TEXT_PLAIN =
new BasicProperties("text/plain",
null,
null,
2,
0, null, null, null,
null, null, null, null,
null, null);
其实就算都设置好了,还是有可能会丢失消息,当你设置了消息是持久化后,生产者写入消息会到磁盘中,但是期间还是有一个很短的时间窗口,如果这个时候宕机了也没有办法(当然rabbitmq不会把所有的消息都同步至硬盘,也有可能放入缓存中,虽然这个持久性保证并不是很强,但是一般的队列够用了)
Fair dispatch
其实消息队列本身不设置任何参数的话,会可能导致的结果就是消费者的资源分配的不是很合理(由于各种原因,服务器的处理性能等)最后造成的结果会是,一个服务器很忙,一个服务器会很闲;
例如一个处理很快的机器和一个处理很慢的机器,然后rabbitmq一致轮询发送,不管他与没有处理完,就会导致消息处理慢的那一方积压
因为 rabbitmq只是负责生产者发送消息的时候在消息进入队列的时候做调度,进去了就不管了
所以我们就要在消费者端口做一些配置
\^\]: 消费者端口要设置的每次都只接收一条消息 如果不需要限制一条一条收可以设置为0
```ini
channel.basicQos(1);
```
这样配置了之后rabbitmq就会在这个队列处理完消息之后才去再给他发送消息,那继续发送的消息会怎么样呢?**当然是给已经处理完任务的,闲着的服务器了**
## Publish/Subscribe
所谓发布订阅:就是规定消息,给哪一个消费者(有多个消费者)

### Exchanges
其实rabbitmq的核心思想就是生产者的消息不是直接就发送给队列中;消费者通常都不知道自己发给了那个队列
那么问题来了,生产者跟谁交互呢?给谁发消息呢?
答:交换机,交换机负责接收生产者的发送的信息,然后转发给相应的队列(到底是发送给精确的队列,还是全部队列,还是丢弃都是由它来决定)
其实在这之前我们都没有了解过什么是交换机,但是我们还是能够发送消息,那是因为系统本来就给我们准备了一个无名的交换机也就是之前发送消息的时候一直填的都是""无名的交换机,然后就根据队列的名字当做路由的key
```csharp
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
```
接下里该如何操作呢?
我们需要使用交换机那肯要创建一个交换机
方法如下
```arduino
channel.exchangeDeclare(?,?)
//源码
Exchange.DeclareOk exchangeDeclare(String exchange, String type) throws IOException;
```
可以看出来需要俩个参数,一个是交换机名字,一个是交换机类型
那么类型有哪些呢?
有如下几个类型,之后会慢慢介绍
direct, topic, headers and fanout
这次案例使用fanout(扇出)其实就跟广播一样
当让还没有结束,我们需要做一些绑定,**之前我们也说了,生产者是跟交换机产生关系,然后交换机在连接队列** 所以我们需要让交换机取绑定**队列**
使用如下方法
```arduino
channel.exchangeBind(?,?,?);
//源码
Exchange.BindOk exchangeBind(String destination, String source, String routingKey) throws IOException;
```
可以看到有这几个参数:
destination:目的地看名字就能知道,这一定是队列的名字
source:资源,那么就一定是交换机的名字了
routingKey:这个是找到一个队列中应该发给那个消费者(可能有很多的消费者绑定了这个队列)的一个标识
最后我们看一下代码
可以对比一下上面的代码我们消费者已经跟队列已经没多少关系了,已经删除了声明队列的操作,转而与交换机发送联系
如上面那句话一样:**生产者的消息不是直接就发送给队列中;消费者通常都不知道自己发给了那个队列**
```ini
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("root");
factory.setPassword("root");
// factory.setHandshakeTimeout(300000000);
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
//声明交换机
channel.exchangeDeclare(PUBLISH_EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
Scanner scanner = new Scanner(System.in);
String message = null;
while (scanner.hasNext()) {
message = scanner.nextLine();
Optional.ofNullable(message).orElse("kong shuju");
channel.basicPublish(PUBLISH_EXCHANGE_NAME, "", null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
} catch (TimeoutException | IOException e) {
throw new RuntimeException(e);
}
}
```
接下来就是消费者端了
由上面可以看出,这个生产者之和交换机发送关系,其他什么都不知道,可以看得出接下来的许多配置都会存在消费者,我们消费者肯定是需要这些
**队列** 肯定是必不可少的,那么队列是不是要和交换机产生关系?所以我们也需要一个跟生产者一样的**交换机** (声明),最后做**绑定**(如果没有队列绑定相对的交换机,消息自动丢弃)
最后如下
```ini
public static void main(String[] args) throws IOException {
Connection connection = RabbitUtils.getRabbitMqChannel();
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(PUBLISH_EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
channel.queueDeclare(PUBLISH_SUBSCRIBE_QUEUE_NAME, false, false, false, null);
//消费者队列与交换机做绑定根据路由key
channel.queueBind(PUBLISH_SUBSCRIBE_QUEUE_NAME,PUBLISH_EXCHANGE_NAME,"");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" 接收的消息WORK0 '" + message + "'");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
channel.basicConsume(PUBLISH_SUBSCRIBE_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
}
```
注意点:
1.虽然我们都绑定好了,别弄错了,其实最后并不是生产者发送消息,俩个都能同时收到,而是还是默认的轮询收到
2.你在fanout交换机中绑定的路由key是不生效的,你无乱写什么,这个交换机都会发给所有与他绑定的队列
## Routing

当我们需要一个消息发送给特定的队列的时候,我们就需要使用这个路由,首先我们要得到这个效果我们肯定不能使用fanout,这个时候我们可以使用Direct类型的交换机
我们可以设置
```arduino
channel.queueBind(PUBLISH_SUBSCRIBE_QUEUE_NAME,PUBLISH_EXCHANGE_NAME,"");
```
原本我们上次设置的是"",这次可以填写一些路由key,然后交换机就可以根据路由key来选择发送给那个队列;
那么问题来了,如果现在这个路由key设置为orange,那么我们生产者哪一方,怎么才能让他发送到对orange的这个队列呢?
答:我们设置为发送方为orange(路由key)
如下
```csharp
channel.basicPublish(PUBLISH_EXCHANGE_NAME, "", null, message.getBytes(StandardCharsets.UTF_8));
```
我们发送方第二个参数就发送路由可以,可以灵活的设置,现在我们可以设置成orange就可以了
如果你想要改成apple,那么接收方就一定要设置成apple这个路由key才能接收
生产者代码如下
```ini
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("root");
factory.setPassword("root");
// factory.setHandshakeTimeout(300000000);
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
//声明交换机
channel.exchangeDeclare(DIRECT_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
Scanner scanner = new Scanner(System.in);
String message = null;
while (scanner.hasNext()) {
message = scanner.nextLine();
Optional.ofNullable(message).orElse("kong shuju");
channel.basicPublish(DIRECT_EXCHANGE_NAME, "orange", null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
} catch (TimeoutException | IOException e) {
throw new RuntimeException(e);
}
}
```
消费者1代码如下
```java
public static void main(String[] args) throws IOException {
Connection connection = RabbitUtils.getRabbitMqChannel();
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(DIRECT_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.queueDeclare(PUBLISH_SUBSCRIBE_QUEUE_NAME, false, false, false, null);
//消费者队列与交换机做绑定根据路由key
channel.queueBind(PUBLISH_SUBSCRIBE_QUEUE_NAME,DIRECT_EXCHANGE_NAME,"orange");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" orange ----> '" + message + "'");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
channel.basicConsume(PUBLISH_SUBSCRIBE_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
}
```
消费者2代码如下
```java
public static void main(String[] args) throws IOException {
Connection connection = RabbitUtils.getRabbitMqChannel();
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(DIRECT_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.queueDeclare(PUBLISH_SUBSCRIBE_QUEUE_NAME2, false, false, false, null);
//绑定交换机
channel.queueBind(PUBLISH_SUBSCRIBE_QUEUE_NAME2,DIRECT_EXCHANGE_NAME,"apple");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" apple -----> '" + message + "'");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
channel.basicConsume(PUBLISH_SUBSCRIBE_QUEUE_NAME2, false, deliverCallback, consumerTag -> { });
}
```
其实我们可以俩个队列都绑定同一个key,这样其实也就是一个扇出交换机的执行一样,都会分发到
## Topic

主题消息队列:就是可以指定路由key匹配的时候用\*或者#来进行匹配(并且只能是单词加上 . 的多个单词组合)
* **:只能代替一个单词**
**#:可以代替多个或者0个单词**
如果你就设置一个word作为你的routingkey那么这个Topic就会跟Direct的Exchange一样
如果就设置了一个#的话功能就像fanout Exchange一样
生产者
```ini
public static void main(String[] args) {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("root");
factory.setPassword("root");
// factory.setHandshakeTimeout(300000000);
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
//声明交换机
channel.exchangeDeclare(TOPIC_EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
Scanner scanner = new Scanner(System.in);
String message = null;
while (scanner.hasNext()) {
message = scanner.nextLine();
Optional.ofNullable(message).orElse("kong shuju");
channel.basicPublish(TOPIC_EXCHANGE_NAME, "A.orange", null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
} catch (TimeoutException | IOException e) {
throw new RuntimeException(e);
}
}
```
消费者
```java
public static void main(String[] args) throws IOException {
Connection connection = RabbitUtils.getRabbitMqChannel();
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(TOPIC_EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
channel.queueDeclare(PUBLISH_SUBSCRIBE_QUEUE_NAME, false, false, false, null);
//消费者队列与交换机做绑定根据路由key
channel.queueBind(PUBLISH_SUBSCRIBE_QUEUE_NAME,TOPIC_EXCHANGE_NAME,"*.orange");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// channel.basicQos(1);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" orange ----> '" + message + "'");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
channel.basicConsume(PUBLISH_SUBSCRIBE_QUEUE_NAME, false, deliverCallback, consumerTag -> { });
}
```
其实和上面的俩种模式差别都不大,只是修改了路由(可以使用rabbitmq专用的匹配符),对获取的数据范围可以变得更广
这些模糊匹配的路由都是写在消费者端口,因为消费者端口用来做队列与交换器的绑定,可以设置一个更广的范围,来获取这个队列感兴趣的范围(使用\*or#),如果是写在消费者,那么就有点说不通了,我们前面就说了**我们的生产者只是知道往那个交换机里面扔消息,并不知道其他的情况**
## TTL
在RabittMQ中可以设置队列或者消息的过期时间,这是消息队列的一个特性,如果超过指定时间就会将这个消息丢弃
队列的过期设置没有多大应用场景,适合不持久化的队列
所有设置TTL的队列或者消息,到了一定的时间都会消失
如何声明?什么时候声明,这是队列的特性,所以我们应该直接在队列的消费者那边进行声明;可以在创建队列的时候声明这条消息的TTL(**非负数**,毫秒计数)
因为实在**消费者端那边设置的队列** ,所以里面的消息的存活时间都是由各自的队列进行判断的,每个队列里面设置的过期时间可能都不一样,或者有的会没有设置过期时间,如果这个消息还是fanout出去的或者是有多个队列接收,**最后就会每个消息最后消失的时间不一定,同样也可能不过期**
可以这样设置如下(消息在队列中存在60秒)
```javascript
Map