RabbitMQ

364 阅读11分钟

基本介绍

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在。

能够解决哪些问题

异步处理
应用解耦
流量消峰
日志处理。。。

常用的同类产品还有哪些

Rocket、activeMQ、Kafka

环境安装配置

安装教程

客户端界面基本介绍

添加一个用户

Virtual hosts 相当于mysql中的db,授权哪些用户,哪些用户才能访问

对用户进行授权

可以登陆自己添加的用户

五种队列

简单队列

  • P就是消息的生产者
  • 红色的是消息队列
  • C就是消费者

简单队列中一个消费者绑定一个队列:生产者将消息发送到队列,消费者从队列中获取消息

创建一个maven工程

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

连接信息

    public static Connection getConn() throws IOException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("root");
        factory.setPassword("root");
        Connection conn = factory.newConnection();
        return conn;
    }

生产者

    public final static String QUEUE_NAME = "q_test_01";
    public static void main(String[] args) throws IOException {
        Connection conn = ConnUtil.getConn();
        Channel channel = conn.createChannel();//从连接中创建通道
        //声明一个队列
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        //消息内容定义
        String message = "HelloWorld2!!!";
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
        System.out.println("message" + message);
        //关闭通道和连接
        channel.close();
        conn.close();
    }

消费者

    public static void main(String[] args) throws IOException, InterruptedException {
        Connection conn = ConnUtil.getConn();
        Channel channel = conn.createChannel();
        channel.queueDeclare(Provider.QUEUE_NAME,false,false,false,null);
        //定义队列消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        //监听队列
        channel.basicConsume(Provider.QUEUE_NAME, true, consumer);
        //获取消息
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String msg = new String(delivery.getBody());
            System.out.println(" [x] Received '" + msg + "'");
        }
    }

启动生产者和消费者可以看到生产者的发送的消息被消费者消费打印在控制台
不足之处:耦合性较高,生产者一一对应消费者,如果想有多个消费者消费队列中的消息就不行了

工作队列

简单队列是生产者消费者一一对应的,实际开发中,生产者发送消息毫不费力,而消费者一般要和业务结合。消费者接收到消息之后要和业务结合处理,需要花费较多时间,这时候队列中就会积压很多消息,这时候我么就用多个消费者来解决这个问题。这种工作队列生产者按照某种规则将消息发送到队列中,绑定队列的消费者消费这条消息,每个消息只能被消费一次。

生产者 发送100条消息

        Connection conn = ConnUtil.getConn();
        Channel channel = conn.createChannel();
        // 声明队列
        channel.queueDeclare(WorkConsumer1.QUEUE_NAME, false, false, false, null);
        for (int i = 0; i < 100; i++) {
            // 消息内容
            String message = "" + i;
            channel.basicPublish("", WorkConsumer1.QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");

            Thread.sleep( 10);
        }
        channel.close();
        conn.close();

消费者1

    public final static String QUEUE_NAME = "test_queue_work4";
    public static void main(String[] args) throws IOException, InterruptedException {
        Connection conn = ConnUtil.getConn();
        Channel channel = conn.createChannel();
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        channel.basicQos(1);//同一时刻服务器只会发送一条消息给消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        //监听队列 true表示自动 false表示手动返回状态
        channel.basicConsume(QUEUE_NAME, true, consumer);
        while (true) {//获取消息
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String msg = new String(delivery.getBody());
            System.out.println(" [1] Received '" + msg + "'");
            //Thread.sleep(10);
            // 返回确认状态,注释掉表示使用自动确认模式
           // channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }

消费者2

    public static void main(String[] args) throws IOException, InterruptedException {
        Connection conn = ConnUtil.getConn();
        Channel channel = conn.createChannel();
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        channel.basicQos(1);//同一时刻服务器只会发送一条消息给消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        //监听队列 true表示自动 false表示手动返回状态
        channel.basicConsume(QUEUE_NAME, true, consumer);
        while (true) {//获取消息
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String msg = new String(delivery.getBody());
            System.out.println(" [2] Received '" + msg + "'");
            Thread.sleep(1000);
            // 返回确认状态,注释掉表示使用自动确认模式
           //channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }

然后通过测试可以看出结果:即使两个消费者在处理消息时处理时间不同,但是它们最终消费的数量是一致的。其中一个消费者消费的消息都是奇数个,另一个是偶数个,这种默认的方式也就是所谓的轮询分发,优点之一就是可以轻易的并行工作。如果我们积压了好多工作,我们可以通过增加工作者(消费者)来解决这一问题,使得系统的伸缩性更加容易。在默认情况下,RabbitMQ将逐个发送消息到在序列中的下一个消费者(而不考虑每个任务的时长等等,且是提前一次性分配,并非一个一个分配)。平均每个消费者获得相同数量的消息。

但是这种在实际使用中显然不合理,因为消费者1线程停顿的时间短。应该是消费者1要比消费者2获取到的消息多才对。也就是说处理消息能力强的消费者应该多消费更多的消息,这时为了解决这个问题就可以使用公平分发的方式。

需要改变消息的应答确定机制

  1. 自动确认 只要消息从队列中获取,无论消费者获取到消息后是否成功消费,都认为是消息已经成功消费
  2. 手动确认 消费者从队列中获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么该消息将一直处于不可用状态。

修改如上代码

// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);
// 监听队列,false表示手动返回完成状态,true表示自动,autoAck = false也可以避免消息丢失的问题,如果正在处理消息的消费者挂了,此消息则会被交付给其他消费者
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);
//在消息消费成功后开启这行 表示使用手动确认模式
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

此时重启执行就可以看到效果“能者多劳”了

消息持久化

Boolan durable = false;
channel.queueDeclare(QUEUE_NAME, durable, false, false, null);

在声明队列的时候设置持久化,可以保证在mq挂了之后重启消息依然存在

发布订阅模式 publish_subscribe

X:交换机,转发器 作用:接受生产者消息和向队列推送消息

  1. 一个生产者,多个消费者
  2. 每个消费者都有自己的队列
  3. 生产者没有直接将消息发送到消息队列,而是发送到转发器exchange
  4. 每个队列都要绑定到转发器上
  5. 生产者发送的消息 经过交换机 到达队列 就能实现一个消息被多个消费者消费

生产者

    public final static String EXCHANGE_NAME = "test_exchange_fanout";
    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnUtil.getConn();
        Channel channel = connection.createChannel();
        // 声明exchange交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        // 消息内容
        String message = "Hello World!";
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");
        channel.close();
        connection.close();
    }

测试可以看到结果:一个消费可以被多个消费者消费,在生产者发送消息前订阅的才可以消费到消息,否则收不到消息
可以在客户端工具中看到队列和交换机绑定的情况

路由模式 routing

处理路由键

每个队列会绑定具有指定路由键【routingKey】的交换机,这时候交换机发送的消息中包含这些routingKey,就可以发送到这些队列中,消费者就能够成功消费。

提供者

    public final static String EXCHANGE_NAME = "test_exchange_direct";
    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnUtil.getConn();
        Channel channel = connection.createChannel();
        // 声明exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");//指定交换机类型
        // 消息内容
        String message = "Hello World!";
        String routingKey = "delete";
        channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");
        channel.close();
        connection.close();
    }

消费者1

    private final static String QUEUE_NAME = "queue_direct_1";
    private final static String EXCHANGE_NAME = "test_exchange_direct";
    public static void main(String[] argv) throws Exception {
        Connection connection = ConnUtil.getConn();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);
        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv1] Received '" + message + "'");
            Thread.sleep(10);
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }

消费者2

    private final static String QUEUE_NAME = "queue_direct_2";
    private final static String EXCHANGE_NAME = "test_exchange_direct";
    public static void main(String[] argv) throws Exception {
        Connection connection = ConnUtil.getConn();
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);
        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv2] Received '" + message + "'");
            Thread.sleep(10);
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }

从上面代码可以看到消费者2绑定了交换机上并且绑定了insert和delete两个路由键,消费者1只绑定了insert,测试当发送insert时,两个消费者都可以消费到,发送delete时只有消费者2可以打印

主题模式 topic

将路由键和某模式匹配

'#' : 匹配一个或多个
'*'' : 匹配一个
同一个消息被多个消费者获取。一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费到消息。

channel.exchangeDeclare(EXCHANGE_NAME, "topic");//分类
String routingKey = "goods.update";
channel.basicPublish(EXCHANGE_NAME,routingKey, null, msg.getBytes());
这个只能匹配增加功能
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.add");
这个可以匹配goods下的所有功能
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

如何保证消息的可靠性

在rabbitmq中可以通过持久化数据 解决rabbitmq服务器异常的数据丢失问题,但是有一个问题,生产者将消息发出去后到底有没有到达mq队列中呢?如果没有的话就会造成数据的丢失。 rabbitmq中有两种解决方法

事务

  1. channel.txSelect()声明启动事务模式;
  2. channel.txComment()提交事务;
  3. channel.txRollback()回滚事务;
提供者
    public static final String QUEUE_NAME = "test_queue_tx";
    public static void main(String[] args) throws IOException {
        Connection conn = ConnUtil.getConn();
        Channel channel = conn.createChannel();
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        String msg = "hello tx";
        try {
            channel.txSelect();
            channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
            channel.txCommit();
        } catch (Exception e) {
            channel.txRollback();
            System.out.println("事务回滚");
        } finally {
            channel.close();
            conn.close();
        }
    }
消费者
   public static void main(String[] args) throws Exception {
        Connection conn = ConnUtil.getConn();
        Channel channel = conn.createChannel();
        channel.queueDeclare(TxSend.QUEUE_NAME, false,false,false,null);
        //定义队列消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        //监听队列
        channel.basicConsume(TxSend.QUEUE_NAME, true, consumer);
        //获取消息
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String msg = new String(delivery.getBody());
            System.out.println(" [tx] Received '" + msg + "'");
        }
    }

经过测试可以得出结论,如果在发送消息时没有出现异常,可以正常将消息发送到队列中,并且消费者正常消费,如果在发送消息时加一个 int i = 10 / 0 而引起异常,这时事务就会回滚

这种方式虽然可以保证消息的可靠性,但是也降低了吞吐量

Confirm模式

生产者将信道【channel】设为confirm模式,一旦信道进入confirm模式,所有在该信道发布的消息都会指派一个唯一的id,就是标识这条消息的id,一旦消息被发送到所匹配的队列中,broker就会发送一个确认给生产者也,包含这个消息的id,【也就是回执一条消息】,这就使得生产者知道了消息是否到达目标队列,如果队列是可持久化的,确认消息写到磁盘后发出,最终传给生产者表明消息已经得到了处理。
confirm模式最大的好处是异步的
confirm模式分为三种:

  1. 普通模式 每发一条消息调用一次waitForConfirms()方法
  2. 批量模式 就是批量的发送消息,最后调用waitForConfirms()方法确定一批消息有没有被处理
  3. 异步模式 Channel对象提供的ConfirmListener()回调方法只包含deliveryTag【当前Chanel发出的消息序号】,我们需要自己为每一个Channel维护一个unconfirm的消息序号集合,每个publish一条数据,集合中元素加1,每回调一次handleAck方法,unconfirm集合删掉对应一条【multiple=false】或多条 【multiple=true】记录。从程序运行效率上看,这个unconfirm集合最好采用有序集合SortedSet存储结构。

普通:

    //队列的名称
    private static final String QUEUE_NAME = "test_simple_confirm";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Connection conn = ConnUtils.getConn();
        //从连接中获取一个通道
        Channel channel = conn.createChannel();
        //创建队列申明
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //生产者调用confirmSelect()将channel设置为confirm模式
        channel.confirmSelect();
        //发送的消息   
        String msg = "hello test_simple_confirm";
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
        //这样处理会大幅度降低rabbitmq的吞吐量

        System.out.println("send msg:" + msg);

        if (!channel.waitForConfirms()) {
            System.out.println("send message failed");
        } else {
            System.out.println("send message ok");
        }
        //关流

批量:

//发送的消息   批量只需要执行多次basicPublish()即可
String msg = "hello test_simple_confirm batch";
for (int i = 0; i < 10; i++) {//批量发送消息
    channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
}

异步:

//队列的名称
private static final String QUEUE_NAME = "test_simple_confirm3";

public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
    Connection conn = ConnUtils.getConn();
    //从连接中获取一个通道
    Channel channel = conn.createChannel();
    //创建队列申明
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    //生产者调用confirmSelect()将channel设置为confirm模式
    channel.confirmSelect();

    SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());

    channel.addConfirmListener(new ConfirmListener() {
        //没有问题
        @Override
        public void handleAck(long l, boolean b) throws IOException {
            if (b) {
                System.out.println("handleAck true");
                confirmSet.headSet(l+1).clear();
            } else {
                System.out.println("handleAck false");
                confirmSet.remove(l);
            }
        }
        //没有问题
        @Override
        public void handleNack(long l, boolean b) throws IOException {
            if (b) {
                System.out.println("handleNack true");
                confirmSet.headSet(l+1).clear();
            } else {
                System.out.println("handleNack false");
                confirmSet.remove(l);
            }
        }
    });

    //发送的消息
    String msg = "hello test_simple_confirm batch async";

    while (true) {
        long seqNo = channel.getNextPublishSeqNo();
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
        confirmSet.add(seqNo);
    }
}