1、RabbitMQ初探

RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等。

AMQP协议介绍

41d7d5639970a13667c9aae1c8ce37a94ed.jpg


RabbitMQ 的模型架构是基于AMQP协议,生产者将消息发送给交换器,队列通过RoutingKey(路由键)绑定对应的交换机 。当生产者发送消息时携带RoutingKey,交换机根据RoutingKey找到对应的队列,将消息存入到该队列,然后消费者通过订阅该队列来获取消息。RabbitMQ 中的交换器、队列、路由键等都是遵循的 AMQP 议中相应的概念。

其中比较重要的概念有 4 个,分别为:虚拟主机,交换机,队列,和绑定

  • 虚拟主机本质上就是一个mini版的mq服务器,有自己的队列、交换器和绑定,最重要的,自己的权限机制。Vhost提供了逻辑上的分离,可以将众多客户端进行区分,又可以避免队列和交换器的命名冲突。Vhost必须在连接时指定,rabbitmq包含缺省vhost:“/”,通过缺省用户和口令guest进行访问。

    rabbitmq里创建用户,必须要被指派给至少一个vhost,并且只能访问被指派内的队列、交换器和绑定。Vhost必须通过rabbitmq的管理控制工具创建。

  • 交换机:Exchange 用于转发消息,但是它不会做存储。这里有一个比较重要的概念:路由键 。消息到交换机的时候,交换机会转发到对应的队列中,那么究竟转发到哪个队列,就要根据该路由键。
  • 绑定:队列需要绑定到交换机。

 

常见问题

1、如果消息达到无人订阅的队列会怎么办?

消息会一直在队列中等待,RabbitMq默认队列是无限长度的。

2、多个消费者订阅到同一队列怎么办?

消息以循环的方式发送给消费者,每个消息只会发送给一个消费者。

3、消息路由到了不存在的队列怎么办?

一般情况下,凉拌,RabbitMq会忽略,当这个消息不存在,也就是这消息丢了。

 

Exchange类型有以下几种:

​ Fanout:广播,将消息交给所有绑定到交换机的队列

​ Direct:定向,把消息交给符合指定routing key 的队列。

​ Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

 

基本消息模型

5fc2322206c10cb65b93980a1526a8a38e8.jpg

每个amqp的实现都必须有一个direct交换器,包含一个空白字符串名称的默认交换器。声明一个队列时,会自动绑定到默认交换器,并且以队列名称作为路由键。

 

b355c9a6edab91e4562987749a24bfb1c32.jpg

点击,可以进行查看该交换机的详情。

1fd7200065083399984e7d8d895b547b19e.jpg

默认的 exchange 是一个由 broker 预创建的匿名的(即名字是空字符串) direct exchagne. 对于简单的程序来说, 默认的 exchange 有一个实用的属性: 如果没有显示地绑定 Exchnge, 那么创建的每个 queue 都会自动绑定到这个默认的 exchagne 中, 并且此时这个 queue 的 route key 就是这个queue 的名字.

 

下面将介绍一个例子体会一下这个最基础的消息模型

首先需要引入依赖

        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.0.0</version>
        </dependency>

 

创建一个连接的工具类ConnectionUtil,添加一个获取连接的方法

public class ConnectionUtil {
    /**
     * 建立与RabbitMQ的连接
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("182.168.6.133");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("/hello");
        factory.setUsername("suzhe");
        factory.setPassword("suzhe");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }

}

 

生产者Producer

/**
 * 生产者
 */
@Slf4j
public class Producer {

    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建一个信道,意味着每个线程单独一个信道
        Channel channel = connection.createChannel();
        // 声明(创建)队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 消息内容
        String message = "Less is more";
        // 向指定的队列中发送消息
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        log.debug("send message:{}", message);
        //关闭通道和连接
        channel.close();
        connection.close();
    }
}

运行后可以看到一条消息已经发送到了rabbitmq,并且从队里的bindings信息可以看到该队列绑定到了默认的交换机。

b26ab56c8159dd8207f6f1faa8580a90864.jpg

消费者Consumer:

/**
 * 消费者
 */
@Slf4j
public class Consumer {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建一个信道,意味着每个线程单独一个信道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                                       byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                log.debug("消费消息:{}",msg);
            }
        };
        // 监听队列,第二个参数:是否自动进行消息确认。
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

运行消费者,可以看到收到了消息。

03502c4dce27e6ce7de76515c28d7b9b933.jpg

在这个例子中, 我们并没有定义 exchange, 也没有显示地将 queue 绑定到 exchange 中, 因此 queue "hello" 就自动绑定到默认的 exchange 中了, 并且在默认的 exchange 中, 其 route key 和 queue 名一致, 都为"hello"。

由于这个原因, 我们就可以使用:

        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

来发送消息。

188655a3b8707a273bf0fae6db3603a46f6.jpg

调用 channel.basicPublish 时, 第一个参数是 exchange 名, 为空就是默认的 exchange, 第二个参数是 route key, 和 queue 名相同,第三个参数AMQP.BasicProperties 提供了一个构造器,可以通过builder() 来设置一些属性,比如

Map<String, Object> headers = new HashMap<String, Object>();
            headers.put("hello", "world");
            headers.put("aaa", "bbb");
 
 AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                    .deliveryMode(2) // 传送方式
                    .contentEncoding("UTF-8") // 编码方式
                    .expiration("10000") // 过期时间
                    .headers(headers) //自定义属性
                    .build();

 

信道

在上面的程序中,从连接中获取一个信道。

 Channel channel = connection.createChannel();

信道,概念:信道是生产消费者与rabbitmq通信的渠道,生产者publish或是消费者subscribe一个队列都是通过信道来通信的。信道是建立在TCP连接上的虚拟连接。什么意思呢?就是说rabbitmq在一条TCP上建立成百上千个信道来达到多个线程处理,这个TCP被多个线程共享,每个线程对应一个信道,信道在rabbitmq都有唯一的ID ,保证了信道私有性,对应上唯一的线程使用。

疑问:为什么不建立多个TCP连接呢?

原因是rabbitmq保证性能,系统为每个线程开辟一个TCP是非常消耗性能,每秒成百上千的建立销毁TCP会严重消耗系统。所以rabbitmq选择建立多个信道(建立在tcp的虚拟连接)连接到rabbitmq上。

 

本篇文章api总结:

发布消息:只用在生产者

channel.basicPublish(String exchange, //路由器的名字,即将消息发到哪个路由器
                       String routingKey, //路由键,即发布消息时,该消息的路由键是什么
                       BasicProperties props, //指定消息的基本属性
                       byte[] body)//消息体,也就是消息的内容,是字节数组

BasicProperties props:指定消息的基本属性,如deliveryMode为2时表示消息持久,2以外的值表示不持久化消息

//BasicProperties介绍
String corrId = "";
String replyQueueName = "";
Integer deliveryMode = 2;
String contentType = "application/json";
AMQP.BasicProperties props = new AMQP.BasicProperties
            .Builder()
            .correlationId(corrId)
            .replyTo(replyQueueName)
            .deliveryMode(deliveryMode)
            .contentType(contentType)
            .build();

接收消息:只用在消费者

channel.basicConsume(String queue, //队列名字,即要从哪个队列中接收消息
                      boolean autoAck, //是否自动确认,默认true
                      Consumer callback)//消费者,即谁接收消息

消费者中一般会有回调方法来消费消息

Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, //该消费者的标签
                                       Envelope envelope,//字面意思为信封:packaging data for the message
                                       AMQP.BasicProperties properties, //message content header data 
                                       byte[] body) //message body
                                       throws IOException {
                    //获取消息示例
                    String message = new String(body, "UTF-8");
                    //接下来就可以根据消息处理一些事情
            }
        };

详细源码地址

https://github.com/suzhe2018/rabbitmq-item

a1b79fdafb902578ce2e63ad1342c2a93f3.jpg

转载于:https://my.oschina.net/suzheworld/blog/3002222

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值