一文理解RabbitMQ原生版

什么是RabbitMQ

RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue 高级消息队列协议 )的开源实现, 能够实现异步消息处理。RabbitMQ是支持持久化消息队列的消息中间件,应用在上下游的层次级业务逻辑中,上级业务逻辑相当于生产者发布消息,下级业务逻辑相当于消费者接受到消息并且消费消息。主流的MQ产品有很多,如ActiveMQ(基于JMS)、RabbitMQ(基于AMQP协议)、RocketMQ(基于JMS)和Kafka。

核心基础概念

Server: 又称之为Broker,接受客户端的连接,实现AMQP实体服务。

Connection: 连接,应用程序与Broker的网络连接。

Channel: 网络信道,几乎所有的操作都在Channel中进行,Channel是进行消息读写的通道。客户端可以建立多个Channel,每个Channel代表一个会话任务。如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。

Message: 消息,服务器和应用程序之间传送的数据,由Message Properties和Body组成。Properties可以对消息进行修饰,比如消息的优先级,延迟等高级特性,Body就是消息体内容。

Virtual Host: 虚拟地址,用于进行逻辑隔离,最上层的消息路由。一个Virtual Host里面可以有若干个Exchange和Queue,同一个Virtual Host里面不能有相同名称的Exchange或者Queue。

Exchange: 交换机,只有转发能力不具备存储消息能力,根据路由键转发消息到绑定的队列。

Binding: Exchange和Queue之间的虚拟连接,binding中可以包含routing key。

Routing key: 一个路由规则,虚拟机可以用它来确定如何路由一个特定消息。

Queue: 也可以称之为Message Queue(消息队列),保存消息并将它们转发到消费者。

windows下安装RabbitMq

安装

直接在官网下载地址下载安装包,安装步骤略(比较简单)

管理界面

登录浏览器 http://localhost:15672 进行查看 初始化默认密码都为guest/guest

添加用户

这里我添加了用户名为admin123,密码为123456,添加完用该用户名登录

五种核心消息模型

引入依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>
    </dependencies>

创建RabbitMQ连接的工具类

package com.example.mqdemo.utils;


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

public class ConnectionUtil {
    /**
     * 建立与RabbitMQ的连接
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("127.0.0.1");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("/test");
        factory.setUsername("admin123");
        factory.setPassword("123456");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

基本消息模型

示例图

P(producer/ publisher):生产者,如寄快递

C(consumer):消费者,如收快递

红色区域:队列,如快递区,等待消费者拿快递

一句话总结

生产者将消息发送到队列,消费者从队列中获取消息,队列是存储消息的缓冲区。

代码例子
发送方
package com.example.mqdemo.service;

import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class Send {

    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] argv) throws Exception {
        // 获取连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建通道
        Channel channel = connection.createChannel();

        // 声明(创建)队列,必须声明队列才能够发送消息,不存在则创建。
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 消息内容
        String message = "Hello World!";
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println("发送者发送了:" + message);

        //关闭通道和连接
        channel.close();
        connection.close();
    }
}

查看控制台

查看管理界面

可以看到新建了一个队列,名字就是simple_queue。这里说明一下消息这列代表什么含义:

  • Ready:待消费的消息总数。
  • Unacked:待应答的消息总数。
  • Total:总数 Ready+Unacked。

点击队列名字进入详情,点击get message,查看消息详情

消费方
package com.example.mqdemo.service;

import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Recv {
    private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" 消费者消费了: : " + msg);
            }
        };
        // 监听队列,第二个参数:是否自动进行消息确认。
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

控制台

查看管理界面

可以看到此时消息已经被消费啦。如果消费者这边程序没有停止,那么会一直在监听队列中是否有新的消息。发送方一发送到队列,消费者就立即消费

面试题1:消息确认机制

从上面例子可以看到消息一旦被消费者接收,队列中的消息就会被删除。那么如果消费者接收了消息却还没执行业务就挂掉了呢?而rabbitmq却不知道啊,那么此时这个消息就丢失掉了。因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。

这种回执ACK分两种情况:

  • 自动ACK:消息一旦被接收,消费者自动发送ACK【场景;消息不太重要】

  • 手动ACK:消息接收后,不会发送ACK,需要手动调用【场景:消息很重要】

演示自动ack存在的问题

发送方不做修改,消费方把代码改造一下

 				@Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" 消费者消费了: : " + msg);
                //模拟出了异常
                int i = 1/0;
            }

发送者发送一条消息

启动消费者进行消费

此时消费者挂掉了,而消息却被消费了

演示手动ACK

修改消费者,去掉异常,把自动改成手动,第二个参数改成false

  // 监听队列,第二个参数:是否自动进行消息确认。 
channel.basicConsume(QUEUE_NAME, false, consumer);

发送方发送一条消息,消费者启动一下

查看管理界面

此时可以看到有一条未确认的消息

停止消费者程序,再查看

这是因为虽然我们设置了手动ACK,但是代码中并没有进行消息确认!所以消息并未被真正消费掉。

当我们关掉这个消费者,消息的状态再次称为Ready

修改代码

 			@Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" 消费者消费了: : " + msg);
                //手动进行ack
                channel.basicAck(envelope.getDeliveryTag(),false);
            }

执行后查看管理界面

work消息模型

示例图

P(producer/ publisher):生产者,如寄快递

C1、C2(consumer):消费者,如收快递

红色区域:队列,如快递区,等待消费者拿快递

代码例子
发送方

跟上面发送方几乎一样,只是改了队列名字以及循环发送10条消息

package com.example.mqdemo.service;

import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class Send {

    private final static String QUEUE_NAME = "work_queue";

    public static void main(String[] argv) throws Exception {
        // 获取连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建通道
        Channel channel = connection.createChannel();

        // 声明(创建)队列,必须声明队列才能够发送消息,不存在则创建。
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 循环发布消息
        for (int i = 0; i < 10; i++) {
            // 消息内容
            String message = "快递【" + i + "】";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println("卖家发送了" + message);
            /*快递运输慢*/
            Thread.sleep(i * 2);
        }
        //关闭通道和连接
        channel.close();
        connection.close();
    }
}
消费方

消费者1

package com.example.mqdemo.service;

import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Recv1 {
    private final static String QUEUE_NAME = "work_queue";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" 消费者拿到了: : " + msg);
                //手动进行ack
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };
        // 监听队列,第二个参数:是否自动进行消息确认。
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

消费者2

跟消费者1一样,只不过拿快递慢了些

 @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" 消费者拿到了: : " + msg);
                try {
                    //模拟去拿快递慢
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
//                    e.printStackTrace();
                }
                //手动进行ack
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };

同时启动好两个消费者,然后启动发送方,结果可以看到消费者各自拿了5个快递

能者多劳

从上面可以看到,为什么消费者2拿快递拿的慢却跟消费者1还拿得一样多呢?不应该走的快拿的快递越多吗?

那怎么实现呢?其实,只需要在消费者1和2添加channel.basicQos(1)即可。这就告诉蜡笔小新他弟不要一直向消费者发送消息,而是要等待消费者的确认了前一个消息。

查看控制台

面试题2:如何避免消息堆积?
  • 采用workqueue,多个消费者监听同一队列。

  • 接收到消息以后,可以通过线程池,进行异步消费。

订阅模型的特点

  • 可以有多个消费者
  • 每个消费者有自己的queue(队列)
  • 每个队列都要绑定到Exchange(交换机)
  • 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
  • 交换机把消息发送给绑定过的所有队列
  • 队列的消费者都能拿到消息。实现一条消息被多个消费者消费

订阅模型-Fanout(广播模式)

在这种订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。

示例图

P:生产者,如寄快递

X: 交换机,相当于快递公司

红色区域:队列,如快递区,等待消费者拿快递

C1、C2:消费者,如收快递

代码例子
生产者

与之前不同点是声明队列改成声明交换机

package com.example.mqdemo.demo2;

import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class SendFanout {

    private final static String EXCHANGE_NAME = "fanout_exchange";

    public static void main(String[] argv) throws Exception {
        // 获取连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建通道
        Channel channel = connection.createChannel();

        // 声明exchange,指定类型为fanout
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        // 消息内容
        String message = "广播快递";
        // 发布消息到Exchange
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println("卖家发送了" + message);

        //关闭通道和连接
        channel.close();
        connection.close();
    }
}
消费者

与之前不同的是这里需要 绑定队列到交换机

package com.example.mqdemo.demo2;

import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class RecvFanout1 {
    private final static String QUEUE_NAME = "fanout_queue_1";

    private final static String EXCHANGE_NAME = "fanout_exchange";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" 消费者1拿到了:" + msg);
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
查看控制台

订阅模型-Direct(路由模式)

在这种订阅模式中,生产者发布消息,消费者有选择性的接收消息。队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)。消息的发送方在向Exchange发送消息时,也必须指定消息的routing key

示例图

P:生产者,如寄快递

X: 交换机,相当于快递公司

红色区域:队列,如快递区,等待消费者拿快递

C1、C2:消费者,如收快递

error、info这些就是我们讲的RoutingKey

代码示例

生产者

如卖家发货发送了两个货品,A货品选择EMS快递,B货品选择了京东快递

package com.example.mqdemo.demo2;

import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class SendFanout {

    private final static String EXCHANGE_NAME = "direct_exchange";

    public static void main(String[] argv) throws Exception {
        // 获取连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建通道
        Channel channel = connection.createChannel();

        // 声明exchange,指定类型为direct
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        // 消息内容
        String message = "EMS快递";
        String message2 = "京东快递";
        // 发布消息到Exchange,
        channel.basicPublish(EXCHANGE_NAME, "EMS", null, message.getBytes());
        System.out.println("卖家发送了" + message);
        channel.basicPublish(EXCHANGE_NAME, "JD", null, message2.getBytes());
        System.out.println("卖家发送了" + message2);
        //关闭通道和连接
        channel.close();
        connection.close();
    }
}
消费者1

消费者1只收EMS快递和顺丰快递

package com.example.mqdemo.demo2;

import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class RecvFanout1 {
    private final static String QUEUE_NAME = "direct_queue_1";

    private final static String EXCHANGE_NAME = "direct_exchange";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机,指定收EMS快递
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "EMS");
        // 绑定队列到交换机,指定收顺丰快递
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "SF");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" 消费者1拿到了:" + msg);
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
消费者2

消费者2只拿京东快递

package com.example.mqdemo.demo2;

import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class RecvFanout2 {
    private final static String QUEUE_NAME = "direct_queue_2";

    private final static String EXCHANGE_NAME = "direct_exchange";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机,指定收京东快递
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "JD");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" 消费者2拿到了:" + msg);
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
查看控制台

订阅模型-Topic(通配符模式)

示例图

Topic类型的ExchangeDirect相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符

Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割

通配符规则:

#:匹配一个或多个词

*:匹配不多不少恰好1个词

代码示例

生产者

快递公司发送红色的EMS快递和蓝色的EMS快递

package com.example.mqdemo.demo2;

import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class SendFanout {

    private final static String EXCHANGE_NAME = "topic_exchange";

    public static void main(String[] argv) throws Exception {
        // 获取连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建通道
        Channel channel = connection.createChannel();

        // 声明exchange,指定类型为topic
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        // 消息内容
        String message = "红色的EMS快递";
        String message2 = "蓝色的EMS快递";
        // 发布消息到Exchange,
        channel.basicPublish(EXCHANGE_NAME, "EMS.RED", null, message.getBytes());
        System.out.println("卖家发送了" + message);
        channel.basicPublish(EXCHANGE_NAME, "EMS.BLUE", null, message2.getBytes());
        System.out.println("卖家发送了" + message2);
        //关闭通道和连接
        channel.close();
        connection.close();
    }
}
消费者1

消费者1只接收红色的EMS快递

package com.example.mqdemo.demo2;

import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class RecvFanout1 {
    private final static String QUEUE_NAME = "topic_queue_1";

    private final static String EXCHANGE_NAME = "topic_exchange";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机,指定收到红色EMS快递
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "EMS.RED");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" 消费者1拿到了:" + msg);
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
消费者2

消费者2只要是EMS快递都接收

package com.example.mqdemo.demo2;

import com.example.mqdemo.utils.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class RecvFanout2 {
    private final static String QUEUE_NAME = "topic_queue_2";

    private final static String EXCHANGE_NAME = "topic_exchange";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机,指定收京东快递
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "EMS.#");

        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" 消费者2拿到了:" + msg);
            }
        };
        // 监听队列,自动返回完成
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
查看控制台

面试题3:如何避免消息丢失?

消费者的ACK机制

除了上文面试题1的消费者的ACK机制,可以防止消费者丢失消息。但是,如果在消费者消费之前,MQ就宕机了,消息就没了,所以解决办法就是对消息进行持久化。

消息进行持久化

将消息持久化,前提是:队列、Exchange都持久化

交换机持久化
   		// 声明exchange,指定类型为topic,第三个参数对交换机进行持久化
        channel.exchangeDeclare(EXCHANGE_NAME, "topic",true);
队列持久化
 		// 声明队列,第二个参数对队列进行持久化
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
消息持久化
		// 发布消息到Exchange,第三个参数对消息进行持久化
        channel.basicPublish(EXCHANGE_NAME, "EMS.RED", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
最后最后,欢迎大家关注我的微信公众号!一个分享java学习资源,实战经验和技术文章的公众号!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值