RabbitMq知识整理以及在java语言下的简单实例

简单介绍:

使用Erlang语言编写,主用于在分布式系统中存储转发消息,使用AMQP协议(ConnectionQueue,Channel,Exchange,Binding等概念,下面在mq介绍中解释),相比于redis而言支持事物保证消息可靠传播,性能高,集群稳定等优势,但是redis更加轻量级,高敏感性。

一、组件介绍:

ConnectionFactory、Connection、Channel都是RabbitMQ对外提供的API中最基本的对象。Connection是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑。ConnectionFactory为Connection的制造工厂。 Channel是我们与RabbitMQ打交道的最重要的一个接口,我们大部分的业务操作是在Channel这个接口中完成的,包括定义Queue、定义Exchange、绑定Queue与Exchange、发布消息等。

1.Queue

Queue(队列)是RabbitMQ的内部对象,用于存储消息;

多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。

2.Exchange

生产者将消息发送到Exchange(交换器,下图中的X),由Exchange将消息路由到一个或多个Queue中(或者丢弃)。

3.routing key

生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联合使用才能最终生效。 在Exchange Type与binding key固定的情况下(在正常使用时一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过指定routing key来决定消息流向哪里。

4.Binding

RabbitMQ中通过Binding将Exchange与Queue关联起来,这样RabbitMQ就知道如何正确地将消息路由到指定的Queue了。

在绑定(Binding)Exchange与Queue的同时,一般会指定一个binding key;消费者将消息发送给Exchange时,一般会指定一个routing key;当binding key与routing key相匹配时,消息将会被路由到对应的Queue中。 在绑定多个Queue到同一个Exchange的时候,这些Binding允许使用相同的binding key。 binding key 并不是在所有情况下都生效,它依赖于Exchange Type,比如fanout类型的Exchange就会无视binding key,而是将消息路由到所有绑定到该Exchange的Queue。

5.Exchange Types

exchange怎么和队列queue绑定呢,按照什么规则呢:

RabbitMQ常用的Exchange Type有fanout、direct、topic:

5.1 fanout

fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。

5.2 direct

direct类型的Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中。

5.3 topic

前面讲到direct类型的Exchange路由规则是完全匹配binding key与routing key,但这种严格的匹配方式在很多情况下不能满足实际业务需求。topic类型的Exchange在匹配规则上进行了扩展,它与direct类型的Exchage相似,也是将消息路由到binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同,它约定:

  • routing key为一个句点号“. ”分隔的字符串

  • binding key与routing key一样也是句点号“. ”分隔的字符串

  • binding key中可以存在两种特殊字符“*”与“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)

二、生产消息(confirm模式的编程实现):

RabbitMq类:

public class RabbitMq
{
    public static final String exchangeName = "exchange";
    
    public static final String queueName = "queue";
    
    public static final String routingKey = "routingKey";
    
    public static final String bindingKey = "routingKey";
    
    public static void main(String[] args)
    {
        
        int count = 1;
        
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setPort(5672);
        
        // 创建生产者
        Sender producer = new Sender(factory, count, exchangeName, queueName, routingKey, bindingKey);
        producer.run();
    }
}

发送消息类:

public class Sender
{
    private ConnectionFactory factory;
    
    private int count;
    
    private String exchangeName;
    
    private String queueName;
    
    private String routingKey;
    
    private String bindingKey;
    
    public Sender(ConnectionFactory factory, int count, String exchangeName, String queueName, String routingKey,
        String bindingKey)
    {
        this.factory = factory;
        this.count = count;
        this.exchangeName = exchangeName;
        this.queueName = queueName;
        this.routingKey = routingKey;
        this.bindingKey = bindingKey;
    }
    
    public void run()
    {
        Channel channel = null;
        try
        {
            Connection connection = factory.newConnection();
            channel = connection.createChannel();
            // 创建exchange,采用direct方式完全匹配
            channel.exchangeDeclare(exchangeName, "direct", true, false, null);
            // 创建队列,true为持久化消息
            channel.queueDeclare(queueName, true, false, false, null);
            // 绑定exchange和queue
            channel.queueBind(queueName, exchangeName, bindingKey);
            channel.confirmSelect();
            // 发送持久化消息
            for (int i = 0; i < count; i++)
            {
                // 第一个参数是exchangeName(默认情况下代理服务器端是存在一个""名字的exchange的,
                // 因此如果不创建exchange的话我们可以直接将该参数设置成"",如果创建了exchange的话
                // 我们需要将该参数设置成创建的exchange的名字),第二个参数是路由键
                channel.basicPublish(exchangeName,
                    routingKey,
                    MessageProperties.PERSISTENT_BASIC,
                    ("第" + (i + 1) + "条消息").getBytes());
            }
            long start = System.currentTimeMillis();
            channel.addConfirmListener(new ConfirmListener() //使用listener异步确认效率高,还有串行confirm,批量串行效率较  低,编程简单
            {
                
                @Override
                public void handleNack(long deliveryTag, boolean multiple)
                    throws IOException
                {
                    System.out.println("nack: deliveryTag = " + deliveryTag + " multiple: " + multiple);
                }
                
                @Override
                public void handleAck(long deliveryTag, boolean multiple)
                    throws IOException
                {
                    System.out.println("ack: deliveryTag = " + deliveryTag + " multiple: " + multiple);
                }
            });
            System.out.println("执行ConfirmListener耗费时间: " + (System.currentTimeMillis() - start) + "ms");
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

三,消费者代码:

public class ConsumerTest
{
    /**
     * 订阅方式其实是向queue注册consumer,通过rpc向queue server发送注册consumer的消息,
     * rabbitMQ Server在收到消息后,根据消息的内容类型判断这是一个订阅消息,
                             这样当MQ 中queue有消息时,会自动把消息通过该socket(长连接)通道发送出去。
     */
    public static void main(String[] args) throws IOException, ShutdownSignalException,
    ConsumerCancelledException, InterruptedException, TimeoutException {  

        // 创建链接工厂
        ConnectionFactory connFac = new ConnectionFactory() ;  

        //默认链接的主机名,RabbitMQ-Server安装在本机,所以可以直接用127.0.0.1
        connFac.setHost("127.0.0.1");  

        //创建链接    
        Connection conn = connFac.newConnection() ;  

        //创建信息管道  
        final Channel channel = conn.createChannel() ;  

        //定义Queue名称  
        String queueName = RabbitMq.queueName;  
        //1.队列名2.是否持久化,3是否局限与链接,4不再使用是否删除,5其他的属性  是否持久化需要和生成者队列设置的一样否则报错
        channel.queueDeclare(queueName, true, false, false, null) ;  

        // 同一时刻服务器只会发一条消息给消费者(能者多劳模式)
        channel.basicQos(1);
        //上面的部分,与Test01是一样的  

        //声明一个消费者,配置好获取消息的方式  
//       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("Customer Received '" + message + "'");
//            }  
//        };    
      
        boolean autoAck = false;//手动确认ACk
 
        //channel.basicConsume(queueName, autoAck, consumer) ;
        channel.basicConsume(queueName, autoAck, "myConsumerTag",
             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("Customer Received '" + message + "'");
                     long deliveryTag = envelope.getDeliveryTag();
                     // (process the message components here ...)
                     channel.basicAck(deliveryTag, false);
                 }
             });
    }
}

代码亲测可以运行。并且持久化成功到本地,需要多个消费者可以用多线程实现。本文只作为互相学习使用,有问题欢迎大家指点!

发布了23 篇原创文章 · 获赞 4 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq1076472549/article/details/81538721