消息队列之—RabbitMQ

目前项目中需要使用消息队列,同时行内开始搞国产化,综合几种消息队列的产品决定引入RabbitMQ,一起来学习下这款消息队列中间件吧。

RabbitMQ

AMQP协议模型

  • AMQP协议(Advanced Message Queuing Protocol,高级消息队列协议)是一个进程间传递异步消息的网络协议.

AMQP协议模型
在这里插入图片描述

  • 流程:(一般流程)
    • 生产者生产消息–》与Server建立连接–》找到对应的Virtual Host–》到对应的交换机–》然后将消息放入到对应 的queue
    • 消费者–》连接Server–》找到对应的Virtual Host–》到对应的queue中取到消息
  • 官方提供7种消息的发布的模式

几种消息队列产品的对比:

在这里插入图片描述

  • Activemq: 性能受到诟病。成熟,完善的手册。
  • kafka: 基于大数据的消息中间件,追求高的吞吐量,开始的目的是用于日志收集与传输。0.8版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格的要求。适合产生大数据的互联网服务数据收集业务。
  • RocketMQ: 官方版本支持事务,开源的版本不支持事务
  • Rabbitmq:erlang语开发的开源消息队列系统,基于AMQP协议来实现。AMQP主要特征面向消息、队列、路由(包括点对点和发布订阅)、可靠性、安全。AMQP协议更多用于企业系统对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。
    在这里插入图片描述
  • 虚拟主机(类比数据库中的表的概念)与用户进行绑定(首先创建用户,然后创建虚拟主机,再将虚拟主机和用户进行绑定)。

rabbitmq官方提供其中消息的方式

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

源码可以到:源码GitHub地址

方式一:直连:简单方式

在这里插入图片描述

  • 生产者代码
public class Provider {
    // 直连方式:生产消息
    @Test
    public void sendMesssge() throws IOException, TimeoutException {
        /*//创建连接mq的工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //设置连接mq的主机
        connectionFactory.setHost("localhost");
        // 设置端口 5672
        connectionFactory.setPort(5672);
        //设置连接的虚拟主机
        connectionFactory.setVirtualHost("/rabbitmqtest1");
        //设置用户名和密码
        connectionFactory.setUsername("rabbitmqtest1user");
        connectionFactory.setPassword("rabbitmqtest1user");

        //获取连接对象
        Connection connection = connectionFactory.newConnection();*/
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        //获取通道对象
        Channel channel = connection.createChannel();
        //通道绑定对应的消息队列
        // 参数1:队列名称(如果首次会自动新建名称)
        // 参数2:durable是否持久化 false 不持久话. true 持久化
        // 参数3: exclusive 是否独占队列 false不独占 ,true 独占队列
        // 参数4: autoDelete 消息消费完后是否删除 true 删除 ,false 不删除
        // 参数5: 其他参数
        channel.queueDeclare("rabbitmqtestqueue1", false, false, false, null);
        //发布消息
        // 参数1:交换机(简单模式没有交换机) 参数2: 队列名称 参数3:传递消息额外设置 参数4:消息的具体内容
        channel.basicPublish("", "rabbitmqtestqueue1", null, "你好".getBytes());

        /*channel.close();
        connection.close();*/
        RabbitMQConnectUtils.closeChannelAndConnection(channel,connection);
    }
}
  • 消费者代码
public class Consumer {

    public static void main(String[] args) throws IOException, TimeoutException {
        // 获取连接工厂对象
       /* ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setPort(5672);
        factory.setVirtualHost("/rabbitmqtest1");
        factory.setUsername("rabbitmqtest1user");
        factory.setPassword("rabbitmqtest1u);

        Connection connection = factory.newConnection();*/
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare("rabbitmqtestqueue1", false, false, false, null);

        //消费消息
        // 参数1:消费哪个队列的消息(队列名称)
        // 参数2:开启消息的自动确认机制
        // 参数3:消费时的回调
        channel.basicConsume("rabbitmqtestqueue1", true, new DefaultConsumer(channel) {
            @Override //最后一个参数 :消息队列中取出的消息
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                System.out.println("接收到消息:--> "+new String(body));

            }
        });
        // consumer端不建议关闭连接和通道(否则可能会接受不到回调,就结束了.)
//        channel.close();
//        connection.close();

    }
}

总结:

消费者和生产者的参数配置必须严格一致,否则可能出现异常问题。
通讯都是基于通道(Channel)的,生产者通过通道往队列里放消息;消费者通过通道从队列里取消息。

方式二:工作队列模型

在这里插入图片描述

在这里插入图片描述

  • 生产者代码
public class Provider {
    @Test
    public void sendMessage() throws IOException {
        //获取连接对象
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        //获取通道
        Channel channel = connection.createChannel();
        //通过通道声明队列
        channel.queueDeclare("workqueue", true, false, false, null);
        for (int i = 0; i < 20; i++) {
            //生产消息
            channel.basicPublish("", "workqueue", MessageProperties.PERSISTENT_TEXT_PLAIN, (i + "< > 你好工作队列").getBytes());
        }
        RabbitMQConnectUtils.closeChannelAndConnection(channel, connection);
    }
}

  • 消费者1代码
public class Consumer1 {
    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare("workqueue", true, false, false, null);
        channel.basicConsume("workqueue", true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                try {
                    Thread.sleep(1000); //由于工作队列默认的是对每个消费则平均分配任务,模拟其中一个处理较慢,是否还是平均分配
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("Consumer1 获取到工作消息--> " + new String(body));
            }
        });

    }
}

  • 消费者2代码

public class Consumer2 {
    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare("workqueue", true, false, false, null);
        channel.basicConsume("workqueue", true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                System.out.println("Consumer1 获取到工作消息--> "+new String(body));
            }
        });

    }
}

总结:平均分配是会产生处理慢的依旧会积累。

By default, RabbitMQ will send each message to the next consumer, in sequence. On average every consumer will get the same number of messages. This way of distributing messages is called round-robin. Try this out with three or more workers.
默认情况下,RabbitMQ会将每条消息依次发送给下一个消费者。平均而言,每个消费者将获得相同数量的消息。这种分发消息的方式称为循环。让三个或更多的工人试试这个方法。

弊端:

1.当一个消费者的处理能力很差,则会导致消息不能被及时的处理掉,造成消息的堆积
2.(自动确认机制中)如果其中一个消费者宕机,则会导致分配给它的任务丢失。

消息确认机制:

在这里插入图片描述

 // 参数:参数2:自动确认 true时, 消费者自动向rabbitmq确认消息消费. false 不会自动确认
channel.basicConsume("workqueue", true, new DefaultConsumer(channel) {}
// 此时消息队列会将消息一次性平均分给不同的消费者(弊端1);自动确认(弊端2)
  • 处理办法
// 1.首先通道告诉队列一次只消费一个消息
channel.basicQos(1);//每次只能消费一个消息
 // 参数:参数2:自动确认 true时, 消费者自动向rabbitmq确认消息消费. false 不会自动确认
// 2.关闭自动确认机制
channel.basicConsume("workqueue", false, new DefaultConsumer(channel) {}
// 业务逻辑处理
// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
//3.手动确认 参数1:手动确认消息标识 ,参数2:false,每次确认一个
channel.basicAck(envelope.getDeliveryTag(),false);
  • 处理问题的代码:
public class Consumer1 {
    public static void main(String[] args) throws IOException {
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        Channel channel = connection.createChannel();
        channel.basicQos(1);//每次只能消费一个消息
        //通道与队列建立连接
        channel.queueDeclare("workqueue", true, false, false, null);

        // 参数:参数2:自动确认 true时, 消费者自动向rabbitmq确认消息消费. false 不会自动确认
        channel.basicConsume("workqueue", false, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                try {
                    Thread.sleep(1000); //由于工作队列默认的是对每个消费则平均分配任务,模拟其中一个处理较慢,是否还是平均分配
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("Consumer1 获取到工作消息--> " + new String(body));
                //手动确认 参数1:手动确认消息标识(确认队列中哪个具体的消息) ,参数2:false,每次确认一个
                channel.basicAck(envelope.getDeliveryTag(),false);

            }
        });
    }
}

总结:

上述代码的优点
1:避免一次平均分配导致处理慢的消费者卡住,实现能者多劳。提高系统吞吐量
2:如果未确认,则在会一直在队列里。

方式三:(fanout)扇出、广播

应用场景:发一条消息被不同的业务系统同时去执行。(购物车结算时:同时调用订单系统、库存系统。)
在这里插入图片描述

  • 消费者1和消费者2,两个拿到的消息是同一个消息只是做不同的处理。

  • 生产者代码

public class Provider {
    public static void main(String[] args) throws IOException {
        //获取连接对象
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        //获取通道
        Channel channel = connection.createChannel();

        // 参数: 参数1: 交换机的名称, 参数2 : 交换机的类型: fanout: 广播类型
        channel.exchangeDeclare("logs", "fanout");

        //发送消息
        channel.basicPublish("logs","", MessageProperties.PERSISTENT_TEXT_PLAIN,"fanout Message".getBytes());

        RabbitMQConnectUtils.closeChannelAndConnection(channel,connection);
    }
}
  • 消费者代码(消费者代码相同,只是最后的业务处理逻辑不同)
  • 特点:所有的消费者拿到的消息都是相同的,处理逻辑不同。(发一条消息被不同的业务系统同时去执行)
public class Consumer1 {
    public static void main(String[] args) throws IOException {
        // 创建连接
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        //创建通道
        Channel channel = connection.createChannel();
        //通道绑定交换机
        channel.exchangeDeclare("logs", "fanout");

        //创建临时队列
        String queueName = channel.queueDeclare().getQueue();
        //将临时队列和交换机建立连接
        channel.queueBind(queueName, "logs", "");

        //消费消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                System.out.println("consumer1获取到了消息: " + new String(body));
            }
        });

    }
}

应用场景:

银行核心发送消息到每一个业务系统,不同的业务系统拿到相同的信息做不同的处理。
购物车模块结算时,需要同时掉订单、库存、支付等模块

方式四:路由模型

在这里插入图片描述

  • 生产者代码:
public class Provider {

    public static void main(String[] args) throws IOException {
        // 获取连接对象
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        //获取通道
        Channel channel = connection.createChannel();
        String exchangeName = "logs_direct";
        //绑定交换机
        channel.exchangeDeclare(exchangeName, "direct");
        //路由模式:需要routingKey
        String routingKey = "error";
        // 参数1:交换机名 参数2:路由key 参数3:持久化消息. 参数4:真正要发送的消息
        channel.basicPublish(exchangeName, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, ("这是路由模式下的rabbitmq消息[" + routingKey + "]请接收").getBytes());
        //关闭资源
        RabbitMQConnectUtils.closeChannelAndConnection(channel, connection);
    }
}
  • 消费者1代码:
public class Consumer1 {
    public static void main(String[] args) throws IOException {
        //创建连接
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        //获取通道
        Channel channel = connection.createChannel();
        //通道绑定队列
        channel.exchangeDeclare("logs_direct", "direct");
        // 获取临时queue
        String queue = channel.queueDeclare().getQueue();
        //将队列和交换机绑定,同时声明接受那些routing key
        channel.queueBind(queue, "logs_direct", "info");
        channel.queueBind(queue, "logs_direct", "error");
        channel.queueBind(queue, "logs_direct", "warning");
        channel.basicConsume(queue, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag,
                                       Envelope envelope,
                                       AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                System.out.println("Consumer1 获取消息--> " + new String(body));
            }
        });
    }
}

总结:

模式4:基于不同的应用场景对消息进行不同的执行路线,指定特定的程序去处理不同的业务。获取有选择的接收可能。
弊端:但是还不够灵活,不能基于多个标准进行路由。

方式五:Topics

topics类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topics类型Exchange可以让队列绑定Routing key 的时候使用通配符。这种模式的routingkey 一般是由一个或多个单词组成,多个单词之间以”.“ 分割。如:account.name
在这里插入图片描述

通配符:

*(star) can substitute for exactly one word. 仅匹配一个词
#(hash) can substitute for zero or more words. 匹配0个或多个

例如:

account.# 可以匹配: account 或account.name.value或account.name 等
account.* 可以匹配: account.name 或account.value

  • 消费者代码:
public class Provider {

    public static void main(String[] args) throws IOException {
        //获取连接
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        //获取通道Channel
        Channel channel = connection.createChannel();
        //交换机和通道绑定
        String exchangeName="logs_topic";
        channel.exchangeDeclare(exchangeName, "topic");
        //发布消息
        String routingKey = "user.save.aaa";//路由key
        channel.basicPublish(exchangeName, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, ("模型5基于动态路由的方式发布消息RoutingKey-> " + routingKey + "<").getBytes());
        RabbitMQConnectUtils.closeChannelAndConnection(channel, connection);
    }
}
  • 消费者1代码:仅支持"user.*" user后面跟一个词的情况
public class Consumer1 {
    public static void main(String[] args) throws IOException {

        //获取连接
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        //获取通道
        Channel channel = connection.createChannel();
        //通道绑定交换机
        String exchangeName = "logs_topic";
        channel.exchangeDeclare(exchangeName, "topic");
        //获取临时队列
        String queue = channel.queueDeclare().getQueue();
        //队列绑定交换机
        String routingKey = "user.*";
        channel.queueBind(queue, exchangeName, routingKey);
        //消费消息
        channel.basicConsume(queue, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("Consumer1 接收到了消息--> " + new String(body));
            }
        });

    }
}
  • 消费者2代码:支持"user.#" user后面跟0个或多个词的情况
public class Consumer2 {
    public static void main(String[] args) throws IOException {

        //获取连接
        Connection connection = RabbitMQConnectUtils.getRabbitMQConnection();
        //获取通道
        Channel channel = connection.createChannel();
        //通道绑定交换机
        String exchangeName = "logs_topic";
        channel.exchangeDeclare(exchangeName, "topic");
        //获取临时队列
        String queue = channel.queueDeclare().getQueue();
        //队列绑定交换机
        String routingKey = "user.#";
        channel.queueBind(queue, exchangeName, routingKey);
        //消费消息
        channel.basicConsume(queue, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("Consumer2 接收到了消息--> " + new String(body));
            }
        });
    }
}

总结:

topics 模式和模式4 路由模式的区别在于topic支持动态路由,即:路由key可以使用通配符。

方式六:RPC

方式七: Publisher Confirms

总结

区别:

  • 1.是否使用到交换机。
    • a:如果不使用交换机,生产者和消费者直接通过队列进行数据发布与获取(当然需要在Channel的帮助下)。
    • b:如果使用交换机:则生产者和消费者则是通过Exchange交换机进行数据的交换。
  • 2:不使用交换机的情况:
    • a:模式1:一对一,生产者和消费者一一对应。
    • b:模式2:一对多,一个生产者对应多个消费者,消息被平均分给每一个消费者。(需要关闭自动消息确认机制,提高服务器的吞吐量)
  • 3:使用交换机的情况 :
    • a: 模式3:扇出模型:一个消息可以被多个消费者消费。
    • b: 模式4:路由模型:一个消息可以指定被某一个消费者消费
    • c: 模式5:topics动态路由模型:在模式4的基础上可以使用动态路由key
生产者代码编写:
//获取连接
//获取通道Channel
//交换机和通道绑定
//发布消息
消费者代码编写:
//获取连接
//获取通道
 //通道绑定交换机
 //获取临时队列
 //队列绑定交换机
 //消费消息	
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

benboerdong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值