RabbitMQ消息队列速通

RebbitMQ基本使用

介绍

RabbitMQ 是一个开源的消息代理(Message Broker)和队列服务,它实现了高级消息队列协议(AMQP, Advanced Message Queuing Protocol),允许分布式系统之间进行可靠、异步的消息传递 。

用途

它主要用于处理系统之间的异步通信和解耦,提供了一种可靠的、灵活的消息传输机制

安装rabbitMQ
1.

拉镜像:Docker安装rabbitmq

docker pull rabbitmq:management

注:获取镜像要获取management版本的才有界面

运行到容器

docker run -itd \
--name my-rabbitmq \
-p 5672:5672 -p 15672:15672 \
--hostname my-rabbitmq-host \
-e RABBITMQ_DEFAULT_VHOST=my_vhost \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=admin \
--restart=always \
rabbitmq:management

--hostname:主机名

-e:指定环境变量

RABBITMQ_DEFAULT_VHOST:默认虚拟机名

RABBITMQ_DEFAULT_USER:默认的用户名

RABBITMQ_DEFAULT_PASS:默认用户名的密码

容器启动后,可以通过docker logs查看日志

2.
去网上下载windows的版本,后台运行即可

进入管理界面

主机名(虚拟机ip):15672

账号密码都是admin 登录进去之后

Overview:概述

Connections:已连接的机器

Channels:监听

Exchanges:交换机

Queues:队列

开始写代码

创建两个 模块 消费者consumer和生产者publisher

在pom文件添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
yml文件配置
server:
port: 808x
spring:
application:
name: xx
rabbitmq:
host: 192.168.27.110
password: 123456
port: 5672
username: spring
virtual-host: my_vhost
生产者中写代码
@Configuration
@SuppressWarnings("all")
public class RabbitConfig {
    @Bean
    public Queue firstQueue() {
        //会生成一个firstQueue队列
        return new Queue("firstQueue");
    }
}
写一个类测试
@RestController
public class TestContriller {

    @Autowired
    private AmqpTemplate rabbitTemplate;



    @RequestMapping("/send1")
    public String sendFirst() {
        ///向消息队列发消息
        rabbitTemplate.convertAndSend("firstQueue", "Hello World");
        return "🤡";
    }
}

把两个模块运行测试一下方法

mq里面会出现一个队列,就是我们刚刚那个

注意:如果消费者运行报错,请先启动生产者,运行方法把队列创建出来

ok,在消费者写一个类接收

@Component
@SuppressWarnings("all")
@Slf4j
@RabbitListener(queues = "firstQueue")//监听firstQueue有消息就会执行
public class Receiver {
    @RabbitHandler //最终处理方法
    public void process(String msg) {
        log.warn("接收到:" + msg);
    }
}

写好在浏览器刷几遍小丑,消费者里面会出现消息,说明监听成功,已解耦

自定义数据发送

在生产者写实体类 必须要实现序列化 不然会报错
@SuppressWarnings("all")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable{
    private String username;
    private String userpwd;
}

在RabbitConfig里面再生成一个队列userQueue

@Bean
public Queue userQueue() {
    //连接上会自动生成一个firstQueue队列
    return new Queue("userQueue");
}
​

在sender里面再写一个方法

public String sendUser() {
    User jack = new User("jack", "123");
    ///向消息队列发消息
    rabbitTemplate.convertAndSend("userQueue", jack);
    return "🤡";
}
 

成功!

再把上面的user复制过来 但是很麻烦,它要两个模块的包名类名都一样

解决方法

1.写一个公共模块,专门放公共类

2.发json模式 推荐

把send2方法修改

@Autowired
private ObjectMapper objectMapper;
@RequestMapping("/send2")
public String sendUser() throws Exception{
    User jack = new User("jack", "123");
    //把数据转成json格式
    String s = objectMapper.writeValueAsString(jack);
    ///向消息队列发消息
    rabbitTemplate.convertAndSend("userQueue", s);
    return "🤡";
}

在消费者写UserReceiver

@Component
@SuppressWarnings("all")
@Slf4j
//@RabbitListener(queues = "userQueue")
public class UserReceiver {
    @Autowired
    private ObjectMapper objectMapper;
    @RabbitHandler
    public void process(String msg) throws Exception{
        //把json格    式读成对象
        User user = objectMapper.readValue(msg, User.class);
        log.warn("接收到:" + user);
    }
}

成功!

交换机 Exchange

概念

在RabbitMQ中,交换机(Exchange)是消息路由的核心组件,它负责接收生产者发送的消息,并根据预定义的规则(绑定(binding))将这些消息路由到一个或多个队列。

主要用途

再rabbitMQ中,生产者发送消息不会直接将消息投递到队列中,而是先将消息投递到交换机中,在有交换机转发到具体的队列,队列再将消息以推送或者拉去方式给消费者进行消费

RabbitMQ支持多种类型的交换机,以适应不同的消息路由策略

路由键(Routingkey)

生产者将消息发送给交换机的时候,会指定Routingkey指定路由规则

绑定键(Bindingkey)

通过绑定键将交换机与队列关联起来,这样rabbitmq就知道如何正确将消息路由到队列

交换机类型
直连交换机:Direct exchange 默认

将消息推送到binding key与该消息的routing key相同的队列

同一个绑定键可以绑定到不同的队列上去,可以增加一个交换机与队列Q2的绑定键,在这种情况下,直连交换机会将消息推送到所有匹配的队列。一个路由键为bb的消息会被同时推送到队列q1和q2

缺点:

直连交换机的routing key方案非常简单,如果我们希望一条消息发送给多个队列,那么这个交换机需要绑定上非常多的routing key,如果每个多绑定一对routing key。那么消息的管理就会异常的困难

主题交换机:topic exchange
主题交换机的特点:

发送到主题交换机的消息不能有任意的routing key,必须是由点号分开的一串单词,可以是任意的,但通常是与消息相关的特征,单词可以有很多,最大限制是255bytes

topic交换机的绑定键有两个特殊的情况

*:表示匹配任意一个单词

#:表示匹配任意一个或多个单词

当一个队列的绑定键是“#”,它将会接收所有的消息,而不再考虑接收消息的路由键

的一个队列的绑定键没有用到"#"和"*"时,它又像直连交换机一样工作

它可以解决直连交换机的缺点

扇形交换机:fanout exchange

扇形交换机时最基本的交换机类型,它做的事情时广播消息

扇形交换机会把能够接收到的消息全部发送给绑定在自己身上的队列,因此广播不需要思考,所以扇形交换机处理消息的速度也是所有交换机类型里面最快的

首部交换机:headers exchange

首部交换机和扇形交换机都不需要路由键,交换机通过headers头部来将消息映射到队列的。

相比直连交换机,首部交换机的优势是匹配的规则不被限定为字符串而是object类型

默认交换机

实际上就是名字为空字符串的直连交换机

死信交换机 Dead Letter Exchange

专门处理死了的信息

消息变成死信一般是三种情况:

消息被拒绝,并且设置requeue参数为false

消息过期(默认情况下rabbit中的消息不过期,但是可以设置队列的过期时间和消息的过期时间以达到消息过期的效果)

队列达到最大长度(一般设置了最大队列长度或大小并达到最大值时)

当满足了三名三种情况时,消息会变成死信消息,并通过死信交换机投递到相应的队列中

我们只要监听相应队列,就可以对死信消息进行最后处理 比如订单的超时处理

直连交换机 实践

在consummer里面新建两个队列

新键一个直连交换机

直连交换机绑定到队列中并设置键

代码如下:

//新建两个队列
    @Bean
    public Queue queue1(){
        return new Queue("queue1");
    }
​
    @Bean
    public Queue queue2(){
        return new Queue("queue2");
    }
​
    //新建一个直连交换机
    @Bean
    public DirectExchange e1(){
        return new DirectExchange("directExchange");
    }
​
    //绑定关系
    @Bean
    public Binding binding01(){
        //把DirectExchange交换机绑定到queue1队列中用aa键绑定
        return BindingBuilder
                .bind(queue1())
                .to(e1())
                .with("aa");
    }
​
    @Bean
    public Binding binding02(){
        //把DirectExchange交换机绑定到queue2队列中用bb键绑定
        return BindingBuilder
                .bind(queue2())
                .to(e1())
                .with("bb");
    }

在测试类里面测试

@RestController
public class TestContriller {

    @Autowired
    private AmqpTemplate rabbitTemplate;



   
    //测试
    @RequestMapping("/send3")
    public String sendUser3(){
        rabbitTemplate.convertAndSend("directExchange","bb","hhh");
        return "🤡";
    }
}

在消费者consumer创建队列Q1,Q2

@Component
@SuppressWarnings("all")
@Slf4j
@RabbitListener(queues = "queue1")
public class ReceiverQ1 {
    @RabbitHandler    
    public void process(String msg) {
        log.warn("Q1接收到:" + msg);
    }
 }

根据上面的代码创建一个Q2,改一下就好了

浏览器输出结果

主题交换机 实践
注:一个队列可以绑定多个键

新建一个主题交换机

 @Bean
    public TopicExchange topicExchange(){
        return new TopicExchange("topicExchange");
    }

进行队列与交换机的绑定

@Bean
    public Binding binding03(){
        return BindingBuilder.bind(queue1())
                             .to(topicExchange())
                             .with("*.*.aa");
    }
​
​
    @Bean
    public Binding binding04(){
        return BindingBuilder.bind(queue2())
                .to(topicExchange())
                .with("*.*.bb");
    }
​
    @Bean
    public Binding binding05(){
//        要绑定多个的话不能一次性绑完
        return BindingBuilder.bind(queue1())
                .to(topicExchange())
                .with("mq.#");
    }
    @Bean
    public Binding binding06(){
        return BindingBuilder.bind(queue2())
                .to(topicExchange())
                .with("mq.#");
    }

在测试用例里面测试

   @RequestMapping("/send4")
    public String send4(String rex){
        rabbitTemplate.convertAndSend("topicExchange",rex,"hhh");
        return "🤡";
    }
扇形交换机 群发,广播

注:它里面的队列不需要binding key

   
 //新建一个扇形交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanoutExchange");
    }
​
    @Bean
    public Binding binding07(){
        //因为不需要绑定键所以不用with
        return BindingBuilder.bind(queue1())
                .to(fanoutExchange());
    }
​
    @Bean
    public Binding binding08(){
        return BindingBuilder.bind(queue2())
                .to(fanoutExchange());
    }

测试

    
@RequestMapping("/send5")
    public String send5(){
        rabbitTemplate.convertAndSend("fanoutExchange","","hhh");//中间那个值必须要写上
        return "🤡";
    }

最后运行效果

持久化:交换机可以设置为持久化,即使在RabbitMQ服务器重启后,该交换机也会被重新创建出来。这对于保证系统的高可用性和可靠性非常重要。

小结

解耦生产者与消费者:通过Exchange,生产者无需关心消息将被哪个消费者消费,只需将消息发送到特定类型的Exchange,由Exchange负责消息的分发,从而实现了生产者和消费者的解耦。

总的来说,RabbitMQ交换机是整个消息路由的核心,它灵活且强大的路由机制使得RabbitMQ能够适应多种应用场景和需求

死信队列(延迟队列)

死信(Dead Letter)是一种RabbitMQ的一种消息队列。

用途:当一个队列一致无法消费某条数据,那么可以把这条数据放入死信队列里面。等待条件满足了再从死信队列中取出来再次消费,从而避免消息丢失。

死信消息来源:

1.消息ttl过期

2.队列满了,无法再次添加数据

3.消息被拒绝(reject或nack),并且requeue=false

代码编写

在RabbitConfig编写代码

@Configuration
@SuppressWarnings("all")
public class RabbitConfig {
    //模拟订单
    //先创建一个queueA的正常队列
    @Bean
    public Queue queueA(){
        Map<String, Object> config = new HashMap<>();
        //message在该队列queue的存活时间最大为10秒
        config.put("x-message-ttl", 10000);//单位毫秒
        //x-dead-letter-exchange参数是设置该队列的死信交换器(DLX)
        config.put("x-dead-letter-exchange", "ExchangeB");
        //x-dead-letter-routing-key参数是给这个DLX指定路由键
        config.put("x-dead-letter-routing-key", "bb");
        //第一个参数是要新建的队列名
        //第二个参数是durable:持久化
        //第三个参数是exclusive:排他
        //第四个参数是autoDelete:自动删除
        return new Queue("queueA",true,false,false,config);
    }
    
    
    //创建一台交换机,名字叫ExchangeA
    //死信的本质也是一台直连交换机
    @Bean
    public DirectExchange exchangeA(){
        return new DirectExchange("ExchangeA");
    }
    
    
    //把队列和交换机绑定路由为aa
    @Bean
    public Binding bindingA(){
        return BindingBuilder.bind(queueA())
                .to(exchangeA())
                .with("aa");
    }
    
    
    //再创建一个queueB的死信队列
    @Bean
    public Queue queueB(){
        return new Queue("queueB");
    }
​
    //创建交换机exchangeB
    @Bean
    public DirectExchange exchangeB(){
        return new DirectExchange("ExchangeB");
    }
​
    //同样的配方,绑定
    @Bean
    public Binding bindingB(){
        return BindingBuilder.bind(queueB())
                .to(exchangeB())
                .with("bb");
    }
​
}

完成后编写测试类

@RestController
public class TestContriller {
    @Autowired
    private AmqpTemplate rabbitTemplate;
    
    @RequestMapping("/send6")
    public String send6(){
        //现在aa中运行并且发送消息
        rabbitTemplate.convertAndSend("ExchangeA","aa","你干嘛,哎呦");
        return "🤡";
    }
    
​
}

在消费者模块创建两个类

里面内容如下

@Component
@SuppressWarnings("all")
@Slf4j
@RabbitListener(queues = "queueA")
public class ReceiverA {
    @RabbitHandler
    public void process(String msg, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
        log.error("QA接收到:" + msg);
        //确认消息channel.basicAck(tag,true);
        channel.basicReject(tag,false);//false会直接拒绝入队,变成死信
        Thread.sleep(1000);//过了延迟时间会交给QB
    }
}
@Component
@SuppressWarnings("all")
@Slf4j
@RabbitListener(queues = "queueB")
public class ReceiverB {
    @RabbitHandler
    public void process(String oid){
        log.warn("QB接收到:" + oid);
        //假设收到了oid
        //去数据库做修改
        //把支付状态改成取消
​
    }
}

这里有channel的三种消息

channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);   //确认消息
channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否认消息
channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //拒绝消息
消息接收确认

消息通过 ACK 确认是否被正确接收,每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK

消息确认模式有
  • AcknowledgeMode.NONE:自动确认

  • AcknowledgeMode.AUTO:根据情况确认

  • AcknowledgeMode.MANUAL:手动确认

在application.yml文件里编写即可

spring:
  rabbitmq:
    listener:
      simple:
        #手动确认
        acknowledge-mode: manual

可以运行代码了

注意事项:

如果消费者(consumer)启动报错,先启动生产者把队列创建出来

运行之后队列QueueB中刚才那个请求没处理,堆积了

此时我们的死信队列应该也要处理这个消息

把上面的queueB队列重新写

@Component
@SuppressWarnings("all")
@Slf4j
@RabbitListener(queues = "queueB")
public class ReceiverB {
    @RabbitHandler
    public void process(String oid, Channel channel,@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
        log.warn("QB接收到:" + oid);
        channel.basicAck(tag,true);
        //假设收到了oid
        //去数据库做修改
        //把支付状态改成取消
​
    }
}

这样死信队列就可以处理消息了

重启运行之后会发现消息被处理了

死信队列小结:

总体用下来,死信队列的优点还是很多的,如:

数据完整保护:

通过设置消息策略,当消息在正常队列中无法被正确消费时(例如,消费者拒绝消费、消息过期或者达到最大重试次数等),这些消息会被转移到死信队列中,而不是丢失,从而保证了业务数据的完整性。

问题隔离与排查:

将不能正常处理的消息从主业务流程中分离出来,有助于运维人员集中分析和排查那些处理失败或异常的消息原因,而不影响正常的业务消息流 。

故障恢复机制:

某些场景下,消息进入死信队列可能是暂时的系统故障或者是业务逻辑需要重新处理。通过设计合理的重试或补偿策略,可以从死信队列中重新拉取并尝试处理这些消息,以实现系统的自我修复能力 。

资源释放:

避免无效消息占用过多内存资源,确保主队列高效运行。如果主队列中有大量因某种原因无法正常处理的消息堆积,可能会导致资源耗尽;而使用死信队列则能及时将这些问题消息移除,释放主队列的空间。

以上就RabbitMQ的大致内容了,如果有什么问题可以私信我哦

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值