• RabbitMQ-个人笔记


    1. 什么是消息队列

    在学习Java基础的过程中,使用线程池,其中一个参数就是阻塞队列,因为有核心线程数和最大线程数的限制,当超过了最大最大线程数时,当有新的线程进来,就需要有一个阻塞队列,这里的队列就相当于缓存的作用.
    依次类比,消息中间件就是对消息的缓存,在生成消息消费消息中间创建一个缓冲区,一个只管放,一个只管取。用专业的名称来讲,一个叫做生产者,一个叫做消费者
    在这里插入图片描述

    2. MQ的优势和劣势

    优势

    • 应用解耦:如下图所示,订单系统是整个消息的入口,库存系统出现问题,会导致订单系统也出现问题,那么整个系统可能也会随之崩溃,这就是耦合度太高
      在这里插入图片描述
      使用MQ,库存系统在MQ中取数据,出错也不会影响订单系统以及其他系统。
      在这里插入图片描述

    • 异步提速:典型的下订单的例子,如图所示:如果等后面的所以系统都完成,需要920ms
      在这里插入图片描述
      而使用MQ,下完订单就返回,剩下的慢慢处理,用户体验不就上来了
      在这里插入图片描述

    • 削峰填谷:如果同时有大量的请求过来,服务器可能无法撑住,但是如果有MQ作为中间件,所以的流量全部打到MQ上,服务器再慢慢的从MQ中取数据,这样就避免了高峰。
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

    劣势

    • 系统的可用性降低
    • 系统的复杂度提高
    • 数据的一致性问题
      在这里插入图片描述

    既然MQ有这些缺点,那应该在什么样的情况下选择MQ呢?

    • 生产者不需要从消费者处获得反馈。引入消息队列之前的直接调用,其接口的返回值应该为空,这才让明明下层的动作还没做,上层却当成动作做完了继续往后走,即所谓异步成为了可能。
    • 容许短暂的不一致性。
    • 确实是用了有效果。即解耦、提速、削峰这些方面的收益,超过加入MQ,管理MQ这些成本。

    3. MQ的对比

    在这里插入图片描述

    4.RabbitMQ 简介

    RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。RabbitMQ官方地址:https://www.rabbitmq.com
    基础架构如图所示:
    在这里插入图片描述

    基本概念:

    • Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker
    • Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网
      络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多
      个vhost,每个用户在自己的 vhost 创建 exchange/queue 等
    • Connection:publisher/consumer 和 broker 之间的 TCP 连接
    • Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销
    • Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)
    • Queue:消息最终被送到这里等待 consumer 取走
    • Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

    5. RabbitMQ模式及其使用

    现在的RabbitMQ共有七种模式,分别为:
    Hello World模式
    Work queues 工作队列模式
    Publish/Subscribe 发布订阅模式
    Routing 路由模式
    Topics 主题模式
    RPC 远程调用模式
    Publisher Confirms 发布确认模式

    在Maven中,导入一下坐标

    	<dependencies>
            <dependency>
                <groupId>com.rabbitmqgroupId>
                <artifactId>amqp-clientartifactId>
                <version>5.6.0version>
            dependency>
        dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    从最简单的模式开始,永远的Hello World

    // 公共配置
    		// 1. 创建连接工厂
    		ConnectionFactory factory = new ConnectionFactory();
    
            // 2. 设置参数
            factory.setHost("xxx.xx.xxx.xxx"); //主机地址
            factory.setPort(5672); // 端口号
            factory.setVirtualHost("/shang"); //设置虚拟机,类似于在linux系统下 / 后面有多个子目录一样
            factory.setUsername("lcp"); // 用户名
            factory.setPassword("lcp"); // 用户名密码
    
            // 3. 创建连接 Connection
            Connection connection = factory.newConnection();
    
            // 4. 创建Channel
            Channel channel = connection.createChannel();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    5.1 Hello World

    在这里插入图片描述

    我们看图说话,一个生产者将消息放入队列中,消费者从队列中取得消息,可以看到是一对一的关系。

    注意点:

    • connection :于RabbitMQ服务器建立连接
    • channel:和服务器建立通道
    • queue:队列,队列需要自己创建,或者进行指定,队列使用名字进行区分。如果创建队列时,该队列已存在,则直接返回。
    // 生产者
            // 5. 创建队列Queue
            /*
            queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
                                     Map arguments)
                   1. queue 队列名称
                   2. durable 是否持久化,当mq重启后,是否还在
                   3. exclusive
                        - 是否独占 只能有一个消费者监听这个端口
                        - 当connection关闭时,是否删除队列
                   4. autoDelete 是否自动删除,当没有消费者时
                   5. arguments 参数
             */
            channel.queueDeclare("hello", true, false, false, null);
    
            // 6. 发送消息 -> queue
            /*
            String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body
                   1. exchange 交换机名称。简单模式下交换机会会使用默认的 "
                   2. routingKey 路由名称
                   3. props :配置信息
                   4. body : 发送消息
             */
            channel.basicPublish("", "hello", null, "Hello".getBytes());
    
            // 7. 释放队列
            channel.close();
            connection.close();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    // 消费者
    		// 4. 创建Channel
            Channel channel = connection.createChannel();
    
            // 5. 创建队列Queue
            channel.queueDeclare("hello", true, false, false, null);
    
            // 6. 接收消息,异步方法
            /*
            String queue, boolean autoAck, Consumer callback
                callback 回调对象
             */
            Consumer consumer = new DefaultConsumer(channel) {
                // 回调方法
                /*
                    String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body
                        1. consumerTag 消息标识
                        2. envelope 获取一些信息 :路由, 交换机
                        3. properties 配置信息
                        4. 真实数据
                 */
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    System.out.println("consumerTag:" + consumerTag);
                    System.out.println("Exchange:" + envelope.getExchange());
                    System.out.println("RoutingKey:" + envelope.getRoutingKey());
                    System.out.println("properties:" + properties);
                    System.out.println("body: " + new String(body));
                }
            };
            channel.basicConsume("hello", true, consumer);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    总体步骤而言:

    1. 双方于服务器建立连接
    2. 获取channel
    3. 生产者和消费者 使用或者创建队列,对队列的参数进行定义
    4. 生产者生产消息
    5. 消费者通过异步回调的方式来处理消息

    5.2 Work queues

    在这里插入图片描述
    与hello不同的是 一个生产者 -> 多个消费者,在消息处理过慢时,可以使用多个消费者进行消费,且这些消费者之间是竞争关系

    关注点:

    • connection :于RabbitMQ服务器建立连接
    • channel:和服务器建立通道
    • queue:队列,队列需要自己创建,或者进行指定,队列使用名字进行区分。如果创建队列时,该队列已存在,则直接返回。
    • 多个消费者绑定同一个队列
    // 生产者
    // 5. 创建队列Queue
            /*
            queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
                                     Map arguments)
                   1. queue 队列名称
                   2. durable 是否持久化,当mq重启后,是否还在
                   3. exclusive
                        - 是否独占 只能有一个消费者监听这个端口
                        - 当connection关闭时,是否删除队列
                   4. autoDelete 是否自动删除,当没有消费者时
                   5. arguments 参数
             */
            channel.queueDeclare("workQueues", true, false, false, null);
    
            // 6. 发送消息 -> queue
            /*
            String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body
                   1. exchange 交换机名称。简单模式下交换机会会使用默认的 "
                   2. routingKey 路由名称
                   3. props :配置信息
                   4. body : 发送消息
             */
            for (int i = 0; i < 10; i++) {
                channel.basicPublish("", "workQueues", null, ("Hello" + i).getBytes());
            }
    
    
            // 7. 释放队列
    //        channel.close();
    //        connection.close();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    // 消费者 只需绑定一个队列即可
    // 5. 创建队列Queue
            channel.queueDeclare("workQueues", true, false, false, null);
    
            // 6. 接收消息
            /*
            String queue, boolean autoAck, Consumer callback
                callback 回调对象
             */
            Consumer consumer = new DefaultConsumer(channel) {
                // 回调方法
                /*
                    String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body
                        1. consumerTag 消息标识
                        2. envelope 获取一些信息 :路由, 交换机
                        3. properties 配置信息
                        4. 真实数据
                 */
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    //                System.out.println("consumerTag:" + consumerTag);
    //                System.out.println("Exchange:" + envelope.getExchange());
    //                System.out.println("RoutingKey:" + envelope.getRoutingKey());
    //                System.out.println("properties:" + properties);
                    System.out.println("body: " + new String(body));
                }
            };
            channel.basicConsume("workQueues", true, consumer);
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    总体步骤而言:

    1. 双方于服务器建立连接
    2. 获取channel
    3. 生产者和消费者 使用或者创建队列,对队列的参数进行定义
    4. 生产者生产消息
    5. 多个消费者通过异步回调的方式来处理消息

    5.3 Publish/Subscribe

    在这里插入图片描述
    在这个模式中,出现了交换机的概念,生产者将消费发送到交换机,交换机再分别将消息转发到与之绑定的队列

    关注点:

    • connection :于RabbitMQ服务器建立连接
    • channel:和服务器建立通道,并且绑定交换机
    • queue:队列,队列需要自己创建,或者进行指定,队列使用名字进行区分。如果创建队列时,该队列已存在,则直接返回。
    • exchange:创建交换机,让交换机和队列进行绑定,模式为fanout
    // 生产者
    		// 5. 创建交换机
            /*
    	        String exchange, 路由名称
    	        BuiltinExchangeType type, 路由类型
    	            DIRECT("direct"), 定向
    	            FANOUT("fanout"), 扇形(广播)
    	            TOPIC("topic"), 通配符
    	            HEADERS("headers"); 参数
    	        boolean durable, 是否持久化
    	        boolean autoDelete, 是否删除
    	        boolean internal, 内部使用,一般用false
    	        Map arguments 参数
             */
            String exchangeName = "testFanout";
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT,
                    false, false, null);
    
            // 6. 创建队列
            String queueFanout1 = "testQueueFanout1";
            String queueFanout2 = "testQueueFanout2";
            channel.queueDeclare(queueFanout1, true, false, false, null);
            channel.queueDeclare(queueFanout2, true, false, false, null);
    
            /*
                String queue, String exchange, String routingKey
                    routingKey 路由键,绑定规则
                    如果路由规则为Fanout,则为空字符串
             */
            // 7. 绑定队列和交换机
            channel.queueBind(queueFanout1, exchangeName, "");
            channel.queueBind(queueFanout2, exchangeName, "");
    
            // 8. 发送消息
            String body = "这是最好的时代";
            channel.basicPublish(exchangeName, "", null, body.getBytes());
    
            // 9. 关闭
    //        channel.close();
    //        connection.close();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    // 消费者 只关心队列
    	// 6. 接收消息
            /*
            String queue, boolean autoAck, Consumer callback
                callback 回调对象
             */
            Consumer consumer = new DefaultConsumer(channel) {
                // 回调方法
                /*
                    String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body
                        1. consumerTag 消息标识
                        2. envelope 获取一些信息 :路由, 交换机
                        3. properties 配置信息
                        4. 真实数据
                 */
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    System.out.println("consumerTag:" + consumerTag);
                    System.out.println("Exchange:" + envelope.getExchange());
                    System.out.println("RoutingKey:" + envelope.getRoutingKey());
                    System.out.println("properties:" + properties);
                    System.out.println("body: " + new String(body));
                }
            };
            channel.basicConsume("testQueueFanout1", true, consumer);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    总体步骤:

    1. 双方于服务器建立连接
    2. 获取channel
    3. 创建交换机 exchange,指定名称、交换机类型和其他参数
    4. 生产者和消费者 使用或者创建队列,对队列的参数进行定义
    5. 将队列和交换机进行绑定
    6. 生产者生产消息
    7. 多个消费者通过异步回调的方式来处理消息

    整个过程中,消费者只关心队列,不关心这个队列之前的所以设置

    5.4 Routing

    在这里插入图片描述
    在订阅/发布的模式上,增加了routingKey:路由键,发布者发布消息时,不但需要指定具体的路由,并且需要指定具体路由键,那么这个消息只会发送到与这个路由和路由键绑定的队列中

    举个例子:
    A和B两个队列都绑定卡了同一个路由,但是A处理的消息是订单,B处理的消息是库存,那么根据消息的不同分类,将消息放到对应的队列中,这个就是路由

    关注点:

    • connection :于RabbitMQ服务器建立连接
    • channel:和服务器建立通道,并且绑定交换机
    • queue:队列,队列需要自己创建,或者进行指定,队列使用名字进行区分。如果创建队列时,该队列已存在,则直接返回。
    • exchange:创建交换机,让交换机和队列进行绑定,模式为BuiltinExchangeType.DIRECT
    • routingKey:路由键,交换机和队列绑定的钥匙,生产者发送消息需要指定路由键
    // 生产者
    		// 5. 创建交换机,指定交换机类型为DIRECT
            String exchangeName = "testDirect";
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT,
                    false, false, null);
    
            // 6. 创建队列
            String queueFanout1 = "testQueueDirect1";
            String queueFanout2 = "testQueueDirect2";
            channel.queueDeclare(queueFanout1, true, false, false, null);
            channel.queueDeclare(queueFanout2, true, false, false, null);
    
            /*
                String queue, String exchange, String routingKey
                    routingKey 路由键,绑定规则
                    如果路由规则为Fanout,则为空字符串
             */
            // 7. 绑定队列和交换机
            // 队列1绑定error
            channel.queueBind(queueFanout1, exchangeName, "error");
    
            // 队列2绑定error info debug
            channel.queueBind(queueFanout2, exchangeName, "error");
            channel.queueBind(queueFanout2, exchangeName, "info");
            channel.queueBind(queueFanout2, exchangeName, "debug");
    
            // 8. 发送消息
            String body = "这是最好的时代";
            channel.basicPublish(exchangeName, "info", null, body.getBytes());
    
            // 9. 关闭
    //        channel.close();
    //        connection.close();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    // 消费者
     Consumer consumer = new DefaultConsumer(channel) {
                // 回调方法
                /*
                    String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body
                        1. consumerTag 消息标识
                        2. envelope 获取一些信息 :路由, 交换机
                        3. properties 配置信息
                        4. 真实数据
                 */
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    System.out.println("consumerTag:" + consumerTag);
                    System.out.println("Exchange:" + envelope.getExchange());
                    System.out.println("RoutingKey:" + envelope.getRoutingKey());
                    System.out.println("properties:" + properties);
                    System.out.println("body: " + new String(body));
                }
            };
            channel.basicConsume("testQueueDirect2", true, consumer);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    总体步骤:

    1. 双方与服务器建立连接
    2. 获取channel
    3. 创建交换机 exchange,指定名称、交换机类型和其他参数
    4. 生产者和消费者 使用或者创建队列,对队列的参数进行定义
    5. 将队列和交换机进行绑定,并且指定路由键
    6. 生产者生产消息,并且指定路由键
    7. 多个消费者通过异步回调的方式来处理消息

    5.5 Topics

    Topic 主题模式可以实现 Pub/Sub 发布与订阅模式和 Routing 路由模式的功能,只是 Topic 在配置routing key 的时候可以使用通配符,显得更加灵活。

    关注点:

    • connection :于RabbitMQ服务器建立连接
    • channel:和服务器建立通道,并且绑定交换机
    • queue:队列,队列需要自己创建,或者进行指定,队列使用名字进行区分。如果创建队列时,该队列已存在,则直接返回。
    • exchange:创建交换机,让交换机和队列进行绑定,模式为BuiltinExchangeType.TOPIC
    • routingKey:路由键,交换机和队列绑定的钥匙,生产者发送消息需要发送于路由键匹配的字符
    		String exchangeName = "testTopic";
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC,
                    false, false, null);
    
            // 6. 创建队列
            String queueFanout1 = "testQueueTopic1";
            String queueFanout2 = "testQueueTopic2";
            channel.queueDeclare(queueFanout1, true, false, false, null);
            channel.queueDeclare(queueFanout2, true, false, false, null);
    
            /*
                String queue, String exchange, String routingKey
                    routingKey 路由键,绑定规则
                    如果路由规则为Fanout,则为空字符串
             */
            // 7. 绑定队列和交换机
            // * 代表零个或一个
            // # 代表零个或多个
            // 根据字符串匹配进行路由分发
            channel.queueBind(queueFanout1, exchangeName, "#.error");
            channel.queueBind(queueFanout1, exchangeName, "order.*");
            channel.queueBind(queueFanout2, exchangeName, "#.#");
    
            // 8. 发送消息
            String body = "这是最好的时代";
            channel.basicPublish(exchangeName, "goods", null, body.getBytes());
    
            // 9. 关闭
    //        channel.close();
    //        connection.close();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    Consumer consumer = new DefaultConsumer(channel) {
                // 回调方法
                /*
                    String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body
                        1. consumerTag 消息标识
                        2. envelope 获取一些信息 :路由, 交换机
                        3. properties 配置信息
                        4. 真实数据
                 */
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    System.out.println("consumerTag:" + consumerTag);
                    System.out.println("Exchange:" + envelope.getExchange());
                    System.out.println("RoutingKey:" + envelope.getRoutingKey());
                    System.out.println("properties:" + properties);
                    System.out.println("body: " + new String(body));
                }
            };
            channel.basicConsume("testQueueTopic1", true, consumer);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    总体步骤:

    1. 双方与服务器建立连接
    2. 获取channel
    3. 创建交换机 exchange,指定名称、交换机类型和其他参数
    4. 生产者和消费者 使用或者创建队列,对队列的参数进行定义
    5. 将队列和交换机进行绑定,指定正则表达式
    6. 生产者生产消息,并且指定路由键
    7. 多个消费者通过异步回调的方式来处理消息

    5.6 总结

    在这里插入图片描述

  • 相关阅读:
    8.5 数据结构——归并排序和基数排序
    Rstudio开不开了怎么办?R is taking longer to start than usual
    ChatGLM 实践指南
    微信小程序前后端交互与WXS的应用
    NoSQL技术——Redis
    【CSS3】CSS3 3D 转换 ② ( 3D 透视视图 | “ 透视 “ 概念简介 | 视距与成像关系 | CSS3 中 “ 透视 “ 属性设置 | “ 透视 “ 语法设置 | 代码示例 )
    MaxKey单点登录认证系统v3.5.10GA发布
    4、项目第五阶段——商品分页
    HTTP的演变
    Django-(8)
  • 原文地址:https://blog.csdn.net/weixin_45483328/article/details/126560441