ActiveMQ从入门到精通

ActiveMQ

MQ

1.入门概述

MQ=消息中间件,

消息: 微信/短信/语音

中间件:

在何种场景下使用消息中间件?

为什么要在系统中引入消息中间件

kafka , RabbitMQ ,RocketMQ ActiveMQ

解耦,削峰,异步,

链式调用时我们在写程序时候的一般流程,为了完成一个整体功能会将其拆分成多个函数,比如模块A调用模块B,模块B调用模块C,模块C调用模块D,但是在大型分布式应用中,系统间的RPC交互复杂,

一个概念背后要调用上百个接口也有可能,从单机架构过度到分布式微服务架构的通例

下游系统完成秒杀业务逻辑:

读取订单,库存检查,库存冻结,余额检查,余额冻结,订单生成,余额扣减,库存扣减,生成流水,余额解冻,库存解冻

RPC接口基本上是同步调用,整体的服务性能遵循木桶理论,整体系统的耗时取决于链路中最慢的那个接口,

消息中间件通过提供消息传递和消息排队模型在分布式环境下提供应用解耦、弹性伸缩、冗余存储,流量削峰、异步通信,数据同步等功能,

大致过程:

发送者将消息发送给消息服务器,消息服务器将消息存放在若干队列/主题中,在合适的时候,消息服务器会将消息转发给接收者,在这个过程中,发送和接收是异步的,也就是发送无需等待,而且发送者和接收者的生命周期没有必然关系,

尤其是在发布订阅模式下,也可以完成一对多通信,即让一个消息有多个接收者

消息发送者可以发送一个消息而无须等待响应,消息发送者将消息发送到一条虚拟的通道上,消息接收者订阅或监听该通道,一条信息可能最终转发给一个或多个消息接收者,这些接收者都无需对消息发送者做出同步回应,整个过程都是异步的,

也就是说,一个系统跟另外一个系统通信的时候,A希望发送一个消息给B去处理,

但是A不关注B到底也没有处理好,所以A把消息发送给MQ,然后就不管了,接着B从MQ里面消费出来处理即可,

2.安装和控制台

  1. 官网上下载MQ, http://activemq.apache.org/activemq-5157-release.html
  2. 传输到 /opt目录下面
  3. tar -zxvf activemq
  4. 在根目录下 mkdir /myactiveMQ
  5. 移动过去 cp -r activemq /myactivemq
  6. cd myactivemq 进入文件夹
  7. ./activemq start 普通启动了
  8. activemq的默认进程端口是61616
  9. ps -ef|grep activemq grep -v grep 查看启动进程 -v还可以屏蔽掉有 grep的内容
  10. netstat -anp|grep 61616 查看61616端口是否被占用
  11. lsof -i:61616 如果有信息,说明activeMQ成功启动
  12. 如果lsof不能使用,就执行 yum install lsof安装一下
  13. ./activemq stop 关闭
  14. 带日志的启动方式: ./activemq start > /myactiveMQ/myrunmq.log

ActiveMQ控制台

后端服务启动,但是前端看不到啊

win客户端需要访问linux服务器里面的内容

service iptables stop //停止服务器的内容

因为是在云服务器上面部署的,所以通过远程访问, 默认是8161端口

账号和密码都是admin

友情提示:如果是云服务器,那么做完什么练习之后一定要把这个东西给关闭了,要不然下次内存就没有了,什么都做不了,都忘了以前启动过什么,还要去清理一些乱七八糟的东西

在点对点的消息传递域中,目的地被称为队列

在发布订阅消息传递域中,目的地被成为主题

3.java编码实现ActiveMQ通信

需要导入一些jar包

<dependencies>
        <!--active-mq所需要的jar包-->
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-all</artifactId>
            <version>5.15.9</version>
        </dependency>
        <dependency>
            <groupId>org.apache.xbean</groupId>
            <artifactId>xbean-spring</artifactId>
            <version>3.16</version>
        </dependency>


        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
public class JmsProduce {
    public static final String ACTIVEMQ_URL = "tcp://121.42.13.151:61616";
    public static final String QUEUE_NAME = "queue01";
    public static void main(String[] args) throws JMSException {
        //1.创建连接工厂,按照给定的url地址,采用默认用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        //2.通过连接工厂,获得连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

        //3.创建会话session,
        //有两个参数,第一个事务/第二个叫签收
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        //4.创建目的地(具体是队列还是主题)
        Queue queue = session.createQueue(QUEUE_NAME);
        //5.创建消息的生产者
        MessageProducer producer = session.createProducer(queue);
        //6.通过消息生产者生产3个消息发送到MQ的队列里面
        for (int i = 0; i < 3; i++) {
            //7.创建消息,好比学生们按照老师要求写的东西
            TextMessage textMessage = session.createTextMessage("msg" + (i + 1));//可以理解为一个字符串
            //8.通过MessageProducer发送给mq
            producer.send(textMessage);
        }
        //9.关闭资源
        producer.close();
        session.close();
        connection.close();

        System.out.println("------- message send success -------");
    }
}
public class JmsConsumer {
    public static final String ACTIVEMQ_URL = "tcp://121.42.13.151:61616";
    public static final String QUEUE_NAME = "queue01";

    public static void main(String[] args) throws JMSException, IOException {
        System.out.println("二号消费者");
        //1.创建连接工厂,按照给定的url地址,采用默认用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        //2.通过连接工厂,获得连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

        //3.创建会话session,
        //有两个参数,第一个事务/第二个叫签收
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        //4.创建目的地(具体是队列还是主题)
        Queue queue = session.createQueue(QUEUE_NAME);
        //5.创建消费者,创建的时候并且声明一下去哪里进行消费
        MessageConsumer consumer = session.createConsumer(queue);

        /*
        同步阻塞方式(receive())
        订阅者或接收者调用MessageConsumer的receive() 方法来接收消息,receive方法在能够
        接收到消息之前将一直阻塞
        while (true) {
            //可以设置等待时间,超过一定的时间没有收到消息就自动走人,如果没有设置就会一直在等消息
            TextMessage msg = (TextMessage) consumer.receive(4000L);
            if (null != msg) {
                System.out.println("接收到了" + msg.getText());
            } else {
                break;
            }
        }
        consumer.close();
        session.close();
        connection.close();
        */

        //通过监听的方式来消费消息
        consumer.setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
                if (null != message && message instanceof TextMessage) {
                    TextMessage textMessage = (TextMessage) message;
                    try {
                        System.out.println("接收到消息" + textMessage.getText());
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        //让这个程序一直旋在这里
        System.in.read();
        consumer.close();
        session.close();
        connection.close();

        /*
        1.先生产,启动一号消费者,再启动二号消费者,二号消费者还可以消费到消息吗?
        不能
        2.先启动两个消费者,再进行生产,谁会得到?
        都会得到,一人一半,负载均衡,轮询
         */
    }
}

总结一下JMS开发的基本步骤

  1. 根据连接地址创建一个 connection factory
  2. 通过 factory 创建一个 connection
  3. 启动connection
  4. 通过connection 创建 session
  5. 创建 JMS destination queue或者topic
  6. 创建 JMS producer 或者创建 JMS message 并把 destination 设置进去
  7. 创建JMS consumer 或者注册一个 JMS message listener :这是两种接收数据的方式
  8. 发送或者接受 JMS message
  9. 关闭所有的 JMS 资源

点对点消息传递域的特点:

  1. 每个消息只能有一个消费者,类似于1对1的关系,
  2. 消息的生产者和消费者之间没有时间上的相关性,无论消费者在生产者发送消息的时候是否处于运行状态,消费者都可以提取消息,
  3. 消息被消费后队列中不会再存储,所以消费者不会消费掉已经被消费掉的信息

发布订阅的特点:

  1. 生产者将消息发布到topic中,每个消息可以有多个消费者,
  2. 生产者和消费者之间有时间上的相关性,订阅某一个主题的消费者只能消费自它订阅之后发布的消息,
  3. 生产者生产时,topic不保存消息它时无状态的不落地,加入无人订阅就去生产,那就是一条废消息,所以,一般都是先启动消费者再启动生产者
  4. 不过JMS规范允许客户创建持久订阅,这在一定程度上放松了时间上的相关性要求,持久订阅允许消费者消费它在未处于激活状态时发送的消息,

主题的消费者有些不一样,不过大体上来说都是一样的,

public class JmsTopicConsumer {
    public static final String ACTIVEMQ_URL = "tcp://121.42.13.151:61616";
    public static final String TOPIC_NAME = "topic-atguigu";

    public static void main(String[] args) throws JMSException, IOException {
        System.out.println("topic 1 消费者");
        //1.创建连接工厂,按照给定的url地址,采用默认用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        //2.通过连接工厂,获得连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

        //3.创建会话session,
        //有两个参数,第一个事务/第二个叫签收
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        //4.创建目的地(具体是队列还是主题)
        Topic topic = session.createTopic(TOPIC_NAME);
        //5.创建消费者,创建的时候并且声明一下去哪里进行消费
        MessageConsumer consumer = session.createConsumer(topic);

        //通过监听的方式来消费消息
        /*consumer.setMessageListener(new MessageListener() {
            @Override
            public void onMessage(Message message) {
                if (null != message && message instanceof TextMessage) {
                    TextMessage textMessage = (TextMessage) message;
                    try {
                        System.out.println("topic 接收到消息" + textMessage.getText());
                    } catch (JMSException e) {
                        e.printStackTrace();
                    }
                }
            }
        });*/
        consumer.setMessageListener((message) -> {
            if (null != message && message instanceof TextMessage) {
                TextMessage textMessage = (TextMessage) message;
                try {
                    System.out.println("topic 接收到消息" + textMessage.getText());
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        });
        //让这个程序一直旋在这里
        System.in.read();
        consumer.close();
        session.close();
        connection.close();
    }
}

接收消息其实也可以使用函数式接口

        consumer.setMessageListener((message) -> {
            if (null != message && message instanceof TextMessage) {
                TextMessage textMessage = (TextMessage) message;
                try {
                    System.out.println("topic 接收到消息" + textMessage.getText());
                } catch (JMSException e) {
                    e.printStackTrace();
                }
            }
        });

topic 和 queue的总结

topic模式的下处理性能会随着订阅者的增加而明显降低,并且还要结合不同消息协议自身的性能差异,

queue由于只发送给一个消费者,所以就算消费者再多,性能也不会有明显的降低,并且要求消费者ack信息,

4.JMS规范和落地产品

JMS是什么?

Java消息服务指的是两个应用程序之间进行异步通信的API,它为标准消息协议和消息服务提供了一组通用接口,包括创建、发送、读取消息等,用于支持JAVA应用程序开发,在JavaEE中,当两个应用程序使用JMS进行通信时,他们之间不是直接相连的而是通过一个共同的消息收发服务组件关联起来以达到解耦/异步/削峰的效果

javaEE是什么? javaEE是一系列的规范

JMS 组成结构和消息规范

JMS provider 也就是实现JMS接口和规范的消息中间件,也就是我们的MQ服务器

JMS producer 消息生产者,创建和发送JMS消息的客户端应用

JMS consumer 消息消费者,接收和处理JMS消息的客户端应用

JMS message

消息头:

​ JMSDestination 目的地

​ JMSDeliveryMode: 持久和非持久 一条持久性消息,应该被传送仅仅一次,这意味着如果JMS提供者出现故障,该消息不会丢失,它会在服务器恢复之后再次传递,一条非持久的消息最多会传送一次,这意味着服务器出现故障,该消息将永远丢失,

​ JMSExpiration:过期时间,可以设置消息在一定时间后过期,默认是永不过期,

​ JMSPriority:消息优先级,从0–9十个级别,0-4是普通消息,5-9是加急消息,JMS不要求MQ严格按照这十个优先级发送消息,但必须保证加急消息要先于普通消息到达,默认是4级

​ JMSMessageID :唯一识别每一个标识由消息中间件产生的

消息体:

TextMessage: 普通字符串消息,包含一个String

MapMessage:一个Map类型的消息,key为string类型,值为java的基本类型

BytesMessage:二进制数组消息,包含一个byte[]

StreamMessage:Java数据流消息,用标准流操作来顺序的填充和读取

ObjectMessage:对象消息,包含一个可序列化的Java对象

消息属性

如果需要除消息头字段以外的值,那么可以使用消息属性

识别/去重/重点标注等操作非常有用的方法

是什么?

他们是以属性名和属性值对的形式制定的,可以将属性视为消息头的扩展,属性指定一些消息头里面没有包括的附加信息,比如可以在属性里面指定消息选择器

消息的属性就像可以分配给一条消息的附加消息头一样,他们允许开发者添加有关消息的不透明附加消息,它们还用于暴露消息选择器在消息过滤时使用的数据

消息的可靠性

持久性:

		//5.创建消息的生产者
        MessageProducer producer = session.createProducer(queue);
        producer.setDeliveryMode(DeliveryMode.PERSISTENT);

当服务器宕机,再次重连,消息依然存在

队列默认是持久还是非持久:默认是持久的

队列的默认传送模式是持久化的,可靠性是优先考虑的因素

可靠性的另一个重要方面是确保持久性消息传送至目标后,消息服务在向消费者传送它们之前不会丢失这些消息

持久化的topic,如果当前不在线,上线的时候会收到消息

生产者

public static final String ACTIVEMQ_URL = "tcp://121.42.13.151:61616";
    public static final String QUEUE_NAME = "queue01";
    public static void main(String[] args) throws JMSException {
        //1.创建连接工厂,按照给定的url地址,采用默认用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        //2.通过连接工厂,获得连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

        //3.创建会话session,
        //有两个参数,第一个事务/第二个叫签收
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        //4.创建目的地(具体是队列还是主题)
        Queue queue = session.createQueue(QUEUE_NAME);
        //5.创建消息的生产者
        MessageProducer producer = session.createProducer(queue);
        producer.setDeliveryMode(DeliveryMode.PERSISTENT);
        //6.通过消息生产者生产3个消息发送到MQ的队列里面
        for (int i = 0; i < 3; i++) {
            //7.创建消息,好比学生们按照老师要求写的东西
            TextMessage textMessage = session.createTextMessage("msg" + (i + 1));//可以理解为一个字符串
            //8.通过MessageProducer发送给mq
            producer.send(textMessage);
        }
        //9.关闭资源
        producer.close();
        session.close();
        connection.close();

        System.out.println("------- message send success -------");
    }

消费者

public class JmsTopicConsumerPersist {
    public static final String ACTIVEMQ_URL = "tcp://121.42.13.151:61616";
    public static final String TOPIC_NAME = "topic-persist";

    public static void main(String[] args) throws JMSException, IOException {
        System.out.println("topic 1 消费者");
        //1.创建连接工厂,按照给定的url地址,采用默认用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        //2.通过连接工厂,获得连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.setClientID("z3");
        //3.创建会话session,
        //有两个参数,第一个事务/第二个叫签收
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        //4.创建目的地(具体是队列还是主题)
        Topic topic = session.createTopic(TOPIC_NAME);
        TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic,"remark....");

        connection.start();

        Message message = topicSubscriber.receive();
        while (null != message) {
            TextMessage textMessage = (TextMessage)message;
            System.out.println("***收到的持久化topic: "+textMessage.getText());
            message = topicSubscriber.receive(5000L);
        }
        session.close();
        connection.close();
    }
}

不过必须先要订阅一次

事务:事务偏生产者

如果事务设置为false,也就是不用事务, 那么只要执行send,就进入到队列中。关闭事务,那第二个签收参数的设置需要有效

如果设置为true,先执行send再执行commit,消息才被真正提交到队列中,消息需要批量发送,需要缓冲区处理

很多人可能会觉得多次一举,因为开启了事务还需要手动的commit一下,但是目前只是小型的项目,如果是大型的项目的话,那么还是需要回滚的,某一个出错之后,其他的也会跟着进行回滚

        //1.创建连接工厂,按照给定的url地址,采用默认用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        //2.通过连接工厂,获得连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

        //3.创建会话session,
        //有两个参数,第一个事务/第二个叫签收
        Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
        //4.创建目的地(具体是队列还是主题)
        Queue queue = session.createQueue(QUEUE_NAME);
        //5.创建消息的生产者
        MessageProducer producer = session.createProducer(queue);
        producer.setDeliveryMode(DeliveryMode.PERSISTENT);
        //6.通过消息生产者生产3个消息发送到MQ的队列里面
        for (int i = 0; i < 3; i++) {
            //7.创建消息,好比学生们按照老师要求写的东西
            TextMessage textMessage = session.createTextMessage("msg" + (i + 1));//可以理解为一个字符串
            //8.通过MessageProducer发送给mq
            producer.send(textMessage);
        }
        //9.关闭资源
        producer.close();
        session.commit();
        session.close();
        connection.close();

事务模式下消费者接收消息也非常的重要,如果没有提交的话,可能会造成消息的重复消费

        //1.创建连接工厂,按照给定的url地址,采用默认用户名和密码
        ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVEMQ_URL);
        //2.通过连接工厂,获得连接connection并启动访问
        Connection connection = activeMQConnectionFactory.createConnection();
        connection.start();

        //3.创建会话session,
        //有两个参数,第一个事务/第二个叫签收
        Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);
        //4.创建目的地(具体是队列还是主题)
        Queue queue = session.createQueue(QUEUE_NAME);
        //5.创建消费者,创建的时候并且声明一下去哪里进行消费
        MessageConsumer consumer = session.createConsumer(queue);

        //通过监听的方式来消费消息
        while (true) {
            //可以设置等待时间,超过一定的时间没有收到消息就自动走人,如果没有设置就会一直在等消息
            TextMessage msg = (TextMessage) consumer.receive(4000L);
            if (null != msg) {
                System.out.println("接收到了" + msg.getText());
            } else {
                break;
            }
        }
        consumer.close();
        session.commit();
        session.close();
        connection.close();

签收:签收偏向于消费者

非事务的情况下:

自动签收:Session.AUTO_ACKNOWLEDGE

手动签收:Session.CLIENT_ACKNOWLWDGE 这个是客户端调用acknowledge方法手动签收

允许重复消息的时候:Session.DUPS_OK_ACKNOWLEDGE,不过这个一般不经常用

            System.out.println("接收到了" + msg.getText());
            msg.acknowledge();



在事务的情况下

> 使用 CLIENT_ACKNOWLWDGE 模式基本上跟没有使用一样,因为事务本身只有在commit之后才能准确的确定被消费了,如果没有commit,发送消息回执也是无法消费掉消息的


在事务性会话中,当一个事务被成功提交则消息被自动签收,如果事务回滚,则消息会被再次传送,非事务性会话中,消息何时被确认取决于创建会话时的应答模式

<a name="6cd7ee2f"></a>
#### JMS的点对点总结

点对点模式时基于队列的,生产者发消息到队列,消费者从队列接收消息,队列的存在使得消息的异步传输成为可能,

1.如果在session关闭时有部分消息已被收到但是还没有被签收,那当消费者下次连接到相同的队列时,这些消息还会被再次签收

2.队列可以长久地保存消息知道消费者收到消息,消费者不需要因为担心消息会丢失而时刻和队列保持激活的连接状态,充分体现了异步传输模式的优势

持久订阅:

客户端首先向MQ注册一个自己的身份ID识别号,当这个客户端处于离线时,生产者会为这个ID保存所有发送到消息的主题,当客户再次连接到MQ时会根据消费者的ID得到所有当自己处于离线时发送到主题的消息,

非持久订阅状态下,不能恢复或重新派送一个未签收的消息

持久订阅才能恢复或重新派送一个未签收的消息

当所有的消息必须被接收,则用持久订阅,当丢失消息可以被容忍,则可以非持久订阅

<a name="7982aa28"></a>
### 5.ActiveMQ的Broker

相当于一个ActiveMQ服务器实例,

说白了,Broker其实就是实现了用代码的形式启动ActiveMQ将MQ嵌入到Java代码中,以便随时启动,在用的时候再去启动这样能节省了资源,也保证了可靠性

<a name="a337f8de"></a>
#### 指定配置文件启动

./activemq start xbean:file:/路径/xx.xml

<a name="01b81cf1"></a>
#### 嵌入式Broker

用ActiveMQ Broker 作为独立的消息服务器来构建JAVA应用

ActiveMQ也支持在vm中通信基于嵌入式的broker,能够无缝的集成其他java应用

配置文件

```xml
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.5</version>
        </dependency>

6.Spring整合ActiveMQ

pom文件

<dependencies>
        <!--active-mq所需要的jar包-->
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-all</artifactId>
            <version>5.15.9</version>
        </dependency>
        <dependency>
            <groupId>org.apache.xbean</groupId>
            <artifactId>xbean-spring</artifactId>
            <version>3.16</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.5</version>
        </dependency>
        <!--activemq 对JMS的至此, 整合SPirng和Acrivemq-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jms</artifactId>
            <version>4.3.23.RELEASE</version>
        </dependency>
        <!--activemq所需要的pool包配置-->
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-pool</artifactId>
            <version>5.15.9</version>
        </dependency>
        <!--Spring-aop 等相关的jar-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>4.3.23.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.23.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.3.23.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>4.3.23.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.6.1</version>
        </dependency>
        <dependency>
            <groupId>aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.5.3</version>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.1_2</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd">

    <!--  开启包的自动扫描  -->
    <context:component-scan base-package="com.atguigu.activemq"/>

    <!--  配置生产者  -->
    <bean id="jmsFactory" class="org.apache.activemq.pool.PooledConnectionFactory" destroy-method="stop">
        <property name="connectionFactory">
            <!--      正真可以生产Connection的ConnectionFactory,由对应的JMS服务商提供      -->
            <bean class="org.apache.activemq.spring.ActiveMQConnectionFactory">
                <property name="brokerURL" value="tcp://121.42.13.151:61616"/>
            </bean>
        </property>
        <property name="maxConnections" value="100"/>
    </bean>

    <!--  这个是队列目的地,点对点的Queue  -->
    <bean id="destinationQueue" class="org.apache.activemq.command.ActiveMQQueue">
        <!--    通过构造注入Queue名    -->
        <constructor-arg index="0" value="spring-active-queue"/>
    </bean>

    <!--  这个是队列目的地,  发布订阅的主题Topic-->
    <bean id="destinationTopic" class="org.apache.activemq.command.ActiveMQTopic">
        <constructor-arg index="0" value="spring-active-topic"/>
    </bean>

    <!--  Spring提供的JMS工具类,他可以进行消息发送,接收等  -->
    <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
        <!--    传入连接工厂    -->
        <property name="connectionFactory" ref="jmsFactory"/>
        <!--    传入目的地    -->
        <property name="defaultDestination" ref="destinationQueue"/>
        <!--    消息自动转换器    -->
        <property name="messageConverter">
            <bean class="org.springframework.jms.support.converter.SimpleMessageConverter"/>
        </property>
    </bean>
</beans>

生产者

@Service
public class SpringMQProducer {
    @Autowired
    private JmsTemplate jmsTemplate;

    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");

        SpringMQProducer producer = (SpringMQProducer) ac.getBean("springMQProducer");
        /*producer.jmsTemplate.send(new MessageCreator() {
            public Message createMessage(Session session) throws JMSException {
                TextMessage textMessage = session.createTextMessage("** Spring  and ActiveMQ unify case **");
                return textMessage;
            }
        });*/
        producer.jmsTemplate.send((session) -> {
            TextMessage textMessage = session.createTextMessage("** Spring  and ActiveMQ unify case **");
            return textMessage;
        });
        System.out.println("*** send task over ***");
    }
}

消费者

@Service
public class SpringMQConsumer {
    @Autowired
    private JmsTemplate jmsTemplate;
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
        SpringMQConsumer consumer = (SpringMQConsumer) ac.getBean("springMQConsumer");

        String msg = (String) consumer.jmsTemplate.receiveAndConvert();
        System.out.println("consumer receive message: "+msg);
        System.out.println("*** consumer task over ***");
    }
}

在Spring里面实现消费者不启动,直接通过配置监听完成

在配置文件里面配置一个默认的监听器,把这个工厂,目的地,都监听起来,再传入自己定义的监听器类

    <!--  配置Jms消息监听器  -->
    <bean id="defaultMessageListenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
        <!--  Jms连接的工厂     -->
        <property name="connectionFactory" ref="jmsFactory"/>
        <!--   设置默认的监听目的地     -->
        <property name="destination" ref="destinationTopic"/>
        <!--  指定自己实现了MessageListener的类     -->
        <property name="messageListener" ref="myMessageListener"/>
    </bean>
    <bean id="myMessageListener" class="com.atguigu.activemq.spring.MyMessageListener"/>

自己编写的监听器类实现一些接口

public class MyMessageListener implements MessageListener {
    @SneakyThrows
    @Override
    public void onMessage(Message message) {
        if (null != message && message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage)message;
            System.out.println(textMessage.getText());
        }
    }
}

然后在这种情况下,只要启动生产者就可以收到消息

7.SpringBoot整合ActiveMQ

pom文件

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
    </parent>
    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-activemq</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>
    </dependencies>

yml配置文件

#Springboot启动端口
server:
  port: 8080

#ActiveMQ配置
spring:
  activemq:
    broker-url: tcp://121.42.13.151:61616 #ActiveMQ服务器IP
    user: admin #ActiveMQ连接用户名
    password: admin #ActiveMQ连接密码
  jms:
    #指定连接队列还是主题
    pub-sub-domain: false # false = Queue |  true = Topic

#定义服务上的队列名
myQueueName: springboot-activemq-queue
@EnableJms //开启Springboot的Jms

代表着开启了Jms使用的注解

接下来我们想要每隔三秒钟都去发送一条消息

    //间隔时间3s定投
    @Scheduled(fixedDelay = 3000)
    public void producerMsgScheduled(){
        jmsMessagingTemplate.convertAndSend(activeMQQueue,"***Scheduled"+UUID.randomUUID().toString().substring(0,6));
    }

还要在主启动类里面声明开启

@EnableScheduling

对于消费者其实也差不多,只不过更加简便了一些,依赖都是一样的

server:
  port: 8081

spring:
  activemq:
    broker-url: tcp://121.42.13.151:61616
    user: admin
    password: admin
  jms: #指定连接的是队列(Queue)还是主题(Topic)
    pub-sub-domain: false #false代表队列,true代表主题

#定义连接的队列名
myQueueName: springboot-activemq-queue

只要加入一个注解声明一下监听就可以了

@Component
public class QueueConsumer {
    @JmsListener(destination = "${myQueueName}")
    public void receive(TextMessage textMessage) throws JMSException {
        System.out.println("consumer receive "+textMessage.getText());
    }
}

topic消息其实和这个差不多,jar包都一样,只不过是配置文件声明一下是topic,然后在操作数据的时候使用的是topic对象罢了,

发送者

server:
  port: 8083

spring:
  activemq:
    broker-url: tcp://121.42.13.151:61616
    user: admin
    password: admin

  jms:
    pub-sub-domain: true

myTopicName: springboot-activemq-topic
@Component
@EnableJms
public class ActiveMQConfigBean {
    @Value("${myTopicName}")
    private String topicName;

    @Bean
    public ActiveMQTopic activeMQTopic() {
        return new ActiveMQTopic(topicName);
    }
}
@Component
@EnableScheduling
public class Topic_Producer {
    private JmsMessagingTemplate jmsMessagingTemplate;
    private ActiveMQTopic activeMQTopic;

    @Scheduled(fixedDelay = 3000)
    public void producer() {
        jmsMessagingTemplate.convertAndSend(activeMQTopic, "主题消息:    " + UUID.randomUUID().toString());
    }

    public Topic_Producer(JmsMessagingTemplate jmsMessagingTemplate, ActiveMQTopic activeMQTopic) {
        this.jmsMessagingTemplate = jmsMessagingTemplate;
        this.activeMQTopic = activeMQTopic;
    }
}

接收者

@Component
public class Topic_Consumer {

    //需要在监听方法指定连接工厂
    @JmsListener(destination = "${myTopicName}")
    public void consumer(TextMessage textMessage) throws JMSException {
        System.out.println("订阅着收到消息:    " + textMessage.getText());
    }
}

8.ActiveMQ传输协议

ActiveMQ支持很多 client-broker 通讯协议: TVP ,NIO 、UDP SSL Http(s) VM

在配置文件里面可以进行查看

可以看到 transportConnectors 里面 支持多种协议, tcp 在 openwire那里,默认就是这个协议

还有一些其他的,

1、这是默认的Broker配置,TCP的Client监听端口61616

2、在网络传输数据前,必须要序列化数据,消息是通过一个叫 wire protocol 的来序列化成字节流,默认情况下 ActiveMQ 把 wire protocol 叫做 OpenWire,它的目的是促使网络上的效率和数据快速交互

3、TCP连接的URI形式:tcp://localhost:port?key=value&key=value

4、TCP传输的优点:可靠,稳定,字节流传递,效率高,应用广泛,支持任何平台

NIO也是一个比较好的协议

1.NIO和TCP协议类似但NIO更加侧重于底层的访问操作,它允许开发人员对同一资源有更多的client调用和服务端有更多的负载,

2.适合使用NIO协议的场景

​ 可能有大量的Client去连接到Broker上,一般情况大量的CLient去连接Broker是被操作系统的线程所限制的,因此,NIO的实现比TCP需要更少的线程去运行,所以建议使用NIO协议

​ 可能对于Broker有一个很迟钝的网络传输,NIO比TCP提供更好的性能

3、NIO连接的URI形式: nio://hostname:port?key=value

如何配置NIO

<transportConnectors>
      <transportConnector name="nio" uri="nio://0.0.0.0:61618?trace=true" />
</transportConnectors>

在activemq的配置文件中修改一下,加上这一句话就可以了,这里的端口是61618,记得连接的时候修改一下

这个时候就可以配置连接使用了,

NIO的性能听不错,如何进一步进行优化呢?

URI格式头以nio开头,表示这个端口使用以TCP协议为基础的NIO网络IO模型,但是这样的设置方式,只能使这个端口支持Openwire协议,

那么如何既让这个端口支持NIO网络IO模型,又让他支持多个协议呢

还是在配置文件里面进行修改

​ transportConnector 里面的 name 改成 auto+nio uri也改成 auto+nio://

这样使用什么协议都可以连接这个端口 不过我们常用的也就是 tcp和 nio 因为这是java比较常用的,使用其他的可能会报错

amqp stomp mqtt ws ssl

9.ActiveMQ的消息存储和持久化

MQ的高可用:

事务 持久 签收 可持久化

AMQ的持久化机制:为了避免意外宕机以后丢失信息,需要做到重启后可以恢复消息队列,消息系统一般都会采用持久化机制,

Active MQ的消息持久化机制有 JDBC,AMQ,KahaDB和 LevelDB,无论使用哪种持久化方式,消息的存储逻辑都是一致的,

就是在发送者将消息发送出去后,消息中心首先将消息存储到本地数据文件,内存数据库或者远程数据库等再试图将消息发送给接收者,成功则将消息从存储中删除,失败则继续尝试发送,

消息中心启动以后首先要检查指定的存储位置,如果有未发送成功的消息,则需要把消息发送出去

KahaDB:基于日志文件,从AMQ 5.4开始默认的持久化插件, 类似于AOF

配置文件上面已经声明过了,这是默认的持久化方式,然后保存的数据在data目录下的kahadb

db-1.log db.data db.redo lock 这是kahadb下面的四个文件 db.free

消息存储使用一个事务日志和仅仅用一个索引文件来存储它所有的地址,

KahaDB是一个专门针对消息持久化的解决方案,它对典型的消息使用模式进行了优化,数据被追加到 data logs 中,当不再需要log文件中的数据的时候,log文件会被丢弃

db-numer.log KahaDB存储消息到预定义大小的数据记录文件中,当数据文件慢了之后,就会创建一个新的,number也会增加,它随着消息数量的增多,如每32m一个文件,文件名按照数字进行编号,当不再有引用到数据文件中的任何消息时,文件会被删除或归档

db.data 文件中包含了持久化的BTree索引,索引了消息数据记录中的消息,它时消息的索引文件,本质上是B-Tree 使用索引指向 db-number.log 中存储的消息

db.free 当前db.data文件里面哪些页面时空闲的,文件具体内容是所有空闲页的ID

db.redo 用来进行消息恢复,如果KahaDB消息存储在强制退出后启动,用于恢复BTree索引

LevelDB:

<persistenceAdapter>
      <levelDB directory="activemq-data"/>
</persistenceAdapter>

配置成功,这是5.8版本之后引进的,和KahaDB比较相似,也是基于文件的本地数据库存储形式,但是它提供比KahaDB更快的持久性,

它不使用自定义 B-Tree 来实现索引预写日志,而是使用基于LevelDB的索引

JDBC消息存储

添加jar包到 lib文件夹下面

jdbcPersistenceProperties配置

​ 把原来配置KahaDB的地方修改

		<persistenceAdapter> 
             <jdbcPersistenceAdapter dataSource="#mysql-ds" /> 
         </persistenceAdapter>

dataSource指定将要引入的持久化数据库的bean名称, 还有一个属性是 createTablesOnStartup, 是否在启动的时候创建数据表, 默认为true,一般第二次再设置为false

配置数据库连接

<bean id="mysql-ds" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close"> 
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> 
    <property name="url" value="jdbc:mysql://121.42.13.151:3306/activemq?relaxAutoCommit=true"/> 
    <property name="username" value="root"/> 
    <property name="password" value="123456"/> 
    <property name="poolPreparedStatements" value="true"/> 
</bean>

把这个连接复制到broker和import之间即可

我们再指定机器上建立一个activemq数据库, 如果运行一切ok,会自动建立三张表,这三张表就是用来保证持久化的,

ACTIVEMQ_MSGS ACTIVEMQ_ACKS ACTIVEMQ_LOCK

然后启动生产者,一会儿数据库里面也会有相应的信息

在点对点类型中,

当DeliveryMode设置为NON_PERSISTENCE 时,消息被保存在内存中,不会记录下来

当被设置为 PERSISTENCE时,消息保存在broker的相应文件或数据库中,

而且点对点类型中消息一旦被Consumer消费就从broker中删除 ACTIVEMQ_MSGS

在一对多模型中,

topic 发送者发送过之后的东西都会被记录下来,

小总结:

如果时queue,在没有消费者消费的情况下会将消息保存到 activemq_msgs表中,只要有任意一个消费者已经消费过了,消费之后这些消息将会立即被删除,

如果是topic,一般是先启动消费订阅然后再生产的情况下会将消息保存到 activemq_acks

但是消息基本上都会被保存下来

journal

如果长时间的持久化,推荐使用JDBC和 high performance journal,

对于数据库来说,读写都是极其消耗性能的, 所以应该使用 ActiveMQ Journal,使用高速缓存写入技术,大大提高性能,

当消费者速度能够及时跟上生产者消息的生产速度时, journal文件能够大大减少需要写入到DB中的信息,等于说在journal这个缓存里面就被消费掉了,然后只把那么长久没有被消费的消息缓存起来即可,

当消费者速度也很慢的时候,这个时候journal文件可以使消息以批量方式写到DB

配置:

<persistenceFactory>        
              <journalPersistenceAdapterFactory 
                                   journalLogFiles="5" 
                                   journalLogFileSize="32768" 
                                   useJournal="true" 
                                   useQuickJournal="true" 
                                   dataSource="#mysql-ds" 
                                   dataDirectory="../activemq-data" /> 
</persistenceFactory>

这个短时间内是不会跑到mysql里面的,但是如果时间长了的话,就会添加到mysql里面了

AMQ:基于日志文件

KahaDB:基于日志文件,是默认的持久化插件

JDBC:基于第三方数据库

LevelDB:基于文件的本地数据库存储, 引擎比较好,

Replicated LevelDB Store:基于LevelDB和Zookeeper的数据复制方式,

10.ActiveMQ多节点集群

高可用:

基于Zookeeper和LevelDB 搭建 ActiveMQ集群,集群仅仅提供主备方式的高可用集群功能,避免单点故障

使用zookeeper集群注册所以的ActiveMQ Broker,但是只有一个Broker可以提供服务,作为Master,其他的视为Slave,

如果Master因故障而不能提供服务zookeeper会从slave中选举出一个新的master

原来宕机的master恢复后作为slave

所有需要同步的消息操作都将等待存储状态被复制到其他法定的节点的操作完成后才能完成,

如果设置 replicase=3 ,那么法定大小是 (3/2)+1 = 2 master将会存储并更新然后等待 2-1=1个slave存储和更新完成,才汇报success,为什么是2-1呢,半数以上的节点为2,2-master=2-1=1

zookeeper :win 2181

activeMQ 61616 8161

​ 61617 8162

​ 61618 8163

建立一个集群的目录, 把activemq的文件夹放进去

放进去三份, node1 node2 node3

这三个都是原始的,端口都是8161,修改2和3,端口分别改成8162,8163,

这个端口是在 conf 的 jetty.xml 文件里面定义的,在这里进行修改端口

hostname名字映射: window修改host 类似于 给地址变成一个域名,这个已经知道了

vim /etc/hosts 121.42.13.151 zzyymq-server

ActiveMQ 集群配置: 去配置,三个节点的brokename需要全部一致, brokerName很随意,叫什么都行,

3个节点的持久化配置:

<persistenceAdapter>
   <replicatedLevelDB
                        directory="${activemq.data}/leveldb"
                        replicas="3"
                        bind="tcp://0.0.0.0:62621" zkAddress="192.168.10.130:2181,192.168.10.132:2181,192.168.10.133:2181"
                        hostname="121.42.13.151"
                        zkPath="/activemq/leveldb-stores"
                        />
  </persistenceAdapter>

在broker里面加入这些,把默认的持久化配置注释

zkAddress:集群里面服务器的地址

hostname:就是我们刚才映射的那个,也可以直接写ip地址, zookeeper有关

zkPath:如果被zookeeper管理,存储的位置

修改另外两个节点的消息端口 61617 61618

按顺序启动3个ActiveMQ节点,前提是zk集群已经成功启动运行,

创建一个小脚本 zk_batch.sh

cd /myzookeeper/zk01/bin

./zkServer.sh start

cd /myzookeeper/zk02/bin

./zkServer.sh start

cd /myzookeeper/zk03/bin

./zkServer.sh start

这个可以直接全部启动zookeeper集群, stop也是类似的原理

3台zk集群连接任意一台, 随便进入一台zookeeper的目录bin

./zkCli.sh -server 127.0.0.1:2181

然后就可以连接上去,

然后发现出现了目录 ls /activemq/leveldb-stores

发现有三个连接的zookeeper

集群可用性测试

ActiveMQ的客户端只能访问Master的Broker,其他处于Slave的Broker不能访问,所以客户端连接的Broker应该使用failover协议

当一个ActiveMQ节点挂掉或者一个Zookeeper节点挂掉,ActiveMQ服务依然正常运转,如果仅剩一个ActiveMQ节点,由于不能选举Master,所以ActiveMQ不能正常运行

如果只剩下半数一下,那就不行了,因为这样选举不出来master

生产者和消费者都需要修改

11.高级特性和大厂面试

引入消息队列之后该如何保证高可用:

​ 事务 持久 签收, 集群

异步投递 Async Sends :

ActiveMQ支持同步、异步两种发送模式将消息发送到broker,模式的选择对发送延时有巨大的影响,producer能达到怎样的产出率主要收发送延时的影响,使用异步发送可以显著的提高发送性能

默认是异步发送的,除非明确指定使用同步或者在未使用事务的前提下发送持久化的消息,这是同步的

如果没有使用事务且发送的是持久化的消息,每一次发送都是同步的且会阻塞producer直到broker返回一个确认,表示消息已经被安全的持久化到硬盘,确认机制提供了消息安全的保障但同时会阻塞客户端造成很大的延迟

很多高性能的应用,允许在失败的情况下有少量的数据丢失

异步发送:可以最大化producer端的发送效率,通常在发送信息量比较密集的情况下使用异步发送,可以很大的提示producer性能,

不过需要消耗较多的CLient端内存,同时也会导致broker端性能消耗增加,此外不能有效的确保消息的发送成功, 在useAysncSend=true的情况下,客户端需要容忍消息丢失的可能

1.在连接的时候 tcp://localhost:61616?jms.useAsyncSend=true

2.factory.setUseAsyncSend(true);

3.connect.set

异步发送如何确保发送成功?

理想化的情况下,生产者认为所有send的消息均被成功发送至MQ,

如果MQ突然宕机,此时生产者端内存中尚未发送至MQ的消息都会丢失,所以正确的异步发送方法是需要接收回调的,

同步发送等send不阻塞了就表示一定发送成功了,异步发送需要接收回执并由客户端再判断一次是否发送成功

//6.通过消息生产者生产3个消息发送到MQ的队列里面
        for (int i = 0; i < 3; i++) {
            //7.创建消息,好比学生们按照老师要求写的东西
             textMessage = session.createTextMessage("jdbc msg" + (i + 1));//可以理解为一个字符串
            //8.通过MessageProducer发送给mq
            textMessage.setJMSMessageID(UUID.randomUUID().toString()+"----order");
            String msgID = textMessage.getJMSMessageID();
            producer.send(textMessage, new AsyncCallback() {
                @Override
                public void onSuccess() {
                    System.out.println(msgID+"has been send successfully");
                }

                @Override
                public void onException(JMSException e) {
                    System.out.println(msgID+"fail to send to mq");
                }
            });
        }

这个时候需要设置一个回调函数

延时投递和定时投递

1.在服务器的配置文件中设置一下,可以使用定时

2.java客户端代码

            TextMessage textMessage = session.createTextMessage("delay msg" + (i + 1));//可以理解为一个字符串
            //延迟投递时间
            textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY,delay);
            //重复投递时间的间隔
            textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD,period);
            //重复次数
            textMessage.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT,repeat);
            producer.send(textMessage);

分发策略

ActiveMQ消费重试机制

具体哪些情况会引发消息重发,重发间隔和次数,

有毒消息 Poison ACK 谈谈你的理解

重发

在客户端 session中调用了rollback,事务中没有commit或者之前就关闭,

ack机制下,在session中调用了recover()

间隔默认每秒钟重发6次

一个消息被重发次数超过6次,消费端会给MQ发送一个posion ack 来表示这个消息有毒,告诉broker不要再发了,这个时候broker会把这个消息放到死信队列, DLQ

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值