承接上文
承接上一篇文章【算法數(shù)據(jù)結(jié)構(gòu)專題】「延時(shí)隊(duì)列算法」史上手把手教你針對(duì)層級(jí)時(shí)間輪(TimingWheel)實(shí)現(xiàn)延時(shí)隊(duì)列的開(kāi)發(fā)實(shí)戰(zhàn)落地(上)】我們基本上對(duì)層級(jí)時(shí)間輪算法的基本原理有了一定的認(rèn)識(shí),本章節(jié)就從落地的角度進(jìn)行分析和介紹如何通過(guò)Java進(jìn)行實(shí)現(xiàn)一個(gè)屬于我們自己的時(shí)間輪服務(wù)組件,最后,在告訴大家一下,其實(shí)時(shí)間輪的技術(shù)是來(lái)源于生活中的時(shí)鐘。
時(shí)間輪演示結(jié)構(gòu)總覽
無(wú)序列表時(shí)間輪
【無(wú)序列表時(shí)間輪】主要是由LinkedList鏈表和啟動(dòng)線程、終止線程實(shí)現(xiàn)。
遍歷定時(shí)器中所有節(jié)點(diǎn),將剩余時(shí)間為 0s 的任務(wù)進(jìn)行過(guò)期處理,在執(zhí)行一個(gè)周期。
- 無(wú)序鏈表:每一個(gè)延時(shí)任務(wù)都存儲(chǔ)在該鏈表當(dāng)中(無(wú)序存儲(chǔ))。
- 啟動(dòng)線程: 直接在鏈表后面push ,時(shí)間復(fù)雜度 O(1)。
- 終止線程: 直接在鏈表中刪除節(jié)點(diǎn),時(shí)間復(fù)雜度 O(1) 。
遍歷周期:需要遍歷鏈表中所有節(jié)點(diǎn),時(shí)間復(fù)雜度 O(n),所以伴隨著鏈表中的元素越來(lái)越多,速度也會(huì)越來(lái)越慢!
無(wú)序列表時(shí)間輪的長(zhǎng)度限制了其適用場(chǎng)景,這里對(duì)此進(jìn)行優(yōu)化。因此引入了有序列表時(shí)間輪。
有序列表時(shí)間輪
與無(wú)序列表時(shí)間輪一樣,同樣使用鏈表進(jìn)行實(shí)現(xiàn)和設(shè)計(jì),但存儲(chǔ)的是絕對(duì)延時(shí)時(shí)間點(diǎn)。
- 啟動(dòng)線程:有序插入,比較時(shí)間按照時(shí)間大小有序插入,時(shí)間復(fù)雜度O(n),主要耗時(shí)在插入操作。
- 終止線程:鏈表中查找任務(wù),刪除節(jié)點(diǎn),時(shí)間復(fù)雜度O(n),主要耗時(shí)在插入操作。
找到執(zhí)行最后一個(gè)過(guò)期任務(wù)即可,無(wú)需遍歷整個(gè)鏈表,時(shí)間復(fù)雜度 O(1),從上面的描述「有序列表定時(shí)器」的性能瓶頸在于插入時(shí)的任務(wù)排序,但是換來(lái)的就是縮短了遍歷周期。
所以我們?nèi)绻岣咝?,就必須要提升一下插入和刪除以及檢索的性能,因此引入了「樹(shù)形有序列表時(shí)間輪」在「有序列表定時(shí)器」的基礎(chǔ)上進(jìn)行優(yōu)化,以有序樹(shù)的形式進(jìn)行任務(wù)存儲(chǔ)。
樹(shù)形有序列表時(shí)間輪
- 啟動(dòng)定時(shí)器: 有序插入,比較時(shí)間按照時(shí)間大小有序插入,時(shí)間復(fù)雜度 O(logn)
- 終止定時(shí)器: 在鏈表中查找任務(wù),刪除節(jié)點(diǎn),時(shí)間復(fù)雜度 O(logn)
- 周期清算: 找到執(zhí)行最后一個(gè)過(guò)期任務(wù)即可,無(wú)需遍歷整個(gè)鏈表,時(shí)間復(fù)雜度 O(1)
層級(jí)時(shí)間輪
整體流程架構(gòu)圖,如下所示。
對(duì)應(yīng)的原理,在這里就不進(jìn)行贅述了,之前本人已經(jīng)有兩篇文章對(duì)層級(jí)式時(shí)間輪進(jìn)行了較為詳細(xì)的介紹了,有需要的小伙伴,可以直接去前幾篇文章去學(xué)習(xí),接下來(lái)我們進(jìn)行相關(guān)的實(shí)現(xiàn)。
時(shí)間輪數(shù)據(jù)模型
時(shí)間輪(TimingWheel)是一個(gè)存儲(chǔ)定時(shí)任務(wù)的環(huán)形隊(duì)列,數(shù)組中的每個(gè)元素可以存放一個(gè)定時(shí)任務(wù)列表,其中存放了真正的定時(shí)任務(wù),如下圖所示。
時(shí)間輪的最基本邏輯模型,由多個(gè)時(shí)間格組成,每個(gè)時(shí)間格代表當(dāng)前時(shí)間輪的基本時(shí)間跨度(tickMs),所以我們先來(lái)設(shè)計(jì)和定義開(kāi)發(fā)對(duì)應(yīng)的時(shí)間輪的輪盤模型。命名為Roulette類。
輪盤抽象類-Roulette
之所以定義這個(gè)抽象類
public abstract class Roulette {
// 鏈表數(shù)據(jù)-主要用于存儲(chǔ)每個(gè)延時(shí)任務(wù)節(jié)點(diǎn)
List<TimewheelTask> tasks = null;
// 游標(biāo)指針?biāo)饕? protected int index;
// 時(shí)間輪輪盤的容量大小,如果是分鐘級(jí)別,一般就是60個(gè)格
protected int capacity;
// 時(shí)間輪輪盤的層級(jí),如果是一級(jí),它的上級(jí)就是二級(jí)
protected Integer level;
private AtomicInteger num = new AtomicInteger(0);
// 構(gòu)造器
public Roulette(int capacity, Integer level) {
this.capacity = capacity;
this.level = level;
this.tasks = new ArrayList<>(capacity);
this.index = 0;
}
// 獲取當(dāng)前下表的索引對(duì)應(yīng)的時(shí)間輪的任務(wù)
public TimewheelTask getTask() {
return tasks.get(index);
}
// init初始化操作機(jī)制
public List<TimewheelTask> init() {
long interval = MathTool.power((capacity + 1), level);
long add = 0;
TimewheelTask delayTask = null;
for (int i = 0; i < capacity; i++) {
add += interval;
if (level == 0) {
delayTask = new DefaultDelayTask(level);
} else {
delayTask = new SplitDelayTask(level);
}
//已經(jīng)轉(zhuǎn)換為最小的時(shí)間間隔
delayTask.setDelay(add, TimeUnitProvider.getTimeUnit());
tasks.add(delayTask);
}
return tasks;
}
// 索引下標(biāo)移動(dòng)
public void indexAdd() {
this.index++;
if (this.index >= capacity) {
this.index = 0;
}
}
// 添加對(duì)應(yīng)的任務(wù)到對(duì)應(yīng)的隊(duì)列里面
public void addTask(TimewheelTask task) {
tasks.add(task);
}
// 給子類提供的方法進(jìn)行實(shí)現(xiàn)對(duì)應(yīng)的任務(wù)添加功能
public abstract void addTask(int interval, MyTask task);
}
時(shí)間輪盤的熟悉信息介紹
鏈表數(shù)據(jù)-主要用于存儲(chǔ)每個(gè)延時(shí)任務(wù)節(jié)點(diǎn)。
List<TimewheelTask> tasks = null;
tasks也可以改成雙向鏈表 + 數(shù)組的結(jié)構(gòu):即節(jié)點(diǎn)存貯的對(duì)象中有指針,組成環(huán)形,可以通過(guò)數(shù)組的下標(biāo)靈活訪問(wèn)每個(gè)節(jié)點(diǎn),類似 LinkedHashMap。
游標(biāo)指針?biāo)饕?/p>
protected int index;
時(shí)間輪輪盤的容量大小,如果是分鐘級(jí)別,一般就是60個(gè)格
protected int capacity;
時(shí)間輪輪盤的層級(jí),如果是一級(jí),它的上級(jí)就是二級(jí)
protected Integer level;
init初始化時(shí)間輪輪盤對(duì)象模型,主要用于分配分配每一個(gè)輪盤上面元素的TimewheelTask,用于延時(shí)隊(duì)列的執(zhí)行任務(wù)線程,已經(jīng)分配對(duì)應(yīng)的每一個(gè)節(jié)點(diǎn)的延時(shí)時(shí)間節(jié)點(diǎn)數(shù)據(jù)。
public List<TimewheelTask> init() {
// 那么整個(gè)時(shí)間輪的總體時(shí)間跨度(interval)
long interval = MathTool.power((capacity + 1), level);
long add = 0;
TimewheelTask delayTask = null;
for (int i = 0; i < capacity; i++) {
add += interval;
if (level == 0) {
delayTask = new ExecuteTimewheelTask(level);
} else {
delayTask = new MoveTimewheelTask(level);
}
//已經(jīng)轉(zhuǎn)換為最小的時(shí)間間隔
delayTask.setDelay(add, TimeUnitProvider.getTimeUnit());
tasks.add(delayTask);
}
return tasks;
}
- 整數(shù)a的n次冪:interval,計(jì)算跨度,主要是各級(jí)別之間屬于平方倍數(shù)
例如,第一層:20 ,第二層:20^2 ......
//例如 n=7 二進(jìn)制 0 1 1 1
//a的n次冪 = a的2次冪×a的2次冪 × a的1次冪×a的1次冪 ×a
public static long power(long a, int n) {
int rtn = 1;
while (n >= 1) {
if((n & 1) == 1){
rtn *= a;
}
a *= a;
n = n >> 1;
}
return rtn;
}
TimeUnitProvider工具類
主要用于計(jì)算時(shí)間單位操作的轉(zhuǎn)換
public class TimeUnitProvider {
private static TimeUnit unit = TimeUnit.SECONDS;
public static TimeUnit getTimeUnit() {
return unit;
}
}
代碼簡(jiǎn)介:
- interval:代表著初始化的延時(shí)時(shí)間數(shù)據(jù)值,主要用于不同的層次的出發(fā)時(shí)間數(shù)據(jù)
- for (int i = 0; i < capacity; i++) :代表著進(jìn)行for循環(huán)進(jìn)行添加對(duì)應(yīng)的延時(shí)隊(duì)列任務(wù)到集合中
- add += interval,主要用于添加對(duì)應(yīng)的延時(shí)隊(duì)列的延時(shí)數(shù)據(jù)值!并且分配給當(dāng)前輪盤得到所有數(shù)據(jù)節(jié)點(diǎn)。
獲取當(dāng)前下標(biāo)的索引對(duì)應(yīng)的時(shí)間輪的任務(wù)節(jié)點(diǎn)
public TimewheelTask getTask() {
return tasks.get(index);
}
層級(jí)時(shí)間輪的Bucket數(shù)據(jù)桶
在這里我們建立了一個(gè)TimewheelBucket類實(shí)現(xiàn)了Roulette輪盤模型,從而進(jìn)行建立對(duì)應(yīng)的我們的層級(jí)時(shí)間輪的數(shù)據(jù)模型,并且覆蓋了addTask方法。
public class TimewheelBucket extends Roulette {
public TimewheelBucket(int capacity, Integer level) {
super(capacity, level);
}
public synchronized void addTask(int interval, MyTask task) {
interval -= 1;
int curIndex = interval + this.index;
if (curIndex >= capacity) {
curIndex = curIndex - capacity;
}
tasks.get(curIndex).addTask(task);
}
}
添加addTask方法,進(jìn)行獲取計(jì)算對(duì)應(yīng)的下標(biāo),并且此方法add操作才是對(duì)外開(kāi)發(fā)調(diào)用的,在這里,我們主要實(shí)現(xiàn)了根據(jù)層級(jí)計(jì)算出對(duì)應(yīng)的下標(biāo)進(jìn)行獲取對(duì)應(yīng)的任務(wù)執(zhí)行調(diào)度點(diǎn),將我們外界BizTask,真正的業(yè)務(wù)操作封裝到這個(gè)BizTask模型,交由我們的系統(tǒng)框架進(jìn)行執(zhí)行。
public synchronized void addTask(int interval, BizTask task) {
interval -= 1;
int curIndex = interval + this.index;
if (curIndex >= capacity) {
curIndex = curIndex - capacity;
}
tasks.get(curIndex).addTask(task);
}
時(shí)間輪輪盤上的任務(wù)點(diǎn)
我們針對(duì)于時(shí)間輪輪盤的任務(wù)點(diǎn)進(jìn)行設(shè)計(jì)和定義對(duì)應(yīng)的調(diào)度執(zhí)行任務(wù)模型。一個(gè)調(diào)度任務(wù)點(diǎn),可以幫到關(guān)系到多個(gè)BizTask,也就是用戶提交上來(lái)的業(yè)務(wù)任務(wù)線程對(duì)象,為了方便采用延時(shí)隊(duì)列的延時(shí)處理模式,再次實(shí)現(xiàn)了Delayed這個(gè)接口,對(duì)應(yīng)的實(shí)現(xiàn)代碼如下所示:
Delayed接口
public interface Delayed extends Comparable<Delayed> {
/**
* Returns the remaining delay associated with this object, in the
* given time unit.
*
* @param unit the time unit
* @return the remaining delay; zero or negative values indicate
* that the delay has already elapsed
*/
long getDelay(TimeUnit unit);
}
TimewheelTask時(shí)間輪刻度點(diǎn)
@Getter
public abstract class TimewheelTask implements Delayed {
private List<BizTask> tasks = new ArrayList<BizTask>();
private int level;
private Long delay;
private long calDelay;
private TimeUnit calUnit;
public TimewheelTask(int level) {
this.level = level;
}
public void setDelay(Long delay, TimeUnit unit) {
this.calDelay=delay;
this.calUnit=unit;
}
public void calDelay() {
this.delay = TimeUnit.NANOSECONDS.convert(this.calDelay, this.calUnit) + System.nanoTime();
}
public long getDelay(TimeUnit unit) {
return this.delay - System.nanoTime();
}
public int compareTo(Delayed o) {
long d = (getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS));
return (d == 0) ? 0 : ((d < 0) ? -1 : 1);
}
public void addTask(BizTask task) {
synchronized (this) {
tasks.add(task);
}
}
public void clear() {
tasks.clear();
}
public abstract void run();
}
-
業(yè)務(wù)任務(wù)集合:
private List<BizTask> tasks = new ArrayList<BizTask>();
- 層級(jí)
private int level;
- 延時(shí)時(shí)間
private Long delay;
- 實(shí)際用于延時(shí)計(jì)算的時(shí)間,就是底層是統(tǒng)一化所有的延時(shí)時(shí)間到對(duì)應(yīng)的延時(shí)隊(duì)列
private long calDelay;
- 實(shí)際用于延時(shí)計(jì)算的時(shí)間,就是底層是統(tǒng)一化所有的延時(shí)時(shí)間到對(duì)應(yīng)的延時(shí)隊(duì)列(用于統(tǒng)一化的時(shí)間單位)
private TimeUnit calUnit;
添加對(duì)應(yīng)的業(yè)務(wù)延時(shí)任務(wù)到輪盤刻度點(diǎn)
public void addTask(BizTask task) {
synchronized (this) {
tasks.add(task);
}
}
刻度點(diǎn)的實(shí)現(xiàn)類
因?yàn)閷?duì)應(yīng)的任務(wù)可能會(huì)需要將下游的業(yè)務(wù)任務(wù)進(jìn)行升級(jí)或者降級(jí),所以我們會(huì)針對(duì)于執(zhí)行任務(wù)點(diǎn)分為,執(zhí)行任務(wù)刻度點(diǎn)和躍遷任務(wù)刻度點(diǎn)兩種類型。
- 執(zhí)行任務(wù)延時(shí)隊(duì)列刻度點(diǎn)
public class ExecuteTimewheelTask extends TimewheelTask {
public ExecuteTimewheelTask(int level) {
super(level);
}
//到時(shí)間執(zhí)行所有的任務(wù)
public void run() {
List<BizTask> tasks = getTasks();
if (CollectionUtils.isNotEmpty(tasks)) {
tasks.forEach(task -> ThreadPool.submit(task));
}
}
}
再次我們就定義執(zhí)行這些任務(wù)的線程池為:
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 100, 3, TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>(10000),
new MyThreadFactory("executor"), new ThreadPoolExecutor.CallerRunsPolicy());
- 躍遷任務(wù)延時(shí)隊(duì)列刻度點(diǎn)
public class MoveTimewheelTask extends TimewheelTask {
public MoveTimewheelTask(int level) {
super(level);
}
//躍遷到其他輪盤,將對(duì)應(yīng)的任務(wù)
public void run() {
List<BizTask> tasks = getTasks();
if (CollectionUtils.isNotEmpty(tasks)) {
tasks.forEach(task -> {
long delay = task.getDelay();
TimerWheel.adddTask(task,delay, TimeUnitProvider.getTimeUnit());
});
}
}
}
致辭整個(gè)時(shí)間輪輪盤的數(shù)據(jù)模型就定義的差不多了,接下來(lái)我們需要定義運(yùn)行在時(shí)間輪盤上面的任務(wù)模型,BizTask基礎(chǔ)模型。
BizTask基礎(chǔ)模型
public abstract class BizTask implements Runnable {
protected long interval;
protected int index;
protected long executeTime;
public BizTask(long interval, TimeUnit unit, int index) {
this.interval = interval;
this.index = index;
this.executeTime= TimeUnitProvider.getTimeUnit().convert(interval,unit)+TimeUnitProvider.getTimeUnit().convert(System.nanoTime(),TimeUnit.NANOSECONDS);
}
public long getDelay() {
return this.executeTime - TimeUnitProvider.getTimeUnit().convert(System.nanoTime(), TimeUnit.NANOSECONDS);
}
}
主要針對(duì)于任務(wù)執(zhí)行,需要交給線程池去執(zhí)行,故此,實(shí)現(xiàn)了Runnable接口。
- protected long interval;:跨度操作
- protected int index;:索引下表,在整個(gè)隊(duì)列里面的下表處理
- protected long executeTime;:對(duì)應(yīng)的執(zhí)行時(shí)間
其中最重要的便是獲取延時(shí)時(shí)間的操作,主要提供給框架的Delayed接口進(jìn)行判斷是否到執(zhí)行時(shí)間了。
public long getDelay() {
return this.executeTime - TimeUnitProvider.getTimeUnit().convert(System.nanoTime(), TimeUnit.NANOSECONDS);
}
層級(jí)時(shí)間輪的門面TimerWheel
最后我們要進(jìn)行定義和設(shè)計(jì)開(kāi)發(fā)對(duì)應(yīng)的整體的時(shí)間輪層級(jí)模型。
public class TimerWheel {
private static Map<Integer, TimewheelBucket> cache = new ConcurrentHashMap<>();
//一個(gè)輪表示三十秒
private static int interval = 30;
private static wheelThread wheelThread;
public static void adddTask(BizTask task, Long time, TimeUnit unit) {
if(task == null){
return;
}
long intervalTime = TimeUnitProvider.getTimeUnit().convert(time, unit);
if(intervalTime < 1){
ThreadPool.submit(task);
return;
}
Integer[] wheel = getWheel(intervalTime,interval);
TimewheelBucket taskList = cache.get(wheel[0]);
if (taskList != null) {
taskList.addTask(wheel[1], task);
} else {
synchronized (cache) {
if (cache.get(wheel[0]) == null) {
taskList = new TimewheelBucket(interval-1, wheel[0]);
wheelThread.add(taskList.init());
cache.putIfAbsent(wheel[0],taskList);
}
}
taskList.addTask(wheel[1], task);
}
}
static{
interval = 30;
wheelThread = new wheelThread();
wheelThread.setDaemon(false);
wheelThread.start();
}
private static Integer[] getWheel(long intervalTime,long baseInterval) {
//轉(zhuǎn)換后的延時(shí)時(shí)間
if (intervalTime < baseInterval) {
return new Integer[]{0, Integer.valueOf(String.valueOf((intervalTime % 30)))};
} else {
return getWheel(intervalTime,baseInterval,baseInterval, 1);
}
}
private static Integer[] getWheel(long intervalTime,long baseInterval,long interval, int p) {
long nextInterval = baseInterval * interval;
if (intervalTime < nextInterval) {
return new Integer[]{p, Integer.valueOf(String.valueOf(intervalTime / interval))};
} else {
return getWheel(intervalTime,baseInterval,nextInterval, (p+1));
}
}
static class wheelThread extends Thread {
DelayQueue<TimewheelTask> queue = new DelayQueue<TimewheelTask>();
public DelayQueue<TimewheelTask> getQueue() {
return queue;
}
public void add(List<TimewheelTask> tasks) {
if (CollectionUtils.isNotEmpty(tasks)) {
tasks.forEach(task -> add(task));
}
}
public void add(TimewheelTask task) {
task.calDelay();
queue.add(task);
}
@Override
public void run() {
while (true) {
try {
TimewheelTask task = queue.take();
int p = task.getLevel();
long nextInterval = MathTool.power(interval, Integer.valueOf(String.valueOf(MathTool.power(2, p))));
TimewheelBucket timewheelBucket = cache.get(p);
synchronized (timewheelBucket) {
timewheelBucket.indexAdd();
task.run();
task.clear();
}
task.setDelay(nextInterval, TimeUnitProvider.getTimeUnit());
task.calDelay();
queue.add(task);
} catch (InterruptedException e) {
}
}
}
}
}
TimerWheel的模型定義
private static Map<Integer, TimewheelBucket> cache = new ConcurrentHashMap<>();
一個(gè)輪表示30秒的整體跨度。
private static int interval = 30;
創(chuàng)建整體驅(qū)動(dòng)的執(zhí)行線程文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-404562.html
private static wheelThread wheelThread;
static{
interval = 30;
wheelThread = new wheelThread();
wheelThread.setDaemon(false);
wheelThread.start();
}
static class wheelThread extends Thread {
DelayQueue<TimewheelTask> queue = new DelayQueue<TimewheelTask>();
public DelayQueue<TimewheelTask> getQueue() {
return queue;
}
public void add(List<TimewheelTask> tasks) {
if (CollectionUtils.isNotEmpty(tasks)) {
tasks.forEach(task -> add(task));
}
}
public void add(TimewheelTask task) {
task.calDelay();
queue.add(task);
}
@Override
public void run() {
while (true) {
try {
TimewheelTask task = queue.take();
int p = task.getLevel();
long nextInterval = MathTool.power(interval, Integer.valueOf(String.valueOf(MathTool.power(2, p))));
TimewheelBucket timewheelBucket = cache.get(p);
synchronized (timewheelBucket) {
timewheelBucket.indexAdd();
task.run();
task.clear();
}
task.setDelay(nextInterval, TimeUnitProvider.getTimeUnit());
task.calDelay();
queue.add(task);
} catch (InterruptedException e) {
}
}
}
獲取對(duì)應(yīng)的時(shí)間輪輪盤模型體系
private static Integer[] getWheel(long intervalTime,long baseInterval) {
//轉(zhuǎn)換后的延時(shí)時(shí)間
if (intervalTime < baseInterval) {
return new Integer[]{0, Integer.valueOf(String.valueOf((intervalTime % 30)))};
} else {
return getWheel(intervalTime,baseInterval,baseInterval, 1);
}
}
private static Integer[] getWheel(long intervalTime,long baseInterval,long interval, int p) {
long nextInterval = baseInterval * interval;
if (intervalTime < nextInterval) {
return new Integer[]{p, Integer.valueOf(String.valueOf(intervalTime / interval))};
} else {
return getWheel(intervalTime,baseInterval,nextInterval, (p+1));
}
}
到這里相信大家,基本上應(yīng)該是了解了如何去實(shí)現(xiàn)對(duì)應(yīng)的時(shí)間輪盤的技術(shù)實(shí)現(xiàn)過(guò)程,有興趣希望整個(gè)完整源碼的,可以聯(lián)系我哦。謝謝大家!文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-404562.html
到了這里,關(guān)于【算法數(shù)據(jù)結(jié)構(gòu)專題】「延時(shí)隊(duì)列算法」史上手把手教你針對(duì)層級(jí)時(shí)間輪(TimingWheel)實(shí)現(xiàn)延時(shí)隊(duì)列的開(kāi)發(fā)實(shí)戰(zhàn)落地(下)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!