為獲得更好的閱讀體驗(yàn),請(qǐng)?jiān)L問原文:傳送門

一、分布式任務(wù)調(diào)度概述
什么是任務(wù)調(diào)度平臺(tái)
任務(wù)調(diào)度是指基于給定的時(shí)間點(diǎn),給定的時(shí)間間隔又或者給定執(zhí)行次數(shù)自動(dòng)的執(zhí)行任務(wù)。我們可以思考一下在以下場(chǎng)景中,我們應(yīng)該怎么實(shí)現(xiàn):
- 支付系統(tǒng)每天凌晨 1 點(diǎn),進(jìn)行一天清算,每月 1 號(hào)進(jìn)行上個(gè)月清算;
- 電商整點(diǎn)搶購(gòu),商品價(jià)格8點(diǎn)整開始優(yōu)惠
- 12306 購(gòu)票系統(tǒng),超過 30 分鐘沒有成功支付訂單的,進(jìn)行回收處理
為什么需要任務(wù)調(diào)度平臺(tái)
定時(shí)任務(wù)是程序員不可避免的話題,很多業(yè)務(wù)場(chǎng)景需要我們某一特定的時(shí)刻去做某件任務(wù)。一般來說,系統(tǒng)可以使用消息傳遞代替部分定時(shí)任務(wù)(比如商品成功發(fā)貨后,需要向客戶發(fā)送短信提醒),兩者有很多相似之處,一些場(chǎng)景下也可以相互替換,但是有一些不能:
- 時(shí)間驅(qū)動(dòng)/ 事件驅(qū)動(dòng): 內(nèi)部系統(tǒng)一般可以通過事件來驅(qū)動(dòng),但如果涉及到外部系統(tǒng),則只能使用時(shí)間驅(qū)動(dòng)。如爬取外部網(wǎng)站價(jià)格,每小時(shí)爬一次。
- 批量處理/ 逐條處理: 批量處理堆積的數(shù)據(jù)更加高效,在不需要實(shí)時(shí)性的情況下比消息中間件更有優(yōu)勢(shì)。而且有的業(yè)務(wù)邏輯只能批量處理,如移動(dòng)每個(gè)月結(jié)算我們的花費(fèi)。
- 實(shí)時(shí)性/ 非實(shí)時(shí)性: 消息中間件能夠做到實(shí)時(shí)處理數(shù)據(jù),但是有些情況下并不需要實(shí)時(shí),比如:vip 升級(jí)。
- 系統(tǒng)內(nèi)部/ 系統(tǒng)解耦: 定時(shí)任務(wù)調(diào)度一般是在系統(tǒng)內(nèi)部,而消息中間件可用于兩個(gè)系統(tǒng)間
并且對(duì)于分布式系統(tǒng)來說,如果處理不當(dāng),會(huì)存在同一系統(tǒng)不同節(jié)點(diǎn)之間定時(shí)任務(wù)相互影響的問題,再考慮上監(jiān)控、日志、信息面板,加上不同系統(tǒng)之間管理維護(hù)的問題,自己實(shí)現(xiàn)一套的成本又上來了..所以我們可以考慮一些比較成熟的任務(wù)調(diào)度平臺(tái)來使用。
任務(wù)調(diào)度框架選型
Java 領(lǐng)域主要分布式調(diào)度系統(tǒng)如下:
- xxl-job:是一個(gè)輕量級(jí)分布式任務(wù)調(diào)度平臺(tái),其核心設(shè)計(jì)目標(biāo)是開發(fā)迅速、學(xué)習(xí)簡(jiǎn)單、輕量級(jí)、易擴(kuò)展 。
- Elastic-Job: 當(dāng)當(dāng)開源的分布式調(diào)度解決方案,由兩個(gè)相互獨(dú)立的子項(xiàng)目Elastic-Job-Lite和Elastic-Job-Cloud組成;Elastic-Job-Lite定位為輕量級(jí)無中心化解決方案,使用jar包的形式提供分布式任務(wù)的協(xié)調(diào)服務(wù);Elastic-Job-Cloud采用自研Mesos Framework的解決方案,額外提供資源治理、應(yīng)用分發(fā)以及進(jìn)程隔離等功能;
- Saturn:是唯品會(huì)開源的一個(gè)分布式任務(wù)調(diào)度平臺(tái),在當(dāng)當(dāng)開源的Elastic Job基礎(chǔ)上,取代傳統(tǒng)的Linux Cron/Spring Batch Job的方式,做到全域統(tǒng)一配置,統(tǒng)一監(jiān)控,任務(wù)高可用以及分片并發(fā)處理;
- light-task-scheduler:阿里員工開源的個(gè)人項(xiàng)目,主要用于解決分布式任務(wù)調(diào)度問題,支持實(shí)時(shí)任務(wù),定時(shí)任務(wù)和Cron任務(wù)。有較好的伸縮性,擴(kuò)展性,健壯穩(wěn)定性
- Quartz: Java定時(shí)任務(wù)的標(biāo)配。利用數(shù)據(jù)庫(kù)的鎖機(jī)制實(shí)現(xiàn)集群調(diào)度,業(yè)務(wù)代碼需要考慮調(diào)度的邏輯,對(duì)業(yè)務(wù)代碼有入侵。
在這之前,我是一個(gè)都不知道的..有很多文章對(duì)他們進(jìn)行對(duì)比,我們就參考其中一篇(下 2),選擇熱門且成熟的 XXL-JOB 來上手研究一下。
二、XXL-JOB
概述
XXL-JOB是一個(gè)輕量級(jí)分布式任務(wù)調(diào)度平臺(tái),其核心設(shè)計(jì)目標(biāo)是開發(fā)迅速、學(xué)習(xí)簡(jiǎn)單、輕量級(jí)、易擴(kuò)展?,F(xiàn)已開放源代碼并接入多家公司線上產(chǎn)品線,開箱即用。
快速入門 - 本地運(yùn)行
先定個(gè)小目標(biāo),先把它在本地跑起來先。
第一步:下載代碼到本地
找一個(gè)合適的目錄,然后執(zhí)行下列語句把代碼下載到本地:
$ git clone https://github.com/xuxueli/xxl-job.git
第二步:執(zhí)行初始化 SQL,再用 IDEA 打開
找到 /xxl-job/doc/db/table_xxl_job.sql 初始化 SQL 腳本,并在本地執(zhí)行。
然后按照 Maven 格式將源碼導(dǎo)入 IDEA,源碼結(jié)構(gòu)如下:
xxl-job-admin:調(diào)度中心
xxl-job-core:公共依賴
xxl-job-executor-samples:執(zhí)行器Sample示例(選擇合適的版本執(zhí)行器,可直接使用,也可以參考其并將現(xiàn)有項(xiàng)目改造成執(zhí)行器)
:xxl-job-executor-sample-springboot:Springboot版本,通過Springboot管理執(zhí)行器,推薦這種方式;
:xxl-job-executor-sample-spring:Spring版本,通過Spring容器管理執(zhí)行器,比較通用;
:xxl-job-executor-sample-frameless:無框架版本;
:xxl-job-executor-sample-jfinal:JFinal版本,通過JFinal管理執(zhí)行器;
:xxl-job-executor-sample-nutz:Nutz版本,通過Nutz管理執(zhí)行器;
第三步:配置并啟動(dòng) "調(diào)度中心"
調(diào)度中心配置文件地址:
/xxl-job/xxl-job-admin/src/main/resources/xxl-job-admin.properties
調(diào)度中心配置內(nèi)容說明:
### 調(diào)度中心JDBC鏈接:鏈接地址請(qǐng)保持和初始化時(shí)創(chuàng)建的數(shù)據(jù)庫(kù)保持一致
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?Unicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=root_pwd
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
### 報(bào)警郵箱
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
### xxl-job, access token
xxl.job.accessToken=
### xxl-job, i18n (default empty as chinese, "en" as english)
xxl.job.i18n=
在第一次啟動(dòng)的項(xiàng)目的時(shí)候可能會(huì)遇到找不到 log 文件的錯(cuò)誤(Failed to create),我們只需要自己手動(dòng)創(chuàng)建一下就好了,具體可以參照這篇文章:https://blog.csdn.net/leeue/article/details/100779424,記得之后再手動(dòng)把當(dāng)前目錄權(quán)限置為可寫狀態(tài)哦:sudo chmod 777 xxl-job
當(dāng)一切配置好了之后,我們就可以啟動(dòng)項(xiàng)目了,調(diào)度中心訪問地址:http://localhost:8080/xxl-job-admin(該地址執(zhí)行期將會(huì)使用到,作為回調(diào)地址),默認(rèn)登錄賬號(hào) "admin/123456",登錄后運(yùn)行界面如下圖所示:

至此,「調(diào)度中心」項(xiàng)目已經(jīng)部署成功了,調(diào)度中心集群(可選)配置可參考官方文檔。
第四步:配置啟動(dòng)"執(zhí)行器"
執(zhí)行器配置,配置文件地址:
/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties
執(zhí)行器配置,配置內(nèi)容說明:
### 調(diào)度中心部署跟地址 [選填]:如調(diào)度中心集群部署存在多個(gè)地址則用逗號(hào)分隔。執(zhí)行器將會(huì)使用該地址進(jìn)行"執(zhí)行器心跳注冊(cè)"和"任務(wù)結(jié)果回調(diào)";為空則關(guān)閉自動(dòng)注冊(cè);
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 執(zhí)行器AppName [選填]:執(zhí)行器心跳注冊(cè)分組依據(jù);為空則關(guān)閉自動(dòng)注冊(cè)
xxl.job.executor.appname=xxl-job-executor-sample
### 執(zhí)行器IP [選填]:默認(rèn)為空表示自動(dòng)獲取IP,多網(wǎng)卡時(shí)可手動(dòng)設(shè)置指定IP,該IP不會(huì)綁定Host僅作為通訊實(shí)用;地址信息用于 "執(zhí)行器注冊(cè)" 和 "調(diào)度中心請(qǐng)求并觸發(fā)任務(wù)";
xxl.job.executor.ip=
### 執(zhí)行器端口號(hào) [選填]:小于等于0則自動(dòng)獲??;默認(rèn)端口為9999,單機(jī)部署多個(gè)執(zhí)行器時(shí),注意要配置不同執(zhí)行器端口;
xxl.job.executor.port=9999
### 執(zhí)行器通訊TOKEN [選填]:非空時(shí)啟用;(注意與調(diào)度中心保持一致)
xxl.job.accessToken=
### 執(zhí)行器運(yùn)行日志文件存儲(chǔ)磁盤路徑 [選填] :需要對(duì)該路徑擁有讀寫權(quán)限;為空則使用默認(rèn)路徑;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 執(zhí)行器日志保存天數(shù) [選填] :值大于3時(shí)生效,啟用執(zhí)行器Log文件定期清理功能,否則不生效;
xxl.job.executor.logretentiondays=-1
同樣,也要注意一下日志文件的創(chuàng)建和權(quán)限問題,解決方法同上。
當(dāng)配置完成之后運(yùn)行起來,我們就可以在剛才的任務(wù)調(diào)度中心的主頁,在右上角的「執(zhí)行器的數(shù)量」上 + 1 了。
第五步:開發(fā)第一個(gè)任務(wù)
當(dāng)「調(diào)度中心」和「執(zhí)行器」都啟動(dòng)之后,我們可以直接在「調(diào)度中心」的任務(wù)管理界面新增一條配置如下圖所示(參考)的任務(wù):

我們點(diǎn)擊「操作」按鈕下的「GLUE IDE」可以手動(dòng)編寫我們要執(zhí)行的腳本,我們可以把我們的任務(wù)代碼改寫成如下的樣子:
package com.xxl.job.service.handler;
import com.xxl.job.core.log.XxlJobLogger;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import java.util.concurrent.TimeUnit;
public class DemoGlueJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) throws Exception {
XxlJobLogger.log("XXL-JOB, Hello World.");
for (int i = 0; i < 5; i++) {
XxlJobLogger.log("beat at:" + i);
TimeUnit.SECONDS.sleep(2);
}
return SUCCESS;
}
}
點(diǎn)擊「保存」,然后繼續(xù)在「操作」按鈕下點(diǎn)擊「執(zhí)行一次」的操作,就可以在「調(diào)度日志」中看到我們的任務(wù)執(zhí)行情況啦:

可以看到默認(rèn)執(zhí)行器中的日志輸出了:

回頭理解一下過程
到目前為止,我們整個(gè)搭建運(yùn)行的過程都比較順滑,沒有出現(xiàn)什么阻礙,現(xiàn)在我們稍微來理解一下這個(gè)過程。
首先我們?cè)诒镜爻跏蓟丝蚣芴峁┑?SQL 語句,里面定義的結(jié)構(gòu)足夠我們不管是單機(jī)還是分布式的任務(wù)管理需求。然后我們簡(jiǎn)單配置了一下連接的數(shù)據(jù)庫(kù)、報(bào)警郵件、token 等信息成功啟動(dòng)了「調(diào)度中心」項(xiàng)目。這個(gè)時(shí)候項(xiàng)目中默認(rèn)注冊(cè)一個(gè)名字為 xxl-job-exectutor-sample 的執(zhí)行器(名字同默認(rèn)執(zhí)行器的 AppName),并且采用的是自動(dòng)注冊(cè)的方式。
等我們把執(zhí)行器配置項(xiàng)里的 xxl,job.admin.addresses 填寫上「調(diào)度中心」實(shí)際的地址,然后 token 保持與「調(diào)度中心」一致,啟動(dòng)執(zhí)行器時(shí),執(zhí)行器就會(huì)把自身的一些基礎(chǔ)信息發(fā)送給「調(diào)度中心」,這時(shí)候「調(diào)度中心」會(huì)把接收到的注冊(cè)信息與自身注冊(cè)列表里的 AppName 進(jìn)行對(duì)比(AppName 是每一個(gè)執(zhí)行器的唯一標(biāo)示),有匹配時(shí)就會(huì)把 ip 自動(dòng)填寫上(多個(gè)節(jié)點(diǎn)就寫多個(gè)地址),并在 xxl_job_registry 表上更新信息。執(zhí)行器可以簡(jiǎn)單理解為項(xiàng)目?jī)?nèi)嵌了端口為 9999(默認(rèn)端口)的一個(gè) Server。(架構(gòu)圖如下)

任務(wù) "運(yùn)行模式"
在剛才的「快速入門」中,我們新建了一個(gè)「GLUE模式(Java)」模式的任務(wù),我們?cè)谛陆ㄈ蝿?wù)時(shí)可以直接在「調(diào)度中心」上編輯代碼,然后讓我們的 ”執(zhí)行器“ 執(zhí)行,這樣的一種模式是把代碼直接放在「調(diào)度中心」的做法,它的原理是:每個(gè) "GLUE模式(Java)" 任務(wù)的代碼,實(shí)際上是“一個(gè)繼承自 “IJobHandler” 的實(shí)現(xiàn)類的類代碼”,“執(zhí)行器”接收到“調(diào)度中心”的調(diào)度請(qǐng)求時(shí),會(huì)通過 Groovy 類加載器加載此代碼,實(shí)例化成 Java 對(duì)象,同時(shí)注入此代碼中聲明的 Spring 服務(wù)(請(qǐng)確保 Glue 代碼中的服務(wù)和類引用在“執(zhí)行器”項(xiàng)目中存在),然后調(diào)用該對(duì)象的 execute 方法,執(zhí)行任務(wù)邏輯。
另外一種方式是你提前把代碼寫進(jìn)「執(zhí)行器」程序中,這樣的模式在 XXL-JOB 中叫做「Bean模式」:每個(gè) Bean 模式任務(wù)都是一個(gè) Spring 的 Bean 類實(shí)例,它被維護(hù)在“執(zhí)行器”項(xiàng)目的 Spring 容器中。任務(wù)類需要加 “@JobHandler(value="名稱")” 注解,因?yàn)椤皥?zhí)行器”會(huì)根據(jù)該注解識(shí)別 Spring 容器中的任務(wù)。任務(wù)類需要繼承統(tǒng)一接口 “IJobHandler”,任務(wù)邏輯在 execute 方法中開發(fā),因?yàn)椤皥?zhí)行器”在接收到調(diào)度中心的調(diào)度請(qǐng)求時(shí),將會(huì)調(diào)用 “IJobHandler” 的 execute 方法,執(zhí)行任務(wù)邏輯。
例如在 XXL-JOB 提供的實(shí)例代碼中就有下面這么一段兒:
package com.xxl.job.executor.service.jobhandler;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.handler.annotation.JobHandler;
import com.xxl.job.core.log.XxlJobLogger;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 任務(wù)Handler示例(Bean模式)
*
* 開發(fā)步驟:
* 1、繼承"IJobHandler":“com.xxl.job.core.handler.IJobHandler”;
* 2、注冊(cè)到Spring容器:添加“@Component”注解,被Spring容器掃描為Bean實(shí)例;
* 3、注冊(cè)到執(zhí)行器工廠:添加“@JobHandler(value="自定義jobhandler名稱")”注解,注解value值對(duì)應(yīng)的是調(diào)度中心新建任務(wù)的JobHandler屬性的值。
* 4、執(zhí)行日志:需要通過 "XxlJobLogger.log" 打印執(zhí)行日志;
*
* @author xuxueli 2015-12-19 19:43:36
*/
@JobHandler(value="demoJobHandler")
@Component
public class DemoJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) throws Exception {
XxlJobLogger.log("XXL-JOB, Hello World.");
for (int i = 0; i < 5; i++) {
XxlJobLogger.log("beat at:" + i);
TimeUnit.SECONDS.sleep(2);
}
return SUCCESS;
}
}
我們就能在創(chuàng)建任務(wù)時(shí)直接按照下圖這樣創(chuàng)建,那么在調(diào)用任務(wù)時(shí),"執(zhí)行器" 就能夠如愿的執(zhí)行上面的邏輯:

當(dāng)然 XXL-JOB 還能支持一些腳本語言類型的模式:
- shell腳本:任務(wù)運(yùn)行模式選擇為 "GLUE模式(Shell)"時(shí)支持 "shell" 腳本任務(wù);
- python腳本:任務(wù)運(yùn)行模式選擇為 "GLUE模式(Python)"時(shí)支持 "python" 腳本任務(wù);
- nodejs腳本:務(wù)運(yùn)行模式選擇為 "GLUE模式(NodeJS)"時(shí)支持 "nodejs" 腳本任務(wù);
三、接入指南
- 前提:已經(jīng)搭建并成功運(yùn)行了「調(diào)度中心」服務(wù)。
快速接入
第一步,我們需要在 pom 文件中引入 xxl-job-core 的 Maven 依賴,不過比較奇怪的是,明明 Github 上最新版本是 2.1.1,Maven 倉(cāng)庫(kù)上卻沒有最新的包,所以只能用 2.1.0 的:
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.1.0</version>
</dependency>
第二步,在配置文件中加入 xxl 相關(guān)的配置文件信息,不管 yml 格式還是 properties 都行,上面提供了 properties 的版本,這了就提供一個(gè) yml 格式的作參考吧:
xxl:
job:
accessToken: xxxx
admin:
addresses: http://127.0.0.1:8080/xxl-job-admin
executor:
appname: test
logpath: /data/applogs/xxl-job/jobhandler
logretentiondays: -1
ip:
port: 9999
第三步,在合適的包目錄下新建 XxlJobConfig 配置類:
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.executor.appname}")
private String appName;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean(initMethod = "start", destroyMethod = "destroy")
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppName(appName);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
/**
* 針對(duì)多網(wǎng)卡、容器內(nèi)部署等情況,可借助 "spring-cloud-commons" 提供的 "InetUtils" 組件靈活定制注冊(cè)IP;
*
* 1、引入依賴:
* <dependency>
* <groupId>org.springframework.cloud</groupId>
* <artifactId>spring-cloud-commons</artifactId>
* <version>${version}</version>
* </dependency>
*
* 2、配置文件,或者容器啟動(dòng)變量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、獲取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/
}
至此,我們的項(xiàng)目就差不多完成了我們的接入工作了,就只剩下開發(fā) Handler 的工作量了。
第四步,建一個(gè)示例 DemoJobHandler 在平臺(tái)上自測(cè)一下:
@JobHandler(value="demoJobHandler")
@Component
public class DemoJobHandler extends IJobHandler {
@Override
public ReturnT<String> execute(String param) throws Exception {
XxlJobLogger.log("XXL-JOB, Hello World.");
for (int i = 0; i < 5; i++) {
XxlJobLogger.log("beat at:" + i);
TimeUnit.SECONDS.sleep(2);
}
return SUCCESS;
}
}
然后我們可以啟動(dòng)項(xiàng)目,看看「調(diào)度中心」是否已經(jīng)成功注冊(cè)當(dāng)前項(xiàng)目的「執(zhí)行器」,再使用上面介紹的「新建任務(wù)」的方法,來測(cè)試一下是否正常接入。
小結(jié)
總體來說 XXL-JOB 非常的容易上手,并且官方提供了很友好的實(shí)例代碼,包括一些高級(jí)特性「分片」、「遠(yuǎn)程調(diào)用」等多種任務(wù)都能夠很好的通過示例代碼理解和使用,這里就不再詳細(xì)贅述了..官方文檔已經(jīng)很完善了,感興趣的小伙伴可以去閱讀以下。
參考資料
- https://www.expectfly.com/2017/08/15/%E5%88%86%E5%B8%83%E5%BC%8F%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1%E6%96%B9%E6%A1%88%E6%8A%80%E6%9C%AF%E9%80%89%E5%9E%8B/ - 分布式定時(shí)任務(wù)調(diào)度系統(tǒng)選型
- https://www.yzhu.name/2019/03/30/Schedule-Job/ - 分布式調(diào)度系統(tǒng)選型
- https://blog.csdn.net/qq924862077/article/details/82708610 - XXL-JOB原理--執(zhí)行器注冊(cè)(二)
按照慣例黏一個(gè)尾巴:
歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處!
獨(dú)立域名博客:wmyskxz.com
簡(jiǎn)書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關(guān)注公眾微信號(hào):wmyskxz
分享自己的學(xué)習(xí) & 學(xué)習(xí)資料 & 生活
想要交流的朋友也可以加qq群:3382693