Rabbitmq学习笔记

1 什么是MQ

MQ(Message Queue): 是一种跨进程的通信机制, 用于上下游传递消息 . 在互联网架构中, MQ是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务 .

JMS(Java Message Server) : java 消息服务应用程序接口, 是一个java平台面向消息中间件的技术规范(API接口规范) .

Amqp(Advanced Message Queuing Protocol) : 高级消息队列协议使得遵从该规范的客户端应用和消息中间件服务器的全功能互操作成为可能. 跨平台,跨语言.

2 MQ 使用场景
  • 任务依赖:task3需要使用task2的输出作为输入, task2需要使用task1的输出作为输入 .
  • 不关心执行结果: 用户注册完成后 , 给用户发放注册券,通知用户领取新手任务等
  • 保证数据最终一致性: 如TCC (try-confirm-cancel)
  • 逻辑解耦&&物理解耦:
  • 错峰控流: 错开高峰,限制流量, 如秒杀
4 rabbitmq 主要特点
  • 可靠性:RabbitMQ使用一些机制来保证可靠性,如持久化、传输确认及发布确认等。
  • 灵活的路由:在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ己经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。
  • 扩展性:多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。
  • 高可用性:队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队仍然可用。
  • 多种协议:RabbitMQ除了原生支持AMQP协议,还支持STOMP,MQTT等多种消息中间件协议。
  • 多语言客户端:RabbitMQ几乎支持所有常用语言,比如Jav a、Python、Ruby、PHP、C#、JavaScript等。
  • 管理界面:RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。
  • 插件机制:RabbitMQ提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。
5 rabbitMQ的安装

RabbitMq是采用erlang语言编写的,安装Rabbitmq之前,必须安装erlang,

常见安装错误: 1 安装Erlang时openssl版本太低出错; 2 erlang和rabbitmq版本不匹配 ;

安装教程

6 RabbitMQ
6.1 The First Demo
<!-- 引入依赖 -->
<dependencies>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>3.4.1</version>
        </dependency>
</dependencies>

1 创建连接

package com.wunlie.rabbitmq.demo1;

import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.wunlie.rabbitmq.config.RabbitConfig;

/**
 * @Description: rabbit 连接工具
 * @author: wangjie
 * @createAt: 2019-09-09-15:07
 */
public class ConnectionUtil {

    public static Connection getRabbitConnection() throws Exception{
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost(RabbitConfig.HOST);
        //端口
        factory.setPort(RabbitConfig.PORT);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost(RabbitConfig.VIRTUAL_HOST);
        factory.setUsername(RabbitConfig.NAME);
        factory.setPassword(RabbitConfig.PASS);
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

2 发送消息

package com.wunlie.rabbitmq.demo1;


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wunlie.rabbitmq.config.RabbitConfig;

/**
 * @Description: 发送消息
 * @author: wangjie
 * @createAt: 2019-09-09-15:17
 */
public class Send {

    public static void main( String[] args ) throws  Exception {

        /**
         * 1 创建连接
         */
        Connection connection = ConnectionUtil.getRabbitConnection();

        /**
         * 2 从连接中创建通道
         */
        Channel channel = connection.createChannel();

        /**
         * 3 创建队列
         * String queue  队列名称,
         * boolean durable 持久的,
         * boolean exclusive 专有的,独有的,
         * boolean autoDelete  是否自动删除,
         * Map<String, Object> arguments  队列绑定的参数 如:超时时间,死信交换机
         */
        channel.queueDeclare(RabbitConfig.QUEUE_NAME,false,false,false,null);

        String message = "hello word!";

        /**
         * 发送消息
         * String exchange, 交换机
         * String routingKey, 路由
         * boolean mandatory, [可选] 为true时如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者。
         *                  为false时出现上述情形broker会直接将消息扔掉
         * boolean immediate, [可选] 为true时如果exchange在将消息route到queue(s)时发现对应的queue上没有消费者,那么这条消息不会放入队列中。
         *                  当与消息routeKey关联的所有queue(一个或多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。
         * BasicProperties props,  属性参数 14个属性
         * byte[] body 消息体
         */
        channel.basicPublish("",RabbitConfig.QUEUE_NAME,null,message.getBytes());

        channel.close();
        connection.close();
    }
}

3 接受消息

package com.wunlie.rabbitmq.demo1;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.wunlie.rabbitmq.config.RabbitConfig;

/**
 * @Description: 接收者
 * @author: wangjie
 * @createAt: 2019-09-09-15:41
 */
public class Recv {

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getRabbitConnection();
        // 从连接中创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(RabbitConfig.QUEUE_NAME, false, false, false, null);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);

        // 监听队列
        channel.basicConsume(RabbitConfig.QUEUE_NAME, true, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [x] Received '" + message + "'");
        }
    }
}

注意: 消息体属性参数

public static class BasicProperties extends AMQBasicProperties {
    /**
     * 消息类型如(text/plain)
     */
    private String contentType;
    /**
     * 消息内容编码
     */
    private String contentEncoding;
    /**
     *
     */
    private Map<String, Object> headers;
    /**
     * 消息持久化
     * 1 不持久化 2 持久化
     */
    private Integer deliveryMode;
    /**
     * 优先级
     * 0到 9
     */
    private Integer priority;
    /**
     * 相关标识 - 与这个相关的消息,用于将RPC响应与请求相关联, 比如 请求这里消息的请求。 建议应用程序使用这里属性,而不是将这里信息放入消息负载。
     * 值:任何值
     */
    private String correlationId;
    /**
     * 通常用于命名回调队列,(用于指定回复的队列的名称)
     */
    private String replyTo;
    /**
     * 过期时间
     */
    private String expiration;
    /**
     *  消息id
     */
    private String messageId;
    /**
     * 发送消息时的时间戳
     */
    private Date timestamp;
    /**
     * 类型
     */
    private String type;
    /**
     * 用户id
     */
    private String userId;
    /**
     * 应用程序id
     */
    private String appId;
    /**
     *
     */
    private String clusterId;
}

在这里插入图片描述

6.2 Exchange 交换机

The core idea in the messaging model in RabbitMQ is that the producer never sends any messages directly to a queue. Actually, quite often the producer doesn’t even know if a message will be delivered to any queue at all.

Instead, the producer can only send messages to an exchange. An exchange is a very simple thing. On one side it receives messages from producers and the other side it pushes them to queues. The exchange must know exactly what to do with a message it receives. Should it be appended to a particular queue? Should it be appended to many queues? Or should it get discarded. The rules for that are defined by the exchange type.

rabbitmq消息传递模型的核心思想是,生产者从不将任何消息直接发送到队列。实际上,生产者常常根本不知道消息是否会被传递到任何队列。

相反,生产者只能向交换发送消息。交换是一件很简单的事。一方面它接收来自生产者的消息,另一方面它将接收到的消息推送到队列中。交换必须确切地知道如何处理它接收到的消息。是否应将其附加到特定队列?它应该附加到许多队列中吗?或者应该被丢弃。其规则由交换类型定义。

交换机的类型有:direct, topic, fanout, headers. 通常我们只会用到前面三种

交换机和队列创建之后, 交换必须确切地知道如何处理它接收到的消息,所以我们必须告诉交换机将消息推送给哪个或哪些队列, 交换机与队列之间的这个关系称之为绑定(Binding), 通过这种绑定关系, 交换机处理它接受的消息.

在这里插入图片描述

生产者发送消息时,会指定交换器(exchange)和路由键(Routing key), 交换机会根据绑定键(Binding key),将消息推送到对应的队列(Queue)上.

  • 广播类型(fanout)的交换机, 没有绑定键(Binding key), 所以生产者在发送消息时不需要指定路由键(Routing key)

  • 直连类型(direct)的交换机, 只有当 Binding key == Routing key 时, 交换器才会将消息推送给对应的队列上, 而且直连交换机支持以一个绑定键(Binding key)与多个队列绑定.

  • 主题交换机(topic)的交换机, 绑定键(Binding key)可以含有通配符, 在Routing Key满足通配符的条件下,交换机将消息推送给对应的队列. * 可以是一个或多个单词, # 是一个单词.

7 死信队列
7.1 死信消息

在3种情况下, 消息会变成死信:

  • 1 消息被拒接, 并且 requeue = false
  • 2 消息的TTL过期
  • 3 队列达到最大长度
7.2 死信消息的处理过程

死信交换机(DLX): dead-letter-exchange, 它也只是一个正常的交换机, 通过属性x-dead-letter-exchange绑定到队列上,

当队列中的消息成为死信时, RabbitMQ就会自动将这个消息重新推送到设置的死信交换机(DLX)上, 进而贝推送到相应的队列上, 可以监听这个队列,对死信进行相应的处理.

7.3 示例
public class Provider {

    public static void main( String[] args ) throws  Exception {
        
        Connection connection = ConnectionUtil.getRabbitConnection();
        Channel channel = connection.createChannel();
        
        Map<String, Object> arguments = new HashMap<String,Object>(16);
        // 为队列设置队列交换器
        arguments.put("x-dead-letter-exchange", RabbitConfig.dlxExchangeName);
        // 设置队列中的消息 10s 钟后过期
        arguments.put("x-message-ttl", 30000);
        //arguments.put("x-dead-letter-routing-key", "为 dlx exchange 指定路由键,如果没有特殊指定则使用原队列的路由键");

        //交换机
        channel.exchangeDeclare(RabbitConfig.orderExchangeName, "topic", true, false, null);
        //队列
        channel.queueDeclare(RabbitConfig.orderQueueName, true, false, false, arguments);
        //绑定
        channel.queueBind(RabbitConfig.orderQueueName, RabbitConfig.orderExchangeName, RabbitConfig.orderRoutingKey);

        // 创建死信交换器和队列
        channel.exchangeDeclare(RabbitConfig.dlxExchangeName, "topic", true, false, null);
        channel.queueDeclare(RabbitConfig.dlxQueueName, true, false, false, null);
        channel.queueBind(RabbitConfig.dlxQueueName, RabbitConfig.dlxExchangeName, RabbitConfig.orderRoutingKey);
        
        for (int i = 0; i < 10; i++) {
            String message = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 创建订单." + i;
            //设置消息的过期时间
            AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                    .deliveryMode(2)
                    .contentEncoding("UTF-8")
                    .expiration("10000")
                    .build();

            channel.basicPublish(RabbitConfig.orderExchangeName, "order.save",
                    properties,
                    message.getBytes("UTF-8"));
            Thread.sleep(5000);
        }

        System.err.println("消息发送完成......");
    }
}
7.4 利用死信队列实现消息的延迟发送

消息的延迟发送,有两种实现方式, 1 利用TTL; 2 使用插件rabbitmq-delayed-message-exchange

//1 采用TTL
@Configuration
public class TtlExchangeAndQueueConfig {

    // 创建一个立即消费队列
    @Bean
    public Queue immediateQueue() {
        // 第一个参数是创建的queue的名字,第二个参数是是否支持持久化
        return new Queue(Constants.IMMEDIATE_QUEUE, true);
    }

    // 创建一个延时队列
    @Bean
    public Queue delayQueue() {
        Map<String, Object> params = new HashMap<>();
        // x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称,
        params.put("x-dead-letter-exchange", Constants.IMMEDIATE_EXCHANGE);
        // x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。
        params.put("x-dead-letter-routing-key", Constants.IMMEDIATE_ROUTING_KEY);
        return new Queue(Constants.DELAY_QUEUE, true, false, false, params);
    }

    @Bean
    public DirectExchange immediateExchange() {
        // 一共有三种构造方法,可以只传exchange的名字,
        // 第二种,可以传exchange名字,是否支持持久化,是否可以自动删除,
        // 第三种在第二种参数上可以增加Map,Map中可以存放自定义exchange中的参数
        return new DirectExchange(Constants.IMMEDIATE_EXCHANGE, true, false);
    }

    @Bean
    public DirectExchange deadLetterExchange() {
        // 一共有三种构造方法,可以只传exchange的名字, 第二种,可以传exchange名字,是否支持持久化,是否可以自动删除,
        //第三种在第二种参数上可以增加Map,Map中可以存放自定义exchange中的参数
        return new DirectExchange(Constants.DEAD_LETTER_EXCHANGE, true, false);
    }

    @Bean
    //把立即消费的队列和立即消费的exchange绑定在一起
    public Binding immediateBinding() {
        return BindingBuilder.bind(immediateQueue()).to(immediateExchange()).with(Constants.IMMEDIATE_ROUTING_KEY);
    }

    @Bean
    public Binding delayBinding() {
        return BindingBuilder.bind(delayQueue()).to(deadLetterExchange()).with(Constants.DELAY_ROUTING_KEY);
    }
}
//2 使用插件`rabbitmq-delayed-message-exchange
@Configuration
public class ExchangeAndQueueConfig {
    @Bean
    public CustomExchange delayExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        /**
         * name: 交换机名称
         * type: 类型
         * durable: 持久化
         * autoDelete: 自动删除
         * args: 参数
         */
        return new CustomExchange("test_exchange", "x-delayed-message",true, false,args);
    }

    @Bean
    public Queue queue() {
        Queue queue = new Queue("test_queue_1", true);
        return queue;
    }

    @Bean
    public Binding binding() {
        return BindingBuilder.bind(queue()).to(delayExchange()).with("test_queue_1").noargs();
    }
}

推送消息:

@Component
@Slf4j
public class RabbitmqProviderServer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendMessageWithTtl( String msg, int delayTime){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("sendMessageWithTtl 消息发送时间:"+sdf.format(new Date()));
        rabbitTemplate.convertAndSend(Constants.DEAD_LETTER_EXCHANGE, Constants.DELAY_ROUTING_KEY, msg, message -> {
            message.getMessageProperties().setExpiration(delayTime + "");
            return message;
        });
    }

    /**
     *
     * @param msg
     * @param delayTime 毫秒
     */
    public void sendMessageWithPlugins( String msg, int delayTime){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("sendMessageWithPlugins 消息发送时间:"+sdf.format(new Date()));
        rabbitTemplate.convertAndSend("test_exchange", "test_queue_1", msg, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage( Message message) throws AmqpException {
                message.getMessageProperties().setHeader("x-delay",delayTime);
                return message;
            }
        });
    }

}

c

//测试
    @Test
    public void send() throws Exception{
        for (int i = 3; i > 0; i--) {
            int delayTime = 5000 * i;
            String msg = "ttl 发送消息,当前时间:"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
            messageService.sendMessageWithTtl(msg,delayTime);

            Thread.sleep(1000);
        }

    }

    @Test
    public void send2()throws Exception {
        for (int i = 3; i > 0; i--) {
            int delayTime = 5000 * i;
            String msg = "plugin 发送消息,当前时间:"+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
            messageService.sendMessageWithPlugins(msg,delayTime);
            Thread.sleep(3000);
        }
    }

要求: 模拟发送三条消息 , 第一条在发送后15秒后消费,第二条在发送后10秒后消费; 第三条在发送后5秒消费.

测试结果:

消息消费时间:[2019-09-18 16:04:24] 消息内容:[ttl 发送消息,当前时间:2019-09-18 16:04:09]
消息消费时间:[2019-09-18 16:04:24] 消息内容:[ttl 发送消息,当前时间:2019-09-18 16:04:10]
消息消费时间:[2019-09-18 16:04:24] 消息内容:[ttl 发送消息,当前时间:2019-09-18 16:04:11]

消息消费时间:[2019-09-18 16:04:43] 消息内容:[plugin 发送消息,当前时间:2019-09-18 16:04:38]
消息消费时间:[2019-09-18 16:04:45] 消息内容:[plugin 发送消息,当前时间:2019-09-18 16:04:35]
消息消费时间:[2019-09-18 16:04:47] 消息内容:[plugin 发送消息,当前时间:2019-09-18 16:04:32]

测试结果说明, 采用TTL 发送延迟消息,并不能达到我们自有自定义延迟消费时间的要求, 而采用插件形式可以实现.

从结果可以看出,采用TTL形式,在队列中的第一条消息没有消费之前,后面的消息不能被消费. 采用插件形式,消息的消费是按照延迟时间到期的先后顺序消费的, 并不一定是"先进先出"消费的.

在具体的功能业务中,并不一定是先发送的消息需要先消费. 比如营销活动被审核通过之后,并不是马上就会上线使用,也可能后审核通过的营销活动先上线,在这种情况下,我们就需要采用插件形式; 还有一种情况,比如订单在创建之后,如果30分钟没有支付,订单的状态就需要改成为"已取消", 这种情况下,消息是"先进先出"的,我们就可以采用TTL的形式.

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值