RabbitMQ学习

  1. 如何安装RabbitMQ服务?

  • 先安装erlang环境。

  • 再安装rabbit服务。

  • 启动服务。

rabbitmq-plugins enable rabbitmq_management
  • 启动监控管理器。

rabbitmq-service start
  1. RabbitMQ的基础架构是什么?

  • 架构图如下所示:

  • Broker(中间件)中的Queue(队列)是和消费者绑定的,绑定的消费者会消费队列中的消息。如果多个消费者绑定一个队列,会轮询消费。Rabbit是推模式的,也就是会将消息轮着推给不同的消费者。

  • 队列是有一个BindingKey(绑定key)的,可以理解为URL地址。消息上是有RoutingKey(路由key)的,可以理解为访问地址。

  • publisher就是消息生产者,消息生产者产出消息后,可以根据指定的exchangeName(交换器名)和路由key将消息推到不同的队列中,进而推给不同的消费者使用。

  • exchange(交换器)就是为了处理路由key和绑定key之间的关系的,常用的有3种,还有一种head,这里不做介绍了。

  • 直连,路由key和绑定key一致。

  • 扇形,不考虑key,所有对接了该交换器的队列都发一份。

  • 主题,依据规则,推送给负荷条件的队列。

  • 注:交换器可以理解为DNS服务器。

  • 注:消息如果在交换器中没有找到对应的队列,会被丢弃。

  1. 交换器是如何创建的?

  • 可以在监控器中创建。

  • 可以在生产者端/消费者端创建。创建代码如下:

//绑定交换器
channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT);
  • 可以不指定交换器,会自动匹配一个默认交换器,默认交换器是直连交换器。

  1. 消息队列是如何创建的?

  • 在监控平台创建。

  • 在消费者端创建,示例代码如下:

channel.queueDeclare(queueName, false, false, false, null);
  1. 生产者如何向中间件发送数据,并保证数据的安全?

  • 注:需要先创建新用户,rabbit默认不允许使用guest用户连接中间件。

  • maven项目添加依赖

<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.7.3</version>
</dependency>
  • 直连交换器示例代码如下:

public static void main(String[] args) throws IOException, TimeoutException {
    String exchangeName = "exchange1";
    String routingKey = "queue.com1";

    //创建连接工厂
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    factory.setPort(5672);
    factory.setUsername("root");
    factory.setPassword("root");
    //创建连接
    Connection connection = factory.newConnection();
    //创建通道
    Channel channel = connection.createChannel();
    //绑定交换器
    channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT);//类型是直连,扇形、主题可以依据类型更改。
    //通道发送数据
    for (int i = 1 ; i < 11; i++) {
        String message = "Hello! RabbitMQ, 第" + i + "次";
        channel.basicPublish(exchangeName, routingKey, false, null, message.getBytes(Charset.defaultCharset()));
    }
    //关闭通道
    channel.close();
    //关闭连接
    connection.close();
    System.out.println("OVER!");
}
  • 关于生产者发送消息的安全性保证,我们先大致确定发送消息失败会出现的两种情况:

  • 失败通知,交换器没有将消息传给队列。

  • 发送方确认,RabbitMQ中间件出现异常。

  • 我们先讨论关于失败方确认,这个我们可以从交换器传递消息到队列可能存在的几种情况入手:

  • 发送到无人订阅的队列怎么办?这个RabbitMQ会保存在中间件中,等待消费者订阅后推送。

  • 发送到多个消费者订阅的队列怎么办?会将消息分别发送给不同的消费者。

  • 发送到不存在的队列怎么办?会将消息丢弃。

  • 如果想要避免发送到不存在的队列时消息丢失的情况,可以添加mandatory=true属性,并配置返回值监听器,代码如下:

//添加返回监听器
channel.addReturnListener((int var1, String var2, String var3, String var4, AMQP.BasicProperties var5, byte[] var6)->{
    String log = "replyCode:【%s】, 异常信息:【%s】, exchange:%s, routingKey:%s, 消息体:%s";
    System.out.println(String.format(log, var1, var2, var3, var4, new String(var6, Charset.defaultCharset())));
});
//通道发送数据
for (int i = 1 ; i < 11; i++) {
    String message = "Hello! RabbitMQ, 这是失败通知测试数据第" + i + "条, time:" + LocalDateTime.now().get(ChronoField.SECOND_OF_MINUTE), log = "RabbitMQ发送消息,路由KEY:【%s】,消息体【%s】";
    /*
     * 第一个,交换器名称
     * 第二个,路由可以
     * 第三个,mandatory,是否开通失败通知 AMQP.BasicProperties 通过channel.addReturnListener添加监听。
     * 第四个,properties属性,可以添加超时时间
     * 第五个,二进制消息体。
     * 这里注意第三个参数原先使用的是false,这里改成true。
     */
    channel.basicPublish(exchangeName, routingKey, true, null, message.getBytes(Charset.defaultCharset()));
    System.out.println(String.format(log, routingKey, message));
    TimeUnit.SECONDS.sleep(2);
}
  • 注:猜测mandatory属性配置的消息中后,RabbitMQ在没有找到队列后会给生产者发送返回消息。

  • 接下来是发送方确认,也就是RabbitMQ会反馈给生产者消息接收情况,可以通过该情况确认RabbitMQ是否完成接收。这个在失败通知之前,那个是代表消息路由失败。代码如下:

//添加确认监听器
channel.addConfirmListener(new ConfirmListener() {
    @Override
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("消息发送成功!deliveryTag:" + deliveryTag + ", multiple: " + multiple);
    }

    @Override
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("消息发送失败!deliveryTag:" + deliveryTag + ", multiple: " + multiple);
    }
});
//开启消息确认
channel.confirmSelect();
  • 注:开启需要调用channel.confirmSelect方法。

  1. 中间件如何保证数据的安全?

  • 首先,RabbitMQ的消息是默认是不会储存在硬盘,而是直接消费的,如果出现宕机的情况,默认情况下消息是会丢失的。

  • RabbitMQ不能指定交换器持久化,但消息可以完成持久化。是通过消息的属性指定的,消息的属性中有deliveryMode属性,其中1为持久化,2为持久化,默认是1。

  • RabbitMQ客户端为了方便开发人员配置消息属性,添加了枚举类MessageProperties,常用枚举类中的PERSISTENT_TEXT_PLAIN。

  • 代码如下:

/*
 * 第一个,交换器名称
 * 第二个,路由可以
 * 第三个,mandatory,是否开通失败通知 AMQP.BasicProperties 通过channel.addReturnListener添加监听。
 * 第四个,properties属性,可以添加超时时间
 * 第五个,二进制消息体。
 * MessageProperties.PERSISTENT_TEXT_PLAIN中deliveryMode的属性值为2。
 */
channel.basicPublish(exchangeName, routingKey, true, 
    MessageProperties.PERSISTENT_TEXT_PLAIN, 
    message.getBytes(Charset.defaultCharset()));
  • 其次,RabbitMQ的队列信息默认也是不会持久化的。

  • 因为队列的一般是由消费者创建,我们可以在创建的时候指定是否持久化。代码如下:

/*
 * 第一个:队列名称
 * 第二个:durable,是否持久化
 * 第三个:exclusive,是否独有
 * 第四个:autoDelete,是否在最后消息推送完成后自动删除
 * 第五个:Map<String, Object> props 队列相关参数
 */
channel.queueDeclare(queueName, false, false, false, props);
  • springboot默认就完成了持久化。

  1. 消费者如何从中间件获取数据,并保证数据的安全?

  • 消费者代码如下

public static void main(String[] args) throws IOException, TimeoutException {
    String exchangeName = "exchange2", bindingKey = "queue.com1", queueName = "com1";

    //创建连接工厂
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    factory.setPort(5672);
    factory.setUsername("root");
    factory.setPassword("root");
    //创建连接
    Connection connection = factory.newConnection();
    //创建通道
    Channel channel = connection.createChannel();
    //绑定队列
    channel.queueDeclare(queueName, false, false, false, null);
    //队列绑定交换器
    channel.queueBind(queueName, exchangeName, bindingKey);

    //实例化消费者
    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, Charset.defaultCharset());
            System.out.println("收到的消息为:" + message);
        }
    };

    channel.basicConsume(queueName, true, consumer);
  • RabbitMQ在消费端默认是自动提交消费确认。代码如下:

/*
 * 开始消费者监听
 * 队列名称
 * 是否自动提交
 * consumer
 */
channel.basicConsume(queueName, true, consumer);
  • 消费者也可以关闭自动提交改为手动提交

  • 先关闭手动提交,代码如下:

channel.basicConsume(queueName, false, consumer);
  • 在每次消费完成后手动提交,代码如下:

/*
 * 消费响应,这里确认消息消费情况。
 * param1 deliveryTag,转发数据位置
 * param2 requeue,是否重新返回原队列,会返回开始位置,返回多次后会进入死信队列。
 */
channel.basicAck(envelope.getDeliveryTag(), false);
  1. 主题交换器的匹配规则是什么?

  • 绑定key中可以存在“#”和“*”。

  • “#”号表示多个单词。

  • “*”号表示一个单词。

  • 路由一般是有“.”连接的多个单词构成的。

  • 比如:

  • 队列的绑定key是A.#,就可以匹配A.B或者A.B.C。

  • 绑定key还可以是*.B.*,可以匹配D.B.E,或者E.B.A,但匹配不了A.B。

延时任务?

  • 延时任务就是经过指定时候后触发的任务。

  1. Rabbit死信队列是什么?

  • 死信队列的消息是消费者无法消费的信息,或者消息超时。

  • RabbitMQ中的队列可以指定超时时间。

  • 消息也可以指定,但不友好。

  • 死信消息的判断条件:

  • 是由消费者判断的,消费者不消费。

  • 超时。

  • 队列满了。

  • 消费者回退,但并没有通知到死信队列,进入死循环达到哦一定次数。

  1. Rabbit死信交换机是什么?

  • 处理死信数据的交换机,一般和死信队列匹配。

  1. 延时任务的有那些实现方式?Rabbit又是如何实现的?

  1. 其他实现方式,包括数据库、JDK、时间轮算法、Redis等等。
  1. 使用数据库如何完成延时任务?
  • 在数据库中存储一条数据,在数据上表明执行时间以及状态。

  • 开启一个循环任务,在数据库中不断查询执行事件小于当前时间的数据,并执行。

  1. 使用jdk如何完成延时任务?
  • 使用DelayedQueue类。

  • 队列中的元素是Delayed,其中包含两个方法,getDelayTime()获取到当前时间的差值和compareTo()排序。所以这个队列是有序的。是按照到当前时间的大小,正序排列的。

  • 这个有个问题是如果是集群的话需要做特殊处理。

  1. 使用netty时间轮如何完成延时任务?
  • 使用时间轮算法,将任务绑定到对应的时间中,但进入该时间就执行对应的任务。

  1. 使用Redis完成延时任务?
  • 使用zset类型。

  • zset类型的可以为每个值添加打分,然后Redis中会依据打分将数据排序存储。这里打分可以理解为KEY。

  • zset获取数据的时候,可以通过指定score的最大值和最小值来获取在这个范围内的数据。

  • 开启单独线程,循环获取score小于当前时间的数据,完成消费。

  1. 如何使用RabbitMQ完成延时任务?
  • 队列设置有效时间。代码如下:

Map<String, Object> props = new HashMap<>(3);

String TAXI_DEAD_QUEUE_EXCHANGE = "dead_exchange", TAXI_DEAD_KEY = "deadKey";
// x-dead-letter-exchange    这里声明当前队列绑定的死信交换机
props.put("x-dead-letter-exchange", TAXI_DEAD_QUEUE_EXCHANGE);
// x-dead-letter-routing-key  这里声明当前队列的死信路由key
props.put("x-dead-letter-routing-key", TAXI_DEAD_KEY);
// x-message-ttl  声明队列的TTL 延迟时间3s
props.put("x-message-ttl", 3000);

//绑定队列
channel.queueDeclare(queueName, false, false, false, props);
  • 创建队列后,关闭消费者。

  • 创建延迟队列对应消费者。代码如下:

public static void main(String[] args) throws IOException, TimeoutException {
    String exchangeName = "dead_exchange", bindingKey = "dead_key", queueName = "dead_queue";

    //创建连接工厂
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    factory.setPort(5672);
    factory.setUsername("root");
    factory.setPassword("root");
    //创建连接
    Connection connection = factory.newConnection();
    //创建通道
    Channel channel = connection.createChannel();

    //创建死信交换器
    channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT);

    Map<String, Object> props = new HashMap<>(3);
    //绑定队列
    channel.queueDeclare(queueName, false, false, false, props);

    //队列绑定交换机
    channel.queueBind(queueName, exchangeName, bindingKey);

    //实例化消费者
    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, Charset.defaultCharset());
            System.out.println("收到的消息为:" + message + ", time-second:" + LocalDateTime.now().get(ChronoField.SECOND_OF_MINUTE));
        }
    };

    /*
     * 开始消费者监听
     * 队列名称
     * 是否自动提交
     * consumer
     */
    channel.basicConsume(queueName, true, consumer);
    System.out.println("dead consumer start...........");
}
  • 等数据超过有效时间后,数据会传递到死信交换器。

  • 死信交换器将数据分配给死信队列,在有死信队列推送给消息的消费者。

  1. 扩展问题:主键唯一性应该如何保证?

  • mysql推荐使用数值类型的字段作为主键。

  • 在分库分表的时候,需要处理自增主键的生成。

  • 可以使用雪花(snowflow)算法生成唯一主键。

  • 使用一个long类型的数值。

  • 第一个比特位恒定为0。

  • 第2到41位存一个时间戳。

  • 第42到52位存一个机器码。

  • 第53位到64位存存一个自增序列。

  • 可以使用Redis的incr方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

田秋浩

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

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

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

打赏作者

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

抵扣说明:

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

余额充值