消息队列:Rabbit MQ详解一篇搞定-程序员宅基地

技术标签: Rabbit MQ  rabbitmq  

目录:
请添加图片描述

为什么使用消息队列

有一个支付场景,大家都使用过微信、支付宝支付,比如自动售卖机购买饮料,需要扫码、支付、查询支付结果,出商品。

我们可以发现,售卖机请求支付二维码后,这个请求就结束了,查询支付结果又是另外一个请求,分两种方式,一种是使用http的方式,另外一种是使用推送(消息队列)的方式。不管使用什么方式,我们可以发现,支付的整套流程并不是一次请求就可以搞定。如果一次搞定,高并发的时候服务器的压力就很大,一直在等待支付结果,但用户什么时候支付也不知道。再比如鱼皮哥的做的智能BI项目,AI返回结果的时间会根据用户输入的信息来决定。

消息队列就可以很好解决了同步的问题,采用异步的实现方式。那么消息队列是如何解决同步的问题?

什么是消息队列

存储消息的队列。

  1. 消息:比如字符串,对象,二进制数据,json等等。
  2. 队列:先进先出的数据结构。

消息队列的应用场景:

  1. 耗时的场景(比如支付结果查询)
  2. 异步化的场景(比如远程控制)
  3. 应用解耦的场景

消息队列的好处

  1. 异步处理
  2. 削峰填谷
  3. 应用解耦

消息队列的缺点:

  1. 要给系统引入额外的中间件,维护成本,资源成本,学习成本。
  2. 消息队列:就需要考虑,消息丢失,消费消息,数据的一致性。

中间件

  1. 消息队列也是中间件的一种,我们可以注意到,它是作为生产和消费的中间人,进行传递消息,实现解耦
  2. 中间件可以简单理解为连接不同系统、应用、网络和数据的一种软件层。中间件将不同系统之间的数据传输与转换进行抽象化和封装,让应用程序只需关注数据流的定义以及操作,而不必关心系统之间的连接细节。

Rabbit MQ是什么

Rabbit MQ是消息队列的一种,生态好,好学习,易于理解,时效性强,支持很多不同语言的客户端,扩展性、可用性都很不错。学习性价比非常高的消息队列,适用于绝大多数中小规模分布式系统。

MQ是消息通信的模型,并发具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。

  1. JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
  2. JMS规定了两种消息模型;而AMQP的消息模型更加丰富 ,RabbitMQ是基于AMQP协议,erlang语言开发。

那么接下来我们还需要了解什么才能快速入门?

基本概念

生产者(Publisher)、交换机(Exchange)、路由(Routes)、队列(Queue)、消费者(Consumer)
图片.png基本流程

  1. 生产者会先和rabbit mq建立tcp连接。填写host、username等
  2. 生产者发送消息给rabbit mq,有Exchange将消息进行路由转发。(如果没有队列需要创建;如果没有交换机需要创建,或者使用默认)
  3. Exchang将消息路由转发到指定的Queue。
  4. 消费者会先和rabbit mq建立tcp连接。填写host、username等
  5. 消费者监听指定的Queue,
  6. 如果有消息道道Queue,rabbit mq 则将消息推送给消费者
  7. 消费者接收到消息后,回复确认收到ack。

每一个环节它都是如何实现,怎么进行?这个只是基本的流程,还有更加细节的。但这里我们可以快速入门体验一波。

快速入门

安装

  1. 官方网站:https://www.rabbitmq.com/
  2. get started

图片.png图片.png

  1. 点击windows install

图片.png

  1. 安装rabbit mq之前,还需要安装erlang

图片.png图片.png

  1. 安装完erlang后,就可以安装rabbit mq

图片.png

  1. 安装完成后,我们需要启用Rabbit MQ的管理插件(可视化管理界面)

rabbitmq-plugins.bat enable rabbitmq_management
图片.png

  1. services.msc重启mq后生效

图片.png

  1. 访问:http://localhost:15672/,账户名密码都是guest,程序连接端口是5672

图片.png

Hello World

案例:我们怎么实现,生产一条信息,发送给消费者?

图片.png

  1. 引入依赖
<!-- rabbitmq依赖 -->
 <dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.17.0</version>
</dependency>

  1. 构建生产者,往队列里面发送消息

(1)首先,需要和rabbit mq建立连接
(2)然后使用Channel,创建出可以进行操作队列、发送消息的工具。
(3)声明队列
(4)发送消息

public class HelloWorldProducer {
    

    //队列名
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
    
        //创建到服务器的连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             //在 RabbitMQ 中,Channel 指的是在连接(Connection)上创建的一个逻辑通道,用来进行发送和接收消息的操作。每一个 Channel 都会拥有独立的 ID,可以根据这个 ID 与 RabbitMQ 服务器进行通信。
             //通过 Channel,应用程序可以进行以下操作:
             //    声明队列(Queue)和交换器(Exchange)。
             //    将队列绑定到交换器上。
             //    发布消息到指定的交换器上。
             //    消费指定队列上的消息。
             //在 RabbitMQ 中,每个 Connection 都支持多个 Channel,应用程序可以根据自己的需求创建多个 Channel,从而实现并发和优化网络带宽的利用。但是需要注意,对于一个 Connection 可能存在的并发限制,在应用程序中需要合理控制 Channel 的数量。
             Channel channel = connection.createChannel()) {
    
            //这段代码是通过 RabbitMQ 的 Java 客户端创建一个名为 `QUEUE_NAME` 的队列。其中,代码参数的含义如下:
            //    `QUEUE_NAME`:队列名,即要创建的队列的名称。
            //    `false`:指定是否为持久化队列。设置为 `false` 表示创建的队列在 RabbitMQ 服务器重启后会被删除。
            //    `false`:指定是否为排他队列。设置为 `false` 表示队列可以被其他连接访问。
            //    `false`:指定队列是否应该自动删除。设置为 `false` 表示当没有任何消费者使用该队列时,该队列不会自动删除。
            //    `null`:指定队列的属性。设置为 `null` 表示不需要为队列设置任何属性。
            //当该方法被成功执行后,就可以使用 `channel.basicPublish()` 方法向队列发送消息,并使用 `channel.basicConsume()` 方法从队列中获取消息。该队列的状态信息也可以通过 `com.rabbitmq.client.AMQP.Queue.DeclareOk` 对象来进行监控。
            channel.queueDeclare(QUEUE_NAME,false,false,false,null);

            String message = "Hello World";
            //这段代码是通过 RabbitMQ 的 Java 客户端向名为 `QUEUE_NAME` 的队列发送消息。具体来说,代码参数的含义如下:
            //    `""`:交换机名,这里使用空字符串表示直接发送到队列中而不经过交换机。
            //    `QUEUE_NAME`:队列名,即要发送消息的队列的名称。
            //    `null`:消息的其他属性,这里未设置任何属性。
            //    `message.getBytes(StandardCharsets.UTF_8)`:消息体的字节数组,即要发送的消息内容。
            //当该方法被成功执行后,消息就会被发送到 `QUEUE_NAME` 队列中等待消费者来处理。如果需要将消息发送到指定的交换机(Exchange),则需要将第一个参数 `""` 修改为实际的交换机名称,并设置好 Exchange 的类型、路由键等信息。
            channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
            System.out.println(" [x] Sent '" + message + "'");

        }

    }
}
  1. 构建消费者,监听队列面的消息

(1)首先,需要和rabbit mq建立连接
(2)然后使用Channel,创建出可以进行操作队列、发送消息的工具。
(3)声明队列
(4)监听消息

public class HelloWorldConsumer {
    
    //队列名
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
    
        //创建到服务器的连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //用于处理消费者接收到消息的回调函数
        DeliverCallback deliverCallback = new DeliverCallback() {
    
            @Override
            //`consumerTag`:是消费者在订阅队列时分配到的唯一标识符,用于在取消订阅时标识消费者。
            public void handle(String consumerTag, Delivery message) throws IOException {
    
                String s = new String(message.getBody(), StandardCharsets.UTF_8);
                System.out.println(consumerTag + " [x] Received '" + s + "'");

            }
        };
        //
        //`QUEUE_NAME`:表示要消费的队列名。
        //`true`:表示自动确认消息,当消费者接收到一条消息后就会自动向消息队列发送确认消息,告诉消息队列这条消息已经被消费处理完成。
        //`deliverCallback`:表示接收到消息后的处理逻辑,将在 `handle` 方法中执行。
        //`consumerTag -> {}`:表示用于接收消费者标识的回调函数。在上面的 `basicCancel` 方法中同样需要传入该回调函数中的消费者标识才能成功取消消费者的订阅。
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {
    
            System.out.println(consumerTag);
        });
    }

}

  1. 启动生产者

图片.png
图片.png

  1. 启动消费者

图片.png
图片.png
图片.png
好啦,hello world就结束了。注意:生产者和消费者,创建的队列,必须一摸一样,设置也是。

详细介绍一些状态:
图片.png

  1. Name:就是队列名
  2. Type:表示队列的类型, classic 是先进先出的类型, 当消费者连接到 classic 类型的队列并请求消费消息时,RabbitMQ 将会把消息依次发送给消费者,可以有多个消费者同时从同一个 classic 队列中取消息。classic 队列使用内存存储消息,因此适用于低延迟的场景,但消息的容错能力较差,如果 RabbitMQ 宕机或者重启,未被消费的消息会丢失。
  3. Features:表示队列的功能特性
    1. D: 持久化(Durability),指消息队列、交换机和绑定是否持久化存储在磁盘上。如果队列被标记为持久化,那么即使 RabbitMQ 服务器重启,该队列也仍然存在,并且其中的消息也不会丢失。
    2. TTL:生存时间(Time To Live),指消息在队列中存留的时间。如果一条消息的 TTL 到期后仍未被消费,那么 RabbitMQ 将会自动删除该消息。
    3. DLX:死信交换机(Dead Letter Exchange),指一种特殊的交换机,它用于处理未被消费的消息。如果一条消息无法被消费,那么 RabbitMQ 将会将其发送到 DLX 中指定的队列中。
    4. DLK:死信路由键(Dead Letter Routing Key),指用于路由死信消息的路由键。当消息被发送到 DLX 时,RabbitMQ 根据该路由键将消息路由到指定的队列中。
  4. State: 当前的队列状态以及它是否处于正在使用的状态
    1. idle:表示队列空闲,没有任何消费者消费。
    2. running:表示队列正在被使用和消费。
    3. blocked:表示队列被阻塞,可能出现内存空间不足、磁盘空间不足等问题。
    4. deleting:表示队列正在被删除,如果队列中仍有未处理的消息,则这些消息将被返回给生产者或转移到 Dead Letter Exchange 中。
    5. terminated:表示队列已被删除。
  5. Message:消息的状态
    1. Ready:指队列中已经准备好可以被消费者消费的消息条数。
    2. Unacked:指队列中已经被消费者取走但还没有被确认的消息条数。这些未被确认的消息一般是因为消费者出现故障导致,或者消费者在处理消息时还没有进行确认。
    3. Total:指队列中所有的消息条数,包括已经被消费者取走,但还没有被确认的消息和还没有被消费者取走的消息。
  6. Message Rate(消息吞吐量)是 RabbitMQ 中的一个性能指标,表示一段时间内消息的接收和处理速率
    1. Incoming Rate:指消息发送者向 RabbitMQ 发送消息的速率。
    2. Deliver / Get Rate:指 RabbitMQ 从队列中获取消息并将其推送给消费者或者推送到 Exchange 中的速率。
    3. Ack Rate:指消费者手动发送 ack 消息来确认消息已被消费的速率。

队列模式

分为work消息模型和publish/subscribe模型。

work消息模型

单向发送(work消息模型)

  1. 指的是1对1
  2. 生产者队列发送消息,消费者接到队列路由来的消息。

图片.png
就是用hello world的例子。这里不过多介绍

多消费者(work消息模型)

  1. 指的是1对多
  2. 生产者队列发送消息,多个消费者竞争消息。

谁抢到,谁执行,work消息模型,竞争消费者模式。
图片.png
其实和单向发送的写法一样,只不过多了一个消费者

package com.yupi.springbootinit.mq;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import org.apache.poi.ss.formula.functions.T;

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;

/**
 * 单向发送,多个消费者
 */
public class MultiProducer {
    

    private final static String TASK_QUEUE_NAME = "multi_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
    
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try(Connection connection = factory.newConnection();
            Channel channel = connection.createChannel()){
    
            channel.queueDeclare(TASK_QUEUE_NAME,true,false,false,null);

            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()){
    
                String s = scanner.nextLine();
                channel.basicPublish("", TASK_QUEUE_NAME,
                        MessageProperties.PERSISTENT_TEXT_PLAIN,
                        s.getBytes("UTF-8"));
                System.out.println(" [x] send :"+ s);
            }
        }

    }
}

package com.yupi.springbootinit.mq;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
 * 单向发送,多个消费者
 */
public class MultiConsumer {
    
    private final static String TASK_QUEUE_NAME = "multi_queue";

    public static void main(String[] args) throws Exception {
    
        //创建到服务器的连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        for (int i = 0; i < 2; i++) {
    
            Channel channel = connection.createChannel();
            channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

            //`1`:表示预取的消息数量为 `1`。这个值指定消费者从消息队列一次预取的消息数量,也称为“批量大小”或“流量控制窗口”。在 `basicConsume` 方法中,默认值为 `0`,即不限制预取消息的数量,这会导致消费者处理消息的效率过低,同时也会对 RabbitMQ 服务器的资源造成压力,因此需要手动设置合理的预取消息数量,根据实际场景和消费者的处理能力来调整该值。
            // 建议将 `Qos` 设置为 1,单次拉取消息一个,这种场景分配是因为可以保证每一次处理完一个消息之后,再去请求下一条消息,降低服务器的压力,从而防止消息积压。
            channel.basicQos(1);
            //用于处理消费者接收到消息的回调函数
            int finalI = i;
            DeliverCallback deliverCallback = new DeliverCallback() {
    
                @Override
                //`consumerTag`:是消费者在订阅队列时分配到的唯一标识符,用于在取消订阅时标识消费者。
                public void handle(String consumerTag, Delivery message) throws IOException {
    
                    try {
    
                        // 处理工作
                        System.out.println(" [x] Received '" + "编号:" + finalI + ":" + message + "'");
                        //用于手动确认消息已经被消费的方法
                        //`message.getEnvelope().getDeliveryTag()`:获取消息信封中的投递标签(delivery tag),标识消息的唯一编号,用于在手动确认消息时指定要确认的消息。
                        //`false`:表示不批量确认,该参数为 `true` 时表示要一次性确认多个投递标签指定的消息。在这里,为了保证消息确认的可靠性和准确性,只确认单个消息,因此该参数值为 `false`。
                        channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
                        // 停 20 秒,模拟机器处理能力有限
                        Thread.sleep(20000);
                    } catch (InterruptedException e) {
    
                        e.printStackTrace();
                        //参数三,表示是否重新入队,可用于重试。这里不进行
                        channel.basicNack(message.getEnvelope().getDeliveryTag(), false, false);
                    } finally {
    
                        System.out.println(" [x] Done");
//                        channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
                    }


                }
            };
            //
            //`QUEUE_NAME`:表示要消费的队列名。
            //`true`:表示自动确认消息,当消费者接收到一条消息后就会自动向消息队列发送确认消息,告诉消息队列这条消息已经被消费处理完成。
            //`deliverCallback`:表示接收到消息后的处理逻辑,将在 `handle` 方法中执行。
            //`consumerTag -> {}`:表示用于接收消费者标识的回调函数。在上面的 `basicCancel` 方法中同样需要传入该回调函数中的消费者标识才能成功取消消费者的订阅。
            channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> {
    
                System.out.println(consumerTag);
            });
        }

    }
}

publish/subscribe模型

Work 消息模型:在这种模型中,消息被发送到一个队列中,多个消费者从该队列中接收消息并按照一定的顺序进行处理。消息只能被一个消费者接收 。如果我们想要一条消息被多个消费者消费怎么办呢?就需要使用到我们的 Publish/Subscribe 模型 ,订阅模型。
图片.png
可以看到,之前生产者直接对接队列
图片.png

  1. 现在变成了,生成者对接交换机,交换机下发到对应的队列。通过路由把消息转发到不同的队列上,把交换机和队列关联起来。
  2. 绑定规则是什么,规则就是有多种模式,交换机有多少类别:fanout\direct\topic

交换机做了什么呢?

  1. 接收生产者发送的消息。另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。
  2. 只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么给交换机发送消息,消息会丢失!
  3. 使用了交换机,生产者不在声明Queue,发送消息给Exchange,不在发送到Queue

fanout:广播模型

图片.png
特点:消息会被路由到所有绑定到该交换机的队列上。
举个场景:有10台自动售卖机,我们想给他推送广告,那么我们就可以使用这样的交换机来推送。

接下来我们看一下如何实现的。

/**
 * 广播交换机:生产者不需要声明队列。它直接和交换机对接
 */
public class FanoutProducer {
    

    private static final String EXCHANGE_NAME = "fanout-exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
    
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try(Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();){
    
            //创建交换机
            channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()){
    
                String message = scanner.next();
                channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8"));
                System.out.println("[x] Sent'"+message+"'");
            }
        }
    }
}
/**
 * 广播交换机:消费者
 */
public class FanoutConsumer {
    

    private static final String EXCHANGE_NAME = "fanout-exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
    
        //创建到服务器的连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel1 = connection.createChannel();
        Channel channel2 = connection.createChannel();

        //声明交换机
        channel1.exchangeDeclare(EXCHANGE_NAME,"fanout");
        // 创建队列,随机分配一个队列名称
        String queueName = "xiaowang_queue";
        channel1.queueDeclare(queueName, true, false, false, null);
        channel1.queueBind(queueName, EXCHANGE_NAME, "");

        String queueName2 = "xiaoli_queue";
        channel2.queueDeclare(queueName2, true, false, false, null);
        channel2.queueBind(queueName2, EXCHANGE_NAME, "");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback1 = (consumerTag, delivery) -> {
    
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [小王] Received '" + message + "'");
        };

        DeliverCallback deliverCallback2 = (consumerTag, delivery) -> {
    
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [小李] Received '" + message + "'");
        };
        channel1.basicConsume(queueName, true, deliverCallback1, consumerTag -> {
     });
        channel2.basicConsume(queueName2, true, deliverCallback2, consumerTag -> {
     });

    }
}

  1. 创建了两个队列。绑定同一个交换机

图片.png
图片.png
图片.png图片.png

图片.png图片.png

Direct:定向模型

绑定:可以让交换机,发送消息给某个队列,上面是全部发,这里是指定发,通过routingKey路由键。也就是,交换机也可以单独发送到指定的队列。
绑定关系:完全匹配字符串
图片.png
P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
C1:消费者,其所在队列指定了需要routing key 为 orange 的消息
C2:消费者,其所在队列指定了需要routing key 为 black、green的消息

/**
 * Direct交换机:
 */
public class DirectProducer {
    

    private static final String EXCHANGE_NAME = "direct-exchange";

    public static void main(String[] args)throws Exception {
    
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try(Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();){
    
            //创建交换机
            channel.exchangeDeclare(EXCHANGE_NAME,"direct");
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()){
    
                String userInput  = scanner.next();
                String[] strings = userInput.split(",");
                if (strings.length < 1) {
    
                    continue;
                }
                String message = strings[0];
                String routingKey = strings[1];
                //之前routingKey不写的,现在定义了。
                channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
                System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");

            }
        }
    }
}
/**
 * Direct交换机
 */
public class DirectConsumer {
    

    private static final String EXCHANGE_NAME = "direct-exchange";

    public static void main(String[] args)throws Exception {
    
        //创建到服务器的连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel1 = connection.createChannel();
        Channel channel2 = connection.createChannel();

        //声明交换机:如果交换机不存在呢?
        channel1.exchangeDeclare(EXCHANGE_NAME,"direct");
        // 创建队列,随机分配一个队列名称
        String queueName = "xiaowang_queue1";
        channel1.queueDeclare(queueName, true, false, false, null);
        channel1.queueBind(queueName, EXCHANGE_NAME, "xiaowang");//这里需要指定接收

        String queueName2 = "xiaoli_queue1";
        channel2.queueDeclare(queueName2, true, false, false, null);
        channel2.queueBind(queueName2, EXCHANGE_NAME, "xiaoli");这里需要指定接收

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback1 = (consumerTag, delivery) -> {
    
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [小王] Received '" + message + "'");
        };

        DeliverCallback deliverCallback2 = (consumerTag, delivery) -> {
    
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [小李] Received '" + message + "'");
        };
        channel1.basicConsume(queueName, true, deliverCallback1, consumerTag -> {
     });
        channel2.basicConsume(queueName2, true, deliverCallback2, consumerTag -> {
     });

    }
}

可以看到,我们这里使用上了路由键。xiaowang,xiaoli
图片.png图片.png
图片.png图片.png
图片.png
疑问:如果路由键不指定呢??

  1. 如果生产者在发送消息时未指定 routing key,那么消息到达 Exchange 后无法判断应该转发到哪个队列,而且也没有任何与 Exchange 绑定的队列可以接收这个消息。因此,在 Direct exchange 模型中,如果生产者未指定 routing key,那么消息将无法被消费者收到。

Topic:通配符

消息会根据一个 模糊的 路由键转发到指定的队列
绑定关系:可以模糊匹配多个绑定·

  1. :匹配一个单词,比如.orange,那么 a.orange, b.orange 都能匹配·
  2. #:匹配 0个或多个单词,比如 a.#,那么 a.a, a.b, a.a.a 都能匹配

图片.png
图片.png
图片.png

package com.yupi.springbootinit.mq;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.util.Scanner;

/**
 * topic交换机:
 */
public class TopicProducer {
    

    private static final String EXCHANGE_NAME = "topic-exchange";

    public static void main(String[] args)throws Exception {
    
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try(Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();){
    
            //创建交换机
            channel.exchangeDeclare(EXCHANGE_NAME,"topic");
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()){
    
                String userInput  = scanner.next();
                String[] strings = userInput.split(",");
                if (strings.length < 1) {
    
                    continue;
                }
                String message = strings[0];
                String routingKey = strings[1];
                //之前routingKey不写的,现在定义了。
                channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
                System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");

            }
        }
    }
}

package com.yupi.springbootinit.mq;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

/**
 * Direct交换机
 */
public class TopicConsumer {
    

    private static final String EXCHANGE_NAME = "topic-exchange";

    public static void main(String[] args)throws Exception {
    
        //创建到服务器的连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel1 = connection.createChannel();
        Channel channel2 = connection.createChannel();

        //声明交换机:如果交换机不存在呢?
        channel1.exchangeDeclare(EXCHANGE_NAME,"topic");
        // 创建队列,随机分配一个队列名称
        String queueName = "xiaowang_queue_topic";
        channel1.queueDeclare(queueName, true, false, false, null);
        channel1.queueBind(queueName, EXCHANGE_NAME, "#.xiaowang.#");//这里需要指定接收

        String queueName2 = "xiaoli_queue_topic";
        channel2.queueDeclare(queueName2, true, false, false, null);
        channel2.queueBind(queueName2, EXCHANGE_NAME, "#.xiaoli.#");这里需要指定接收

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        DeliverCallback deliverCallback1 = (consumerTag, delivery) -> {
    
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [小王] Received '" + message + "'");
        };

        DeliverCallback deliverCallback2 = (consumerTag, delivery) -> {
    
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [小李] Received '" + message + "'");
        };
        channel1.basicConsume(queueName, true, deliverCallback1, consumerTag -> {
     });
        channel2.basicConsume(queueName2, true, deliverCallback2, consumerTag -> {
     });

    }
}

图片.png

核心特性

消息过期机制


可以给每条消息设置一个有效期,一段时间内未被消费者处理就过期了
场景:比如消费者没有同网络,那么这个时候随意发送的消息就会积累过多,其实这些消息当时如果没有用,后面就不需要了,那么可以设置一个有效期。分两种:给这个队列所有的消息指定过期时间和给指定消息过期时间:

队列所有消息指定过期时间

在队列声明的时候,需要声明过期时间参数的设置。

// 创建队列,指定消息过期参数:但,为什么是在消费者这边设置呢?其实都可以。
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 5000);
// args 指定参数
channel.queueDeclare(QUEUE_NAME, false, false, false, args);

 // 消费消息,会持续阻塞,取消自动ack来测试消息的过期
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> {
     });

如果在过期时间内,还没有消费者取消息,消息才会过期。
注意,如果消息已经接收到,但是没确认,是不会过期的。就会出现如下这种情况,收到了但是没有确认。
图片.png如果消息处于待消费状态并且过期时间到达后,消息将被标记为过期。但是,如果消息已经被消费者消费,并且正在处理过程中,即使过期时间到达,消息仍然会被正常处理。如果重启消费者,消息也不会重新消费,直接没了???真的吗?

给某条消息指定过期时间

在发消息的时候,指定过期时间

 // 给消息指定过期时间
            AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                    .expiration("1000")
                    .build();
            channel.basicPublish("my-exchange", "routing-key", properties, message.getBytes(StandardCharsets.UTF_8));
            System.out.println(" [x] Sent '" + message + "'");

消息确认机制

为什么需要消息确认机制呢? 它确保了消息的可靠性传递和处理 。如果你发送了一条消息,比如消费者在处理过程中,出现异常,那么如果你还没确认消息接收到,消费者重新起来的时候,就可以又继续消费。服务器也知道消息被消费或其他情况,也就是需要给我一个反馈。反馈方式有三种:

  1. ack:消费成功,如果配置autoack,那么消费者一收到消息,会自动执行ack。
  2. nack:消费失败
  3. reject:拒绝

代码实现:

channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
channel.basicReject(message.getEnvelope().getDeliveryTag(), false);

第一个参数:标识消息的唯一编号,用于在手动确认消息时指定要确认的消息。
第二个参数:批量确认,表示是否一次性确认所有历史消息,包括当前这一条。false表示不批量确认
第三个参数:表示是否重新入队,可用于重试。false表示不进行

  • basicNack() 方法可以批量拒绝多个消息并选择是否重新入队;
  • basicReject() 方法只能拒绝单个消息并选择是否重新入队。

死信队列

死信:过期的消息、拒收的消息、消息队列满了、处理失败的消息的统称
死信队列,死信交换机:都是普通的交换机,和我们平常使用的没什么区别,只是起了个名字

package com.yupi.springbootinit.mq;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.util.Scanner;

public class DlxDirectProducer {
    

    private static final String DEAD_EXCHANGE_NAME = "dlx-direct-exchange";//死信队列

    private static final String WORK_EXCHANGE_NAME = "direct2-exchange"; //工作队列


    public static void main(String[] argv) throws Exception {
    
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
    
            // 声明死信交换机
            channel.exchangeDeclare(DEAD_EXCHANGE_NAME, "direct");

            // 1.创建死信队列,随机分配一个队列名称,创建两个队列
            String queueName = "laoban_dlx_queue";
            channel.queueDeclare(queueName, true, false, false, null);
            channel.queueBind(queueName, DEAD_EXCHANGE_NAME, "laoban");

            String queueName2 = "waibao_dlx_queue";
            channel.queueDeclare(queueName2, true, false, false, null);
            channel.queueBind(queueName2, DEAD_EXCHANGE_NAME, "waibao");
        	// 死信队列的消费者
            DeliverCallback laobanDeliverCallback = (consumerTag, delivery) -> {
    
                String message = new String(delivery.getBody(), "UTF-8");
                // 拒绝消息
                channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
                System.out.println(" [laoban] Received '" +
                        delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
            };

            DeliverCallback waibaoDeliverCallback = (consumerTag, delivery) -> {
    
                String message = new String(delivery.getBody(), "UTF-8");
                // 拒绝消息
                channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
                System.out.println(" [waibao] Received '" +
                        delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
            };

            channel.basicConsume(queueName, false, laobanDeliverCallback, consumerTag -> {
    
            });
            channel.basicConsume(queueName2, false, waibaoDeliverCallback, consumerTag -> {
    
            });

        	//2. 给工作队列发送消息
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()) {
    
                String userInput = scanner.nextLine();
                String[] strings = userInput.split(" ");
                if (strings.length < 1) {
    
                    continue;
                }
                String message = strings[0];
                String routingKey = strings[1];

                channel.basicPublish(WORK_EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
                System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
            }

        }
    }


}

package com.yupi.springbootinit.mq;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;

import java.util.HashMap;
import java.util.Map;

public class DlxDirectConsumer {
    


    private static final String DEAD_EXCHANGE_NAME = "dlx-direct-exchange";

    private static final String WORK_EXCHANGE_NAME = "direct2-exchange";

    /**
     * 整体流程:
     * 1. 创建工作队列,创建队列的时候,指定死信队列,也就是消息被解决的时候,那么就会发送给死信队列
     * 2. 如果生产者发送消息给工作队列,工作队列拒绝处理,则会自动发送给死信队列
     * @param argv
     * @throws Exception
     */
    public static void main(String[] argv) throws Exception {
    
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(WORK_EXCHANGE_NAME, "direct");

        // 指定死信队列参数
        Map<String, Object> args = new HashMap<>();
        // 要绑定到哪个交换机
        args.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
        // 指定死信要转发到哪个死信队列
        args.put("x-dead-letter-routing-key", "waibao");

        // 1.创建队列,随机分配一个队列名称
        String queueName = "xiaodog_queue";
        channel.queueDeclare(queueName, true, false, false, args);
        channel.queueBind(queueName, WORK_EXCHANGE_NAME, "xiaodog");

        Map<String, Object> args2 = new HashMap<>();
        args2.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);//被解决的会放到这里面,指定的交换机
        args2.put("x-dead-letter-routing-key", "laoban");//指定的routing key

        // 创建队列,随机分配一个队列名称
        String queueName2 = "xiaocat_queue";
        channel.queueDeclare(queueName2, true, false, false, args2);
        channel.queueBind(queueName2, WORK_EXCHANGE_NAME, "xiaocat");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        //2. 处理消息
        DeliverCallback xiaoyuDeliverCallback = (consumerTag, delivery) -> {
    
            String message = new String(delivery.getBody(), "UTF-8");
            // 拒绝消息
            channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
            System.out.println(" [xiaodog] Received '" +
                               delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        DeliverCallback xiaopiDeliverCallback = (consumerTag, delivery) -> {
    
            String message = new String(delivery.getBody(), "UTF-8");
            // 拒绝消息
            channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
            System.out.println(" [xiaocat] Received '" +
                               delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        channel.basicConsume(queueName, false, xiaoyuDeliverCallback, consumerTag -> {
    
        });
        channel.basicConsume(queueName2, false, xiaopiDeliverCallback, consumerTag -> {
    
        });
    }


}

Spring整合RibbitMQ

  1. 引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. 添加rabbit mq的配置
spring:
  rabbitmq:
    host: 192.168.1.103
  1. 创建交换机,创建队列,绑定
@Configuration
public class RabbitmqConfig {
    
    public static final String QUEUE_EMAIL = "queue_email";//email队列
    public static final String QUEUE_SMS = "queue_sms";//sms队列
    public static final String EXCHANGE_NAME="topic.exchange";//topics类型交换机
    public static final String ROUTINGKEY_EMAIL="topic.#.email.#";
    public static final String ROUTINGKEY_SMS="topic.#.sms.#";
 
    //声明交换机
    @Bean(EXCHANGE_NAME)
    public Exchange exchange(){
    
        //durable(true) 持久化,mq重启之后交换机还在
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }
 
    //声明email队列
    /*
     *   new Queue(QUEUE_EMAIL,true,false,false)
     *   durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列
     *   auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
     *   exclusive  表示该消息队列是否只在当前connection生效,默认是false
     */
    @Bean(QUEUE_EMAIL)
    public Queue emailQueue(){
    
        return new Queue(QUEUE_EMAIL);
    }
    //声明sms队列
    @Bean(QUEUE_SMS)
    public Queue smsQueue(){
    
        return new Queue(QUEUE_SMS);
    }
 
    //ROUTINGKEY_EMAIL队列绑定交换机,指定routingKey
    @Bean
    public Binding bindingEmail(@Qualifier(QUEUE_EMAIL) Queue queue,
                                @Qualifier(EXCHANGE_NAME) Exchange exchange){
    
        return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_EMAIL).noargs();
    }
    //ROUTINGKEY_SMS队列绑定交换机,指定routingKey
    @Bean
    public Binding bindingSMS(@Qualifier(QUEUE_SMS) Queue queue,
                              @Qualifier(EXCHANGE_NAME) Exchange exchange){
    
        return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_SMS).noargs();
    }
 
}
/**
 * 用于创建测试程序用到的交换机和队列(只用在程序启动前执行一次)
 */
public class BiInitMain {
    

    public static void main(String[] args) {
    
        try {
    
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("localhost");
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
            String EXCHANGE_NAME =  BiMqConstant.BI_EXCHANGE_NAME;
            channel.exchangeDeclare(EXCHANGE_NAME, "direct");

            // 创建队列,随机分配一个队列名称
            String queueName = BiMqConstant.BI_QUEUE_NAME;
            channel.queueDeclare(queueName, true, false, false, null);
            channel.queueBind(queueName, EXCHANGE_NAME,  BiMqConstant.BI_ROUTING_KEY);
        } catch (Exception e) {
    

        }

    }
}

  1. 创建生产者,发送消息
@SpringBootTest
@RunWith(SpringRunner.class)
public class Send {
    
 
    @Autowired
    RabbitTemplate rabbitTemplate;
    
    @Test
    public void sendMsgByTopics(){
    
 
        /**
         * 参数:
         * 1、交换机名称
         * 2、routingKey
         * 3、消息内容
         */
        for (int i=0;i<5;i++){
    
            String message = "恭喜您,注册成功!userid="+i;
            rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE_NAME,"topic.sms.email",message);
            System.out.println(" [x] Sent '" + message + "'");
        }
 
    }
}
  1. 创建消费者,监听消费消息。
@Component
public class ReceiveHandler {
    
 
    //监听邮件队列
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "queue_email", durable = "true"),
            exchange = @Exchange(
                    value = "topic.exchange",
                    ignoreDeclarationExceptions = "true",
                    type = ExchangeTypes.TOPIC
            ),
            key = {
    "topic.#.email.#","email.*"}))
    public void rece_email(String msg){
    
        System.out.println(" [邮件服务] received : " + msg + "!");
    }
 
    //监听短信队列
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "queue_sms", durable = "true"),
            exchange = @Exchange(
                    value = "topic.exchange",
                    ignoreDeclarationExceptions = "true",
                    type = ExchangeTypes.TOPIC
            ),
            key = {
    "topic.#.sms.#"}))
    public void rece_sms(String msg){
    
        System.out.println(" [短信服务] received : " + msg + "!");
    }
}
 // 指定程序监听的消息队列和确认机制
    @SneakyThrows
    @RabbitListener(queues = {
    BiMqConstant.BI_QUEUE_NAME}, ackMode = "MANUAL")
    public void receiveMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
    
        log.info("receiveMessage message = {}", message);
        if (StringUtils.isBlank(message)) {
    
            // 如果失败,消息拒绝
            channel.basicNack(deliveryTag, false, false);
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, "消息为空");
        }
        long chartId = Long.parseLong(message);
        Chart chart = chartService.getById(chartId);
        if (chart == null) {
    
            channel.basicNack(deliveryTag, false, false);
            throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "图表为空");
        }
        // 先修改图表任务状态为 “执行中”。等执行成功后,修改为 “已完成”、保存执行结果;执行失败后,状态修改为 “失败”,记录任务失败信息。
        Chart updateChart = new Chart();
        updateChart.setId(chart.getId());
        updateChart.setStatus("running");
        boolean b = chartService.updateById(updateChart);
        if (!b) {
    
            channel.basicNack(deliveryTag, false, false);
            handleChartUpdateError(chart.getId(), "更新图表执行中状态失败");
            return;
        }
        // 调用 AI
        String result = aiManager.doChat(CommonConstant.BI_MODEL_ID, buildUserInput(chart));
        String[] splits = result.split("【【【【【");
        if (splits.length < 3) {
    
            channel.basicNack(deliveryTag, false, false);
            handleChartUpdateError(chart.getId(), "AI 生成错误");
            return;
        }
        String genChart = splits[1].trim();
        String genResult = splits[2].trim();
        Chart updateChartResult = new Chart();
        updateChartResult.setId(chart.getId());
        updateChartResult.setGenChart(genChart);
        updateChartResult.setGenResult(genResult);
        // todo 建议定义状态为枚举值
        updateChartResult.setStatus("succeed");
        boolean updateResult = chartService.updateById(updateChartResult);
        if (!updateResult) {
    
            channel.basicNack(deliveryTag, false, false);
            handleChartUpdateError(chart.getId(), "更新图表成功状态失败");
        }
        // 消息确认
        channel.basicAck(deliveryTag, false);
    }

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_40853919/article/details/131504930

智能推荐

c# 调用c++ lib静态库_c#调用lib-程序员宅基地

文章浏览阅读2w次,点赞7次,收藏51次。四个步骤1.创建C++ Win32项目动态库dll 2.在Win32项目动态库中添加 外部依赖项 lib头文件和lib库3.导出C接口4.c#调用c++动态库开始你的表演...①创建一个空白的解决方案,在解决方案中添加 Visual C++ , Win32 项目空白解决方案的创建:添加Visual C++ , Win32 项目这......_c#调用lib

deepin/ubuntu安装苹方字体-程序员宅基地

文章浏览阅读4.6k次。苹方字体是苹果系统上的黑体,挺好看的。注重颜值的网站都会使用,例如知乎:font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, Source Han Sans SC, Noto Sans CJK SC, W..._ubuntu pingfang

html表单常见操作汇总_html表单的处理程序有那些-程序员宅基地

文章浏览阅读159次。表单表单概述表单标签表单域按钮控件demo表单标签表单标签基本语法结构<form action="处理数据程序的url地址“ method=”get|post“ name="表单名称”></form><!--action,当提交表单时,向何处发送表单中的数据,地址可以是相对地址也可以是绝对地址--><!--method将表单中的数据传送给服务器处理,get方式直接显示在url地址中,数据可以被缓存,且长度有限制;而post方式数据隐藏传输,_html表单的处理程序有那些

PHP设置谷歌验证器(Google Authenticator)实现操作二步验证_php otp 验证器-程序员宅基地

文章浏览阅读1.2k次。使用说明:开启Google的登陆二步验证(即Google Authenticator服务)后用户登陆时需要输入额外由手机客户端生成的一次性密码。实现Google Authenticator功能需要服务器端和客户端的支持。服务器端负责密钥的生成、验证一次性密码是否正确。客户端记录密钥后生成一次性密码。下载谷歌验证类库文件放到项目合适位置(我这边放在项目Vender下面)https://github.com/PHPGangsta/GoogleAuthenticatorPHP代码示例://引入谷_php otp 验证器

【Python】matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距-程序员宅基地

文章浏览阅读4.3k次,点赞5次,收藏11次。matplotlib.plot画图横坐标混乱及间隔处理_matplotlib更改横轴间距

docker — 容器存储_docker 保存容器-程序员宅基地

文章浏览阅读2.2k次。①Storage driver 处理各镜像层及容器层的处理细节,实现了多层数据的堆叠,为用户 提供了多层数据合并后的统一视图②所有 Storage driver 都使用可堆叠图像层和写时复制(CoW)策略③docker info 命令可查看当系统上的 storage driver主要用于测试目的,不建议用于生成环境。_docker 保存容器

随便推点

网络拓扑结构_网络拓扑csdn-程序员宅基地

文章浏览阅读834次,点赞27次,收藏13次。网络拓扑结构是指计算机网络中各组件(如计算机、服务器、打印机、路由器、交换机等设备)及其连接线路在物理布局或逻辑构型上的排列形式。这种布局不仅描述了设备间的实际物理连接方式,也决定了数据在网络中流动的路径和方式。不同的网络拓扑结构影响着网络的性能、可靠性、可扩展性及管理维护的难易程度。_网络拓扑csdn

JS重写Date函数,兼容IOS系统_date.prototype 将所有 ios-程序员宅基地

文章浏览阅读1.8k次,点赞5次,收藏8次。IOS系统Date的坑要创建一个指定时间的new Date对象时,通常的做法是:new Date("2020-09-21 11:11:00")这行代码在 PC 端和安卓端都是正常的,而在 iOS 端则会提示 Invalid Date 无效日期。在IOS年月日中间的横岗许换成斜杠,也就是new Date("2020/09/21 11:11:00")通常为了兼容IOS的这个坑,需要做一些额外的特殊处理,笔者在开发的时候经常会忘了兼容IOS系统。所以就想试着重写Date函数,一劳永逸,避免每次ne_date.prototype 将所有 ios

如何将EXCEL表导入plsql数据库中-程序员宅基地

文章浏览阅读5.3k次。方法一:用PLSQL Developer工具。 1 在PLSQL Developer的sql window里输入select * from test for update; 2 按F8执行 3 打开锁, 再按一下加号. 鼠标点到第一列的列头,使全列成选中状态,然后粘贴,最后commit提交即可。(前提..._excel导入pl/sql

Git常用命令速查手册-程序员宅基地

文章浏览阅读83次。Git常用命令速查手册1、初始化仓库git init2、将文件添加到仓库git add 文件名 # 将工作区的某个文件添加到暂存区 git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件...

分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120-程序员宅基地

文章浏览阅读202次。分享119个ASP.NET源码总有一个是你想要的_千博二手车源码v2023 build 1120

【C++缺省函数】 空类默认产生的6个类成员函数_空类默认产生哪些类成员函数-程序员宅基地

文章浏览阅读1.8k次。版权声明:转载请注明出处 http://blog.csdn.net/irean_lau。目录(?)[+]1、缺省构造函数。2、缺省拷贝构造函数。3、 缺省析构函数。4、缺省赋值运算符。5、缺省取址运算符。6、 缺省取址运算符 const。[cpp] view plain copy_空类默认产生哪些类成员函数

推荐文章

热门文章

相关标签