美文网首页RabbitMQJava
RabbitMQ入门-消息订阅模式

RabbitMQ入门-消息订阅模式

作者: Jackie_Zheng | 来源:发表于2017-08-06 18:11 被阅读255次

    消息派发

    上篇《RabbitMQ入门-消息派发那些事儿》发布之后,收了不少反馈,其中问的最多的还是有关消息确认以及超时等场景的处理。

    
    楼主,有遇到消费者后台进程不在,但consumer连接还在,当前消息是unacked状态,导致这个消息一直不被消费
    队列在等待回复的时候,这个消息是怎么存放的?如果一直没有返回有超时么?
    ...
    

    这里再对消息确认做以下补充
    有关超时
    RabbitMQ是没有超时概念的,如果一个消费者消费一条消息要花费很长时间,比如10分钟,那么这个过程会一直进行下去。除非你采用其他策略来中断它或者重试。

    消费者挂了怎么办
    如果我们不打开自动确认的标识autoAck,那么消费者在消费完成消息之后会发送一个确认标识给RabbitMQ。RabbitMQ接收到这个标识之后,就会将这条消息从内存中删除。

    但是正如上面的网友提到的那样,如果消费者后台进程不在即消费者挂了,这时候RabbiMQ会一直傻等着么?当然不会,RabbitMQ发现消费者挂了之后,它会很快将这条消息转而批发给下一个消费者消费,这样做也能够避免消息丢失的情况。

    下面我们启动了两个消费者,一个发送端,在第二个消费者接收到消息的时候,手动让其进程结束,这时候我们会发现最终生产的4条消息都被第一个消费者消费了,并没有出现消息丢失的情况。

    消息确认中断的处理.gif

    消息持久化

    上面的情况是在RabbitMQ正常提供服务时避免了消息丢失的情况,但是如果遇到RabbitMQ服务挂了,该如何保证消息不丢失呢?这时候就需要做持久化,细心的同学应该已经发现,在Work模式的发送端和接收端都做了持久化。****

    channel.basicPublish("", TASK_QUEUE_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8"));

    这里第三个参数指明了要持久化

    channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

    这里第二个参数知名了要持久化

    订阅者模式

    订阅者模式.png
    模型组成
    一个消费者Producer,一个交换机Exchange,多个消息队列Queue,多个消费者Consumer

    Exchange

    相比较于前两种模型Hello World和Work,这里多一个一个Exchange。其实Exchange是RabbitMQ的标配组成部件之一,前两种没有提到Exchange是为了简化模型,即使模型中没有看到Exchange的声明,其实还是声明了一个默认的Exchange。

    RabbitMQ中实际发送消息并不是直接将消息发送给消息队列,消息队列也没那么聪明知道这条消息从哪来要到哪去。RabbitMQ会先将消息发送个Exchange,Exchange会根据这条消息打上的标记知道该条消息从哪来到哪去。

    Exchange凭什么知道消息的何去何从,因为Exchange有几种类型:direct,fanout,topic和headers。这里说的订阅者模式就可以认为是fanout模式了。

    订阅者模式有何不同
    订阅者模式相对前面的Work模式有和不同?Work也有多个消费者,但是只有一个消息队列,并且一个消息只会被某一个消费者消费。但是订阅者模式不一样,它有多个消息队列,也有多个消费者,而且一条消息可以被多个消费者消费,类似广播模式。下面通过实例代码看看这种模式是如何收发消息的。

    发送端

    /**
     * Created by jackie on 17/8/6.
     */
    public class EmitLog {
    
        private static final String EXCHANGE_NAME = "logs";
    
        public static void main(String[] argv) throws Exception {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("192.168.3.161");
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
    
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
    
            String message = getMessage(argv);
    
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + "'");
    
            channel.close();
            connection.close();
        }
    
        private static String getMessage(String[] strings){
            if (strings.length < 1)
                return "info: Hello World!";
            return joinStrings(strings, " ");
        }
    
        private static String joinStrings(String[] strings, String delimiter) {
            int length = strings.length;
            if (length == 0) return "";
            StringBuilder words = new StringBuilder(strings[0]);
            for (int i = 1; i < length; i++) {
                words.append(delimiter).append(strings[i]);
            }
            return words.toString();
        }
    }
    
    • channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);添加了Exchange的声明,并且采用的是fanout类型

    • channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));声明了Exchange的名称,而不是像之前那样给了个空值

    接收端

    /**
     * Created by jackie on 17/8/6.
     */
    public class ReceiveLogs {
        private static final String EXCHANGE_NAME = "logs";
    
        public static void main(String[] argv) throws Exception {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("192.168.3.161");
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
    
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
            String queueName = channel.queueDeclare().getQueue();
            channel.queueBind(queueName, EXCHANGE_NAME, "");
    
            System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
    
            Consumer consumer = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope,
                                           AMQP.BasicProperties properties, byte[] body) throws IOException {
                    String message = new String(body, "UTF-8");
                    System.out.println(" [x] Received '" + message + "'");
                }
            };
            channel.basicConsume(queueName, true, consumer);
        }
    }
    
    • 这里通过String queueName = channel.queueDeclare().getQueue();来声明队列,采取该种方式会生成一个随机名字的消息队列,并且在断开连接时队列会自动删除,但是这并不会影响订阅者模式,因为该场景下所有绑定的queue都会收到消息

    • 通过channel.queueBind(queueName, EXCHANGE_NAME, "");将新建的Queue和Exchange绑定,因为是fanout模式,所以不需要指定routing key的值

    订阅者消息消费.gif
    • 启动了两个随机名称的消费者,它们Queue的名称不同

    • 启动生产者,发送一条消息,这时候可以发现两个接受端都收到了消息,这就是订阅者模式

    相关文章

      网友评论

        本文标题:RabbitMQ入门-消息订阅模式

        本文链接:https://www.haomeiwen.com/subject/zjknlxtx.html