rabbitmq 学习

中文官网:https://rabbitmq.cn    https://rabbitmq.cn/docs/confirms

英文官网:https://www.rabbitmq.com

三种常交换机

1.fanout 广播

复制代码
package org.example.mq.test;

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

import java.io.IOException;
import java.util.stream.IntStream;

/**
 * fanout 广播模式 : 发布/订阅  (将消息发送给多个消费者)     (生产者-无路由键)
 *
 * RabbitMQ 中消息传递模型的核心思想是生产者永远不会直接向队列发送任何消息。实际上,生产者通常甚至不知道消息是否会被传递到任何队列。
 * 交换机类型:direct、topic、headers 和 fanout。
 *      fanout :  生产者的路由键是空字符串
 *      direct :  生产者的路由键是个精确的字符串
 *      topic  :  生产者的路由键是个正则字符串
 * 生产者只能将消息发送到交换机。
 * 交换机必须准确地知道:
 *   1.如何处理它接收到的消息。
 *   2.应该将其追加到特定队列?
 *   3.应该将其追加到多个队列?
 *   4.还是应该将其丢弃?
 * 这些规则由交换机类型定义。
 *
 * fanout : 将接收到的所有消息【广播】到它"绑定"的所有队列。
 *
 *
 * 生产者:声明队列、发送消息  (交换机是空字符串, 队列是自定义队列名)
 * 消费者:声明队列、接收消息
 *
 * 路由key是可以共用同一个的,也可以是任意值,因为fanout算法是忽略路由key, 消息发送给所有绑定到交换机的队列。
 *
 **/
public class TestFanout {
    private static final String EXCHANGE_NAME = "hello_fanout_exchange";
    private static final String ROUTING_KEY = "hello_fanout_routing_key";

    private final static String QUEUE_NAME = "hello_queue";
    private final static String QUEUE_NAME2 = "hello_queue2";
    private final static String QUEUE_NAME3 = "hello_queue3";

    public static class TestSimple_Send {

        public static void main(String[] argv) throws Exception {
            try (
                    Connection connection = TestCommon.getConnectionFactory().newConnection();
                    Channel channel = connection.createChannel()
            ) {
                //声明交换机
                channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);

                //声明队列 因为我们可能在消费者之前启动发布者,所以我们要确保队列存在 (不加时,需要先启动消费者)
//                channel.queueDeclare(QUEUE_NAME, false, false, false, null);

                String message = "Hello World!";

                IntStream.range(0, 1000).forEach(origin -> {
                    // 广播模式 : 忽略任何 routingKey
                    try {
                        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println(" [x] Sent '" + message + "'");
                });

            }
        }
    }

    public static class TestSimple_Recv1 {

        public static void main(String[] argv) throws Exception {
            //连接抽象了套接字连接,并为我们处理协议版本协商、身份验证等
            Connection connection = TestCommon.getConnectionFactory().newConnection();
            Channel channel = connection.createChannel();

            //声明交换机
//            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);

            //声明队列
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);

            //创建一个非持久化、独占、自动删除且具有生成名称的队列
            //String queueName = channel.queueDeclare().getQueue();

            //绑定:交换机-队列 (告诉交换机将消息发送到我们的队列)
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);

            //由于它会异步向我们推送消息,因此我们提供了一个回调,它是一个对象,它将缓冲消息,直到我们准备好使用它们
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + message + "'" + " 1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                // 手动确认消息
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            };
            channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
        }
    }

    public static class TestSimple_Recv2 {

        public static void main(String[] argv) throws Exception {
            Connection connection = TestCommon.getConnectionFactory().newConnection();
            Channel channel = connection.createChannel();
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
            channel.queueDeclare(QUEUE_NAME2, true, false, false, null);
            channel.queueBind(QUEUE_NAME2, EXCHANGE_NAME, ROUTING_KEY);

            //由于它会异步向我们推送消息,因此我们提供了一个回调,它是一个对象,它将缓冲消息,直到我们准备好使用它们
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + message + "'" + " 2");
                // 手动确认消息
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            };
            channel.basicConsume(QUEUE_NAME2, false, deliverCallback, consumerTag -> { });
        }
    }

    public static class TestSimple_Recv3 {

        public static void main(String[] argv) throws Exception {
            Connection connection = TestCommon.getConnectionFactory().newConnection();
            Channel channel = connection.createChannel();
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
            channel.queueDeclare(QUEUE_NAME3, true, false, false, null);
            channel.queueBind(QUEUE_NAME3, EXCHANGE_NAME, ROUTING_KEY);

            //由于它会异步向我们推送消息,因此我们提供了一个回调,它是一个对象,它将缓冲消息,直到我们准备好使用它们
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + message + "'" + " 3");
                // 手动确认消息
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            };
            channel.basicConsume(QUEUE_NAME3, false, deliverCallback, consumerTag -> { });
        }
    }


}
View Code
复制代码

2.direct 直接

 

复制代码
package org.example.mq.test;

import com.rabbitmq.client.*;

import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;

/**
 * 直接模式  :  有选择地接收消息   (生产者-精确路由键)
 *
 * RabbitMQ 中消息传递模型的核心思想是生产者永远不会直接向队列发送任何消息。实际上,生产者通常甚至不知道消息是否会被传递到任何队列。
 * 交换机类型:direct、topic 和 fanout。
 *      fanout :  生产者的路由键是空字符串
 *      direct :  生产者的路由键是个精确的字符串
 *      topic  :  生产者的路由键是个正则字符串
 * 生产者只能将消息发送到交换机。
 * 交换机必须准确地知道:
 *   1.如何处理它接收到的消息。
 *   2.应该将其追加到特定队列?
 *   3.应该将其追加到多个队列?
 *   4.还是应该将其丢弃?
 * 这些规则由交换机类型定义。
 *
 * direct : direct 交换机背后的路由算法很简单 - 消息将发送到其 【绑定键】 完全匹配消息 路由键 的队列。
 *
 * 例如日志: info/error/warning
 *
 *
 * 生产者:声明队列、发送消息  (交换机是空字符串, 队列是自定义队列名)
 * 消费者:声明队列、接收消息
 *
 * 路由key是核心,它决定消息送往哪个队列。
 *
 **/
public class TestDirect {
    private static final String EXCHANGE_NAME = "hello_direct_exchange";

    private static final String ROUTING_KEY = "hello_direct_routing_key";
    private final static String QUEUE_NAME = "hello_direct_queue";

    private static final String ROUTING_KEY2 = "hello_direct_routing_key2";
    private final static String QUEUE_NAME2 = "hello_direct_queue2";

    private static final String ROUTING_KEY3 = "hello_direct_routing_key3";
    private final static String QUEUE_NAME3 = "hello_direct_queue3";

    public static class TestSimple_Send {

        static ConcurrentSkipListMap<Long,String> outstandingConfirms = new ConcurrentSkipListMap<>();

        public static void main(String[] argv) throws Exception {

            try (
                    Connection connection = TestCommon.getConnectionFactory().newConnection();
                    Channel channel = connection.createChannel()
            ) {
                //声明交换机
                channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

                //声明队列 因为我们可能在消费者之前启动发布者,所以我们要确保队列存在 (不加时,需要先启动消费者)
//                channel.queueDeclare(QUEUE_NAME, false, false, false, null);

                //在通道级别启用发布者确认功能  (确认应该只启用一次,而不是为发布的每条消息都启用)
                channel.confirmSelect();

                //异步确认
                ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
                    if (multiple){
                        //2:删除掉已经确认的消息,剩下的就是未确认的消息
                        ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliveryTag);
                        confirmed.clear();
                    }else {
                        outstandingConfirms.remove(deliveryTag);
                    }
                    System.out.println("确认的消息:" + deliveryTag);
                };
                ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
                    String message = outstandingConfirms.get(deliveryTag);
                    System.out.println("发布的消息" + message + "未被确认,序列号" + deliveryTag);
                };

                // 添加 ConfirmListener
                channel.addConfirmListener(ackCallback, nackCallback);

                // 添加 ReturnListener
                channel.addReturnListener((replyCode, replyText, exchange, routingKey, properties, body) -> {
                    String message = new String(body, "UTF-8");
                    System.out.println("消息未被路由到队列: " + message + ", 路由键: " + routingKey);
                });

                String message = "Hello World!";
//                channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, message.getBytes("UTF-8"));
//                channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY2, null, message.getBytes("UTF-8"));
                channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, true,null, message.getBytes("UTF-8"));
                
                //1:此处记录下所有发送的消息
                outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
                

                System.out.println(" [x] Sent '" + message + "'");

            }
        }
    }

    public static class TestSimple_Recv1 {

        public static void main(String[] argv) throws Exception {
            //连接抽象了套接字连接,并为我们处理协议版本协商、身份验证等
            Connection connection = TestCommon.getConnectionFactory().newConnection();
            Channel channel = connection.createChannel();

            //声明交换机
//            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

            //声明队列
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);

            //创建一个非持久化、独占、自动删除且具有生成名称的队列
            //String queueName = channel.queueDeclare().getQueue();

            //绑定:交换机-队列 (告诉交换机将消息发送到我们的队列)
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);

            //由于它会异步向我们推送消息,因此我们提供了一个回调,它是一个对象,它将缓冲消息,直到我们准备好使用它们
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + message + "'" + " 1");
                // 手动确认消息
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            };
            channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
        }
    }

    public static class TestSimple_Recv2 {

        public static void main(String[] argv) throws Exception {
            Connection connection = TestCommon.getConnectionFactory().newConnection();
            Channel channel = connection.createChannel();
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
            channel.queueDeclare(QUEUE_NAME2, true, false, false, null);
            channel.queueBind(QUEUE_NAME2, EXCHANGE_NAME, ROUTING_KEY2);

            //由于它会异步向我们推送消息,因此我们提供了一个回调,它是一个对象,它将缓冲消息,直到我们准备好使用它们
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + message + "'" + " 2");
                // 手动确认消息
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            };
            channel.basicConsume(QUEUE_NAME2, false, deliverCallback, consumerTag -> { });
        }
    }

    /**
     * 消息改用消费者主动"拉取"消息
     **/
    public static class TestSimple_Recv3_pull {

        public static void main(String[] argv) throws Exception {
            Channel channel = TestCommon.getConnectionFactory().newConnection().createChannel();
            channel.queueDeclare(QUEUE_NAME3, true, false, false, null);
            channel.queueBind(QUEUE_NAME3, EXCHANGE_NAME, ROUTING_KEY3);

            // 消费者"拉取"消息
            while (true) {
                //使用 basic.get 方法按需获取消息
                GetResponse response = channel.basicGet(QUEUE_NAME3, false);
                if (response == null) {
                    // 没有消息,则等待一段时间再继续检查
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    continue;
                }

                String message = new String(response.getBody(), "UTF-8");
                System.out.println("Received message: " + message);
                channel.basicAck(response.getEnvelope().getDeliveryTag(), false); // 手动Ack
            }
        }
    }


}
View Code
复制代码

 

3.topic 主题

 

复制代码
package org.example.mq.test;

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

/**
 * 主题模式  :  有选择地接收消息  (routing_key - 它必须是一个由点分隔的单词列表)    (生产者-特殊的路由键)
 *
 * *(星号)可以替换恰好一个单词。
 * #(井号)可以替换零个或多个单词。
 *
 * RabbitMQ 中消息传递模型的核心思想是生产者永远不会直接向队列发送任何消息。实际上,生产者通常甚至不知道消息是否会被传递到任何队列。
 * 交换机类型:direct、topic、headers 和 fanout。
 *      fanout :  生产者的路由键是空字符串
 *      direct :  生产者的路由键是个精确的字符串
 *      topic  :  生产者的路由键是个正则字符串
 * 生产者只能将消息发送到交换机。
 * 交换机必须准确地知道:
 *   1.如何处理它接收到的消息。
 *   2.应该将其追加到特定队列?
 *   3.应该将其追加到多个队列?
 *   4.还是应该将其丢弃?
 * 这些规则由交换机类型定义。
 *
 * topic : info.error
 *
 * 例如日志: info/error/warning
 *
 *
 * 生产者:声明队列、发送消息  (交换机是空字符串, 队列是自定义队列名)
 * 消费者:声明队列、接收消息
 *
 * 路由key是核心,它决定消息送往哪个队列。
 *
 **/
public class TestTopic {
    private static final String EXCHANGE_NAME = "hello_topic_exchange";

    private static final String ROUTING_KEY = "hello_topic_routing_key";
    private final static String QUEUE_NAME = "hello_topic_queue";

    private static final String ROUTING_KEY2 = "hello_topic_routing_key2";
    private final static String QUEUE_NAME2 = "hello_topic_queue2";

    private static final String ROUTING_KEY3 = "hello_topic_routing_key.#";
    private final static String QUEUE_NAME3 = "hello_topic_queue3";

    public static class TestSimple_Send {

        public static void main(String[] argv) throws Exception {
            try (
                    Connection connection = TestCommon.getConnectionFactory().newConnection();
                    Channel channel = connection.createChannel()
            ) {
                //声明交换机
                channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

                //声明队列 因为我们可能在消费者之前启动发布者,所以我们要确保队列存在 (不加时,需要先启动消费者)
//                channel.queueDeclare(QUEUE_NAME, false, false, false, null);

                String message = "Hello World!";
                channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, message.getBytes("UTF-8"));
//                channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY2, null, message.getBytes("UTF-8"));
//                channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY3, null, message.getBytes("UTF-8"));
                System.out.println(" [x] Sent '" + message + "'");
            }
        }
    }

    public static class TestSimple_Recv1 {

        public static void main(String[] argv) throws Exception {
            //连接抽象了套接字连接,并为我们处理协议版本协商、身份验证等
            Connection connection = TestCommon.getConnectionFactory().newConnection();
            Channel channel = connection.createChannel();

            //声明交换机
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

            //声明队列
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);

            //创建一个非持久化、独占、自动删除且具有生成名称的队列
            //String queueName = channel.queueDeclare().getQueue();

            //绑定:交换机-队列 (告诉交换机将消息发送到我们的队列)
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);

            //由于它会异步向我们推送消息,因此我们提供了一个回调,它是一个对象,它将缓冲消息,直到我们准备好使用它们
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + message + "'" + " 1");
                // 手动确认消息
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            };
            channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
        }
    }

    public static class TestSimple_Recv2 {

        public static void main(String[] argv) throws Exception {
            Connection connection = TestCommon.getConnectionFactory().newConnection();
            Channel channel = connection.createChannel();
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
            channel.queueDeclare(QUEUE_NAME2, true, false, false, null);
            channel.queueBind(QUEUE_NAME2, EXCHANGE_NAME, ROUTING_KEY2);

            //由于它会异步向我们推送消息,因此我们提供了一个回调,它是一个对象,它将缓冲消息,直到我们准备好使用它们
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + message + "'" + " 2");
                // 手动确认消息
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            };
            channel.basicConsume(QUEUE_NAME2, false, deliverCallback, consumerTag -> { });
        }
    }

    public static class TestSimple_Recv3 {

        public static void main(String[] argv) throws Exception {
            Connection connection = TestCommon.getConnectionFactory().newConnection();
            Channel channel = connection.createChannel();
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
            channel.queueDeclare(QUEUE_NAME3, true, false, false, null);
            channel.queueBind(QUEUE_NAME3, EXCHANGE_NAME, ROUTING_KEY3);

            //由于它会异步向我们推送消息,因此我们提供了一个回调,它是一个对象,它将缓冲消息,直到我们准备好使用它们
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + message + "'" + " 3");
                // 手动确认消息
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            };
            channel.basicConsume(QUEUE_NAME3, false, deliverCallback, consumerTag -> { });
        }
    }


}
View Code
复制代码

 

4.问题

问题一:一个队列被多个消费者消费时,多个消费者之间是竞争关系,同一条消息只会投递到一个消费者。

 问题二:发布确认机制

 

问题3:WireShark抓包
1.WireShark抓包生产者

复制代码
1.TCP的建立连接的三次握手
2.RabbitMQ的客户端告诉RabbitMQ服务端自己使用的协议及版本
TCP应答信令: 表示服务端告诉发送端已经收到这之前的包
3.Connection.Start RabbitMQ告诉客户端通信的协议和版本、SASL认证机制(详细见)、语言环境以及RabbitMQ的版本信息和支持能力
4.Connection.Start-Ok 客户端带上连接使用的帐号和密码、认证机制、语言环境、客户的信息以及能力
5.Connection.Tune RabbitMQ服务端和客户端开始进行参数协商
6.Connection.Tune-Ok RabbitMQ客户端要么接受服务端过来的参数,要么将这些值变低,在发送给服务端
7.Connection.Open vhost=/ RabbbitMQ客户端打开一个连接,并请求设置vhost值
8.Connection.Open-Ok RabbitMQ服务端对vhost进行验证,如果成功,则返回如下此信息
9.Channel.Open RabbitMQ客户端打开一个新通道
10.Channel.Open-Ok RabbitMQ服务端回复新通道准备完毕
11.Exchange.Declare 客户端向RabbitMQ声明一个Exchange
12.Exchange.Declare-Ok: RabbitMQ收到请求后,如果发现同名的exchange存在且属性相同,则返回如下包,否则抛出异常
13.Basic.Publish 客户端开始发送消息
14.Channel.Close: 客户端请求关闭通道
15.Channel.Close-Ok: RabbitMQ关闭通关,并应答
16.Connection.Close: 客户端请求关闭连接
17.Connection.Close-Ok: RabbitMQ关闭连接,并应答
18.TCP四次挥手关闭连接
复制代码

 

2. WireShark抓包消费者:推送   basic.consume

Queue.Declare: 客户端向RabbitMQ服务端声明队列
Queue.Declare-Ok:RabbitMQ创建队列成功并返回信息给客户端
Queue.Bind:    客户端向RabbitMQ服务端要求将队列绑定到指定交换机
Queue.Bind-Ok: RabbitMQ绑定成功并返回信息给客户端
Basic.Consume    客户端设置要消费的队列
Basic.Consume-Ok RabbitMQ服务端向客户端推送消息 (可能是一条,也可能是多条消息)
Basic.ACK        客户端向RabbitMQ服务端回复收到

 

 3. WireShark抓包消费者:拉取  basic.get

Queue.Declare: 客户端向RabbitMQ服务端声明队列
Queue.Declare-Ok:RabbitMQ创建队列成功并返回信息给客户端
Queue.Bind:         客户端向RabbitMQ服务端要求将队列绑定到指定交换机
Queue.Bind-Ok:    RabbitMQ绑定成功并返回信息给客户端
Basic.Get          客户端设置要消费的队列
Basic.Get-Ok     RabbitMQ服务端向客户端推送消息 (可能是一条,也可能是多条消息)
Basic.ACK         客户端向RabbitMQ服务端回复收到

 

 

 

1.简图

 

 

 

 

 

 

2.概念

交换机 direct   : routingKey 完全匹配
交换机 topic    : routingKey 正则匹配
交换机 fanout  : 不需要routingKey, 连接了就推送

 

 

 

 面试题

 

持久化

 

 

应答机制

默认开启

posted @   Peter.Jones  阅读(1)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
历史上的今天:
2018-06-20 MySQL修改时间函数 1.addDate(date , INTERVAL expr unit) 2.date_format(date,’%Y-%m-%d’) 3.str_to_date(date,’%Y-%m-%d’) 4.DATE_SUB(NOW(), INTERVAL 48 HOUR)
点击右上角即可分享
微信分享提示