摘要:狀態(tài)機(jī)引擎選型概念有限狀態(tài)機(jī)是一種用來進(jìn)行對(duì)象行為建模的工具,其作用主要是描述對(duì)象在它的生命周期內(nèi)所經(jīng)歷的狀態(tài)序列,以及如何響應(yīng)來自外界的各種事件。狀態(tài)機(jī)的要素狀態(tài)機(jī)可歸納為個(gè)要素,即現(xiàn)態(tài)條件動(dòng)作次態(tài)。
狀態(tài)機(jī)引擎選型
date: 2017-06-19 15:50:18
概念有限狀態(tài)機(jī)是一種用來進(jìn)行對(duì)象行為建模的工具,其作用主要是描述對(duì)象在它的生命周期內(nèi)所經(jīng)歷的狀態(tài)序列,以及如何響應(yīng)來自外界的各種事件。在電商場(chǎng)景(訂單、物流、售后)、社交(IM消息投遞)、分布式集群管理(分布式計(jì)算平臺(tái)任務(wù)編排)等場(chǎng)景都有大規(guī)模的使用。
為什么需要狀態(tài)機(jī)狀態(tài)機(jī)的要素
狀態(tài)機(jī)可歸納為4個(gè)要素,即現(xiàn)態(tài)、條件、動(dòng)作、次態(tài)?!艾F(xiàn)態(tài)”和“條件”是因,“動(dòng)作”和“次態(tài)”是果。詳解如下:
①現(xiàn)態(tài):是指當(dāng)前所處的狀態(tài)。
②條件:又稱為“事件”。當(dāng)一個(gè)條件被滿足,將會(huì)觸發(fā)一個(gè)動(dòng)作,或者執(zhí)行一次狀態(tài)的遷移。
③動(dòng)作:條件滿足后執(zhí)行的動(dòng)作。動(dòng)作執(zhí)行完畢后,可以遷移到新的狀態(tài),也可以仍舊保持原狀態(tài)。動(dòng)作不是必需的,當(dāng)條件滿足后,也可以不執(zhí)行任何動(dòng)作,直接遷移到新狀態(tài)。
④次態(tài):條件滿足后要遷往的新狀態(tài)?!按螒B(tài)”是相對(duì)于“現(xiàn)態(tài)”而言的,“次態(tài)”一旦被激活,就轉(zhuǎn)變成新的“現(xiàn)態(tài)”了。狀態(tài)機(jī)動(dòng)作類型
進(jìn)入動(dòng)作(entry action):在進(jìn)入狀態(tài)時(shí)進(jìn)行
退出動(dòng)作:在退出狀態(tài)時(shí)進(jìn)行
輸入動(dòng)作:依賴于當(dāng)前狀態(tài)和輸入條件進(jìn)行
轉(zhuǎn)移動(dòng)作:在進(jìn)行特定轉(zhuǎn)移時(shí)進(jìn)行
有限狀態(tài)機(jī)是一種對(duì)象行為建模工具,適用對(duì)象有一個(gè)明確并且復(fù)雜的生命流(一般而言三個(gè)以上狀態(tài)),并且在狀態(tài)變遷存在不同的觸發(fā)條件以及處理行為。從我個(gè)人的使用經(jīng)驗(yàn)上,使用狀態(tài)機(jī)來管理對(duì)象生命流的好處更多體現(xiàn)在代碼的可維護(hù)性、可測(cè)試性上,明確的狀態(tài)條件、原子的響應(yīng)動(dòng)作、事件驅(qū)動(dòng)遷移目標(biāo)狀態(tài),對(duì)于流程復(fù)雜易變的業(yè)務(wù)場(chǎng)景能大大減輕維護(hù)和測(cè)試的難度。
技術(shù)選型有限狀態(tài)機(jī)的使用場(chǎng)景很豐富,但在技術(shù)選型的時(shí)候我主要調(diào)研了squirrel-foundation(503stars),spring-statemachine(305stars),stateless4j(293stars),這三款finite state machine是github上stars top3的java狀態(tài)機(jī)引擎框架,下面我的一些對(duì)比結(jié)果。
stateless4j 核心模型stateless4j是這三款狀態(tài)機(jī)框架中最輕量簡(jiǎn)單的實(shí)現(xiàn),來源自stateless(C#版本的FSM)
StateRepresentation狀態(tài)表示層,狀態(tài)對(duì)應(yīng),注冊(cè)了每狀態(tài)的entry exit action,以及該狀態(tài)所接受的triggerBehaviours;
StateConfiguration狀態(tài)節(jié)點(diǎn)的配置實(shí)例,通過StateMachineConfig.configure創(chuàng)建,由stateRepresentation組成;
StateMachineConfig狀態(tài)機(jī)配置,負(fù)責(zé)了全局狀態(tài)機(jī)的創(chuàng)建以及保存,維護(hù)了了state到對(duì)應(yīng)StateRepresentation的映射,通過當(dāng)前狀態(tài)找到對(duì)應(yīng)的stateRepresentation,再根據(jù)triggerBehaviours執(zhí)行相應(yīng)的entry exit action;
StateMachine狀態(tài)機(jī)實(shí)例,不可共享,記錄了狀態(tài)機(jī)實(shí)例的當(dāng)前狀態(tài),并通過statemachine實(shí)例來響應(yīng)事件;
核心實(shí)現(xiàn)protected void publicFire(T trigger, Object... args) { ... //獲取triggerBehaviour, destination/trigger/guard AbstractTriggerBehaviour優(yōu)缺點(diǎn)triggerBehaviour = getCurrentRepresentation().tryFindHandler(trigger); if (triggerBehaviour == null) { //異常流程,當(dāng)前state無法處理trigger unhandledTriggerAction.doIt(getCurrentRepresentation().getUnderlyingState(), trigger); return; } S source = getState(); OutVardestination = new OutVar<>(); //狀態(tài)遷移,設(shè)置目標(biāo)狀態(tài) if (triggerBehaviour.resultsInTransitionFrom(source, args, destination)) { Transitiontransition = new Transition<>(source, destination.get(), trigger); //執(zhí)行source的exit action getCurrentRepresentation().exit(transition); //執(zhí)行stateMutator函數(shù)回調(diào),設(shè)置當(dāng)前狀態(tài)為目標(biāo)destination setState(destination.get()); //執(zhí)行destination的entry action getCurrentRepresentation().enter(transition, args); } }
優(yōu)點(diǎn)
足夠輕量,創(chuàng)建StateMachine實(shí)例開銷??;
支持基本的事件遷移、exit/entry action、guard、dynamic permit(相同的事件不同的condition可到達(dá)不同的目標(biāo)狀態(tài));
核心代碼千行左右,基于現(xiàn)有代碼二次開發(fā)的難度也比較低;
缺點(diǎn)
支持的動(dòng)作只包含了entry exit action,不支持transition action;
在狀態(tài)遷移的模型中缺少全局的observer(缺少interceptor擴(kuò)展點(diǎn)),例如要做state的持久化就很惡心(擴(kuò)展stateMutator在設(shè)置目標(biāo)狀態(tài)的同時(shí)完成持久化的方案將先于entry進(jìn)行persist實(shí)際上并不是一個(gè)好的解決方案);
狀態(tài)遷移的模型過于簡(jiǎn)單,這也導(dǎo)致了本身支持的action和提供的擴(kuò)展點(diǎn)有限;
結(jié)論
stateless4j足夠輕量,同步模型,在app中使用比較合適,但在服務(wù)端解決復(fù)雜業(yè)務(wù)場(chǎng)景上stateless4j確實(shí)略顯單薄。
spring statemachine 核心模型spring-statemachine是spring官方提供的狀態(tài)機(jī)實(shí)現(xiàn)。
StateMachineStateConfigurer 狀態(tài)定義,可以定義狀態(tài)的entry exit action;
StateMachineTransitionConfigurer 轉(zhuǎn)換定義,可以定義狀態(tài)轉(zhuǎn)換接受的事件,以及相應(yīng)的transition action;
StateMachineConfigurationConfigurer 狀態(tài)機(jī)系統(tǒng)配置,包括action執(zhí)行器(spring statemachine實(shí)例可以accept多個(gè)event,存儲(chǔ)在內(nèi)部queue中,并通過sync/async executor執(zhí)行)、listener(事件監(jiān)聽器)等;
StateMachineListener 事件監(jiān)聽器(通過Spring的event機(jī)制實(shí)現(xiàn)),監(jiān)聽stateEntered(進(jìn)入狀態(tài))、stateExited(離開狀態(tài))、eventNotAccepted(事件無法響應(yīng))、transition(轉(zhuǎn)換)、transitionStarted(轉(zhuǎn)換開始)、transitionEnded(轉(zhuǎn)換結(jié)束)、stateMachineStarted(狀態(tài)機(jī)啟動(dòng))、stateMachineStopped(狀態(tài)機(jī)關(guān)閉)、stateMachineError(狀態(tài)機(jī)異常)等事件,借助listener可以trace state transition;
StateMachineInterceptor 狀態(tài)攔截器,不同于StateMachineListener被動(dòng)監(jiān)聽,interceptor擁有可以改變狀態(tài)變化鏈的能力,主要在preEvent(事件預(yù)處理)、preStateChange(狀態(tài)變更的前置處理)、postStateChange(狀態(tài)變更的后置處理)、preTransition(轉(zhuǎn)化的前置處理)、postTransition(轉(zhuǎn)化的后置處理)、stateMachineError(異常處理)等執(zhí)行點(diǎn)生效,內(nèi)部的PersistingStateChangeInterceptor(狀態(tài)持久化)等都是基于這個(gè)擴(kuò)展協(xié)議生效的;
StateMachine 狀態(tài)機(jī)實(shí)例,spring statemachine支持單例、工廠模式兩種方式創(chuàng)建,每個(gè)statemachine有一個(gè)獨(dú)有的machineId用于標(biāo)識(shí)machine實(shí)例;需要注意的是statemachine實(shí)例內(nèi)部存儲(chǔ)了當(dāng)前狀態(tài)機(jī)等上下文相關(guān)的屬性,因此這個(gè)實(shí)例不能夠被多線程共享;
核心實(shí)現(xiàn)AbstractStateMachine#sendEventInternal acceptEvent事件響應(yīng)
private boolean sendEventInternal(Messageevent) { ... try { //stateMachineInterceptor事件預(yù)處理 event = getStateMachineInterceptors().preEvent(event, this); } catch (Exception e) { ... } if (isComplete() || !isRunning()) { notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null)); return false; } boolean accepted = acceptEvent(event); stateMachineExecutor.execute(); if (!accepted) { notifyEventNotAccepted(buildStateContext(Stage.EVENT_NOT_ACCEPTED, event, null, getRelayStateMachine(), getState(), null)); } return accepted; }
AbstractStateMachine#acceptEvent 使用隊(duì)列存儲(chǔ)事件
protected synchronized boolean acceptEvent(Messagemessage) { State cs = currentState; ... for (Transitiontransition : transitions) { Statesource = transition.getSource(); Triggertrigger = transition.getTrigger(); if (cs != null && StateMachineUtils.containsAtleastOne(source.getIds(), cs.getIds())) { //校驗(yàn)當(dāng)前狀態(tài)能否接受trigger if (trigger != null && trigger.evaluate(new DefaultTriggerContext(message.getPayload()))) { //存儲(chǔ)遷移事件 stateMachineExecutor.queueEvent(message); return true; } } } ... }
DefaultStateMachineExecutor#scheduleEventQueueProcessing 事件處理
private void scheduleEventQueueProcessing() { TaskExecutor executor = getTaskExecutor(); if (executor == null) { return; } Runnable task = new Runnable() { @Override public void run() { boolean eventProcessed = false; while (processEventQueue()) { //event queue -> tigger queue eventProcessed = true; //最終的transition得到處理,包括interceptor的preTransition、postTransition以及l(fā)istener的事件通知都在這個(gè)過程中被執(zhí)行 //具體實(shí)現(xiàn)可參看DefaultStateMachineExecutor.handleTriggerTrans以及AbstractStateMachine中executor的回調(diào)實(shí)現(xiàn) processTriggerQueue(); while (processDeferList()) { processTriggerQueue(); } } if (!eventProcessed) { processTriggerQueue(); while (processDeferList()) { processTriggerQueue(); } } taskRef.set(null); if (requestTask.getAndSet(false)) { scheduleEventQueueProcessing(); } } }; if (taskRef.compareAndSet(null, task)) { //默認(rèn)實(shí)現(xiàn)為sync executor,執(zhí)行上面的task executor.execute(task); } else { requestTask.set(true); } }優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
Easy to use flat one level state machine for simple use cases.
Hierarchical state machine structure to ease complex state configuration.
State machine regions to provide even more complex state configurations.
Usage of triggers, transitions, guards and actions.
Type safe configuration adapter.
Builder pattern for easy instantiation for use outside of Spring Application context
Recipes for usual use cases
Distributed state machine based on a Zookeeper
State machine event listeners.
UML Eclipse Papyrus modeling.
Store machine config in a persistent storage.
Spring IOC integration to associate beans with a state machine.
listener、interceptor機(jī)制方便狀態(tài)機(jī)monitor以及持久化擴(kuò)展;
缺點(diǎn)
spring statemachine 目前迭代的版本不多,并沒有得到充分的驗(yàn)證,還是存在一些bug的;
StateMachine實(shí)例的創(chuàng)建比較重,以單例方式線程不安全,使用工廠方式對(duì)于類似訂單等場(chǎng)景StateMachineFactory緩存訂單對(duì)應(yīng)的狀態(tài)機(jī)實(shí)例意義不大,并且transition的注解并不支持StateMachineFactory(stackoverflow上的一些討論"using-statemachinefactory-from-persisthandlerconfig"、"withstatemachine-with-enablestatemachinefactor");
我嘗試在將StateMachine實(shí)例緩存在ThreadLocal變量中以到達(dá)復(fù)用目的,但在測(cè)試同一statemachine accept多個(gè)event過程中,如果任務(wù)執(zhí)行時(shí)間過長(zhǎng),會(huì)導(dǎo)致狀態(tài)機(jī)的deadlock發(fā)生(這個(gè)issue目前作者在snapshot版本上已修正);
結(jié)論
spring statemachine由spring組織孵化,長(zhǎng)遠(yuǎn)來看應(yīng)該會(huì)逐漸走上成熟,但目前而言確實(shí)太年輕,離業(yè)務(wù)的落地使用上確實(shí)還有太多坑要踩,鑒于這些原因我也沒有選擇這個(gè)方案。
squirrel-foundation 核心模型squirrel-foundation是一款很優(yōu)秀的開源產(chǎn)品,推薦大家閱讀以下它的源碼。相較于spring statemachine,squirrel的實(shí)現(xiàn)更為輕量,設(shè)計(jì)域也很清晰,對(duì)應(yīng)的文檔以及測(cè)試用例也很豐富。
StateMachineBuilderFactory:StateMachineBuilder工廠類,負(fù)責(zé)解析狀態(tài)定義,根據(jù)狀態(tài)定義創(chuàng)建對(duì)應(yīng)的StateMachineBuilder();
StateMachineBuilder:StateMachine構(gòu)造器,可復(fù)用構(gòu)造器,所有狀態(tài)機(jī)由生成器創(chuàng)建相同的狀態(tài)機(jī)實(shí)例共享相同的狀態(tài)定義;
StateMachine:狀態(tài)機(jī)實(shí)例,通過StateMachineBuilder創(chuàng)建,輕量級(jí)內(nèi)存實(shí)例,不可共享;支持對(duì)afterTransitionCausedException、beforeTransitionBegin、afterTransitionCompleted、afterTransitionEnd、afterTransitionDeclined beforeActionInvoked、afterActionInvoked事件的自定義全局處理流程,作用類似于spring statemachine中的inteceptor;
Condition:squirrel支持動(dòng)態(tài)的transition,同一個(gè)state接受相同的trigger,statecontext不一樣,到達(dá)的目標(biāo)狀態(tài)也可以不一樣;
StateMachineListener:全局事件監(jiān)聽,包括了TransitionBeginListener、TransitionCompleteListener、TransitionExceptionListener等幾類用于監(jiān)聽transition的不同階段的監(jiān)聽器;
squirrel的事件處理模型與spring-statemachine比較類似,squirrel的事件執(zhí)行器的作用點(diǎn)粒度更細(xì),通過預(yù)處理,將一個(gè)狀態(tài)遷移分解成exit trasition entry 這三個(gè)action event,再遞交給執(zhí)行器分別執(zhí)行(這個(gè)設(shè)計(jì)挺不錯(cuò))。
部分核心代碼
AbstractStateMachine#internalFire
private void internalFire(E event, C context, boolean insertAtFirst) { ... if(insertAtFirst) { queuedEvents.addFirst(new Pair(event, context)); } else { //事件隊(duì)列 queuedEvents.addLast(new Pair (event, context)); } //事件消費(fèi),采用這種模型用來支持sync/async事件消費(fèi) processEvents(); }
AbstractStateMachine#processEvents
private void processEvents() { //statemachine是否空閑 if (isIdle()) { writeLock.lock(); //標(biāo)記狀態(tài)機(jī)正在忙碌,避免同一個(gè)狀態(tài)機(jī)實(shí)例的事件消費(fèi)產(chǎn)生掙用 setStatus(StateMachineStatus.BUSY); try { PaireventInfo; E event; C context = null; while ((eventInfo=queuedEvents.poll())!=null) { // response to cancel operation if(Thread.interrupted()) { queuedEvents.clear(); break; } event = eventInfo.first(); context = eventInfo.second(); processEvent(event, context, data, executor, isDataIsolateEnabled); } ImmutableState rawState = data.read().currentRawState(); if(isAutoTerminateEnabled && rawState.isRootState() && rawState.isFinalState()) { terminate(context); } } finally { //標(biāo)記空閑 if(getStatus()==StateMachineStatus.BUSY) setStatus(StateMachineStatus.IDLE); writeLock.unlock(); } } }
AbstractStateMachine#processEvent
private boolean processEvent(E event, C context, StateMachineData優(yōu)缺點(diǎn)originalData, ActionExecutionService executionService, boolean isDataIsolateEnabled) { ... try { //執(zhí)行StateMachine中定義的transitionBegin回調(diào) beforeTransitionBegin(fromStateId, event, context); //執(zhí)行注冊(cè)的listener中transitionBegin回調(diào) fireEvent(new TransitionBeginEventImpl (fromStateId, event, context, getThis())); //明確事件是否可被accept TransitionResult result = FSM.newResult(false, fromState, null); StateContext stateContext = FSM.newStateContext(this, localData, fromState, event, context, result, executionService); //執(zhí)行Condition確認(rèn)目標(biāo)狀態(tài),生成exit state--transition-->entry state 三個(gè)內(nèi)部事件,通過executor的actionBucket存儲(chǔ) fromState.internalFire(stateContext); toStateId = result.getTargetState().getStateId(); if(result.isAccepted()) { //真正執(zhí)行actionBucket中存儲(chǔ)的exit--transition-->entry action executionService.execute(); localData.write().lastState(fromStateId); localData.write().currentState(toStateId); //執(zhí)行l(wèi)istener的transitionComplete回調(diào) fireEvent(new TransitionCompleteEventImpl (fromStateId, toStateId, event, context, getThis())); //執(zhí)行StateMachine中聲明的transitionCompleted函數(shù)回調(diào) afterTransitionCompleted(fromStateId, getCurrentState(), event, context); return true; } else { //事件無法被處理 fireEvent(new TransitionDeclinedEventImpl (fromStateId, event, context, getThis())); afterTransitionDeclined(fromStateId, event, context); } } catch (Exception e) { //標(biāo)記statemachine狀態(tài)為ERROR, 不再響應(yīng)事件處理直至恢復(fù) setStatus(StateMachineStatus.ERROR); lastException = (e instanceof TransitionException) ? (TransitionException) e : new TransitionException(e, ErrorCodes.FSM_TRANSITION_ERROR, new Object[]{fromStateId, toStateId, event, context, "UNKNOWN", e.getMessage()}); fireEvent(new TransitionExceptionEventImpl (lastException, fromStateId, localData.read().currentState(), event, context, getThis())); afterTransitionCausedException(fromStateId, toStateId, event, context); } finally { executionService.reset(); fireEvent(new TransitionEndEventImpl (fromStateId, toStateId, event, context, getThis())); //執(zhí)行StateMachine中聲明的transitionEnd函數(shù)回調(diào) afterTransitionEnd(fromStateId, getCurrentState(), event, context); } return false; }
優(yōu)點(diǎn)
代碼寫的不錯(cuò),設(shè)計(jì)域很清晰,測(cè)試case以及項(xiàng)目文檔都比較詳細(xì);
功能該有的都有,支持exit、transition、entry動(dòng)作,狀態(tài)轉(zhuǎn)換過程被細(xì)化為tranistionBegin->exit->transition->entry->transitionComplete->transitionEnd,并且提供了自定義擴(kuò)展機(jī)制,能夠方便的實(shí)現(xiàn)狀態(tài)持久化以及狀態(tài)trace等功能;
StateMachine實(shí)例創(chuàng)建開銷小,設(shè)計(jì)上就不支持單例復(fù)用,因此狀態(tài)機(jī)的本身的生命流管理也更清晰,避免了類似spring statemachine復(fù)用statemachine導(dǎo)致的deadlock之類的問題;
代碼量適中,擴(kuò)展和維護(hù)相對(duì)而言比較容易;
缺點(diǎn)
注解方式定義狀態(tài)轉(zhuǎn)換,不支持自定義狀態(tài)枚舉、事件枚舉;
interceptor的實(shí)現(xiàn)粒度比較粗,如果需要對(duì)特定狀態(tài)的某些切入點(diǎn)進(jìn)行邏輯處理需要在interceptor內(nèi)部進(jìn)行邏輯判斷,例如在transitionEnd后某些狀態(tài)下需要執(zhí)行一些特定action,需要transitionEnd回調(diào)中分別處理;
結(jié)論:
目前項(xiàng)目已經(jīng)使用squirrel-foundation完成改造并上線,后面會(huì)詳細(xì)介紹下項(xiàng)目中是如何落地實(shí)施squirrel-foundation狀態(tài)機(jī)改造以及如何與spring集成的一些細(xì)節(jié);
更多文章請(qǐng)?jiān)L問我的博客
轉(zhuǎn)載請(qǐng)注明出處
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/67215.html
摘要:騰訊云在年底決定開發(fā)容器產(chǎn)品隨后組建容器技術(shù)團(tuán)隊(duì)并進(jìn)行技術(shù)選型通過對(duì)不同編排工具的分析對(duì)比最終選擇作為容器編排引擎并且迅速在年初推出容器解決方案為用戶提供托管的一站式服務(wù)。但是騰訊云最終選擇了現(xiàn)在看來這個(gè)選擇無比正確。Kubernetes 很火,一大批互聯(lián)網(wǎng)公司早已領(lǐng)先一步,搭建起專有的 PaaS平臺(tái),傳統(tǒng)企業(yè)們看到的 Kubernetes的趨勢(shì),亦不甘落后,在試水的道上一路狂奔。雖然,Ku...
摘要:工欲善其事,必先利其器,我們拿什么工具來壓測(cè)呢我們做了很多前期調(diào)研和論證,最終決定基于開發(fā)有贊自己的分布式全鏈路壓測(cè)引擎。 一年以前,有贊準(zhǔn)備在雙十一到來之前對(duì)系統(tǒng)進(jìn)行一次性能摸底,以便提前發(fā)現(xiàn)并解決系統(tǒng)潛在性能問題,好讓系統(tǒng)在雙十一期間可以從容應(yīng)對(duì)劇增的流量。工欲善其事,必先利其器,我們拿什么工具來壓測(cè)呢?我們做了很多前期調(diào)研和論證,最終決定基于 Gatling 開發(fā)有贊自己的分布式...
摘要:是系統(tǒng)提供的容器化技術(shù),簡(jiǎn)稱,它結(jié)合和技術(shù)為用戶提供了更易用的接口來實(shí)現(xiàn)容器化。公司結(jié)合和以下列出的技術(shù)實(shí)現(xiàn)了容器引擎,相比于,具備更加全面的資源控制能力,是一種應(yīng)用級(jí)別的容器引擎。 showImg(https://segmentfault.com/img/bVbtPbG?w=749&h=192); 題外話 最近對(duì)Docker和Kubernetes進(jìn)行了一番學(xué)習(xí),前兩天做了一次技術(shù)...
閱讀 3093·2021-11-22 09:34
閱讀 593·2021-11-22 09:34
閱讀 2437·2021-10-08 10:18
閱讀 3372·2021-09-22 15:57
閱讀 2585·2021-09-22 15:25
閱讀 2398·2019-08-30 15:54
閱讀 2093·2019-08-30 15:44
閱讀 1799·2019-08-29 11:18