官网:https://www.rabbitmq.com
简介:
RabbitMQ 是一个实现了 AMQP
(Advanced Message Queuing Protocol) 高级消息队列协议的消息队列服务,用 Erlang 语言,是面向消息的中间件。
主要流程:
业务场景一:异步处理
假如一个商城项目,在用户支付模块中,可能会涉及到其它业务,比如:积分折扣、消费券、短信验证等功能。我们传统的执行步骤是逐步执行,需要等待每个业务执行完毕才能支付成功,这样大大影响了用户的体验!
我们使用消息中间件进行异步处理,当用户下单支付同时我们创建消息队列进行异步的处理其它业务,在我们支付模块中最重要的是用户支付,我们可以将一些不重要的业务放入消息队列执行,这样可以大大提高了我们程序运行的速度,用户支付模块中也大大减少了支付时间,为用户带来了更好的体验感!
业务场景二:应用解耦
我们以商城项目为例,订单系统耦合调用支付、库存、物流系统。如果某天其中一个系统出现了异常就会造成订单系统故障!
使用中间件后,订单系统通过 队列
去访问支付、库存、物流系统,队列会监督各个系统完成,如果完不成队列会一直监督,直到完成为止!这样就不会因为一个子系统出现故障而造成整个系统瘫痪。
业务场景三:流量削峰
假设我们有一个订单系统,我们的订单系统最大承受访问量是每秒1万次
,如果说某天访问量过大我们的系统承受不住了,会对服务器造成宕机,这样的话我们的系统就瘫痪了,为了解决该问题我们可以使用中间件对流量进行消峰。
服务器收到用户的请求后,首先写入消息队列,可以设置消息队列的 TTL 和最大长度,达到阈值则直接抛弃用户请求或跳转到错误页面。
这种方式的好处是可以避免系统的宕机瘫痪,坏处是系统速度变慢,但是总比不能使用好。
回到目录…
2.1 添加 RabbitMQ 依赖
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.14.2version>
dependency>
2.2 创建生产者,负责将信息发送到消息队列
public class ProducerDemo {
public static void main(String[] args) {
// 使用ConnectionFactory来创建连接
ConnectionFactory factory = new ConnectionFactory();
// 设定连接信息
factory.setHost("1.15.76.95");
factory.setPort(5672); // 这是amqp协议端口
factory.setUsername("admin");
factory.setPassword("123456");
factory.setVirtualHost("/test"); // 虚拟主机
// 创建连接
try(Connection connection = factory.newConnection();
// 1.通过 Connection 创建新的 Channel
Channel channel = connection.createChannel()) {
// 2.声明队列,如果此队列不存在,会自动创建
channel.queueDeclare("yyds", false, false, false, null);
// 3.将队列绑定到交换机
channel.queueBind("yyds", "amq.direct", "my-yyds");
// 4.发布新的消息,注意消息需要转换为byte[]
channel.basicPublish("amq.direct", "my-yyds", null, "Hello,World!".getBytes());
} catch (Exception e) {
e.printStackTrace();
}
}
}
a. 客户端需要通过连接创建一个新的通道(Channel
),同一个连接下可以有很多个通道,这样就不用创建很多个连接也能支持分开发送了。
b. 其中 queueDeclare
方法的参数如下:
com.rabbitmq.client.AMQP.Queue.DeclareOk queueDeclare(
String queue, // 队列的名称(默认创建后routingKey和队列名称一致)
boolean durable, // 是否持久化
boolean exclusive, // 是否排他,如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。排他队列是基于Connection可见,同一个Connection的不同Channel是可以同时访问同一个连接创建的排他队列,一旦Connection关闭或者客户端退出,该排他队列都会自动被删除。
boolean autoDelete, // 是否自动删除
Map<String, Object> arguments); // 设置队列的其他一些参数
c. 其中 queueBind
方法参数如下:
com.rabbitmq.client.AMQP.Queue.BindOk queueBind(
String queue, // 需要绑定的队列名称
String exchange, // 需要绑定的交换机名称
String routingKey);
d. 其中 basicPublish
方法的参数如下:
void basicPublish(
String exchange, // 对应的Exchange名称,我们这里就使用第二个直连交换机
String routingKey, // 填写绑定时指定的routingKey
BasicProperties props, // 其他的配置
byte[] body); // 消息本体
e. 执行完成后,可以在管理页面中看到我们刚刚创建好的消息队列了:
回到目录…
2.3 创建消费者,读取消息
public class ConsumerDemo {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("1.15.76.95");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("123456");
factory.setVirtualHost("/test");
// 这里不使用try-with-resource,因为消费者是一直等待新的消息到来,所以就不关闭连接了
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 创建一个基本的消费者
channel.basicConsume("yyds",
false, // 是否开启自动应答,默认采用 Ack message requeue false
(s, delivery) -> {
System.out.println(new String(delivery.getBody()));
// 确认应答,第一个参数是当前的消息标签,第二个参数false表示不批量处理队列中所有的消息(只处理当前消息)
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
// 拒绝应答,第二个参数false表示只处理当前消息,第三个参数false表示不把消息丢回队列(丢弃消息)
//channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
// 拒绝此消息,第二个参数false表示是否重新排队
//channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false);
},
s -> {});
}
}
其中 basicConsume
方法参数如下:
String basicConsume(String var1, boolean var2, DeliverCallback var3, CancelCallback var4) throws IOException;
Nack message requeue true
:拒绝消息,也就是说不会将消息从消息队列取出,并且重新排队,一次可以拒绝多个消息。Ack message requeue false
:确认应答,确认后消息会从消息队列中移除,一次可以确认多个消息。Reject message requeue true/false
:也是拒绝此消息,但是可以指定是否重新排队。回到目录…
官方文档:https://docs.spring.io/spring-amqp/docs/current/reference/html
前面我们已经完成了 RabbitMQ 的安装和简单使用,并且通过 Java 连接到服务器。现在我们来尝试在 SpringBoot 中整合消息队列客户端。
①添加依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
②yml 配置文件,配置 RabbitMQ 的信息
spring:
rabbitmq:
addresses: 1.15.76.95
username: admin
password: 123456
virtual-host: /test
③config 配置类,指定交换机、指定消息队列、进行交换机和队列的绑定
@Configuration
public class RabbitConfiguration {
@Bean("directExchange") // 定义交换机Bean,可以很多个
public Exchange exchange(){
return ExchangeBuilder.directExchange("amq.direct").build();
}
@Bean("yydsQueue") // 定义消息队列
public Queue queue(){
return QueueBuilder
.nonDurable("yyds") // 队列名称,非持久化类型
.build();
}
@Bean("binding")
public Binding binding(@Qualifier("directExchange") Exchange exchange,
@Qualifier("yydsQueue") Queue queue){
// 将我们刚刚定义的交换机和队列进行绑定
return BindingBuilder
.bind(queue) // 绑定队列
.to(exchange) // 到交换机
.with("my-yyds") // 使用自定义的routingKey
.noargs();
}
}
回到目录…
@SpringBootTest
class RabbitMqStudyApplicationTests {
@Resource
RabbitTemplate template;
// RabbitTemplate为我们封装了大量的RabbitMQ操作,已经由Starter提供,因此直接注入使用即可
@Test
void test() {
// 使用convertAndSend方法一步到位
// public void convertAndSend(String exchange, String routingKey, Object object)
// SimpleMessageConverter only supports String, byte[] and Serializable payloads
template.convertAndSend("amq.direct", "my-yyds", "Hello,World");
}
}
启动,查看控制台日志,可以看到yyds消息队列的详细信息:
2023-04-02 22:37:13.320 INFO 17512 --- [ main] c.w.r.RabbitMqStudyApplicationTests : Started RabbitMqStudyApplicationTests in 1.662 seconds (JVM running for 2.725)
2023-04-02 22:37:13.469 INFO 17512 --- [ main] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [1.15.76.95:5672]
2023-04-02 22:37:13.661 INFO 17512 --- [ main] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#375b5b7f:0/SimpleConnection@34819867 [delegate=amqp://admin@1.15.76.95:5672//test, localPort= 64448]
2023-04-02 22:37:13.664 INFO 17512 --- [ main] o.s.amqp.rabbit.core.RabbitAdmin : Auto-declaring a non-durable, auto-delete, or exclusive Queue (yyds) durable:false, auto-delete:false, exclusive:false. It will be redeclared if the broker stops and is restarted while the connection factory is alive, but all messages will be lost.
因为消费者实际上就是一直等待消息然后进行处理的角色,这里我们只需要创建一个监听器就行了,它会一直等待消息到来然后再进行处理。
方式一:@RabbitListener
标注在方法上,直接监听指定的队列,此时接收的参数需要与发送的类型一致。
@Component //注册为Bean
public class ConsumeListener {
@RabbitListener(queues = "yyds") //定义此方法为队列yyds的监听器,一旦监听到新的消息,就会接受并处理
public void receiver(Message message){
System.out.println(new String(message.getBody()));
}
}
方式二:@RabbitListener
标注在类上面表示当有收到消息的时候,就交给 @RabbitHandler
的方法处理,根据接收的参数类型进入具体的方法中。
@Component
@RabbitListener(queues = "yyds")
public class ConsumeListener {
@RabbitHandler
public void receiver(Message message){ //接收所有类型的数据
System.out.println(new String(message.getBody()));
}
@RabbitHandler
public void processMessage1(String message) { //接收String类型的数据
System.out.println(message);
}
@RabbitHandler
public void processMessage2(byte[] message) { //接收byte[]类型的数据
System.out.println(new String(message));
}
}
接着我们启动服务器:
可以看到控制台成功输出了我们之前放入队列的消息,并且管理页面中也显示此消费者已经连接了:
此时,我们再通过管理页面添加新的消息,也是可以持续接收的。
回到目录…
如果我们需要确保消息能够被消费者接受并处理,就需要得到消费者的反馈。
// 生产者
@Test
void publisher() {
//会等待消费者消费然后返回响应结果
Object res = template.convertSendAndReceive("amq.direct", "my-yyds", "Hello World!");
System.out.println("收到消费者响应:" + res);
}
// 消费者
@RabbitListener(queues = "yyds") // 定义此方法为队列yyds的监听器,一旦监听到新的消息,就会接受并处理
public String receiver(String message){
System.out.println(message);
return "收到!";
}
测试:先启动消费者,再启动生产者。(因为如果生产者在一定时间内未收到反馈,会自动返回null)
2023-04-02 22:59:03.591 INFO 8552 --- [ main] o.s.a.r.c.CachingConnectionFactory : Attempting to connect to: [1.15.76.95:5672]
2023-04-02 22:59:03.763 INFO 8552 --- [ main] o.s.a.r.c.CachingConnectionFactory : Created new connection: rabbitConnectionFactory#45f24169:0/SimpleConnection@769a58e5 [delegate=amqp://admin@1.15.76.95:5672//test, localPort= 64998]
2023-04-02 22:59:03.780 INFO 8552 --- [ main] o.s.amqp.rabbit.core.RabbitAdmin : Auto-declaring a non-durable, auto-delete, or exclusive Queue (yyds) durable:false, auto-delete:false, exclusive:false. It will be redeclared if the broker stops and is restarted while the connection factory is alive, but all messages will be lost.
2023-04-02 22:59:04.110 INFO 8552 --- [ main] c.w.r.RabbitMqStudyApplicationTests : Started RabbitMqStudyApplicationTests in 2.315 seconds (JVM running for 3.179)
2023-04-02 22:59:04.313 INFO 8552 --- [ main] .l.DirectReplyToMessageListenerContainer : Container initialized for queues: [amq.rabbitmq.reply-to]
2023-04-02 22:59:04.423 INFO 8552 --- [ main] .l.DirectReplyToMessageListenerContainer : SimpleConsumer [queue=amq.rabbitmq.reply-to, index=0, consumerTag=amq.ctag-ktaUOuTLGH-2_YYJnmdVwA identity=6a48a7f3] started
收到消费者响应:收到!
回到目录…
@Data
public class User {
int id;
String name;
}
注意:实体类一定要有默认的无参构造。
- 不能使用
@AllArgsConstructor
注解;如果自定义有参构造,必须加上无参构造。- 否则会 JSON错误提示:(no Creators, like default constructor, exist): cannot deserialize from Object value
①先在配置类中添加 JSON 转换器:
@Configuration
public class RabbitConfiguration {
...
@Bean("jacksonConverter") //直接创建一个用于JSON转换的Bean
public Jackson2JsonMessageConverter converter(){
return new Jackson2JsonMessageConverter();
}
}
②生产者:有了 JSON 转换器,就可以直接发送 Object 类型的消息。
@Test
void publisher2() {
template.convertAndSend("amq.direct", "my-yyds", new User(1, "张三"));
}
③消费者:将消息队列中取出来的JSON数据转为对象,需要指定转换器
@Component
public class TestListener {
// 指定messageConverter为我们刚刚创建的Bean名称
@RabbitListener(queues = "yyds", messageConverter = "jacksonConverter")
public void receiver(User user){ // 直接接收User类型
System.out.println(user);
}
}
我们也可以在网页端发布 JSON 数据,发现都可以转换:{"id":1,"name":"张三"}
回到目录…
如果消息队列中的数据迟迟没有消费者来处理,那么就会一直占用消息队列的空间。
basic.reject
/ basic.nack
),并且 requeue = false
那么如何构建这样的模式呢? 实际上本质就是一个死信交换机 + 绑定的死信队列。当正常队列中的消息被判定为死信时,会被发送到对应的死信交换机,然后再通过交换机发送到死信队列中,死信队列也有对应的消费者去处理消息。
①这里我们直接在配置类中创建一个新的死信交换机和死信队列,并进行绑定:
@Configuration
public class RabbitConfiguration {
@Bean("directDlExchange") //创建一个新的死信交换机
public Exchange dlExchange() {
return ExchangeBuilder.directExchange("dlx.direct").build();
}
@Bean("yydsDlQueue") //创建一个新的死信队列
public Queue dlQueue() {
return QueueBuilder
.nonDurable("dl-yyds") //队列名称
.build();
}
@Bean("dlBinding") //死信交换机和死信队列进绑定
public Binding dlBinding(@Qualifier("directDlExchange") Exchange exchange,
@Qualifier("yydsDlQueue") Queue queue) {
return BindingBuilder
.bind(queue)
.to(exchange)
.with("dl-yyds") //自定义routingKey
.noargs();
}
...
@Bean("yydsQueue") //在普通消息队列中指定死信交换机
public Queue queue(){
return QueueBuilder
.nonDurable("yyds")
.deadLetterExchange("dlx.direct") //指定死信交换机
.deadLetterRoutingKey("dl-yyds") //指定死信RoutingKey
.build();
}
...
}
②接着我们将监听器修改为死信队列监听:
@Component
public class ConsumeListener {
@RabbitListener(queues = "dl-yyds", messageConverter = "jacksonConverter")
public void receiver(User user){
System.out.println("死信队列监听器: " + user);
}
}
配置完成后,删除原本的队列,重启进行定义:
队列列表中已经出现了我们刚刚定义好的死信队列,并且yyds队列也支持死信队列发送功能了。
回到目录…
现在我们先向 yyds 队列中发送一个消息:{"id":1,"name":"张三"}
然后我们取消息的时候拒绝消息,并且不让消息重新排队:
可以看到拒绝后,如果不让消息重新排队,那么就会直接被丢进死信队列中:
现在我们来看看第二种情况,RabbitMQ 支持将超过一定时间没被消费的消息自动删除,这需要消息队列设定 TTL
值,如果消息的存活时间超过了 Time To Live
值,就会被自动删除。如果有死信队列,那么就会进入到死信队列中。
现在我们将 yyds 消息队列设定 TTL 值(毫秒为单位):
@Bean("yydsQueue")
public Queue queue(){
return QueueBuilder
.nonDurable("yyds")
.deadLetterExchange("dlx.direct")
.deadLetterRoutingKey("dl-yyds")
.ttl(10000) //如果10秒没处理,就自动删除
.build();
}
现在我们删除之前的 yyds 队列再重启测试:可以发现 yyds 队列已经具有 TTL 特性了。
我们向 yyds 队列中插入一个新的消息:{"id":1,"name":"张三"}
可以看到消息10秒钟之后就不见了,而是被丢进了死信队列中。
最后我们来看一下当消息队列长度达到最大的情况,现在我们将消息队列的长度进行限制:
@Bean("yydsQueue")
public Queue queue(){
return QueueBuilder
.nonDurable("yyds")
.deadLetterExchange("dlx.direct")
.deadLetterRoutingKey("dl-yyds")
.maxLength(3) //将最大长度设定为3
.build();
}
现在我们重启一下:可以发现 yyds 队列已经具有 Limit 特性了。
我们向 yyds 队列中依次插入4条消息:{"id":1,"name":"张三"}
、{"id":2,"name":"李四"}
、{"id":3,"name":"王五"}
、{"id":4,"name":"赵六"}
可以看到因为长度限制为3,所以最初的消息直接被丢进了死信队列中了。
回到目录…
官方教程:https://www.rabbitmq.com/getstarted.html
最简单的模型,我们上面的学习都是基于一个生产者、一个消费者的模式,发送端把消息放入队列中,接收端从队列中拿消息。
@Resource
RabbitTemplate template;
@Test
void publisher() { //生产者
template.convertAndSend("amq.direct", "my-yyds", "Hello,World");
}
@Component
public class ConsumeListener {
@RabbitListener(queues = "yyds")
public void receiver(Message message){ //消费者
System.out.println(new String(message.getBody()));
}
}
回到目录…
接着我们来了解一下一个生产者多个消费者的情况,实际上这种模式就非常适合多个工人等待新的任务到来的场景,我们的任务有很多个,一个一个丢进消息队列,而此时工人有很多个,那么我们就可以将这些任务分配个各个工人,让他们各自负责一些任务,并且做的快的工人还可以做完成一些(能者多劳)。
①我们只需要创建两个监听器即可:
@Component
public class ConsumeListener {
@RabbitListener(queues = "yyds")
public void receiver1(String message) {
System.out.println("一号消息队列监听器: " + message);
}
@RabbitListener(queues = "yyds")
public void receiver2(String message) {
System.out.println("二号消息队列监听器: " + message);
}
}
重启后,我们可以发现yyds队列下连接了两个消费者:Prefetch count = 250
(预获取数量,一次性获取消息的最大数量)
这是由消费者的 Prefetch count
决定的,默认每个消费者的预获取数量是250,这个参数是针对同一批消息。所以实时发消息会认为每批只有一个消息,会进行轮询;而队列中囤积的消息则属于同一批消息,只有达到250条才会轮询。
②在配置类中添加 containerFactory 的设置:
这里我们需要在 RabbitConfiguration
配置类中定义一个自定义的 ListenerContainerFactory
,可以在这里设定消费者 Channel 的 PrefetchCount 的大小:
@Resource
private CachingConnectionFactory connectionFactory;
@Bean(name = "listenerContainer")
public SimpleRabbitListenerContainerFactory listenerContainer(){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setPrefetchCount(1); //将PrefetchCount设定为1表示一次只能取一个
return factory;
}
接着我们在监听器这边指定即可:
@Component
public class ConsumeListener {
@RabbitListener(queues = "yyds", containerFactory = "listenerContainer")
public void receiver1(String message) throws InterruptedException {
Thread.sleep(500); //让每个消息处理时间长一些,结果会更清晰
System.out.println("一号消息队列监听器: " + message);
}
@RabbitListener(queues = "yyds", containerFactory = "listenerContainer")
public void receiver2(String message) throws InterruptedException {
Thread.sleep(500);
System.out.println("二号消息队列监听器: " + message);
}
}
现在我们再次启动服务器,可以看到 PrefetchCount 被限定为1了:
再次重复上述的实现,可以看到消息不会被同一个消费者给全部抢走了:
③通过 concurrency 参数直接指定消费者的数量:
当然除了去定义两个相同的监听器之外,我们也可以直接在注解中定义,比如我们现在需要10个同样的消费者:
@Component
public class ConsumeListener {
@RabbitListener(queues = "yyds", containerFactory = "listenerContainer", concurrency = "10")
public void receiver(String data){
System.out.println(data);
}
}
回到目录…
我们来看一下发布订阅模式,比如我们在阿里云买了云服务器,但是最近快到期了,那么就会给你的手机、邮箱发送消息,告诉你需要去续费了,但是手机短信和邮件发送并不一定是同一个业务提供的,但是现在我们又希望能够都去执行,所以就可以用到发布订阅模式,简而言之就是,发布一次,消费多个。
实现这种模式其实也非常简单,但是如果使用我们之前的直连交换机,肯定是不行的,我们这里需要用到另一种类型的交换机,叫做 fanout(扇出)类型,这时一种广播类型,消息会被广播到所有与此交换机绑定的消息队列中。
①在配置类中定义扇出交换机,并绑定两个队列:
@Configuration
public class RabbitConfiguration {
@Bean("fanoutExchange")
public Exchange exchange(){
//注意这里是fanoutExchange
return ExchangeBuilder.fanoutExchange("amq.fanout").build();
}
@Bean("yydsQueue1")
public Queue queue(){
return QueueBuilder.nonDurable("yyds1").build();
}
@Bean("yydsQueue2")
public Queue queue2(){
return QueueBuilder.nonDurable("yyds2").build();
}
@Bean("binding")
public Binding binding(@Qualifier("fanoutExchange") Exchange exchange,
@Qualifier("yydsQueue1") Queue queue){
return BindingBuilder
.bind(queue)
.to(exchange)
.with("yyds1")
.noargs();
}
@Bean("binding2")
public Binding binding2(@Qualifier("fanoutExchange") Exchange exchange,
@Qualifier("yydsQueue2") Queue queue){
return BindingBuilder
.bind(queue)
.to(exchange)
.with("yyds2")
.noargs();
}
}
②接着我们搞两个监听器,监听一下这两个队列:
@Component
public class ConsumeListener {
@RabbitListener(queues = "yyds1")
public void receiver(String data){
System.out.println("一号消息队列监听器 " + data);
}
@RabbitListener(queues = "yyds2")
public void receiver2(String data){
System.out.println("二号消息队列监听器 " + data);
}
}
重启后,我们可以发现默认的 amq.fanout
交换机已经绑定了两个队列:
此时,我们向该交换机中发布一条消息:
可以发现,该交换机以广播的形式分发给它绑定的两个消息队列:
回到目录…
①我们就在配置类中,将同一个交换机和同一个队列进行不同 routingKey 的绑定:
@Configuration
public class RabbitConfiguration {
@Bean("directExchange")
public Exchange exchange(){
return ExchangeBuilder.directExchange("amq.direct").build();
}
@Bean("yydsQueue")
public Queue queue(){
return QueueBuilder.nonDurable("yyds").build();
}
@Bean("binding") //使用yyds1绑定
public Binding binding(@Qualifier("directExchange") Exchange exchange,
@Qualifier("yydsQueue") Queue queue){
return BindingBuilder
.bind(queue)
.to(exchange)
.with("yyds1") //routingKey=yyds1
.noargs();
}
@Bean("binding2") //使用yyds2绑定
public Binding binding2(@Qualifier("directExchange") Exchange exchange,
@Qualifier("yydsQueue") Queue queue){
return BindingBuilder
.bind(queue)
.to(exchange)
.with("yyds2") //routingKey=yyds2
.noargs();
}
}
②我们去监听唯一的消息队列 yyds:
@Component
public class ConsumeListener {
@RabbitListener(queues = "yyds")
public void receiver(String data){
System.out.println("yyds消息队列监听器 " + data);
}
}
启动后,可以发现 amq.direct 交换机绑定了两个路由:实际上指向同一个消息队列 yyds
测试一下,无论我们使用 yyds1
和 yyds2
哪个 routingKey,都可以成功路由到 yyds 消息队列中:
回到目录…
*
表示任意的一个单词,如 *.orange.*
可以表示为 a.orange.b、c.orange.c#
表示0个或多个单词,如 lazy.#
可以表示为 lazy、lazy.a、lazy.a.bamq.topic
交换机实现模糊路由转发。amq.rabbitmq.trace
交换机实现消息追踪。应用一:amq.topic 交换机实现模糊路由转发
在配置类中定义主题交换机,并且以模糊匹配的方式绑定一个队列
@Configuration
public class RabbitConfiguration {
@Bean("topicExchange") //这里使用预置的Topic类型交换机
public Exchange exchange(){
return ExchangeBuilder.topicExchange("amq.topic").build();
}
@Bean("yydsQueue")
public Queue queue(){
return QueueBuilder.nonDurable("yyds").build();
}
@Bean("binding")
public Binding binding2(@Qualifier("topicExchange") Exchange exchange,
@Qualifier("yydsQueue") Queue queue){
return BindingBuilder
.bind(queue)
.to(exchange)
.with("*.test.*")
.noargs();
}
}
启动项目,绑定成功:
可以看到只要是满足通配符条件的都可以成功转发到对应的消息队列:
回到目录…
应用二:amq.rabbitmq.trace 交换机实现消息追踪
可以看到它也是 topic
类型的,它是一个内部交换机,用于帮助我们记录和追踪生产者和消费者使用消息队列的交换机。
①首先,我们需要在控制台将虚拟主机 /test
的追踪功能开启:
rabbitmqctl trace_on -p /test
③我们给 amq.rabbitmq.trace 交换机绑定上刚刚的队列: 因为该交换机是内部的,所以只能在 Web 管理页面中绑定
由于发送到此交换机上的 routingKey 为 publish.交换机名称
和 deliver.队列名称
,分别对应生产者投递到交换机的消息,和消费者从队列上获取的消息,因此这里使用 #
通配符进行绑定。
④现在我们来测试一下,往 yyds 队列中发送消息: 会发现 trace 队列中多了2条信息。
通过追踪,我们可以很明确地得知消息发送的交换机、routingKey、用户等信息,包括信息本身:
同样的,消费者在取出数据时也有记录:我们可以明确消费者的地址、端口、具体操作的队列以及取出的消息信息等。
回到目录…
实际上,RabbitMQ 默认的七种交换机一共分为4类: direct
、fanout
、topic
、headers
,当然前3种类型我们已经在之前学习过了。
Name | Type | Features | 描述 |
---|---|---|---|
(AMQP default) | direct | D |
所有虚拟主机都会自带的一个默认交换机,并且此交换机不可删除。 此交换机默认绑定到所有的消息队列,不能解绑,并且通过队列名称进行路由。 |
amq.direct | direct | D |
是一个普通直连交换机,持久化的。 该交换机是具有绑定关系的,默认没有任何绑定,需要手动绑定消息队列。 |
amq.fanout | fanout | D |
是一个扇出类型的交换机,一种广播类型,消息会被广播到所有与此交换机绑定的消息队列中。 应用于发布订阅模式。 |
amq.headers | headers | D |
它不通过RoutingKey进行分发消息,而时通过消息中内容的headers属性进行匹配。 |
amq.match | headers | D |
和 amq.headers 一样,目前不清楚区别在哪儿。 |
amq.rabbitmq.trace | topic | D I |
是一个topic类型的内部交换机,可以实现消息追踪,帮助我们记录和追踪生产者和消费者使用消息队列的情况,应用于主题模式。 |
amq.topic | topic | D |
是一个topic类型的交换机,可以实现模糊路由转发,应用于主题模式。 |
那么,我们现在学习一下 headers
类型的交换机:amq.headers。
它不通过 RoutingKey 进行分发消息,而是根据头部信息来决定的,在我们发送的消息中是可以携带一些头部信息的(类似于HTTP),我们可以根据这些头部信息来决定路由到哪一个消息队列中。
@Configuration
public class RabbitConfiguration {
@Bean("headerExchange") //注意这里返回的是HeadersExchange
public HeadersExchange exchange(){
return ExchangeBuilder
.headersExchange("amq.headers") //RabbitMQ为我们预置了两个,这里用第一个就行
.build();
}
@Bean("yydsQueue")
public Queue queue(){
return QueueBuilder.nonDurable("yyds").build();
}
@Bean("binding")
public Binding binding2(@Qualifier("headerExchange") HeadersExchange exchange, //这里和上面一样的类型
@Qualifier("yydsQueue") Queue queue){
return BindingBuilder
.bind(queue)
.to(exchange)
.where("test").matches("hello"); //设置消息的头部信息中包含test=hello, 才能转发给yyds消息队列
//.whereAny("a", "b").exist(); // 这个是只要存在任意一个指定的头部Key就行
//.whereAll("a", "b").exist(); // 这个是必须存在所有指定的的头部Key
//.whereAny(Collections.singletonMap("test", "hello")).match(); 传入Map也行,批量指定键值对
}
}
启动后,发现 amq.headers
交换机成功绑定了 yyds 消息队列:
结果发现,消息可以成功发送到消息队列,这就是使用头部信息进行路由。
回到目录…
①我们首先在两个服务器上开启 rabbitmq 服务,保证都能正常访问。接下来我们以 rabbit@ebe207194e57
作为主服务器,第二个服务器作为从节点,搭建一主一从的集群。
②从服务器的准备工作:
我们可以在管理页面看到主服务器的名称,之后我们从服务器需要通过这个名称连接主服务器。
a. 需要修改本地的 hosts:为了让服务器能解析到主服务器的IP地址。
vim /etc/hosts
# 添加主服务器地址 -> 名称
1.15.76.95 ebe207194e57
b. 修改相同的 cookie 值:
# 赋予写权限
chmod 777 /var/lib/rabbitmq/.erlang.cookie
# 编辑cookie值和主服务器的值相同
vim /var/lib/rabbitmq/.erlang.cookie
# 改回只读权限
chmod 400 /var/lib/rabbitmq/.erlang.cookie
③连接主服务器: 搭建集群
rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@ebe207194e57
rabbitmqctl start_app
此时,虽然已经搭建好集群了,但是从服务器还没有拷贝功能。目前它只能显示主服务器的信息,但主服务器宕机时,它是无法查看信息的。
实际上添加完成后,主从都有这条政策了,此时集群就彻底搭建完成了。
⑤高可用:主节点掉线重启后,发现从节点变成了主节点,原本的主节点却变成了从节点。
回到目录…
我们之前学习 不同环境下安装 RabbitMQ 时,学会了在 Docker 中部署 RabbitMQ 服务器。那么,我们也可以在 Docker 中搭建 RabbitMQ 集群。
①创建并启动服务器节点
a. 主节点:
docker run -d \
--hostname rabbit1 \
--name myrabbit1 \
--restart always \
-p 5672:5672 -p 15672:15672 \
-e RABBITMQ_ERLANG_COOKIE='rabbitcookie' \
rabbitmq:management
b. 从节点:
docker run -d \
--hostname rabbit2 \
--name myrabbit2 \
--restart always \
-p 5673:5672 -p 15673:15672 \
--link myrabbit1:rabbit1 \
-e RABBITMQ_ERLANG_COOKIE='rabbitcookie' \
rabbitmq:management
注意:
- 给每个 rabbitmq 服务器设置
hostname
主机名称,连接集群时会用到。- 多个容器之间使用
-link
连接,使它们之间可见。- 给每个 rabbitmq 服务器设置相同的
RABBITMQ_ERLANG_COOKIE
,因为 RabbitMQ 是用 Erlang 实现的,Erlang Cookie 相当于不同节点之间相互通讯的秘钥,Erlang 节点通过交换 Erlang Cookie 获得认证。
c. 创建新用户:为了远程登录管理页面,两个服务器都要创建新用户。
rabbitmqctl add_user admin 123456
# 修改用户角色为管理员
rabbitmqctl set_user_tags admin administrator
②搭建集群:需要进入 rabbit2 从服务器中操作。
# 停止RabbitMQ进程
rabbitmqctl stop_app
# mq2加入mq1集群
rabbitmqctl join_cluster rabbit@rabbit1
# 再次启动mq进程
rabbitmqctl start_app
此时,我们登录两个服务器的管理页面,可以看到它们的集群中有两个节点:
尝试在 rabbit1 中创建队列,可以看到如下信息,rabbit2 也是这样的信息,说明从节点只是显示了主节点的信息,而并没有复制信息。
③添加一条政策: 赋予从节点镜像拷贝的功能。
添加完成后,我们发现主从服务器都有了这条政策:
而信息也发生了变化,可以看到 Node + 1
,yyds 存储到了两个服务器节点中了。
④高可用
当主节点 rabbit1 宕机时:
当主节点重启后,发现从节点变成了主节点,原本的主节点却变成了从节点,实现了高可用。
回到目录…
spring:
rabbitmq:
addresses: 1.15.76.95:5672, 1.15.76.95:5673
username: admin
password: 123456
virtual-host: /test
回到目录…
总结:
提示:这里对文章进行总结:
本文是对RabbitMQ的学习,首先我们介绍了消息队列的业务场景,然后学习了Java操作rabbitmq的方式,介绍了死信队列的应用,并且详细学习了RabbitMQ的五种消息模式和七种默认的交换机,最后也学习了搭建集群的方法。