云消息队列RabbitMQ实践解决方案评测

本文涉及的产品
Serverless 应用引擎 SAE,800核*时 1600GiB*时
性能测试 PTS,5000VUM额度
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 一文带你详细了解云消息队列RabbitMQ实践的解决方案优与劣

这是解决方案评测的第八篇,也是开发者新版评测的第八篇。希望大家可以踊跃参加,把你最真实的体验感受和建议分享出来。可点击下方链接前往评测活动首页:

解决方案评测|云消息队列RabbitMQ实践

解决方案评测|基于hologres搭建轻量OLAP分析平台

解决方案评测|10 分钟构建 AI 客服并应用到网站、钉钉或微信中

解决方案评测|函数计算驱动多媒体文件处理

解决方案评测|Serverless高可用架构

解决方案评测|容器化管理云上应用

解决方案评测|通义万相AI绘画创作

解决方案评测|高效构建企业门户网站

方案速览

还是老惯例,先放上整体方案的界面图。可以看到风格依旧,内容排版样式也是依旧,熟悉的味道,熟悉的风格,熟悉的技术栈。

image.png

方案整体依旧是从五个方面进行的阐述。分别是必要性、架构部署、应用场景、优惠购买、推荐方案。接下来将逐个进行阐述。

在必要性的阐述上,先开门见山地提到云消息队列 RabbitMQ的几个突出优势,随后以表格的形式列举了与开源RabbitMQ的对比。

image.png

实际上除了上述罗列的几个突出差异外,还可以展开来列举如下:

维度 开源RabbitMQ 云消息队列 RabbitMQ
兼容性 完全兼容AMQP 0-9-1协议 完全兼容AMQP 0-9-1协议,兼容开源RabbitMQ客户端,完全兼容RabbitMQ开源社区,快速迁移上云
部署与维护 需要自行部署和维护,包括服务器配置、安全更新等 免部署免运维,由阿里云提供专业的自动化运维服务
稳定性 可能面临消息积压、内存泄漏、服务器故障等问题 通过架构优化避免了这些问题,提高了系统的稳定性
扩展性 需要手动扩展节点,配置复杂 支持弹性伸缩,根据业务需求自动调整资源
成本 需要自行承担服务器、网络等基础设施成本 按量计费,根据实际使用量付费,降低了成本
安全性 需要自行配置安全策略,如访问控制、加密传输等 提供多租户隔离、加密传输等安全特性,确保数据安全
监控与告警 需要自行搭建监控系统,并配置告警规则 提供实时监控和告警功能,方便及时发现并解决问题
社区支持 拥有活跃的开源社区,可以获取丰富的技术资源和解决方案 依托阿里云的技术支持和开源RabbitMQ的社区支持
高可用性 可以配置成集群模式,提供高可用性 支持多可用区高可用,即使整个机房不可用仍可正常提供服务
特性与功能 提供基本的消息队列功能,如消息发布、订阅、确认等 增强了部分功能,如延时消息、死信队列等,并支持百万级队列和单队列的横向扩展

相比于往期解决方案的架构不同,本次因为只涉及一个产品,所以架构相对简单不少。整个消息队列流程也就三步,如下:

  1. 生产者向 Exchange 发送消息;
  2. Exchange 根据消息属性将消息路由到 Queue 进行存储;
  3. 消费者从 Queue 拉取消息进行消费。

在应用场景的阐述上,方案列举了四种典型的场景,如果按照功能类别来划分,其实可以归纳如下:

  1. 微服务系统间的事件通知
  • 案例:在一个微服务架构中,多个服务之间需要相互通信以完成业务逻辑。例如,在电商系统中,订单服务在订单状态变更时需要通知库存服务进行库存更新。云消息队列 RabbitMQ 版可以作为微服务之间的消息中间件,确保消息的可靠传递和异步处理,提高系统的响应速度和稳定性。
  1. 异步解耦
  • 案例:在复杂的业务系统中,某些操作可能耗时较长或依赖外部资源。例如,用户注册时需要发送欢迎邮件,但邮件发送的成功与否不影响用户注册的完成。此时,可以使用云消息队列 RabbitMQ 版将邮件发送任务异步化,解耦用户注册流程和邮件发送流程,提高系统的整体性能和用户体验。
  1. 流量削峰填谷
  • 案例:在电商大促或节假日期间,系统可能会面临巨大的访问压力。云消息队列 RabbitMQ 版可以作为系统的缓冲层,接收并存储暂时无法处理的请求,待系统负载降低后再进行处理,有效避免因瞬间高并发导致的系统崩溃或服务不可用。
  1. 数据集成与同步
  • 案例:在分布式系统中,多个数据源之间需要进行数据同步或集成。例如,将数据库中的数据同步到Elasticsearch中进行搜索优化。云消息队列 RabbitMQ 版可以作为一个数据交换的桥梁,实现不同数据源之间的异步数据同步,保证数据的一致性和实时性。
  1. 任务调度与执行
  • 案例:在复杂的业务流程中,可能存在多个相互依赖的任务需要按照一定的顺序执行。云消息队列 RabbitMQ 版可以通过消息队列来管理和调度这些任务,确保任务按照预定的顺序和规则执行,提高业务处理的自动化和智能化水平。

在产品优惠购买部分,非常友好地罗利出了云消息队列 RabbitMQ 版的三个规格类型供选择,但在免费试用中却出现了无法加载问题。

image.png

方案的最后列举了两个云消息队列 RabbitMQ 版的典型应用案例。

image.png

如果你想了解更多技术解决方案,可点击该链接前往。技术解决方案

部署体验

通过上述方案的速览,相信你已经对本次部署体验有了大致的了解了,接下来将通过实操来演示具体的方案部署过程及体验分享。

在正式开始部署体验前,有两个必要条件,第一就是拥有一个阿里云得账号(如果您还没有阿里云账号,请访问阿里云账号注册页面),第二就是确保账户余额大于等于100元(如果账号余额不足,请为阿里云账号充值)。满足了这两个条件后,接下来我们将通过ROS一键部署服务。

单击一键部署前往ROS控制台。相比以往的体验,本次涉及所填信息较少,你只需要修改资源栈名称(默认也行),勾选安全确认后点击下一步即可。

image.png

在信息确认界面,点击创建即可。

image.png

服务创建过程中,你可以通过点击事件标签查看创建进度。

image.png

大约耗时20秒,服务完成创建,此时可通过状态得知。

image.png

简单体验

接下来就是编写代码来实现发送和接受消息了。按照方案中的架构图,编码将涉及如下两个步骤:发送消息和订阅消息。

为了方便体验,这里我就直接参照官网的编码工具来进行。打开IntelliJ IDEA Ultimate,创建一个Java工程,新建一个pom.xml文件并引入java依赖。

<dependency>

   <groupId>com.rabbitmq</groupId>

   <artifactId>amqp-client</artifactId>

   <version>5.13.0</version>  <!-- 使用最新的稳定版本 -->

</dependency>

在编码前需要先准备如下信息,第一个是MQ实例的公网接入点地址、静态的用户名和密码。

实例的公网接入点可以在云消息队列 RabbitMQ 版控制台的实例列表中点击实例名称,在实例详情页面的接入点信息页签找到。如下:

image.png

在ROS输出中查看AK和SK,然后到RabbitMQ控制台设置静态用户名和密码。如下:

image.png

image.png

接着创建生产消息的编码,代码如下:

import com.rabbitmq.client.AMQP;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;

import java.nio.charset.StandardCharsets;

import java.util.HashMap;

import java.util.UUID;

import java.util.concurrent.TimeoutException;

public class Producer {

   public static void main(String[] args) throws IOException, TimeoutException {

       //设置实例的接入点。

       String hostName = "rabbitmq-serverless-cn-y1i3xlh6a02.cn-hangzhou.amqp-19.net.mq.amqp.aliyuncs.com";

       //设置实例的静态用户名密码。

       String userName = "UserName";

       String passWord = "PassWord";

       //设置实例的Vhost。

       String virtualHost = "test-vhost";

       //在生产环境中,建议提前创建好Connection,并在需要时重复使用,避免频繁创建和关闭Connection,

       // 以提高性能、复用连接资源,以及保证系统的稳定性。

       Connection connection = createConnection(hostName, userName, passWord, virtualHost);

       Channel channel = connection.createChannel();

       //设置Exchange、Queue和绑定关系。

       String exchangeName = "test-exchange";

       String queueName = "test-queue";

       String bindingKey = "test-routing-key";

       //设置Exchange类型。

       String exchangeType = "direct";

       //开始发送消息。

       for (int i = 0; i < 10; i++) {

           AMQP.BasicProperties props = new AMQP.BasicProperties.Builder().messageId(UUID.randomUUID().toString()).build();

           channel.basicPublish(exchangeName, bindingKey, true, props,

                   ("消息发送示例Body-" + i).getBytes(StandardCharsets.UTF_8));

           System.out.println("[SendResult] Message sent successfully, messageId: " + props.getMessageId()

                   + ", exchange: " + exchangeName + ", routingKey: " + bindingKey);

       }

       channel.close();

       connection.close();

   }

   public static Connection createConnection(String hostName, String userName, String passWord, String virtualHost)

           throws IOException, TimeoutException {

       ConnectionFactory factory = new ConnectionFactory();

       factory.setHost(hostName);

       factory.setUsername(userName);

       factory.setPassword(passWord);

       //设置为true,开启Connection自动恢复功能;设置为false,关闭Connection自动恢复功能。

       factory.setAutomaticRecoveryEnabled(true);

       factory.setNetworkRecoveryInterval(5000);

       factory.setVirtualHost(virtualHost);

       //默认端口,非加密端口5672,加密端口5671。

       factory.setPort(5672);

       //基于网络环境合理设置超时时间。

       factory.setConnectionTimeout(30 * 1000);

       factory.setHandshakeTimeout(30 * 1000);

       factory.setShutdownTimeout(0);

       Connection connection = factory.newConnection();

       return connection;

   }

}

接着编写订阅消息的代码,如下:

import com.rabbitmq.client.AMQP;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import com.rabbitmq.client.ConnectionFactory;

import com.rabbitmq.client.DefaultConsumer;

import com.rabbitmq.client.Envelope;

import java.io.IOException;

import java.nio.charset.StandardCharsets;

import java.util.HashMap;

import java.util.concurrent.TimeoutException;

public class ConsumerTest {

   public static void main(String[] args) throws IOException, TimeoutException {

       //设置实例的公网接入点。

       String hostName = "rabbitmq-serverless-cn-y1i3xlh6a02.cn-hangzhou.amqp-19.net.mq.amqp.aliyuncs.com";

       //设置实例的静态用户名密码。

       String userName = "UserName";

       String passWord = "PassWord";

       //设置实例的Vhost。

       String virtualHost = "test-vhost";

       Connection connection = createConnection(hostName, userName, passWord, virtualHost);

       final Channel channel = connection.createChannel();

       //声明Queue。

       String queueName = "test-queue";

       //创建${QueueName} ,该Queue必须在云消息队列RabbitMQ版控制台上已存在。

       AMQP.Queue.DeclareOk queueDeclareOk = channel.queueDeclare(queueName, true, false, false,

               new HashMap<String, Object>());

       //开始消费消息。

       channel.basicConsume(queueName, false, "test-consumerTag", new DefaultConsumer(channel) {

           @Override

           public void handleDelivery(String consumerTag, Envelope envelope,

                                      AMQP.BasicProperties properties, byte[] body)

                   throws IOException {

               //接收到的消息,进行业务逻辑处理。

               System.out.println("Received: " + new String(body, StandardCharsets.UTF_8) + ", deliveryTag: "

                       + envelope.getDeliveryTag() + ", messageId: " + properties.getMessageId());

               channel.basicAck(envelope.getDeliveryTag(), false);

           }

       });

   }

   public static Connection createConnection(String hostName, String userName, String passWord, String virtualHost)

           throws IOException, TimeoutException {

       ConnectionFactory factory = new ConnectionFactory();

       factory.setHost(hostName);

       factory.setUsername(userName);

       factory.setPassword(passWord);

       //设置为true,开启Connection自动恢复功能;设置为false,关闭Connection自动恢复功能。

       factory.setAutomaticRecoveryEnabled(true);

       factory.setNetworkRecoveryInterval(5000);

       factory.setVirtualHost(virtualHost);

       // 默认端口,非加密端口5672,加密端口5671。

       factory.setPort(5672);

       factory.setConnectionTimeout(300 * 1000);

       factory.setHandshakeTimeout(300 * 1000);

       factory.setShutdownTimeout(0);

       Connection connection = factory.newConnection();

       return connection;

   }

}

接下来分别运行上面的程序,前往云消息队列 RabbitMQ 版控制台查看消费情况,在左侧导航栏中,单击Dashboard,这里可以查看每个Queue的消息堆积数,消息速率等指标。

image.png

在左侧导航栏中,单击Queue列表,再单击test-queue

image.png

在test-queue详情页面,单击Dashboard,这里可以查看指定Queue的详细指标变化趋势,用于定位问题。

image.png

在左侧导航栏中,单击消息轨迹,在这里,可以根据按Message ID查询和按Queue查询。

  • 按Message ID查询:根据IntelliJ IDEA控制台打印的msgId可以精确查询对应消息的轨迹。

image.png

image.png

  • 按Queue查询:根据Queue名称可以查询对应Queue下所有消息的轨迹。

image.png

选择任意一条消息的轨迹,单击轨迹详情,能查询对应消息的生产和投递轨迹详情。

image.png

性能验证

为了更好地验证RabbitMQ在高并发场景下的性能,下面将通过代码实现多线程模拟大量消息的并发发送。如下:

import com.rabbitmq.client.AMQP;

import com.rabbitmq.client.Channel;

import com.rabbitmq.client.Connection;

import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;

import java.nio.charset.StandardCharsets;

import java.util.HashMap;

import java.util.UUID;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.TimeoutException;

public class ConcurrentProducerTest {

   public static void main(String[] args) throws IOException, TimeoutException {

       // 设置实例的接入点。

       String hostName = "rabbitmq-serverless-cn-y1i3xlh6a02.cn-hangzhou.amqp-19.net.mq.amqp.aliyuncs.com";

       // 设置实例的静态用户名密码。

       String userName = "UserName";

       String passWord = "PassWord";

       // 设置实例的Vhost。

       String virtualHost = "test-vhost";

       // 创建连接

       Connection connection = createConnection(hostName, userName, passWord, virtualHost);

       Channel channel = connection.createChannel();

       // 设置Exchange、Queue和绑定关系。

       String exchangeName = "test-exchange";

       String queueName = "test-queue";

       String bindingKey = "test-routing-key";

       String exchangeType = "direct";

       // 创建线程池

       int threadCount = 50; // 线程数量

       ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

       // 开始发送消息

       for (int i = 0; i < threadCount; i++) {

           executorService.submit(() -> {

               try {

                   for (int j = 0; j < 1000; j++) { // 每个线程发送1000条

                       AMQP.BasicProperties props = new AMQP.BasicProperties.Builder().messageId(UUID.randomUUID().toString()).build();

                       channel.basicPublish(exchangeName, bindingKey, true, props,

                               ("并发消息发送示例Body-" + Thread.currentThread().getId() + "-" + j).getBytes(StandardCharsets.UTF_8));

                       System.out.println("[SendResult] Thread ID: " + Thread.currentThread().getId()

                               + ", Message sent successfully, messageId: " + props.getMessageId());

                   }

               } catch (IOException e) {

                   e.printStackTrace();

               }

           });

       }

       // 关闭线程池

       executorService.shutdown();

       while (!executorService.isTerminated()) {

           // 等待所有线程完成

       }

       // 关闭通道和连接

       channel.close();

       connection.close();

   }

   // 创建连接的方法

   public static Connection createConnection(String hostName, String userName, String passWord, String virtualHost)

           throws IOException, TimeoutException {

       ConnectionFactory factory = new ConnectionFactory();

       factory.setHost(hostName);

       factory.setUsername(userName);

       factory.setPassword(passWord);

       factory.setAutomaticRecoveryEnabled(true);

       factory.setNetworkRecoveryInterval(5000);

       factory.setVirtualHost(virtualHost);

       factory.setPort(5672);

       factory.setConnectionTimeout(30 * 1000);

       factory.setHandshakeTimeout(30 * 1000);

       factory.setShutdownTimeout(0);

       return factory.newConnection();

   }

}

这段代码实现了50个用户并发发送了5万条消息。此时再次回到控制台,点击Dashboard,查看指标变化趋势。

image.png

image.png

image.png

从上述趋势图可以很直观地看到RabbitMQ能够在高并发场景下处理大量并发消息的发送和接收,保持高效的消息传递。低延时的表现确保了实时性要求较高的应用场景能够正常运行。

如果你只想快速体验产品,除了一键部署和手动部署方式可选以往,还有一个非常经济实惠的方式可选择,那就是云起实验室。实验链接如下,点击前往

image.png

写在最后

问题反馈

在整个体验过程中,总体还是顺畅的,但同时也存在如下的几个不足:

1、在方案的优惠购买部分,免费试用无法加载,起初我以为是浏览器兼容问题,可换了好几个浏览器问题依旧,应该是个BUG,望尽快修复。

image.png

2、示例代码中的敏感信息未作单独处理,虽然是个示例,本不应该指出,但作为开发者来说,如此编码实属不妥。应该用单独的文件来处理这部分信息,避免多处引用需要输入多次。

image.png

3、在性能验证阶段,没有提示不要关闭消费代码的运行,作为新手来说,如果此处不提示,大概率是会关掉之前的程序运行,而单独去运行性能验证的代码,这样务必导致发送的5万条信息得不到及时消费,而影响最后性能的验证。

image.png

体验总结

(一)本解决方案的实践原理描述相对清晰,涵盖了云消息队列RabbitMQ版的基本架构、部署流程、以及优势特点。方案从业务需求出发,通过对比开源RabbitMQ与云消息队列RabbitMQ版的差异,明确了云版本在稳定性、高并发、灵活扩缩容等方面的优势。

整体上,方案描述较为清晰,但在一些技术细节上,如Exchange和Queue的具体工作原理、消息路由机制等方面,可以进一步细化,以帮助用户更深入地理解消息队列的运作机制。

不明确之处及建议

  • 增加技术细节:建议在描述Exchange和Queue的工作流程时,增加更多技术细节和图示,以便用户更好地理解消息如何被路由、存储和消费。
  • 术语解释:对于可能引起混淆的技术术语,如“脑裂”问题,应提供简要解释,帮助非技术背景的用户理解。

(二)由于涉及产品单一,部署过程相对简单,提供了Serverless系列的实例创建步骤,并明确了预估费用。在实际部署过程中并未遇到任何异常。

建议

  • 完善部署文档:提供详细且准确的部署指南,包括每一步的操作截图、命令行示例、参数说明等,以提高部署的易用性和成功率。
  • 实时帮助:提供在线客服或技术支持渠道,以便用户在部署过程中遇到问题时能够及时获得帮助。

(三)本解决方案的部署过程设计在一定程度上展现了云消息队列RabbitMQ产品的核心优势,如弹性伸缩、按量计费、高并发处理等。然而,由于缺乏具体的性能指标数据和实际应用场景下的测试,用户可能难以直观感受到这些优势的实际效果。

改进建议

  • 性能测试:建议在部署过程中加入性能测试环节,通过模拟实际业务场景下的消息发送和消费,展示云消息队列RabbitMQ版在性能上的优势。
  • 案例分析:提供具体的应用案例或用户故事,展示云消息队列RabbitMQ版在实际业务中的成功应用,帮助用户更好地理解其价值和优势。

(四)本解决方案旨在解决企业在使用开源RabbitMQ时可能遇到的稳定性问题(如消息堆积、脑裂等),并通过云消息队列RabbitMQ版提供高并发、分布式、灵活扩缩容等能力,降低资源和运维成本。

方案适用于微服务系统间的事件通知、异步解耦等场景,以及期望无缝迁移开源RabbitMQ的用户和面临自建集群稳定性问题的用户。然而,对于特定行业的特殊需求(如金融行业的安全性要求、医疗行业的隐私保护等),方案可能需要进一步定制和优化。

不足与详细说明

  • 行业定制化不足:方案目前主要聚焦于通用场景,对于特定行业的特殊需求支持不足。建议针对不同行业提供定制化的解决方案和最佳实践。
  • 安全性与隐私保护:在描述中未明确提及云消息队列RabbitMQ版在安全性(如数据加密、访问控制等)和隐私保护方面的能力和措施。这是企业用户在选择云服务时非常关注的一个方面,应加以补充和完善。

如果你想获取视频类的资料,今年上新的云端问道栏目恰好有本次评测的产品内容,可点击如下链接前往学习。(建议关注该栏目更新,因为参加实操打卡可以获得小礼品

高弹性低成本的云消息队列 RabbitMQ 版陪跑班

image.png


目录
相关文章
|
4天前
|
编解码 Java 程序员
写代码还有专业的编程显示器?
写代码已经十个年头了, 一直都是习惯直接用一台Mac电脑写代码 偶尔接一个显示器, 但是可能因为公司配的显示器不怎么样, 还要接转接头 搞得桌面杂乱无章,分辨率也低,感觉屏幕还是Mac自带的看着舒服
|
6天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1551 8
|
1月前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
10天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
663 25
|
6天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
213 3
|
1天前
|
Python
【10月更文挑战第10天】「Mac上学Python 19」小学奥数篇5 - 圆和矩形的面积计算
本篇将通过 Python 和 Cangjie 双语解决简单的几何问题:计算圆的面积和矩形的面积。通过这道题,学生将掌握如何使用公式解决几何问题,并学会用编程实现数学公式。
103 59
|
13天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
686 5
|
2天前
|
Java 开发者
【编程进阶知识】《Java 文件复制魔法:FileReader/FileWriter 的奇妙之旅》
本文深入探讨了如何使用 Java 中的 FileReader 和 FileWriter 进行文件复制操作,包括按字符和字符数组复制。通过详细讲解、代码示例和流程图,帮助读者掌握这一重要技能,提升 Java 编程能力。适合初学者和进阶开发者阅读。
100 61
|
13天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
2天前
vue3+Ts 二次封装ElementUI form表单
【10月更文挑战第8天】
109 57