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

RocketMQ底层原理项目实战教程

标签:
中间件
概述

本文深入探讨了RocketMQ的底层原理,并通过项目实战展示了如何在实际开发中应用RocketMQ,涵盖环境搭建、核心概念解析、消息发送与接收、集群部署与配置、性能调优及问题排查等多个方面,帮助读者全面掌握RocketMQ底层原理项目实战。

RocketMQ简介与环境搭建
RocketMQ简介

RocketMQ是由阿里巴巴开源的一款分布式消息中间件,广泛应用于大规模分布式系统中。它主要提供了异步通信、解耦和可靠消息传输等核心功能。RocketMQ具有高可用性、高吞吐量、低延迟、高可扩展性等特性,适用于高频交易、海量数据处理等场景。

RocketMQ主要特点如下:

  • 高吞吐量:每秒可以处理数百万消息,适合高并发场景。
  • 低延迟:平均处理延迟低于10ms,保证消息传递的实时性。
  • 分布式集群支持:支持分布式部署,具有良好的扩展性。
  • 消息持久化:将消息持久化到磁盘,确保消息不丢失。
  • 消息过滤:支持多种消息过滤策略,如标签、SQL过滤等。
  • 消息重试:在消息投递失败时,支持多次重试策略。
  • 消息追踪:支持消息的完整追踪,方便排查问题。
  • 分布式事务支持:支持分布式事务的可靠消息传递。
开发环境搭建

系统环境要求

  • 操作系统:Linux、Windows、macOS
  • Java版本:Java 8及以上
  • Maven版本:Maven 3.5及以上

安装RocketMQ

RocketMQ分为服务端和客户端两部分,其中服务端包含了NameServer和Broker,客户端则包含生产者和消费者。

下载RocketMQ

git clone https://github.com/apache/rocketmq.git
cd rocketmq

启动NameServer

NameServer是RocketMQ的服务发现和路由中心。

nohup sh bin/mqnamesrv &

启动Broker

Broker是实际存储和转发消息的角色。

nohup sh bin/mqbroker -n localhost:9876 &

配置环境变量

编辑~/.bashrc~/.zshrc文件,添加以下内容:

export JAVA_HOME=/path/to/java
export PATH=$JAVA_HOME/bin:$PATH
export ROCKETMQ_HOME=/path/to/rocketmq
export PATH=$ROCKETMQ_HOME/bin:$PATH

检查服务是否启动成功

ps -ef | grep mqnamesrv
ps -ef | grep mqbroker
快速入门案例

创建一个简单的Java项目,并添加RocketMQ的依赖。对于Maven项目,可以在pom.xml中添加以下依赖:

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.9.1</version>
</dependency>

生产者代码示例

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;

public class Producer {
    public static void main(String[] args) throws Exception {
        // 实例化一个DefaultMQProducer对象,并设置Producer的名称
        DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
        // 设置NameServer地址
        producer.setNamesrvAddr("localhost:9876");
        // 启动Producer实例
        producer.start();
        // 创建一个消息
        Message message = new Message("TopicTest", // topic
                "TagA", // tag
                ("Hello RocketMQ").getBytes(RemotingHelper.DEFAULT_CHARSET), // body
                1000 // 额外属性
        );
        // 发送消息到一个Broker
        SendResult sendResult = producer.send(message);
        // 输出消息发送结果
        System.out.printf("%s%n", sendResult);
        // 如果不再发送消息,关闭Producer实例
        producer.shutdown();
    }
}

消费者代码示例

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlySuccess;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.common.consumer.ConsumeOrderlyContext;
import org.apache.rocketmq.common.message.MessageExt;

public class Consumer {
    public static void main(String[] args) throws Exception {
        // 实例化DefaultMQPushConsumer对象,指定Consumer组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
        // 设置NameServer地址
        consumer.setNamesrvAddr("localhost:9876");
        // 订阅指定主题的指定Tag的消息
        consumer.subscribe("TopicTest", "TagA");
        // 注册消息监听器
        consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                System.out.printf("Receive New Messages: %s %n", new String(msg.getBody()));
            }
            return ConsumeOrderlyContext.ConsumeOrderlyStatus.SUCCESS;
        });
        // 启动消费者实例
        consumer.start();
    }
}

运行测试

启动消费者应用,然后启动生产者应用发送消息。观察控制台输出,确认消息已被成功发送并接收到。

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

主题

主题(Topic)是对消息的一种分类方式,用于区分不同种类或主题的消息。每个主题可以有多个队列,一个队列代表一个物理上的消息存储单元。

队列

队列(Queue)是消息的基本存储单元。每个队列都有多个副本,用于保证消息的可靠传输和高可用性。RocketMQ支持一个主题下多个队列,从而提高消息的处理能力,实现负载均衡。

主题与队列的关系

  • 单主题单队列:一个主题只有一个队列,所有消息都在这个队列中传输。
  • 多主题多队列:一个主题可以有多个队列,消息会被分散到不同的队列中,实现负载均衡。

示例

// 创建一个单主题单队列的生产者
Message message = new Message("TopicTest", // topic
        "TagA", // tag
        ("Hello RocketMQ").getBytes(RemotingHelper.DEFAULT_CHARSET), // body
        1000 // 额外属性
);
// 创建一个多主题多队列的生产者
Message message = new Message("TopicTest", // topic
        "TagA", // tag
        ("Hello RocketMQ").getBytes(RemotingHelper.DEFAULT_CHARSET), // body
        1000 // 额外属性
);
生产者与消费者

生产者

生产者(Producer)负责向RocketMQ发送消息。生产者创建消息对象,设置消息属性,然后将消息发送到指定的主题和标签。

消费者

消费者(Consumer)负责从RocketMQ接收消息。消费者订阅一个或多个主题,并根据主题和标签接收消息。RocketMQ支持两种类型的消费者:Push Consumer和Pull Consumer。

Push Consumer

Push Consumer是由RocketMQ服务器主动推送给客户端的消息。这种方式适合实时性强的应用场景。

Pull Consumer

Pull Consumer是由客户端主动从服务器拉取消息。这种方式适合需要控制消息消费节奏的应用场景。

示例

// 创建一个Push Consumer
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("TopicTest", "TagA");
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
    for (MessageExt msg : msgs) {
        System.out.printf("Receive New Messages: %s %n", new String(msg.getBody()));
    }
    return ConsumeOrderlyContext.ConsumeOrderlyStatus.SUCCESS;
});
consumer.start();

// 创建一个Pull Consumer
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("ConsumerGroupName");
consumer.setNamesrvAddr("localhost:9876");
consumer.start();
List<MessageExt> msgs = consumer.pullBlockIfNeed("TopicTest", "*", 0, 100);
for (MessageExt msg : msgs) {
    System.out.printf("Receive New Messages: %s %n", new String(msg.getBody()));
}
消息类型与消息模型

消息类型

RocketMQ支持以下几种类型的消息:

  • 普通消息:最基本的无序消息类型。
  • 顺序消息:按消息发送顺序接收的消息类型。
  • 事务消息:包含事务操作的消息类型。

消息模型

RocketMQ支持以下几种消息模型:

  • 单向消息模型:生产者发送消息到RocketMQ,不等待RocketMQ的响应。
  • 同步消息模型:生产者发送消息到RocketMQ,并等待RocketMQ的响应。
  • 异步消息模型:生产者发送消息到RocketMQ,并通过回调函数获取RocketMQ的响应。

示例

// 创建一个发送同步消息的生产者
Message message = new Message("TopicTest", "TagA", "Hello RocketMQ".getBytes(RemotingHelper.DEFAULT_CHARSET), 1000);
SendResult sendResult = producer.sendSync(message);
System.out.println(sendResult);

// 创建一个发送异步消息的生产者
Message message = new Message("TopicTest", "TagA", "Hello RocketMQ".getBytes(RemotingHelper.DEFAULT_CHARSET), 1000);
producer.send(message, (SendResult sendResult1) -> {
    System.out.println(sendResult1);
});
RocketMQ消息发送与接收详解
消息发送流程

发送流程详解

  1. 创建消息对象:生产者创建一个Message对象,指定消息的主题、标签、内容等。
  2. 设置发送参数:生产者设置发送参数,如消息的延迟时间、发送策略等。
  3. 发送消息:生产者将消息发送到指定的主题和标签。
  4. 接收确认:生产者接收到RocketMQ的发送确认,确认消息是否成功发送。

示例

// 创建并发送普通消息
Message message = new Message("TopicTest", "TagA", "Hello RocketMQ".getBytes(RemotingHelper.DEFAULT_CHARSET), 1000);
SendResult sendResult = producer.send(message);
System.out.println(sendResult);

// 创建并发送序列化消息
MyMessage message = new MyMessage("TopicTest", "TagA", "Hello RocketMQ".getBytes(RemotingHelper.DEFAULT_CHARSET), 1000);
MessageExtBrokerInner brokerInner = MessageExtConverter.toMessageExt(message);
SendResult sendResult = producer.send(brokerInner);
System.out.println(sendResult);
消息接收流程

接收流程详解

  1. 订阅消息:消费者订阅一个或多个主题或标签。
  2. 接收消息:RocketMQ将消息推送给消费者,消费者接收到消息后进行处理。
  3. 处理结果:消费者将处理结果返回给RocketMQ,RocketMQ根据结果进行下一步操作。

示例

// 创建一个订阅TopicTest主题的消息消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("TopicTest", "TagA");
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
    for (MessageExt msg : msgs) {
        System.out.printf("Receive New Messages: %s %n", new String(msg.getBody()));
    }
    return ConsumeOrderlyContext.ConsumeOrderlyStatus.SUCCESS;
});
consumer.start();
消息过滤与路由

消息过滤

RocketMQ支持多种消息过滤策略,如标签过滤、SQL过滤等。

标签过滤

// 创建一个订阅TopicTest主题的消息消费者,并指定标签过滤
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("TopicTest", "TagA");
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
    for (MessageExt msg : msgs) {
        System.out.printf("Receive New Messages: %s %n", new String(msg.getBody()));
    }
    return ConsumeOrderlyContext.ConsumeOrderlyStatus.SUCCESS;
});
consumer.start();

SQL过滤

// 创建一个订阅TopicTest主题的消息消费者,并指定SQL过滤
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("TopicTest", "*", new SQLFilterMessageListener());
consumer.start();

消息路由

RocketMQ中的消息路由是指消息从生产者发送到消费者的过程中,如何选择合适的队列进行传输。RocketMQ的路由信息由NameServer维护,生产者和消费者通过NameServer获取路由信息。

示例

// 创建一个生产者,并指定路由信息
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
producer.setNamesrvAddr("localhost:9876");
producer.setMessageQueueSelector(new MessageQueueSelector() {
    @Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object o) {
        return mqs.get((int) (o % mqs.size()));
    }
});
producer.start();
Message message = new Message("TopicTest", "TagA", "Hello RocketMQ".getBytes(RemotingHelper.DEFAULT_CHARSET), 1000);
producer.send(message, 1);
RocketMQ集群部署与配置
集群模式介绍

单点部署

单点部署是指RocketMQ的各个组件部署在单个节点上。这种方式适合开发和测试环境,不推荐在生产环境中使用。

多点部署

多点部署是指RocketMQ的各个组件部署在多个节点上,实现高可用性和负载均衡。RocketMQ支持以下几种集群模式:

  • 主从模式:一个主节点和多个从节点。主节点负责处理写操作,从节点负责处理读操作。
  • 集群模式:多个节点组成一个集群,每个节点都可以处理读写操作。
  • 多数据中心模式:多个数据中心部署RocketMQ,实现跨数据中心的故障转移和负载均衡。
集群部署步骤

主从模式部署

  1. 安装RocketMQ:在每个节点上安装RocketMQ。
  2. 配置主节点:修改主节点的配置文件broker.properties,设置brokerId为0。
  3. 配置从节点:修改从节点的配置文件broker.properties,设置brokerId为1或更大的数字。
  4. 启动NameServer:在每个节点上启动NameServer。
  5. 启动Broker:在每个节点上启动Broker。

集群模式部署

  1. 安装RocketMQ:在每个节点上安装RocketMQ。
  2. 配置节点:修改每个节点的配置文件broker.properties,设置brokerId为不同的数字。
  3. 启动NameServer:在每个节点上启动NameServer。
  4. 启动Broker:在每个节点上启动Broker。

示例

# 配置文件broker.properties示例
brokerClusterName=DefaultCluster
brokerName=Broker-0
brokerId=0
brokerRole=ASYNC_MASTER
namesrvAddr=localhost:9876
storePathRootDir=/path/to/store
deleteWhen=04
fileReservedDays=7
mapedFileSizeCommitLog=1GB
mapedFileSizeConsumeQueue=512MB
集群配置优化

调整日志配置

RocketMQ的日志配置可以通过修改logback.xml文件进行调整。

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="info">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

调整消息存储配置

RocketMQ的消息存储配置可以通过修改broker.properties文件进行调整。

# 配置文件broker.properties示例
brokerClusterName=DefaultCluster
brokerName=Broker-0
brokerId=0
brokerRole=ASYNC_MASTER
namesrvAddr=localhost:9876
storePathRootDir=/path/to/store
deleteWhen=04
fileReservedDays=7
mapedFileSizeCommitLog=1GB
mapedFileSizeConsumeQueue=512MB

示例

# 配置文件broker.properties示例
brokerClusterName=DefaultCluster
brokerName=Broker-0
brokerId=0
brokerRole=ASYNC_MASTER
namesrvAddr=localhost:9876
storePathRootDir=/path/to/store
deleteWhen=04
fileReservedDays=7
mapedFileSizeCommitLog=1GB
mapedFileSizeConsumeQueue=512MB
RocketMQ性能调优与问题排查
性能监控与调优

监控工具

RocketMQ提供了多种监控工具,包括RocketMQ-Admin和RocketMQ-Tool。这些工具可以帮助开发人员监控RocketMQ的运行状态,获取详细的性能数据。

RocketMQ-Admin

RocketMQ-Admin是一个命令行工具,用于管理RocketMQ集群。

nohup sh bin/mqadmin.sh topicList localhost:9876 &

RocketMQ-Tool

RocketMQ-Tool是一个Java工具,用于监控RocketMQ的性能指标。

nohup sh bin/mqtool.sh -n localhost:9876 -c broker

性能调优

RocketMQ的性能调优可以从以下几个方面进行:

  • 调整网络参数:通过修改配置文件broker.properties中的network参数,优化网络性能。
  • 调整存储参数:通过修改配置文件broker.properties中的store参数,优化存储性能。
  • 调整消息处理参数:通过修改配置文件broker.properties中的message参数,优化消息处理性能。

示例

# 配置文件broker.properties示例
brokerClusterName=DefaultCluster
brokerName=Broker-0
brokerId=0
brokerRole=ASYNC_MASTER
namesrvAddr=localhost:9876
storePathRootDir=/path/to/store
deleteWhen=04
fileReservedDays=7
mapedFileSizeCommitLog=1GB
mapedFileSizeConsumeQueue=512MB
常见问题排查

问题1:消息发送失败

原因

  • 生产者配置参数错误。
  • 消息体过大。
  • 网络异常。

解决方案

  • 检查生产者配置参数是否正确。
  • 调整消息体大小。
  • 检查网络连接是否正常。

示例

// 检查生产者配置参数是否正确
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
producer.setNamesrvAddr("localhost:9876");
producer.start();

// 调整消息体大小
Message message = new Message("TopicTest", "TagA", "Hello RocketMQ".getBytes(RemotingHelper.DEFAULT_CHARSET), 1000);
SendResult sendResult = producer.send(message);
System.out.println(sendResult);

问题2:消息接收失败

原因

  • 消费者配置参数错误。
  • 消息被过滤。
  • 网络异常。

解决方案

  • 检查消费者配置参数是否正确。
  • 检查消息过滤规则是否正确。
  • 检查网络连接是否正常。

示例

// 检查消费者配置参数是否正确
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("TopicTest", "TagA");
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
    for (MessageExt msg : msgs) {
        System.out.printf("Receive New Messages: %s %n", new String(msg.getBody()));
    }
    return ConsumeOrderlyContext.ConsumeOrderlyStatus.SUCCESS;
});
consumer.start();
案例分析与实践

案例一:消息堆积

问题描述

在高并发场景下,消息可能会堆积在RocketMQ中,导致消费者处理延迟。

解决方案

  • 增加消费线程:增加消费者的消费线程数。
  • 优化消息处理逻辑:优化消费者的业务逻辑,提高消息处理效率。
  • 增加Broker节点:增加Broker节点,实现负载均衡。

示例

// 增加消费线程
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("TopicTest", "TagA");
consumer.setMessageModel(MessageModel.CLUSTERING);
consumer.setConsumeMessageBatchMaxSize(10);
consumer.setConsumeThreadMax(10);
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
    for (MessageExt msg : msgs) {
        System.out.printf("Receive New Messages: %s %n", new String(msg.getBody()));
    }
    return ConsumeOrderlyContext.ConsumeOrderlyStatus.SUCCESS;
});
consumer.start();

案例二:消息丢失

问题描述

在高并发场景下,可能由于网络异常等原因导致消息丢失。

解决方案

  • 增加重试机制:配置生产者和消费者的消息重试机制。
  • 增加消息队列:增加消息队列,实现消息的冗余存储。
  • 开启持久化:开启消息持久化,确保消息不丢失。

示例

// 增加重试机制
DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName");
producer.setNamesrvAddr("localhost:9876");
producer.setRetryTimesWhenSendFailed(3);
producer.start();

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ConsumerGroupName");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("TopicTest", "TagA");
consumer.setMessageModel(MessageModel.CLUSTERING);
consumer.setConsumeMessageBatchMaxSize(10);
consumer.setConsumeThreadMax(10);
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
    for (MessageExt msg : msgs) {
        System.out.printf("Receive New Messages: %s %n", new String(msg.getBody()));
    }
    return ConsumeOrderlyContext.ConsumeOrderlyStatus.SUCCESS;
});
consumer.start();
RocketMQ实战项目案例
实战项目背景与需求分析

背景

假设你正在开发一个电商系统,该系统需要处理大量的订单消息。为了保证系统的高可用性和消息的可靠传输,决定使用RocketMQ作为消息中间件。

需求分析

  1. 订单创建:当用户下单时,系统需要创建一条订单消息,发送到RocketMQ。
  2. 订单处理:当订单消息被消费时,系统需要处理订单,包括支付、发货等操作。
  3. 订单查询:当用户查询订单时,系统需要从RocketMQ中获取订单信息。
项目设计与实现

订单创建

在订单创建模块,当用户下单时,系统需要创建一条订单消息,发送到RocketMQ。

// 创建订单消息
Message message = new Message("TopicOrder", "TagOrder", "Create Order".getBytes(RemotingHelper.DEFAULT_CHARSET), 1000);
SendResult sendResult = producer.send(message);
System.out.println(sendResult);

订单处理

在订单处理模块,当订单消息被消费时,系统需要处理订单,包括支付、发货等操作。

// 订阅订单主题的消息消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("OrderConsumerGroupName");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("TopicOrder", "TagOrder");
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
    for (MessageExt msg : msgs) {
        System.out.printf("Receive Order Messages: %s %n", new String(msg.getBody()));
        // 处理订单
    }
    return ConsumeOrderlyContext.ConsumeOrderlyStatus.SUCCESS;
});
consumer.start();

订单查询

在订单查询模块,当用户查询订单时,系统需要从RocketMQ中获取订单信息。

// 拉取订单消息
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("OrderConsumerGroupName");
consumer.setNamesrvAddr("localhost:9876");
consumer.start();
List<MessageExt> msgs = consumer.pullBlockIfNeed("TopicOrder", "*", 0, 100);
for (MessageExt msg : msgs) {
    System.out.printf("Receive Order Messages: %s %n", new String(msg.getBody()));
}
项目部署与维护

部署步骤

  1. 安装RocketMQ:在每个节点上安装RocketMQ。
  2. 配置RocketMQ:修改配置文件broker.propertieslogback.xml等。
  3. 启动RocketMQ:启动NameServer和Broker。
  4. 启动应用:启动订单创建、订单处理和订单查询模块。

维护要点

  • 监控系统:使用RocketMQ-Admin和RocketMQ-Tool监控RocketMQ的运行状态。
  • 备份数据:定期备份RocketMQ的数据,防止数据丢失。
  • 优化配置:根据实际需求调整RocketMQ的配置参数,提高性能。

示例

# 启动NameServer
nohup sh bin/mqnamesrv &
# 启动Broker
nohup sh bin/mqbroker -n localhost:9876 &
# 启动订单创建模块
java -jar order-producer.jar
# 启动订单处理模块
java -jar order-consumer.jar
# 启动订单查询模块
java -jar order-query.jar

通过以上步骤,你将能够成功部署并维护一个基于RocketMQ的电商系统。

点击查看更多内容
TA 点赞

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

评论

作者其他优质文章

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

100积分直接送

付费专栏免费学

大额优惠券免费领

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

举报

0/150
提交
取消