rabbitmq学习


1. centos安装rabbitmq

1.1 安装erlang

rabbitmq是由erlang编写的,所以需要erlang的环境

#拉取erlang
wget -P https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.9/rabbitmq-server-3.8.9-1.el7.noarch.rpm 
#安装 Erlang
rpm -Uvh erlang-23.0-1.el7.x86_64.rpm
#安装 socat
yum install -y socat

1.2 安装rabbitmq

#拉取rabbitmq
wget -P  https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.9/rabbitmq-server-3.8.9-1.el7.noarch.rpm
#安装RabbitMQ
rpm -Uvh rabbitmq-server-3.8.9-1.el7.noarch.rpm

1.3 启动和关闭

#启动
sudo systemctl start rabbitmq-server
#关闭
sudo systemctl status rabbitmq-server
#查看运行状态
sudo systemctl stop rabbitmq-server
#开机自启动
sudo systemctl enable rabbitmq-server

1.4 安装rabbitmq的web控制台

rabbitmq有一个默认的guest用户,但只能通过localhost访问,所以需要添加一个能够远程访问的用户。

#开启插件
rabbitmq-plugins enable rabbitmq_management
#添加用户
rabbitmqctl add_user admin admin
#为用户分配资源权限
rabbitmqctl set_user_tags admin administrator

1.5 添加防火墙规则

RabbitMQ 服务启动后,还不能进行外部通信,需要将端口添加都防火墙

#添加防火墙暴露端口
firewall-cmd --zone=public --add-port=4369/tcp --permanent
firewall-cmd --zone=public --add-port=5672/tcp --permanent
firewall-cmd --zone=public --add-port=25672/tcp --permanent
firewall-cmd --zone=public --add-port=15672/tcp --permanent
#重启防火墙
firewall-cmd --reload

1.6 测试

浏览器输入:http://ip+端口(15672),例如:http://192.168.235.102:15672
在这里插入图片描述

2. springboot集成rabbitmq

pom.xml依赖

        <!--rabbitmq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

application.yml配置

server:
  port: 8021
spring:
  #给项目来个名字
  application:
    name: rabbitmq-provider
  #配置rabbitMq 服务器
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: root
    password: root
    #虚拟host 可以不设置,使用server默认host
    virtual-host: JCcccHost

2.1 直连交换机(direct exchange)

通过new Queue()创建一个队列,传递三个核心参数
durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
new Queue(“TestDirectQueue”,true,true,false);

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
@Configuration
public class DirectRabbitConfig {
 
    //队列 起名:TestDirectQueue
    @Bean
    public Queue TestDirectQueue() {
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
        // return new Queue("TestDirectQueue",true,true,false);
 
        //一般设置一下队列的持久化就好,其余两个就是默认false
        return new Queue("TestDirectQueue",true);
    }
 
    //Direct交换机 起名:TestDirectExchange
    @Bean
    DirectExchange TestDirectExchange() {
      //  return new DirectExchange("TestDirectExchange",true,true);
        return new DirectExchange("TestDirectExchange",true,false);
    }
 
    //绑定  
    //将队列和交换机绑定, 并设置用于匹配键:TestDirectRouting
    @Bean
    Binding bindingDirect() {
        return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("TestDirectRouting");
    }
     
    @Bean
    DirectExchange lonelyDirectExchange() {
        return new DirectExchange("lonelyDirectExchange");
    }
}

创建消息接收监听类,DirectReceiver.java,通过RabbitListener监听TestDirectQueue的消息

@Component
//监听的队列名称 TestDirectQueue
@RabbitListener(queues = "TestDirectQueue")
public class DirectReceiver {
 
    @RabbitHandler
    public void process(Map testMessage) {
        System.out.println("DirectReceiver消费者收到消息  : " + testMessage.toString());
    }
}

然后写个简单的接口进行消息推送(根据需求也可以改为定时任务等等,具体看需求),SendMessageController.java:

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
public class SendMessageController {

    //使用RabbitTemplate,这提供了接收/发送等等方法
    @Autowired
    RabbitTemplate rabbitTemplate;  
 
    @GetMapping("/sendDirectMessage")
    public String sendDirectMessage() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "test message, hello!";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String,Object> map=new HashMap<>();
        map.put("messageId",messageId);
        map.put("messageData",messageData);
        map.put("createTime",createTime);
        //将消息携带绑定键值:TestDirectRouting 发送到交换机TestDirectExchange
        rabbitTemplate.convertAndSend("TestDirectExchange", "TestDirectRouting", map);
        return "ok";
    }
}

调用后会根据TestDirectRouting找到TestDirectExchange上的TestDirectQueue进行消费

2.2 主题交换机(topic exchange)

核心是路由键,以.分隔的多个字母视为一个单词,*号代表一个单词,#号可以替代零个或多个单词。

@Configuration
public class TopicRabbitConfig {
    //绑定键
    public final static String man = "topic.man";
    public final static String woman = "topic.woman";

    @Bean
    public Queue firstQueue() {
        return new Queue(TopicRabbitConfig.man);
    }

    @Bean
    public Queue secondQueue() {
        return new Queue(TopicRabbitConfig.woman);
    }

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

    //将firstQueue和topicExchange绑定,而且绑定的键值为topic.man
    //这样只要是消息携带的路由键是topic.man,才会分发到该队列
    @Bean
    Binding bindingExchangeMessage() {
        return BindingBuilder.bind(firstQueue()).to(exchange()).with(man);
    }

    //将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.#
    // 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列
    @Bean
    Binding bindingExchangeMessage2() {
        return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#");
    }

}

创建消息监听类

@Component
public class Receiver {

    @RabbitListener(queues = "topic.man")
    public void topicProcess(Map testMessage) {
        System.out.println("topic.man消费者收到消息  : " + testMessage.toString());
    }

    @RabbitListener(queues = "topic.woman")
    public void process(Map testMessage) {
        System.out.println("topic.woman消费者收到消息  : " + testMessage.toString());
    }
}

当routingkey传入topic.man时,以上两个队列都会监听到并消费,传入topic.man只有第一个会消费到

rabbitTemplate.convertAndSend("topicExchange", routingKey, manMap);

2.3 消息回调

添加publisher-confirm-type和publisher-returns

spring:
  #配置rabbitMq 服务器
  rabbitmq:
    host: 43.154.193.211
    port: 5672
    username: admin
    password: admin
    virtual-host: /hinata
    #确认消息已发送到交换机(Exchange)
    publisher-confirm-type: correlated
      #确认消息已发送到队列(Queue)
    publisher-returns: true

添加回调方法,消息只要被 rabbitmq broker 接收到就会触发 confirmCallback 回调,消息未能投递到目标 queue 里将触发回调 returnCallback,未能投递到目标queue的消息会丢失或送至死信队列

@Configuration
public class RabbitConfig {

    @Bean
    public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);
        //设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
        rabbitTemplate.setMandatory(true);

        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("ConfirmCallback:     "+"相关数据:"+correlationData);
                System.out.println("ConfirmCallback:     "+"确认情况:"+ack);
                System.out.println("ConfirmCallback:     "+"原因:"+cause);
            }
        });

        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("ReturnCallback:     "+"消息:"+message);
                System.out.println("ReturnCallback:     "+"回应码:"+replyCode);
                System.out.println("ReturnCallback:     "+"回应信息:"+replyText);
                System.out.println("ReturnCallback:     "+"交换机:"+exchange);
                System.out.println("ReturnCallback:     "+"路由键:"+routingKey);
            }
        });
        return rabbitTemplate;
    }
}

2.4 应答模式

rabbitmq共有三种应答模式
none(无应答模式):在这种模式下,不管消费者异常消费,还是正常消费,MQ服务器中的队列都会自动删除已消费的消息
auto(自动应答模式):当mq的应答模式配置为auto,或者没有进行配置时,系统会默认为自动应答模式。在这种情况下,只要我们的消费者,在消费消息的时候没有抛出异常,那服务端MQ会认为,消息消费正常,删除队列中的消息;如果消费过程中,抛出了异常,消息会进行自动补偿,重会队列头部,再次被拉到消费者的缓冲区(prefetch count),进行重复消费。此时如果缓冲区的大小设置为1,那么整个队列就会被阻塞,unacked也会显示为1(单线程消费的情况下)。
manual(手动应答模式):当设置为应手动应答时,我们需要在消费消息的时候手动告诉MQ我们消费的情况,否者MQ会一直等待消费端的消息,如果一直没有应答,当消费数量达到缓冲区大小(prefetch count)后,队列会全部阻塞,缓冲区中的消息会在客户端重启后再进行一次消费,直到被手动提交。

在application.yml,acknowledge-mode设置应答模式,max-attempts设置最大重试次数,也是auto模式下消费端异常最多重复投递次数,超过后会直接丢弃或放至死信队列

rabbitmq:
    host: 43.154.193.211
    port: 5672
    username: admin
    password: admin
    virtual-host: /hinata
    #确认消息已发送到交换机(Exchange)
    publisher-confirm-type: correlated
      #确认消息已发送到队列(Queue)
    publisher-returns: true
    listener:
      simple:
        acknowledge-mode: manual # 配置 Consumer 手动提交
        retry:
          enabled: true
          max-attempts: 5
          initial-interval: 1000

设置为manual手动提交后,有三种提交方法
1.basicAck:表示成功确认,使用此回执方法后,消息会被rabbitmq broker 删除。

void basicAck(long deliveryTag, boolean multiple) 

deliveryTag:表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加。手动消息确认模式下,我们可以对指定deliveryTag的消息进行ack、nack、reject等操作。

multiple:是否批量确认,值为 true 则会一次性 ack所有小于当前消息 deliveryTag 的消息。批量确认的意思是假设我先发送三条消息deliveryTag分别是5、6、7,可它们都没有被确认,当我发第四条消息此时deliveryTag为8,multiple设置为 true,会将5、6、7、8的消息全部进行确认。

2.basicNack:表示失败确认,一般在消费消息业务异常时用到此方法,可以将消息重新投递入队列。

void basicNack(long deliveryTag, boolean multiple, boolean requeue) 

deliveryTag:表示消息投递序号。

multiple:是否批量确认。

requeue:值为 true 消息将重新入队列,值为false消息将丢失或放入死信队列。

3.basicReject:拒绝消息,与basicNack区别在于不能进行批量操作,其他用法很相似。

void basicReject(long deliveryTag, boolean requeue)

deliveryTag:表示消息投递序号。

requeue:值为 true 消息将重新入队列。

消费端完整代码如下

@Component
public class Receiver {

    @RabbitListener(queues = "testDirectQueue")
    @RabbitHandler
    public void directProcess(Map<String, Object> map, Channel channel, Message message)throws IOException {
        try {
            System.out.println(1 / 0);
            System.out.println("DirectReceiver消费者收到消息  : " + map);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            System.out.println("DirectReceiver消费者收到消息,执行异常  : " + map);
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false);
        }
    }
}

2.5 死信队列

“死信”是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:

消息被否定确认,使用 channel.basicNack 或 channel.basicReject ,并且此时requeue属性被设置为false。
消息在队列的存活时间超过设置的生存时间(TTL)时间。
消息队列的消息数量已经超过最大队列长度。

那么该消息将成为“死信”。

“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃
如图为死信队列结构图
在这里插入图片描述
创建配置类DeadLetterRabbitConfig,配置了业务交换机与业务队列绑定,死信队列和死信交换机绑定,其中业务队列配置了死信交换机,当消息出现以上三种情况,会被丢进死信队列中

@Configuration
public class DeadLetterRabbitConfig {

    private static final String BUSINESS_EXCHANGE = "businessExchange";
    private static final String DEAD_LETTER_EXCHANGE = "deadLetterExchange";
    private static final String BUSINESS_QUEUE = "businessQueue";
    private static final String DEAD_LETTER_QUEUE = "deadLetterQueue";
    private static final String DEAD_LETTER_QUEUE_ROUTING_KEY = "deadLetterQueueRoutingKey";

    //业务交换机
    @Bean
    public FanoutExchange businessExchange(){
        return new FanoutExchange(BUSINESS_EXCHANGE);
    }

    //死信交换机
    @Bean
    public DirectExchange deadLetterExchange(){
        return new DirectExchange(DEAD_LETTER_EXCHANGE);
    }

    //业务队列
    @Bean
    public Queue businessQueue(){
        HashMap<String, Object> args = new HashMap<>();
        //指定死信交换机
        args.put("x-dead-letter-exchange",DEAD_LETTER_EXCHANGE);
        //指定死信交换机绑定死信队列的路由键
        args.put("x-dead-letter-routing-key",DEAD_LETTER_QUEUE_ROUTING_KEY);
        return QueueBuilder.durable(BUSINESS_QUEUE).withArguments(args).build();
    }

    //死信队列
    @Bean
    public Queue deadLetterQueue(){
        return new Queue(DEAD_LETTER_QUEUE);
    }

    //声明业务交换机和业务队列的绑定
    @Bean
    public Binding businessBind(){
        return BindingBuilder.bind(businessQueue()).to(businessExchange());
    }

    //声明死信交换机和死信队列的绑定
    @Bean
    public Binding deadLetterBind(){
        return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(DEAD_LETTER_QUEUE_ROUTING_KEY);
    }
}

配置消费者Receiver

@Component
public class Receiver {

    /**
     * 测试死信队列
     *
     * @param map
     * @param channel
     * @param message
     * @throws IOException
     */
    @RabbitListener(queues = "businessQueue")
    @RabbitHandler
    public void businessProcess(Map<String, Object> map, Channel channel, Message message) throws IOException {
        try {
            if (map.containsKey("dead-letter")) {
                System.out.println(1 / 0);
            }
            System.out.println("消费者收到消息  : " + map);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            System.out.println("消费者收到消息,执行异常  : " + map);
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }

    @RabbitListener(queues = "deadLetterQueue")
    @RabbitHandler
    public void deadLetterProcess(Map<String, Object> map, Channel channel, Message message) throws IOException {
        System.out.println("死信队列收到消息  : " + map);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

模拟发送消息

 @ApiOperation(value = "测试死信队列", notes = "测试死信队列")
    @PostMapping("/testDeadLetter")
    public String testDeadLetter(String content) {
        String messageId = String.valueOf(UUID.randomUUID());
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", content);
        map.put("createTime", createTime);
        rabbitTemplate.convertAndSend("businessExchange", "", map);
        return "ok";
    }

最终的执行结果,可以看到再消息basicNack后被扔进了死信队列中
在这里插入图片描述

2.6 延时队列

延时队列是在死信队列的基础上设置消息过期时间(TTL)从而达到延时消费的效果,有两种实现方法

1.设置队列的过期时间,增加x-message-ttl参数

 @Bean
    public Queue businessQueue(){
        HashMap<String, Object> args = new HashMap<>();
        //指定死信交换机
        args.put("x-dead-letter-exchange",DEAD_LETTER_EXCHANGE);
        //指定死信交换机绑定死信队列的路由键
        args.put("x-dead-letter-routing-key",DEAD_LETTER_QUEUE_ROUTING_KEY);
        //指定消息过期时间 单位:毫秒
        args.put("x-message-ttl",5000);
        return QueueBuilder.durable(BUSINESS_QUEUE).withArguments(args).build();
    }

2.设置每条消息的过期时间,在生产端指定每条消息的过期时间

 rabbitTemplate.convertAndSend("businessExchange", "", map,hock->{
            hock.getMessageProperties().setExpiration("5000");
            return hock;
        });

将businessQueue的消费端去掉,等到设定的时间后,会丢弃至死信队列中,监听死信队列完成业务处理。第二种方法并不能保证消息能在指定的时间被丢弃,RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列。尤其是第一个消息时间的延迟很长 30s,第二个消息的延迟很短 10s,第二个消息也不会优先执行,并且10s过去以后,第二条消息不会立即过期,而是会等第一条消息被消费后,消费第二条消息,才会判断过期。

2.7 插件实现延时队列

以上两种方法都有缺点,通过插件的方式用一个队列和一个交换机就可实现延时队列。首先在服务器上安装插件,将其上传至/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.9/plugins下

链接:https://pan.baidu.com/s/1mNRIWJnL-7NNydKdsUOOUw 
提取码:zde2

然后执行

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

添加配置类DelayedQueueRabbitConfig

@Configuration
public class DelayedQueueRabbitConfig {

    private static final String DELAYED_EXCHANGE = "delayedExchange";
    private static final String DELAYED_QUEUE = "delayedQueue";
    private static final String DELAYED_ROUTING_KEY = "delayedRoutingKey";


    //延时交换机
    @Bean
    public CustomExchange delayedExchange() {
        HashMap<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
    }


    //延时队列
    @Bean
    public Queue delayedQueue() {
        return new Queue(DELAYED_QUEUE);
    }

    //声明业务交换机和业务队列的绑定
    @Bean
    public Binding delayedBind() {
        return BindingBuilder.bind(delayedQueue()).to(delayedExchange()).with(DELAYED_ROUTING_KEY).noargs();
    }
}

消费端添加

@RabbitListener(queues = "delayedQueue")
    @RabbitHandler
    public void delayedQueueProcess(Map<String, Object> map, Channel channel, Message message) throws IOException {
        System.out.println("死信队列收到消息  : " + map + ",时间" + new Date());
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }

模拟发送消息

 @ApiOperation(value = "测试延时队列", notes = "测试延时队列")
    @PostMapping("/testDelayedQueue")
    public String testDelayedQueue(String content, Integer time) {
        String messageId = String.valueOf(UUID.randomUUID());
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", content);
        map.put("createTime", createTime);
        rabbitTemplate.convertAndSend("delayedExchange", "delayedRoutingKey", map, hock -> {
            hock.getMessageProperties().setDelay(time);
            return hock;
        });
        return "ok";
    }

分别发送 消息1/20000和消息2/2000,最终结果如下
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值