• Stars
    star
    303
  • Rank 137,655 (Top 3 %)
  • Language
    Java
  • License
    Apache License 2.0
  • Created over 5 years ago
  • Updated almost 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

🔥🔥🔥Mykit体系中提供的简单、稳定、可扩展的延迟消息队列框架,提供精准的定时任务和延迟队列处理功能

作者及联系方式

作者:冰河
QQ:2711098650
微信:sun_shine_lyz
微信公众号: 冰河技术

项目简述

Mykit体系中提供的简单、稳定、可扩展的延迟消息队列框架,提供精准的定时任务和延迟队列处理功能。

项目模块说明

  • mykit-delay-common: mykit-delay 延迟消息队列框架通用工具模块,提供全局通用的工具类
  • mykit-delay-config: mykit-delay 延迟消息队列框架通用配置模块,提供全局配置
  • mykit-delay-queue: mykit-delay 延迟消息队列框架核心实现模块,目前所有主要的功能都在此模块实现
  • mykit-delay-controller: mykit-delay 延迟消息队列框架Restful接口实现模块,对外提供Restful接口访问,兼容各种语言调用
  • mykit-delay-core: mykit-delay 延迟消息队列框架的入口,整个框架的启动程序在此模块实现
  • mykit-delay-rpc:mykit-delay延时消息队列的RPC模块,支持Dubbo、brpc、grpc、Motan、Sofa、SpringCloud、SpringCloud Alibaba等主流RPC的实现
  • mykit-delay-test: mykit-delay 延迟消息队列框架通用测试模块,主要提供Junit单元测试用例

需求背景

  • 用户下订单后未支付,30分钟后支付超时
  • 在某个时间点通知用户参加系统活动
  • 业务执行失败之后隔10分钟重试一次

类似的场景比较多 简单的处理方式就是使用定时任务 假如数据比较多的时候 有的数据可能延迟比较严重,而且越来越多的定时业务导致任务调度很繁琐不好管理。

队列设计

整体架构设计如下图所示。

开发前需要考虑的问题

  • 及时性 消费端能按时收到
  • 同一时间消息的消费权重
  • 可靠性 消息不能出现没有被消费掉的情况
  • 可恢复 假如有其他情况 导致消息系统不可用了 至少能保证数据可以恢复
  • 可撤回 因为是延迟消息 没有到执行时间的消息支持可以取消消费
  • 高可用 多实例 这里指HA/主备模式并不是多实例同时一起工作
  • 消费端如何消费

当然初步选用redis作为数据缓存的主要原因是因为redis自身支持zset的数据结构(score 延迟时间毫秒) 这样就少了排序的烦恼而且性能还很高,正好我们的需求就是按时间维度去判定执行的顺序 同时也支持map list数据结构。

简单定义一个消息数据结构

private String topic;/***topic**/
private String id;/***自动生成 全局惟一 snowflake**/
private String bizKey;
private long delay;/***延时毫秒数**/
private int priority;//优先级
private long ttl;/**消费端消费的ttl**/
private String body;/***消息体**/
private long createTime=System.currentTimeMillis();
private int status= Status.WaitPut.ordinal();

运行原理

  • 用Map来存储元数据。id作为key,整个消息结构序列化(json/…)之后作为value,放入元消息池中。
  • 将id放入其中(有N个)一个zset有序列表中,以createTime+delay+priority作为score。修改状态为正在延迟中
  • 使用timer实时监控zset有序列表中top 10的数据 。 如果数据score<=当前时间毫秒就取出来,根据topic重新放入一个新的可消费列表(list)中,在zset中删除已经取出来的数据,并修改状态为待消费
  • 客户端获取数据只需要从可消费队列中获取就可以了。并且状态必须为待消费 运行时间需要<=当前时间的 如果不满足 重新放入zset列表中,修改状态为正在延迟。如果满足修改状态为已消费。或者直接删除元数据。

客户端

因为涉及到不同程序语言的问题,所以当前默认支持http访问方式。

  • 添加延时消息添加成功之后返回消费唯一ID POST /push {…..消息体}
  • 删除延时消息 需要传递消息ID GET /delete?id=
  • 恢复延时消息 GET /reStore?expire=true|false expire是否恢复已过期未执行的消息。
  • 恢复单个延时消息 需要传递消息ID GET /reStore/id
  • 获取消息 需要长连接 GET /get/topic

用Nginx暴露服务,配置为轮询 在添加延迟消息的时候就可以流量平均分配。

目前系统中客户端并没有采用HTTP长连接的方式来消费消息,而是采用MQ的方式来消费数据这样客户端就可以不用关心延迟消息队列。只需要在发送MQ的时候拦截一下 如果是延迟消息就用延迟消息系统处理。

消息可恢复

实现恢复的原理 正常情况下一般都是记录日志,比如mysql的binlog等。

这里我们直接采用mysql数据库作为记录日志。

目前创建以下2张表:

  • 消息表 字段包括整个消息体
  • 消息流转表 字段包括消息ID、变更状态、变更时间、zset扫描线程Name、host/ip

定义zset扫描线程Name是为了更清楚的看到消息被分发到具体哪个zset中。前提是zset的key和监控zset的线程名称要有点关系 这里也可以是zset key。

支持消息恢复

假如redis服务器宕机了,重启之后发现数据也没有了。所以这个恢复是很有必要的,只需要从表1也就是消息表中把消息状态不等于已消费的数据全部重新分发到延迟队列中去,然后同步一下状态就可以了。

当然恢复单个任务也可以这么干。

数据表设计

这里,我就直接给出创建数据表的SQL语句。SQL语句存放在mykit-delay-config模块下的src/main/resources/sql目录下。

DROP TABLE IF EXISTS `mykit_delay_queue_job`;
CREATE TABLE `mykit_delay_queue_job` (
  `id` varchar(128) NOT NULL,
  `bizkey` varchar(128) DEFAULT NULL,
  `topic` varchar(128) DEFAULT NULL,
  `subtopic` varchar(250) DEFAULT NULL,
  `delay` bigint(20) DEFAULT NULL,
  `create_time` bigint(20) DEFAULT NULL,
  `body` text,
  `status` int(11) DEFAULT NULL,
  `ttl` int(11) DEFAULT NULL,
  `update_time` datetime(3) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `mykit_delay_queue_job_ID_STATUS` (`id`,`status`),
  KEY `mykit_delay_queue_job_STATUS` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for mykit_delay_queue_job_log
-- ----------------------------
DROP TABLE IF EXISTS `mykit_delay_queue_job_log`;
CREATE TABLE `mykit_delay_queue_job_log` (
  `id` varchar(128) NOT NULL,
  `status` int(11) DEFAULT NULL,
  `thread` varchar(60) DEFAULT NULL,
  `update_time` datetime(3) DEFAULT NULL,
  `host` varchar(128) DEFAULT NULL,
  KEY `mykit_delay_queue_job_LOG_ID_STATUS` (`id`,`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

关于高可用

分布式协调还是选用zookeeper。

如果有多个实例最多同时只能有1个实例工作 这样就避免了分布式竞争锁带来的坏处,当然如果业务需要多个实例同时工作也是支持的,也就是一个消息最多只能有1个实例处理,可以选用zookeeper或者redis就能实现分布式锁了。

最终做了一下测试多实例同时运行,可能因为会涉及到锁的问题性能有所下降,反而单机效果很好。所以比较推荐基于docker的主备部署模式。

运行模式

  • 支持 master,slave (HA)需要配置mykit.delay.registry.serverList zk集群地址列表
  • 支持 cluster 会涉及到分布式锁竞争 效果不是很明显 分布式锁采用redissetNx实现
  • StandAlone

目前,经过测试,推荐使用master slave的模式,并且,在升级版本中,进一步增强了Master Slave模式。后期会优化Cluster模式。

如何接入

为了提供一个统一的精准定时任务和延时队列框架,mykit-delay提供了HTTP Rest接口和RPC方式供其他业务系统调用,接口使用简单方便,只需要简单的调用接口,传递相应的参数即可。

RPC方式调用,后续支持的方式有:

  • Dubbo(已实现)
  • brpc(预留支持)
  • grpc(预留支持)
  • Motan(预留支持)
  • Sofa(预留支持)
  • SpringCloud(预留支持)
  • SpringCloud Alibaba(预留支持)

HTTP方式接入

消息体

以JSON数据格式参数 目前提供了http 协议。

  • body 业务消息体
  • delay 延时毫秒 距createTime的间隔毫秒数
  • id 任务ID 系统自动生成 任务创建成功返回
  • status 状态 默认不填写
  • topic 标题
  • subtopic 保留字段
  • ttl 保留字段
  • createTime 创建任务时间 非必填 系统默认

启动HTTP Rest服务

首先,从GitHub Clone项目到本地

git clone https://github.com/sunshinelyz/mykit-delay.git

然后进入mykit-delay框架目录。

cd mykit-delay

执行Maven命令

mvn clean package -Dmaven.test.skip=true

接下来,进入 mykit-delay-coretarget 目录下,运行如下命令。

java -jar mykit-delay-core-xxx.jar

其中,xxx是版本号,以实际下载的版本号为准。

接下来,就可以调用HTTP Restful接口来使用mykit-delay框架了。

添加任务

/push  
    POST application/json
{"body":"{hello world}","delay":10000,"id":"20","status":0,"topic":"ces","subtopic":"",ttl":12}

删除任务

删除任务 需要记录一个JobId

/delete?jobId=xxx
   GET

恢复单个任务

用于任务错乱 脑裂情况 根据日志恢复任务

/reStoreJob?JobId=xxx
   GET

恢复所有未完成的任务

根据日志恢复任务

/reStore?expire=true
   GET

参数expire 表示是否需要恢复已过期还未执行的数据

清空队列数据

根据日志中未完成的数据清空队列中全部数据。清空之后 会删除缓存中的所有任务

/clearAll
 GET

Dubbo方式接入

消息体

以JSON数据格式参数 目前提供了http 协议。

  • body 业务消息体
  • delay 延时毫秒 距createTime的间隔毫秒数
  • id 任务ID 系统自动生成 任务创建成功返回
  • status 状态 默认不填写
  • topic 标题
  • subtopic 保留字段
  • ttl 保留字段
  • createTime 创建任务时间 非必填 系统默认

启动Dubbo服务

首先,从GitHub Clone项目到本地

git clone https://github.com/sunshinelyz/mykit-delay.git

然后进入mykit-delay框架目录。

cd mykit-delay

执行Maven命令

mvn clean package -Dmaven.test.skip=true

接下来,进入 mykit-rpc-dubbo模块下的 mykit-rpc-dubbo-server服务 的 target 目录下,运行如下命令。

mykit-rpc-dubbo-server-xxx.jar

其中,xxx是版本号,以实际下载的版本号为准。

引入mykit-delay依赖

以Dubbo方式接入mykit-delay,需要引入mykit-delay的依赖,如下所示。

<dependency>
    <groupId>io.mykit.delay</groupId>
    <artifactId>mykit-rpc-dubbo-common</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

然后,在需要调用Dubbo服务的类中以如下方式注入MykitDelayDubboInterface。

@DubboReference(version = "1.0.0")
private MykitDelayDubboInterface mykitDelayDubboInterface;

其中,MykitDelayDubboInterface接口的定义如下所示。

/**
 * @author binghe
 * @version 1.0.0
 * @description 发布的Dubbo接口
 */
public interface MykitDelayDubboInterface {
    /**
     * 推送消息
     */
    ResponseMessage push(JobWrapp jobMsg);
    /**
     * 删除任务
     */
    ResponseMessage delete(String jobId);
    /**
     * 完成任务
     */
    ResponseMessage finish(String jobId);
    /**
     * 恢复单个任务
     */
    ResponseMessage reStoreJob(String jobId);
    /**
     * 提供一个方法 假设缓存中间件出现异常 以及数据错乱的情况 提供恢复功能
     * @param expire 过期的数据是否需要重发 true需要, false不需要 默认为true
     */
    ResponseMessage reStore(Boolean expire);
    /**
     * 清除所有的任务
     */
    ResponseMessage clearAll();
}

接下来,就可以以Dubbo方式接入mykit-delay框架了。

注意:无论是以HTTP方式,还是以RPC方式启动mykit-delay服务,都需要通过如下方式加载基本配置信息。

StartGetReady.ready(ConsumeQueueProvider.class.getName());

客户端获取队列方式

目前默认实现了RocketMQActiveMQ的推送方式。依赖MQ的方式来实现延时框架与具体业务系统的解耦。同时,框架已SPI的形式加载相应的MQ,也就是说,集成MQ的方式是可扩展的。

消息体中消息与RocketMQActiveMQ 消息字段对应关系

mykit-delay RocketMQ ActiveMQ 备注
topic topic topic 点对点发送队列名称或者主题名称
subtopic subtopic subtopic 点对点发送队列子名称或者主题子名称
body 消息内容 消息内容 消息内容

关于系统配置

延迟框架与具体执行业务系统的交互方式通过延迟框架配置实现,具体配置文件位置为mykit-delay-config项目下的resources/properties/starter.properties文件中。

测试

需要配置好数据库地址和Redis的地址 如果不是单机模式 也需要配置好Zookeeper

运行mykit-delay-test模块下的测试类io.mykit.delay.test.PushTest添加任务到队列中

启动mykit-delay-test模块下的io.mykit.delay.TestDelayQueue消费前面添加数据 为了方便查询效果 默认的消费方式是consoleCQ 控制台输出

扩展

支持zset队列个数可配置,避免大数据带来高延迟的问题。进一步增强框架的高可用。
目前存在日志和redis元数据有可能不一致的问题 如mysql挂了,写日志不会成功,后续会通过Redis直接同步binlog的方式来解决此问题。

近期规划

  • brpc、grpc、Motan、Sofa、SpringCloud、SpringCloud Alibaba等RPC扩展
  • 支持RabbitMQ、Kafka等消息中间件
  • 分区(buck)支持动态设置
  • redis与数据库数据一致性的问题 (重要
  • 实现自己的推拉机制
  • 支持可切换实现方式,目前只是依赖Redis实现,后续待优化,支持更多的可配置选项
  • 支持Web控制台管理队列
  • 实现消息消费TTL机制
  • 增加对框架和定时任务的监控

扩展

支持zset队列个数可配置 避免大数据带来高延迟的问题。

扫一扫关注微信公众号

你在刷抖音,玩游戏的时候,别人都在这里学习,成长,提升,人与人最大的差距其实就是思维。你可能不信,优秀的人,总是在一起。

扫一扫关注冰河技术微信公众号
微信公众号

More Repositories

1

technology-binghe

主要收录【冰河技术】微信公众号文章,作者为冰河,《海量数据处理与大数据技术实战》、《MySQL技术大全:开发、优化与运维实战》作者。旨在分享各种编程语言、开发技术、分布式与微服务架构、分布式数据库、分布式事务、云原生、大数据与云计算技术和渗透技术。另外,还会分享各种面试题和面试技巧。
319
star
2

BingheGuide

🔥🔥🔥 📚 本代码库是作者冰河多年从事互联网大厂开发、架构的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容、底层原理、架构知识、渗透技术。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
Shell
239
star
3

mykit-db-sync

🔥🔥🔥 mykit中分离出的强大的数据库同步工具——mykit-db-sync。 基于java开发的功能强大、配置灵活的数据库同步工具,目前支持MySQL、SQL Server和Oracle数据库的同步,在实际应用场景中,我们经常需要定期将一个数据库的数据同步到另外一个数据库中,常见的一种做法是将源数据库的数据dump为sql文件,然后到目标数据库执行sql文件完成数据库的导入,但是这种方法至少存在以下问题: - 需要手工操作,效率低 - 当涉及数据表较多时,容易遗漏、出错 - 如果要定期同步,操作人容易忘记 - 难以应付频繁变更数据表或者字段 针对以上存在的问题,将珍贵人力从这种重复、无意义的工作中解脱出来,特意开发这个小工具
Java
177
star
4

mykit-data

🔥🔥🔥支持插件化、可视化的数据异构框架mykit-data开源啦!支持MySQL到MySQL、MySQL到Oracle、Oracle到MySQL、Oracle到Oracle的全量、增量数据同步。完全的插件化、可视化操作。通过日志最大限度的避免同步过程中的数据丢失。支持查看同步的数据和详细的日志信息。
Java
157
star
5

binghe

分享各种编程语言、开发技术、分布式与微服务架构、分布式数据库、分布式事务、云原生、大数据与云计算技术和渗透技术。另外,还会分享各种面试题和面试技巧。
101
star
6

mykit-transaction-message

🔥🔥🔥最终消息一致性的分布式事务框架mykit-transaction-message,使用disruptor极大的提升了框架的性能。目前已使用到生产环境,经受住了分布式事务场景的考验。
Java
93
star
7

mykit-serial

🔥🔥🔥全网首个完全开源的分布式全局有序序列号(分布式ID生成器)服务,支持嵌入式(Jar包)、RPC(Dubbo,motan、sofa、SpringCloud、SpringCloud Alibaba等主流的RPC框架)、Restful API方式发布服务。
Java
72
star
8

Mykit

一个通用的开源工具集,集Web、Restful服务、分布式服务、分布式数据库、分布式事务、大数据、数据分析、云计算、人工智能、深度学习、机器学习、算法等为一体的通用开源解决方案,持续更新中。。。
Java
71
star
9

python-hacker

本项目均为冰河亲自编写并整理的Python渗透脚本集合,可直接拿来用于渗透实战
Python
62
star
10

mykit-concurrent-principle

🔥🔥🔥《深入理解高并发编程:核心原理与案例实战》随书源码
Java
35
star
11

spring-redis

📚 基于Redis解决缓存击穿、穿透和雪崩问题的通用解决方案,拿来即用。支持存储对象、集合、简单数据类型等。无需提前将数据存入Redis,直接使用提供的分布式缓存接口查询数据即可,附带完善的单元测试用例,方便学习使用。
Java
32
star
12

mykit-excel

mykit-excel插件是通用的Excel导出导出框架,旨在提供通用的Excel导入导出功能,支持以注解方式选择JavaBean中的部分字段导出,并提供注解指定Excel列标题和排序功能。
Java
31
star
13

mykit-chat

mykit-chat是基于Netty实现的实时聊天系统服务器端:1. 支持昵称登录; 2. 支持多人同时在线; 3. 同步显示在线人数; 4. 支持文字和表情的内容; 5. 浏览器与服务器保持长连接,定时心跳检测;
Java
31
star
14

spring-annotation-book

🔥🔥🔥《Spring核心技术》专栏源码
Java
25
star
15

mykit-android-signapk

本项目旨在可以为Apk打上系统签名, 目中的SignApk类是从Android的源码中复制出来的系统签名程序, platform.pk8和platform.x509.pem也是从Android源码中复制出来, 通过java -jar mykit-android-signapk.jar platform.x509.pem platform.pk8 需要签名的apk文件 签名后生成的apk文件 即可生成系统签名的Apk安装包
Java
22
star
16

mykit-holiday

纯算法可以获取到1900-2100年之间任意日期的详细信息,包括:节日、是否闰年和闰月、公历和农历、对应的二十四节气以及天干地支信息等。
Java
21
star
17

BuyTickets

Python实现自动购买火车票(基于Python3.6+splinter)
Python
21
star
18

mykit-cache

🔥🔥🔥 mykit架构中独立出来的mykit-cache组件,封装了mykit架构下对于缓存cache的各种操作,使用者只需要引入相关的Jar包,即可实现对缓存的轻松操作。
18
star
19

binghe001.github.io

📚 本静态博客是作者冰河多年从事多年互联网大厂开发、架构的学习历程技术汇总,旨在为大家提供一个清晰详细的学习教程,侧重点更倾向编写Java核心内容、底层原理、架构知识、渗透技术。如果本仓库能为您提供帮助,请给予支持(关注、点赞、分享)!
HTML
18
star
20

spring-annotation

【Spring注解驱动开发】专栏源码
Java
13
star
21

mykit-lock

mykit架构中独立出来的mykit-lock组件,旨在提供高并发架构下分布式系统的分布式锁架构。 分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
Java
13
star
22

mykit-concurrent-jdk

🔥🔥🔥《深入理解高并发编程:JDK核心技术》随书源码
Java
12
star
23

mykit-ddns

域名绑定动态IP
Java
10
star
24

log4j-leak

📚 本仓库是冰河重现Log4j远程过程执行漏洞的代码示例,完整重现了Log4j最新远程过程执行漏洞,如果对你有所帮助,请给个大大的Star,谢谢啦!!
Java
9
star
25

bytecode

java字节码编程,持续更新中,详情关注 冰河技术 微信公众号阅读相关文章
Java
9
star
26

Android-MQ

基于Android的轻量级开源消息通信组件——Android-MQ
Java
8
star
27

mykit-async

🔥🔥🔥 mykit架构中独立出来的mykit-async异步编程框架,本异步框架实现了在Spring的基础上重写和扩展了异步执行的流程,主要提供了如下功能: 1、提供注解声明方式异步执行,对原代码无侵入(解决spring-async对有返回结果的需包装成Future对象问题); 2、提供编程式异步方法; 3、提供异步事件编程; 4、解决多层异步嵌套带来的线程阻塞问题(目前spring-async依然存在此问题);
Java
7
star
28

mykit-ratelimiter

限流示例代码
Java
6
star
29

java-simple-design-patterns

📚 《Java极简设计模式》专栏源码,多一句没有,少一句不行,用最简短的篇幅讲述设计模式最核心的知识。
Java
6
star
30

binghe-chess

中国象棋小游戏,可以直接运行,可以直接用于毕业设计
Java
5
star
31

mycode-generator

mycode-generator项目为一款通用的Java代码生成神器,目前已完全开源,旨在帮助广大的Java开发人员从编写重复冗杂的代码中解放出来。。。
Java
4
star
32

distribute-seckill

💥💥💥冰河技术知识星球《手写秒杀系统》专栏部分源码
4
star
33

RPC-Server

基于Netty, ZooKeeper和Spring的轻量级RPC开源框架
Java
3
star
34

asp_mssql_tool

MSSQL语句执行工具asp版,支持x86/x64系统,数据库版本支持MSSQL2000(x86/x64)、MSSQL2005(x86/x64)、MSSQL2008(x86/x64)、MSSQL2012(x86/x64)
ASP
2
star
35

mykit-learning

学习各种技术的代码示例,关注 冰河技术 微信公众号查看具体文章学习!!
Java
2
star
36

mykit-fastdfs

SpringBoot整合FastDFS的简单示例
Java
2
star
37

dubbo-parent

dubbo-2.7.8源码解析
Java
2
star
38

mykit-weixin

基于mykit-wechat-sdk微信SDK开发的微信第三方服务,以 Spring Boot + MyBatis 实现的兼容多微信公众号接入的微信服务
Java
2
star
39

mykit-android-slienceinstall

Android静默安装的程序实现,系统签名需要结合工程:https://github.com/sunshinelyz/mykit-android-signapk
Java
2
star
40

sharding-jdbc-demo

基于sharding-jdbc实现的各种分库分表、读写分离、柔性事务、分布式主键、分布式治理等示例,持续更新中。。。
Java
2
star
41

vmware

破解版vmware安装包和对应的密钥Key
1
star
42

storm-learning

本工程为Storm合集,为冰河记录的各种功能模块,供其他同学学习
Java
1
star
43

spring-remoting-annotation-server

使用自定义的spring注解标签发布RMI/HTTPInvoker/Hessian/Burlap远程服务
Java
1
star
44

binghe_resources

静态资源
1
star
45

mykit-weixin-sdk

mykit架构中独立出来的mykit-weixin-sdk微信开发SDK,提供通用的微信开发解决方案供其他应用或服务以及第三方应用或服务使用
Java
1
star
46

mykit-chat-sdk

mykit-chat-sdk是mykit-chat实时聊天系统的客户端SDK,目前实现的功能如下:1. 支持昵称登录;2. 支持多人同时在线;3. 同步显示在线人数;4. 支持文字和表情的内容;5. 浏览器与服务器保持长连接,定时心跳检测;
JavaScript
1
star