为了账号安全,请及时绑定邮箱和手机立即绑定

RocketMQ源码项目实战:初学者指南

概述

本文详细介绍了RocketMQ源码的项目实战,包括消息发送与接收的流程、核心类解析以及实战案例。通过对RocketMQ源码的深入分析,读者能够更好地理解和应用RocketMQ的各个组件和功能,从而构建高效可靠的消息传递系统。RocketMQ源码项目实战涵盖了从源码结构到实际应用的全面讲解,帮助开发者掌握RocketMQ的内部机制和应用场景。RocketMQ源码项目实战提供了丰富的示例和详细的解析,使得开发者能够轻松上手并优化RocketMQ的使用。

RocketMQ简介与安装
RocketMQ简介

RocketMQ是由阿里巴巴集团开源的分布式消息中间件,目前由Apache基金会孵化。它提供了异步通信、大规模分布式系统应用的轻量级通信中间件。RocketMQ以其高性能、高可靠、强一致的特点,成为海量消息场景下的首选消息队列。它支持多语言客户端,包括Java、C++、Python等,使得开发者在构建分布式系统时更加灵活。RocketMQ具备强大的消息过滤、消息重试、消费进度回溯等功能,能够满足企业级应用对消息中间件的严格要求。无论是在线交易、日志收集、还是海量数据的实时处理,RocketMQ都能提供可靠的消息传递服务。

RocketMQ环境搭建

为了在本地环境中搭建RocketMQ,需要满足以下条件:

  • JDK 1.8或以上版本
  • Maven 3.5或以上版本
  • Linux或Windows操作系统

安装步骤

  1. 下载RocketMQ
    • 访问官方GitHub仓库https://github.com/apache/rocketmq,下载最新版本的RocketMQ源码包。
  2. 解压源码包
    tar -zxvf rocketmq-all-4.9.4-release.tar.gz
    cd rocketmq-all-所选版本
  3. 编译RocketMQ
    • 使用Maven编译源码
      mvn -Prelease -DskipTests clean install
    • 编译完成后,RocketMQ将存储在rocketmq-所选版本-release目录下。
  4. 启动NameServer
    nohup sh bin/mqnamesrv &
    • 通过tail -f logs/namesrv.log查看日志,确认NameServer是否启动成功。
  5. 启动Broker
    nohup sh bin/mqbroker -n localhost:9876 &
    • 同样,通过tail -f logs/broker.log查看Broker启动日志。
RocketMQ的启动与停止

RocketMQ的启动与管理可以通过命令行工具来完成。以下是具体的启动和停止命令:

启动RocketMQ

  1. 启动NameServer
    • 在终端中,执行:
      nohup sh bin/mqnamesrv &
  2. 启动Broker
    • 启动一个Broker实例:
      nohup sh bin/mqbroker -n localhost:9876 &
    • 如果需要启动多个Broker实例,可以重复上述步骤,注意每个Broker的配置文件需要有所不同。

停止RocketMQ

  1. 停止Broker
    • 使用stoprocketmq.sh脚本来停止Broker:
      sh bin/mqshutdown broker
  2. 停止NameServer
    • 同样使用脚本来停止NameServer:
      sh bin/mqshutdown namesrv

通过这些步骤,你可以成功地安装并启动RocketMQ环境,为后续的消息传递和业务应用做好准备。

RocketMQ核心概念解析
主题与队列

主题(Topic)是RocketMQ中的一个逻辑概念,它类似于消息队列中的主题或频道,用于区分不同的消息流。主题可以定义为具有相同类型或逻辑关系的消息集合。例如,一个电商网站可以定义一个“订单”主题,用来发布和接收与订单相关的消息。

队列(Queue)则是主题内部一个物理上的概念,用于实际存储和传输消息。每个主题可以包含多个队列,这样的设计不仅保证了消息的可靠性,还可以提高并发处理能力。例如,假设“订单”主题有三个队列,一个队列用于存储新订单,一个队列用于处理订单支付,另一个队列用于订单配送。

队列的使用场景

  • 负载均衡:多个队列可以实现消息的负载均衡,使得每个队列都能均匀地处理消息。
  • 消息隔离:不同队列可以隔离不同类型的消息,例如,将紧急消息与普通消息放在不同的队列中。
  • 消息顺序:可以为不同的队列设置不同的消息顺序要求,以满足业务需求。
生产者与消费者

生产者(Producer)和消费者(Consumer)是RocketMQ中两个重要的角色,分别负责发送和接收消息。

生产者

生产者负责向指定的主题发送消息。一个生产者实例可以发送多种类型的消息,如同步消息、异步消息或单向消息。以下是生产者的典型使用场景:

  • 同步发送:发送消息后,等待响应返回,确保消息已被成功接收。
  • 异步发送:发送消息后,不需要等待响应,可以继续执行其他任务。
  • 单向发送:发送消息后,不等待响应,不关心消息是否被成功接收。

消费者

消费者负责从指定的主题中接收并处理消息。消费者可以订阅一个或多个主题,并且可以设置不同的消费模式,如集群消费和广播消费。

  • 集群消费:多个消费者共同处理同一主题的消息,每个消息只被一个消费者处理。
  • 广播消费:消息被所有订阅该主题的消费者接收和处理。

消息模型与消息路由

消息路由是RocketMQ的核心机制之一,它负责把生产者发送的消息路由到正确的队列中。RocketMQ支持多种消息模型,包括顺序消息、定时消息和事务消息。

顺序消息

顺序消息指的是消息按照发送的顺序进行消费。这对于某些需要严格顺序处理的应用场景非常有用,例如订单处理。

定时消息

定时消息是指消息在发送后会在指定的时间点被消费。这种功能可以用于预约提醒等场景。

事务消息

事务消息提供了一种确保消息可靠传递的方式。在发送事务消息时,生产者会首先发送一个预提交的消息,接着执行业务逻辑。如果业务逻辑执行成功,则通过消息服务确认消息;如果失败,则会自动进行消息回滚。

实际应用案例

  • 订单系统:使用RocketMQ实现订单生成后,通知支付系统进行扣款,并通知物流系统进行配送。
  • 推荐系统:收集用户的点击行为,通过RocketMQ将消息发送到推荐系统进行实时推荐计算。

这些概念和模型在实际应用中有着广泛的应用场景,可以通过RocketMQ强大的消息处理能力来提高系统的可靠性和性能。

RocketMQ源码结构与主要类介绍
源码目录结构

RocketMQ源码的目录结构较为清晰,方便开发者理解和定位相关代码。以下是源码的主要目录结构:

  • bin:包含启动脚本,如启动NameServer和Broker的脚本。
  • conf:存放配置文件,包括Broker、NameServer的配置文件。
  • core:RocketMQ的核心模块,包括网络、消息存储、路由管理等。
  • client:客户端相关代码,包括Java客户端、C++客户端等。
  • tools:提供一些辅助工具,如消息查询、集群管理工具等。
  • test:测试代码,包括单元测试和集成测试。

主要模块

  • client:客户端代码模块,包括生产者和消费者相关的逻辑。
  • store:消息存储模块,管理消息的持久化。
  • broker:Broker服务端代码,处理消息的接收和转发。
  • namesrv:NameServer服务端代码,管理Broker的路由信息。
关键类解析

RocketMQ源码中包含许多重要的类,下面介绍几个核心类:

Message

Message类是RocketMQ中消息的基本载体,它封装了消息的内容、主题、标签、消息体等信息。下面是其基本结构:

public class Message {
    private String topic;
    private String tags;
    private String keys;
    private byte[] body;
    private Map<String, String> properties;

    public Message(String topic, String tags, String keys, byte[] body) {
        this.topic = topic;
        this.tags = tags;
        this.keys = keys;
        this.body = body;
    }

    // 其他方法省略
}

DefaultMQProducer

DefaultMQProducer是RocketMQ生产者的基础类,负责消息的发送。下面是它的主要构造方法和发送消息的方法:

public class DefaultMQProducer extends MQProducerInner {
    private String producerGroup;
    private String instanceName;
    private String clientCallbackExecutorThreads;

    public DefaultMQProducer(String producerGroup) {
        this.producerGroup = producerGroup;
    }

    public void send(Message msg) throws MQClientException {
        // 发送消息的逻辑
    }

    public void send(Message msg, MessageQueue mq) throws MQClientException {
        // 发送消息到指定队列的逻辑
    }
}

DefaultMQPullConsumer

DefaultMQPullConsumer是RocketMQ拉取模式消费者的基础类,负责消息的接收。以下是它的主要方法:

public class DefaultMQPullConsumer extends MQConsumerInner {
    private String consumerGroup;
    private String instanceName;
    private String clientCallbackExecutorThreads;

    public DefaultMQPullConsumer(String consumerGroup) {
        this.consumerGroup = consumerGroup;
        this.instanceName = "CID-" + UUIDUtil.uniqueString();
    }

    public void subscribe(String topic, String subscription) {
        // 订阅主题和过滤规则
    }

    public PullResult pull(MessageQueue mq, String topic, String consumerId, long offset, int maxNums) {
        // 拉取消息的逻辑
    }
}

MessageQueue

MessageQueue类表示一个物理上的消息队列,存储消息的详细信息。下面是其基本结构:

public class MessageQueue implements Comparable<MessageQueue> {
    private String topic;
    private String brokerName;
    private String brokerAddr;
    private int queueId;

    public MessageQueue(String topic, String brokerName, String brokerAddr, int queueId) {
        this.topic = topic;
        this.brokerName = brokerName;
        this.brokerAddr = brokerAddr;
        this.queueId = queueId;
    }

    // 其他方法省略
}

MessageExt

MessageExtMessage的扩展类,存储更多详细信息,如消息的发送时间、消耗时间、压缩类型等。

public class MessageExt extends Message {
    private long bornTimestamp;
    private long sendTimeStamp;
    private String bornHost;
    private String storeHost;
    private String properties;
    private int sysFlag;
    private int queueId;
    private String commitLogOffset;
    private String topicQueueId;

    public MessageExt(String topic, String tags, String keys, byte[] body) {
        super(topic, tags, keys, body);
    }

    // 其他方法省略
}
常见使用的源码分析
  • 消息发送流程

    • 生产者向NameServer注册。
    • NameServer向生产者返回Broker列表。
    • 生产者根据路由信息选择合适的Broker发送消息。
    • Broker接收到消息后,根据路由规则将消息写入对应的队列。
    • Broker返回发送结果给生产者。
  • 消息接收流程
    • 消费者向NameServer注册。
    • NameServer向消费者返回Broker列表。
    • 消费者根据路由信息从Broker拉取消息。
    • 消费者处理完消息后,向Broker反馈已消费的消息。
    • Broker更新消息的消费状态。

了解这些类和流程有助于开发者更好地理解和使用RocketMQ。

RocketMQ消息发送与接收实战
生产者消息发送流程

生产者消息发送的流程涉及以下几个主要步骤:

  1. 初始化生产者实例
    • 创建DefaultMQProducer实例,并设置生产者组名。
  2. 连接NameServer
    • 调用start()方法启动生产者,连接到NameServer。
  3. 发送消息
    • 创建Message对象,包含消息体、主题、标签等信息。
    • 调用send()方法发送消息。
public class ProducerDemo {
    public static void main(String[] args) throws MQClientException {
        DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();

        String topic = "TestTopic";
        String tags = "TagA";
        String keys = "key1";
        String content = "Hello RocketMQ";
        byte[] body = content.getBytes(RemotingHelper.DEFAULT_CHARSET);

        Message msg = new Message(topic, tags, keys, body);
        try {
            SendResult sendResult = producer.send(msg);
            System.out.println("SendResult: " + sendResult);
        } catch (MQClientException e) {
            e.printStackTrace();
        } finally {
            producer.shutdown();
        }
    }
}

发送异步消息

异步消息发送允许生产者在发送消息后立即返回,而不需要等待消息发送结果。

public class AsyncProducerDemo {
    public static void main(String[] args) throws MQClientException {
        DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();

        String topic = "TestTopic";
        String tags = "TagA";
        String keys = "key1";
        String content = "Hello Async RocketMQ";
        byte[] body = content.getBytes(RemotingHelper.DEFAULT_CHARSET);

        Message msg = new Message(topic, tags, keys, body);
        producer.send(msg, new SendCallback() {
            public void onSuccess(SendResult sendResult) {
                System.out.println("Send result: " + sendResult);
            }

            public void onException(Throwable e) {
                e.printStackTrace();
            }
        });
        producer.shutdown();
    }
}
消费者消息接收流程

消费者消息接收的流程主要包括以下几个步骤:

  1. 初始化消费者实例
    • 创建DefaultMQPushConsumerDefaultMQPullConsumer实例,并设置消费者组名。
  2. 订阅主题和过滤规则
    • 使用subscribe()方法订阅一个或多个主题。
  3. 消费消息
    • 调用start()方法启动消费者,开始消费消息。
public class ConsumerDemo {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("TestTopic", "*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeReturnType consumeMessage(List<MessageExt> msgs) {
                for (MessageExt msg : msgs) {
                    System.out.println("Received message: " + new String(msg.getBody(), RemotingHelper.DEFAULT_CHARSET));
                }
                return ConsumeReturnType.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}

异常处理

在实际应用中,消息接收可能出现异常情况,如网络中断、服务不可用等。以下代码示例展示了如何处理这些异常情况:

public class ConsumerExceptionDemo {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("TestTopic", "*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeReturnType consumeMessage(List<MessageExt> msgs) {
                for (MessageExt msg : msgs) {
                    try {
                        System.out.println("Received message: " + new String(msg.getBody(), RemotingHelper.DEFAULT_CHARSET));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                return ConsumeReturnType.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
}
实战项目案例讲解

电商订单系统案例

假设有一个电商订单系统,需要创建订单并进行支付。这里可以使用RocketMQ实现订单的创建和支付流程。

生产者(下单模块)

  • 创建订单后,发送一条订单消息。
  • 订阅支付模块,等待支付完成。
public class OrderProducer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQProducer producer = new DefaultMQProducer("OrderProducerGroup");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();

        String topic = "OrderTopic";
        String tags = "OrderCreate";
        String keys = "OrderKey1";
        String content = "Order 1 created";
        byte[] body = content.getBytes(RemotingHelper.DEFAULT_CHARSET);

        Message msg = new Message(topic, tags, keys, body);
        SendResult sendResult = producer.send(msg);
        System.out.println("Send Result: " + sendResult);
        producer.shutdown();
    }
}

消费者(支付模块)

  • 订阅订单主题,接收订单消息。
  • 完成支付后,发送支付完成消息。
public class PaymentConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("PaymentConsumerGroup");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("OrderTopic", "OrderCreate");

        consumer.registerMessageListener((List<MessageExt> msgs, ConsumeContext context) -> {
            for (MessageExt msg : msgs) {
                String content = new String(msg.getBody(), RemotingHelper.DEFAULT_CHARSET);
                System.out.println("Received Order: " + content);
                // 模拟支付逻辑
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 支付完成发送消息
                DefaultMQProducer producer = new DefaultMQProducer("PaymentProducerGroup");
                producer.setNamesrvAddr("localhost:9876");
                producer.start();
                String paymentTopic = "PaymentTopic";
                String paymentTags = "PaymentDone";
                String paymentKeys = "PaymentKey1";
                String paymentContent = "Payment for Order 1 done";
                byte[] body = paymentContent.getBytes(RemotingHelper.DEFAULT_CHARSET);
                Message paymentMsg = new Message(paymentTopic, paymentTags, paymentKeys, body);
                SendResult paymentSendResult = producer.send(paymentMsg);
                producer.shutdown();
                System.out.println("Payment Send Result: " + paymentSendResult);
            }
            return ConsumeReturnType.CONSUME_SUCCESS;
        });
        consumer.start();
    }
}

订单完成后端系统(消息订阅者)

  • 订阅订单完成消息,更新订单状态。
public class OrderCompletionConsumer {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("OrderCompletionConsumerGroup");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("PaymentTopic", "PaymentDone");

        consumer.registerMessageListener((List<MessageExt> msgs, ConsumeContext context) -> {
            for (MessageExt msg : msgs) {
                String content = new String(msg.getBody(), RemotingHelper.DEFAULT_CHARSET);
                System.out.println("Order completion: " + content);
                // 更新订单状态
            }
            return ConsumeReturnType.CONSUME_SUCCESS;
        });
        consumer.start();
    }
}

通过以上代码和流程,可以实现一个简单的电商订单系统,使用RocketMQ进行消息传递和处理,确保订单和支付流程的可靠性和一致性。

RocketMQ集群搭建与配置优化
集群架构解析

RocketMQ集群由多个组件组成,包括NameServer、Broker、生产者(Producer)、消费者(Consumer)等。以下是组件之间的关系:

  1. NameServer

    • 作为RocketMQ集群中的路由管理器,用于管理和维护Broker的路由信息。
    • 如果NameServer宕机,不会丢失数据,因为路由信息存储在Broker上,NameServer只是提供查找服务。
  2. Broker

    • 负责消息的存储和转发。
    • 每个Broker有自己的存储目录,用于持久化消息。
    • 支持高可用配置,通过主备模式实现。
  3. 生产者(Producer)

    • 负责发送消息。
    • 生产者需要向NameServer注册,获取Broker的路由信息。
  4. 消费者(Consumer)
    • 负责接收并处理消息。
    • 消费者需要订阅感兴趣的Topic,并通过NameServer获取Broker的路由信息。

网络通信模型

  • 广播模式:消息被所有订阅该主题的消费者接收并处理。
  • 集群模式:消息只被一个消费者处理,多个消费者共同处理同一主题的消息。
  • 顺序消息:消息按照发送顺序被消费者处理。
集群搭建步骤

搭建RocketMQ集群需要遵守以下步骤:

  1. 安装和配置多个NameServer实例
    • 在多个机器上部署NameServer,并保证它们可以互相通信。
    • 修改conf/rocketmq.properties文件,配置NameServer的监听地址。
# NameServer配置
namesrv.addr=localhost:9876,localhost:9877
  1. 配置Broker实例
    • 根据实际需求,配置多个Broker实例。
    • 修改conf/broker.properties文件,配置Broker的监听地址、存储路径等。
# Broker配置
brokerAddr=localhost:10911
brokerName=broker0
brokerId=0
storePathRootDir=/opt/rocketmq/data
  1. 配置生产者和消费者
    • 为生产者和消费者配置合适的NameServer地址。
    • 生产者和消费者需要知道NameServer的地址,以便获取Broker的路由信息。
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
producer.setNamesrvAddr("localhost:9876,localhost:9877");
producer.start();

具体配置文件代码示例

NameServer配置文件

# NameServer配置文件
namesrv.addr=localhost:9876,localhost:9877

Broker配置文件

# Broker配置文件
brokerAddr=localhost:10911
brokerName=broker0
brokerId=0
storePathRootDir=/opt/rocketmq/data
常见配置优化方案

优化Broker性能

  1. 调整消息存储路径

    • 将Broker的消息存储目录配置到SSD硬盘,提高I/O性能。
    • 配置storePathRootDir属性,例如:
      storePathRootDir=/path/to/ssd
  2. 增大内存缓存
    • 增加Broker的内存缓存大小,提高消息处理速度。
    • 配置brokerMemCommitLogMaxSize属性,例如:
      brokerMemCommitLogMaxSize=1073741824

优化NameServer性能

  1. 增加NameServer数量
    • 部署多个NameServer实例,提升系统的可用性和冗余性。
    • 配置namesrv.addr属性,例如:
      namesrv.addr=localhost:9876,localhost:9877

优化网络通信

  1. 使用集群模式

    • 设置消费者为集群模式,确保消息只被一个消费者处理。
    • 配置consumer.consumeMode属性,例如:
      consumer.setConsumeMode(ConsumeMode.CLUSTERING);
  2. 优化消息过滤
    • 使用消息标签(Tag)进行过滤,提高消息处理效率。
    • 在消费者端订阅特定标签的消息,例如:
      consumer.subscribe("TestTopic", "TagA");

通过以上步骤,可以搭建和优化RocketMQ集群,以满足高并发和大规模消息传递的需求。

RocketMQ常见问题与解决方案
常见调试与问题排查

在使用RocketMQ过程中,可能会遇到各种问题,以下是一些常见的调试和问题排查步骤:

  1. 检查日志

    • 查看RocketMQ的日志文件,定位异常信息。
    • 日志文件位于logs目录下,例如broker.lognamesrv.log
      tail -f logs/broker.log
  2. 检查网络连接

    • 确保生产者、消费者和Broker之间的网络通信正常。
    • 检查防火墙设置,确保端口开放。
      nc -zv localhost 9876
  3. 配置文件检查

    • 检查broker.propertiesrocketmq.properties等配置文件的设置。
    • 确保NameServer、Broker的地址配置正确。
      namesrv.addr=localhost:9876
  4. 使用工具监控
    • 使用RocketMQ提供的监控工具,如mqadmin,监控Broker和NameServer的状态。
      sh bin/mqadmin clusterList -n localhost:9876
常见配置参数解读

RocketMQ提供了丰富的配置参数,以下是一些常用的配置项:

  1. NameServer配置

    • namesrv.addr:NameServer的地址。
      namesrv.addr=localhost:9876,localhost:9877
  2. Broker配置

    • brokerAddr:Broker的地址。
    • brokerName:Broker的名称。
    • brokerId:Broker的ID号。
    • storePathRootDir:消息存储的根目录。
      brokerAddr=localhost:10911
      brokerName=broker0
      brokerId=0
      storePathRootDir=/opt/rocketmq/data
  3. 生产者配置

    • producerGroup:生产者的组名。
    • namesrvAddr:NameServer的地址。
      DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
      producer.setNamesrvAddr("localhost:9876");
  4. 消费者配置
    • consumerGroup:消费者的组名。
    • consumeMode:消费模式,如集群消费或广播消费。
      DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
      consumer.setConsumeMode(ConsumeMode.CLUSTERING);
故障解决方法
  1. 消息发送失败

    • 检查网络连接是否正常。
    • 检查Broker是否正常运行。
    • 确保消息体大小不超过最大限制。
  2. 消息消费失败

    • 检查消费者是否正确订阅主题。
    • 检查消费者的消息处理逻辑,避免死锁。
    • 使用consumingStats属性查看消费者的状态。
  3. 集群节点宕机
    • 配置主备模式,确保高可用。
    • 启动备用节点,接管主节点的功能。
    • 使用clusterList查看集群状态。

通过以上步骤,可以有效地排查和解决RocketMQ在使用过程中遇到的各种问题,确保系统的稳定性和可靠性。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消