RabbitMQ教程,快速上手

版权声明:本文为博主原创文章,转载需注明出处。 https://blog.csdn.net/david_pfw/article/details/83183255

1. 概述

        RabbitMQ是信息传输的中间者,本质上,从生产者接收消息,转达消息给消费者。换句话说,就是根据你指定的规则进行消息转发,缓冲和持久化。

        专业术语:    

         生产者(producer: 消息的发送者 。                                 

         队列(Queue依存于RabbitMQ内部,可以存储任意数量的消息,本质上是一个无限制的缓存,producer向同一队列发送消息,consumer从一个队列上接收消息。

         消费者(consumer等待接收消息的程序

         转发器(exchange生产者发送消息到转发器,转发器再传输消息到队列,一般所用转发器类型:DirectTopicHeaders,Fanout

         发送过程

2. 使用默认转发器体验消息队列

demo:一个producer发送消息,一个接收者接收消息,并在控制台打印出来。

               其中涉及到转发器,由于使用默认转发器,暂时忽略其调用,系统默认调用

     发送端:Send.java

扫描二维码关注公众号,回复: 4792019 查看本文章

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public class send{

    private final static String QUEUE_NAME = "hello";

    public static void main() throws IOException {

        //创建连接工厂

        ConnectionFactory factory = new ConnectionFactory();

        //设置主机和端口号

        factory.setHost("localhost");

        factory.setPort(5672);  //可不设置,默认为此端口

        //创建一个连接

        Connection connection = factory.newConnection();

        //创建一个频道

        Channel channel = connection.createChannel();

        //声明一个队列      参数:队列名,是否持久化,是否排外,是否自动删除,arguments

        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

 

 

        String message = "hello world";

        //发送一个消息

        //参数:  转发器,此处默认,队列名,

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

        //关闭频道和连接

        channel.close();

        connection.close();

    } 

}

接收端:Receive.java

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public class Receive{

    private final static String QUEUE_NAME = "hello";

    public static void main(String args[]) throws IOException,InterruptedException{

        //打开连接  与发送端一致

        ConnectionFactory factory = new ConnectionFactory();

        factory.setHost("localhost");

        Connection connection = factory.newConnection();

        Channel channel = connection.createChannel();

        //声明队列

        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        //创建消费者

        QueueingConsumer consumer = new QueueingConsumer(channel);

 

 

        //指定消费队列

        channel.basicConsume(QUEUE_NAME,true,consumer);

        //接收消息

        while(true){

            //接收消息的阻塞方法

            QueueingConsumer.Delivery delivery = consumer.nextDelivery();

            String message = new String(delivery.getBody());

            System.out.println(message);

        }

    }

}

值得注意的是队列只会在它不存在的时候创建,多次声明并不会重复创建。信息的内容是字节数组,也就意味着你可以传递任何数据。

3. 工作队列

Round-robin转发,消息应答,消息持久化与公平转发

工作队列的主要任务是:避免立刻执行资源密集型任务,然后必须等待其完成。相反地,我们进行任务调度:我们把任务封装为消息发送给队列。工作进行在后台运行并不断的从队列中取出任务然后执行。当你运行了多个工作进程时,任务队列中的任务将会被工作进程共享执行。

这样的概念在web应用中极其有用,当在很短的HTTP请求间需要执行复杂的任务。

3.1 Round-robin 转发

如果我们按照第2章发送和接收方式,并且每条消息均是耗时任务,先运行三个消费者,然后再运行一个生产者连续发送10个消息,那么3个生产者将会接收到的消息会如下分配

[x] Sent 'helloworld.1'
[x] Sent 'helloworld..2'
[x] Sent 'helloworld...3'
[x] Sent 'helloworld....4'
[x] Sent 'helloworld.....5'
[x] Sent 'helloworld......6'
[x] Sent 'helloworld.......7'
[x] Sent 'helloworld........8'
[x] Sent 'helloworld.........9'
[x] Sent 'helloworld..........10'

工作者1

Received 'helloworld.1'

Received 'helloworld....4'

Received 'helloworld.......7'

Received 'helloworld..........10'

工作者2

Received 'helloworld..2'

Received 'helloworld.....5'

Received 'helloworld........8'

工作者3

Received 'helloworld...3'

Received 'helloworld......6'

Received 'helloworld.........9'

RabbitMQ会一个一个的发送信息给下一个消费者(consumer),而不考虑每个任务的时长等等,且是一次性分配,并非一个一个分配。平均的每个消费者将会获得相等数量的消息。这样分发消息的方式叫做round-robin

3.2 消息应答

执行一个任务需要花费几秒钟。一旦RabbItMQ交付了一个信息给消费者,会马上从内存中移除这个信息。在这种情况下,如果一个工作者在执行任务时发生中断。 我们会丢失它正在处理的信息。我们也会丢失已经转发给这个工作者且它还未执行的消息。

为了保证消息永远不会丢失,RabbitMQ支持消息应答(message acknowledgments)。消费者发送应答给RabbitMQ,告诉它信息已经被接收和处理,然后RabbitMQ可以自由的进行信息删除。
如果消费者被杀死而没有发送应答,RabbitMQ会认为该信息没有被完全的处理,然后将会重新转发给别的消费者。通过这种方式,你可以确认信息不会被丢失,即使消者偶尔被杀死。

这种机制并没有超时时间这么一说,RabbitMQ只有在消费者连接断开是重新转发此信息。如果消费者处理一个信息需要耗费特别特别长的时间是允许的。

示例代码->消费者代码之中

1

2

3

channel.basicConsume(QUEUE_NAME,false,consume);  //关闭自动应答

//另外需要在每次处理完成一个消息后,手动发送一次应答。

channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

3.3 消息持久化

RabbitMQ退出或者异常退出,将会丢失所有的队列和信息,除非告诉它不要丢失。

所以需要给所有的队列和消息,设置持久化的标志。

第一, 需要声明队列为持久化的。

1

2

boolean durable = true;

channel.queueDeclare("task_queue", durable, false, false, null);

注:RabbitMQ不允许使用不同的参数重新定义一个队列,所以已经存在的队列,我们无法修改其属性。

第二, 需要标识信息为持久化的。

通过设置MessagePropertiesimplements BasicProperties)值为PERSISTENT_TEXT_PLAIN

1

channel.basicPublish("", "task_queue",MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());

3.4 公平转发

有一种情况,对于两个消费者,有一系列的任务,奇数任务特别耗时,而偶数任务却很轻松,这样造成一个消费者一直繁忙,另一个消费者却很快执行完任务后等待。

造成这样的原因是RabbitMQ仅仅是当消息到达队列进行转发消息。并不在乎有多少任务消费者并未传递一个应答给RabbitMQ。仅仅盲目转发所有的奇数给一个消费者,偶数给另一个消费者。

为了解决这样的问题,使用basicQos方法,传递参数为prefetchCount = 1。这样告诉RabbitMQ不要在同一时间给一个消费者超过一条消息。换句话说,只有在消费者空闲的时候会发送下一条信息。

1

2

int prefetchCount = 1;

channel.basicQos(prefetchCount);

    此部分完整代码:

生产者:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

public class NewTask

{

    //队列名称

    private final static String QUEUE_NAME = "workqueue";

    public static void main(String[] args) throws IOException

    {

        //创建连接和频道

        ConnectionFactory factory = new ConnectionFactory();

        factory.setHost("localhost");

        Connection connection = factory.newConnection();

        Channel channel = connection.createChannel();

        //声明队列

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //发送10条消息,依次在消息后面附加1-10个点

        for (int i = 0; i < 10; i++)

        {

            String dots = "";

            for (int j = 0; j <= i; j++)

            {

                dots += ".";

            }

            String message = "helloworld" + dots+dots.length();

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

            System.out.println(" [x] Sent '" + message + "'");

        }

        //关闭频道和资源

        channel.close();

        connection.close();

    }

}

  

消费者:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

public class Work

{

    //队列名称

    private final static String QUEUE_NAME = "workqueue";

    public static void main(String[] argv) throws java.io.IOException,

            java.lang.InterruptedException

    {

        //区分不同工作进程的输出

        int hashCode = Work.class.hashCode();

        //创建连接和频道

        ConnectionFactory factory = new ConnectionFactory();

        factory.setHost("localhost");

        Connection connection = factory.newConnection();

        Channel channel = connection.createChannel();

        //声明队列

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        System.out.println(hashCode

                + " [*] Waiting for messages. To exit press CTRL+C");

        QueueingConsumer consumer = new QueueingConsumer(channel);

        // 指定消费队列

        channel.basicConsume(QUEUE_NAME, true, consumer);

        while (true)

        {

            QueueingConsumer.Delivery delivery = consumer.nextDelivery();

            String message = new String(delivery.getBody());

            System.out.println(hashCode + " [x] Received '" + message + "'");

            doWork(message);

            System.out.println(hashCode + " [x] Done");

        }

    }

 

    /**

    * 每个点耗时1s

    * @param task

    * @throws InterruptedException

    */

    private static void doWork(String task) throws InterruptedException

    {

        for (char ch : task.toCharArray())

        {

            if (ch == '.')

                Thread.sleep(1000);

        }

    }

}

  

4. 转发器(exchange)

RabbitMQ消息模型的核心理念是生产者永远不会直接发送任何消息给队列,一般的情况生产者甚至不知道消息应该发送到哪些队列。

相反的,生产者只能发送消息给转发器(Exchange)。转发器是非常简单的,一边接收从生产者发来的消息,另一边把消息推送到队列中。转发器必须清楚的知道消息如何处理它收到的每一条消息。是否应该追加到一个指定的队列?是否应该追加到多个队列?或者是否应该丢弃?这些规则通过转发器的类型进行定义。

一般可用的转发器类型:Direct    Topic    Headers    Fanout

声明转发器及其类型

channel.exchangeDeclare("logs","fanout");

生产者消息发送到转发器:

channel.basicPublish("转发器","routingKey",null,message.getBytes());

//routingKey(第二个参数),消息由routingKey决定发送到哪个队列

4.1 订阅与发布   fanout转发器

把一个消息发给多个消费者,这种模式称之为发布/订阅(类似观察者模式)。

Fanout转发器

channel.exchangeDeclare("logs","fanout");

fanout类型转发器特别简单,把所有它介绍到的消息,广播到所有它所知道的队列,此类方法最适合做发布与订阅。

临时队列(Temporary queues

前面我们都为队列指定了一个特定的名称,生产者和消费者间共享队列时,为队列命名是很重要的。

不过,对于不关心队列的名称,只想要接收到所有的消息,而且也只对当前正在传递的数据的感兴趣的。使用如下方案:

第一, 无论什么时间连接到Rabbit我们都需要一个新的空的队列。为了实现,我们可以使用随机数创建队列,或者更好的,让服务器给我们提供一个随机的名称。

第二, 一旦消费者与Rabbit断开,消费者所接收的那个队列应该被自动删除。

Java中可以使用queueDeclare()方法,不传递任何参数,来创建一个非持久的、唯一的、自动删除的队列且队列名称由服务器随机产生

String queueName = channel.queueDeclare().getQueue();

 

绑定(Bindings

创建好fanout转发器和队列之后,需要通过binding告诉转发器把消息发送给他们的队列

channel.queueBind(queueName,"logs""");

//参数:队列名称 转发器名称 routingKey

4.2 路由选择 Direct转发器

4.2.1 绑定(Bindings)

channel.queueBind(queueName, EXCHANGE_NAME, "");
绑定表示转发器与队列之间的关系。我们也可以简单的认为:队列对该转发器上的消息感兴趣。

绑定可以附带一个额外的参数routingKey。为了与避免basicPublish方法(发布消息的方法)的参数混淆,我们准备把它称作绑定键(binding key)。下面展示如何使用绑定键(binding key)来创建一个绑定:

channel.queueBind(queueName, EXCHANGE_NAME, "black");

绑定键的意义依赖于转发器的类型。对于fanout类型,忽略此参数。

4.2.2 直接转发(Direct exchange

irect类型的转发器背后的路由转发算法很简单:消息会被推送至绑定键(binding key)和消息发布附带的选择键(routing key)完全匹配的队列。

图解:

上图,我们可以看到direct类型的转发器与两个队列绑定。第一个队列与绑定键orange绑定,第二个队列与转发器间有两个绑定,一个与绑定键black绑定,另一个与green绑定键绑定。
这样的话,当一个消息附带一个选择键(routing key orange发布至转发器将会被导向到队列Q1。消息附带一个选择键(routing keyblack或者green将会被导向到Q2.所有的其他的消息将会被丢弃。

4.2.3 多重绑定(multiple bindings

使用一个绑定键(binding key)绑定多个队列是完全合法的。如上图,一个附带选择键(routing key)的消息将会被转发到Q1Q2

4.3 主题   Topic转发器

虽然使用direct类型改良了系统,但是仍然存在一些局限性:它不能够基于多重条件进行路由选择。

在我们的日志系统中,我们有可能希望不仅根据日志的级别而且想根据日志的来源进行订阅。这个概念类似unix工具:syslog,它转发日志基于严重性(info/warning/crit…)和设备(auth/cron/kern…
这样可能给我们更多的灵活性:我们可能只想订阅来自’cron’的致命错误日志,而不是来自’kern’的。

为了在我们的系统中实现上述的需求,我们需要学习稍微复杂的主题类型的转发器(topic exchange)。

4.3.1 主题转发(Topic Exchange

发往主题类型的转发器的消息不能随意的设置选择键(routing_key),必须是由点隔开的一系列的标识符组成。标识符可以是任何东西,但是一般都与消息的某些特性相关。一些合法的选择键的例子:"stock.usd.nyse", "nyse.vmw","quick.orange.rabbit".可以定义任何数量的标识符,上限为255个字节。

绑定键和选择键的形式一样。主题类型的转发器背后的逻辑和直接类型的转发器很类似:一个附带特殊的选择键将会被转发到绑定键与之匹配的队列中。需要注意的是:关于绑定键有两种特殊的情况。
*可以匹配一个标识符。

#可以匹配0个或多个标识符。

例:绑定转发器和队列

channel.queueBind(queueName, EXCHANGE_NAME, "kernel.*");

4.3.2 图解

我们准备发送关于动物的消息。消息会附加一个选择键包含3个标识符(两个点隔开)。第一个标识符描述动物的速度,第二个标识符描述动物的颜色,第三个标识符描述动物的物种:<speed>.<color>.<species>
我们创建3个绑定键:Q1*.orange.*绑定Q2*.*.rabbitlazy.#绑定。
可以简单的认为:
Q1对所有的橙色动物感兴趣。
Q2想要知道关于兔子的一切以及关于懒洋洋的动物的一切。
一个附带quick.orange.rabbit的选择键的消息将会被转发到两个队列。附带lazy.orange.elephant的消息也会被转发到两个队列。另一方面quick.orange.fox只会被转发到Q1lazy.brown.fox将会被转发到Q2lazy.pink.rabbit虽然与两个绑定键匹配,但是也只会被转发到Q2一次。quick.brown.fox不能与任何绑定键匹配,所以会被丢弃。
如果我们违法我们的约定,发送一个或者四个标识符的选择键,类似:orangequick.orange.male.rabbit,这些选择键不能与任何绑定键匹配,所以消息将会被丢弃。
另一方面,lazy.orange.male.rabbit,虽然是四个标识符,也可以与lazy.#匹配,从而转发至Q2
注:主题类型的转发器非常强大,可以实现其他类型的转发器。
当一个队列与绑定键#绑定,将会收到所有的消息,类似fanout类型转发器。

当绑定键中不包含任何#*时,类似direct类型转发器。


如果您觉得博主写的文章对您有帮助,可以请博主喝杯茶哦,O(∩_∩)O~

博主:诸葛本不亮

简介:毕业后做过多年程序猿、架构设计、项目经理、部门总监,待过传统软件公司,也在大型互联网公司负责过平台研发部,在这个行业浸淫十多年,在系统架构、SaaS平台搭建、项目管理以及部门管理等方面有着多年的一线实践经验。

目前与好友合伙创办了一个软件工作室,提供各类系统解决方案、咨询服务及技术合作,企业软件(SaaS平台、企业应用、商城、微信公众号及小程序开发)定制开发服务,大数据服务(数据采集及清洗、数据分析等),以及程序猿职业规划及转型咨询服务(程序猿转架构师、转项目管理、转技术管理等,可以提供相应的一线资料帮助您成功转型,获得大厂offer)。

微信号:danwang2138

猜你喜欢

转载自blog.csdn.net/david_pfw/article/details/83183255