異步任務串行解決方案

問題描述

在業務開發時,有時不僅僅是拉取一個數據接口展示列表這麼簡單。舉一個購買場景:

  • 第一步調用網絡接口登錄
  • 第二步調用網絡接口購買
  • 第三步查詢購買結果
  • 第四步調用DBApi將購買結果寫入本地
  • 第五步向外回調結果

這裏所有的操作都是異步的,再舉一個聊天業務場景的例子。當收到有新消息通知。需要拉取獲取新消息的網絡接口以獲得新消息。有這樣幾步:

  • 拉取本地DBApi查詢本地保持的最大消息Id,以此作爲查詢新消息的參數
  • 拉取查詢新消息的網絡接口
  • 將查詢到的消息數據保存在DB
  • 保存後向外回調

這裏每一步都是異步的,同時上一步的結果將作爲下一步的入參。這就是典型的異步任務串行的場景。其實Rxjava提供了這種場景的解決方案,今天我們試着自己寫一套解決該問題的方案。

思考

方案一

這裏說到底就是一個任務分發的問題,我們可以建立一個TaskCenter。在每一個異步任務回調中,將異步任務的結果和標記自身的tag。轉發給TaskCenter,TaskCenter根據tag和結果來創建下一個異步任務。並執行。這種方案可能是我們最容易想到的方案,也是代碼實現上最簡單的方案。類似於中介者模式。但是這種方式也有如下缺點

  • TaskCenter中承載了所有業務的邏輯,像是一團線中打的結。所有的任務都需要通過它尋找下一個任務節點
  • Task類過於多,每一個任務,我們都需要寫一個類。來承載他的邏輯。當業務複雜時,需要創建很多Task類

圖示如下:

image

方案二

我們上述提到,每一個任務就是一個節點,一個節點執行完去尋找下一個節點。那麼我們可以想到一種常用的數據結構:鏈表。每一個業務場景都是一個任務鏈表,一個節點一個節點執行。直到執行到鏈表的尾端。當我們編寫一個業務場景時,首先確立好有哪幾個步驟,接着創建這樣一個鏈表,最後開始執行。優點:

  • 每個業務場景都是一個任務鏈,各場景間不會有代碼邏輯上耦合
  • 代碼可讀性增強,很清晰的能看到業務場景的每一步都是做什麼

圖示如下

image

代碼設計

這裏我們實現方案二,我們需要解決如下幾個問題,代碼也就寫出來了

  • 鏈表建立及節點插入
  • 啓動鏈表執行第一個節點任務
  • 任務結果傳遞給下一個節點並啓動節點任務
  • 線程調度

這裏我們給出設計類圖,再依次介紹每個類的作用

image

AsyncTaskNode

任務節點,其中會維護下一節點的指針,並定義抽象方法doLogic()由具體任務實現
。action方法提供給任務鏈AsyncTaskChain調用,action方法負責將doLogic()拋給線程調度器處理或直接執行doLogic方法

AsyncTaskChain

任務鏈,其中維護了鏈表的頭結點、尾節點及當前節點。提供插入節點的doNext()方法及移動當前節點指針的goNext()方法。goNext()方法中會調用節點的action方法並將當前指針向前移動

BaseCallback

實現Callback接口中onResult()方法,其中引用任務鏈。在onResult()方法中調用任務鏈的doNext()方法向前移動指針。

SingleThreadExexutor

封裝線程及隊列,提供schedule()方法向任務隊列裏面插入任務。作爲線程調度器。

代碼實現

這裏我直接貼出代碼實現

AsyncTaskNode

public abstract class AsyncTaskNode implements SingleThreadExecutor.Callable {
    public String tag;
    protected AsyncTaskNode next;

    private SingleThreadExecutor executor;
    private Object param;

    public AsyncTaskNode(String tag) {
        this.tag = tag;
    }

    public AsyncTaskNode(String tag, SingleThreadExecutor executor) {
        this.tag = tag;
        this.executor = executor;
    }

    abstract void doLogic(Object o);

    protected void action(Object o) {
        this.param = o;
        if (executor != null) {
            //若線程調度器不爲空,將任務交給調度器調度執行
            executor.schedule(this);
        } else {
            //否則直接執行邏輯
            doLogic(o);
        }
    }

    public Object getParam() {
        return param;
    }

    @Override
    public void call() {
        doLogic(param);
    }
}

AsyncTaskChain

public class AsyncTaskChain {
    public AsyncTaskNode head;
    public AsyncTaskNode tail;
    public AsyncTaskNode current;

    public void startTask() {
        current = head.next;
        if (head != null) {
            head.action(null);
        }
    }

    public AsyncTaskChain doNext(AsyncTaskNode task) {
        if (head == null) {
            head = task;
            tail = task;
            current = head;
        } else {
            tail.next = task;
            tail = tail.next;
        }
        return this;
    }

    public void goNext(Object o) {
        if (current != null) {
            System.out.println("goNext current:" + current.tag + "current thread:" + Thread.currentThread().getName());
            current.action(o);
            current = current.next;
        }

    }
}

SingleThreadExecutor

public class SingleThreadExecutor {
    private String name;
    private Thread thread;
    private LinkedBlockingQueue<Callable> blockingQueue;
    private AtomicBoolean stopFlag;


    public SingleThreadExecutor(String name) {
        this.name = name;
        stopFlag = new AtomicBoolean();
        stopFlag.set(false);
        blockingQueue = new LinkedBlockingQueue<>();
        thread = new Thread(this::loopQueue);
        thread.setName(name);
        thread.start();
    }

    private void loopQueue() {
        while (blockingQueue != null && !stopFlag.get() && thread != null && (!thread.isInterrupted())) {
            Callable task = null;
            try {
                task = blockingQueue.take();
                task.call();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }

    public void schedule(Callable callable) {
        if (blockingQueue != null && (!stopFlag.get())) {
            blockingQueue.offer(callable);
        }
    }

    public void destroy() {
        stopFlag.set(true);
        blockingQueue.clear();
        thread.interrupt();
        thread = null;
    }

    public interface Callable {
        void call();
    }
}

Callback

public interface Callback {
    void onResult(Object o);
}

BaseCallback

public class BaseCallback implements Callback {
    private AsyncTaskChain chain;

    public BaseCallback(AsyncTaskChain chain) {
        this.chain = chain;
    }

    @Override
    public void onResult(Object o) {
        if (chain != null) {
            chain.goNext(o);
        }
    }
}

測試代碼

NetMission

該類提供模擬異步的網絡方法

public class NetMission {
    public static void doLogin(String phone, Callback callBack) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("doLogin: params = [" + phone + "]");
                callBack.onResult("login result");
            }
        }).start();

    }

    public static void doBuy(String str, Callback callBack) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("doBuy params = [" + str + "]");
                callBack.onResult("dobuy result");
            }
        }).start();

    }

    public static void queryResult(String str, Callback callBack) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("doQueryResult params = [" + str + "]");
                callBack.onResult("queryResult result");
            }
        }).start();

    }
}

測試類

public class Test {
    public static void main(String[] args) {
        //不使用線程調度器
        AsyncTaskChain taskChain1 = new AsyncTaskChain();
        taskChain1
                .doNext(new AsyncTaskNode("LoginTask") {
                    @Override
                    void doLogic(Object o) {
                        NetMission.doLogin("login params", new BaseCallback(taskChain1));
                    }
                })
                .doNext(new AsyncTaskNode("BuyTask") {
                    @Override
                    void doLogic(Object o) {
                        NetMission.doBuy((String) o, new BaseCallback(taskChain1));
                    }
                })
                .doNext(new AsyncTaskNode("third") {
                    @Override
                    void doLogic(Object o) {
                        NetMission.queryResult((String) o, new BaseCallback(taskChain1));
                    }
                }).startTask();

        //使用線程調度器
        SingleThreadExecutor businessQueue = new SingleThreadExecutor("BusinessQueue");
        SingleThreadExecutor callBackQueue = new SingleThreadExecutor("CallbackQueue");

        AsyncTaskChain chain = new AsyncTaskChain();
        chain
                .doNext(new AsyncTaskNode("LoginTask", businessQueue) {
                    @Override
                    void doLogic(Object o) {
                        System.out.println("goLogic:doLogin current thread:" + Thread.currentThread().getName());
                        NetMission.doLogin("login params", new BaseCallback(chain));
                    }
                })
                .doNext(new AsyncTaskNode("BuyTask", businessQueue) {
                    @Override
                    void doLogic(Object o) {
                        System.out.println("goLogic:doBuy current thread:" + Thread.currentThread().getName());
                        NetMission.doBuy((String) o, new BaseCallback(chain));
                    }
                })
                .doNext(new AsyncTaskNode("QueryResultTask", businessQueue) {
                    @Override
                    void doLogic(Object o) {
                        System.out.println("goLogic:doQueryResult current thread:" + Thread.currentThread().getName());
                        NetMission.queryResult((String) o, new BaseCallback(chain));
                    }
                })
                .doNext(new AsyncTaskNode("CallbackTask", callBackQueue) {
                    @Override
                    void doLogic(Object o) {
                        System.out.println("goLogic:Callback current thread:" + Thread.currentThread().getName());
                        System.out.println("callBack result:" + o);
                    }
                })
                .startTask();
    }
}

打印結果

goLogic:doLogin current thread:BusinessQueue
doLogin: params = [login params]
goNext current:BuyTaskcurrent thread:Thread-2
goLogic:doBuy current thread:BusinessQueue
doBuy params = [login result]
goNext current:QueryResultTaskcurrent thread:Thread-3
goLogic:doQueryResult current thread:BusinessQueue
doQueryResult params = [dobuy result]
goNext current:CallbackTaskcurrent thread:Thread-4
goLogic:Callback current thread:CallbackQueue
callBack result:queryResult result

可以看到順序調用了異步任務,並且上一個任務的結果作爲下一個任務的參數,把任務鏈串聯起來。這裏我們需要關注一個問題,那就是線程調度。可以看到dologic()方法是在我們創建AsyncTaskNode時指定的線程調度器中執行。但是,dologic()中可能調用到其他組件,例如網路API,數據庫API。那麼代碼就跳入了其他組件的工作線程中執行(例如log中的Thread-2/Thread-3),那麼調用其他組件時傳入的callback的onResult()方法自然也在其他組件的工作線程執行。我們需要保證下一個任務的doLogic()方法跳回指定的線程調度器中執行,如何做到這一點呢?節點的action()方法中將自己拋給線程調度器,線程調度器會在輪詢到任務時,調用其doLogic()方法。保證了線程的可靠性。

結語

最終leader並沒有採用我的方案,原因如下

  • 1、沒必要自己維護鏈表,採用Java提供的隊列即可實現順序功能

  • 2、沒必要自己維護線程隊列,採用Java單線程池即可

  • 3、最嚴重的問題,也是我沒有考慮到的問題:訪問鏈表goNext()是不同線程訪問的。這樣有可能導致線程安全問題,但實際上不會,因爲雖然是不同線程訪問,但是是順序訪問。

最後,我也對自己的設計進行了反思,會針對leader提出的問題重寫設計,儘量使用Java jdk中組件。希望能對大家在解決異步任務串行問題時有所幫助。

轉載請註明出處

https://blog.csdn.net/u012545728/article/details/88708344

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章