問題描述
在業務開發時,有時不僅僅是拉取一個數據接口展示列表這麼簡單。舉一個購買場景:
- 第一步調用網絡接口登錄
- 第二步調用網絡接口購買
- 第三步查詢購買結果
- 第四步調用DBApi將購買結果寫入本地
- 第五步向外回調結果
這裏所有的操作都是異步的,再舉一個聊天業務場景的例子。當收到有新消息通知。需要拉取獲取新消息的網絡接口以獲得新消息。有這樣幾步:
- 拉取本地DBApi查詢本地保持的最大消息Id,以此作爲查詢新消息的參數
- 拉取查詢新消息的網絡接口
- 將查詢到的消息數據保存在DB
- 保存後向外回調
這裏每一步都是異步的,同時上一步的結果將作爲下一步的入參。這就是典型的異步任務串行的場景。其實Rxjava提供了這種場景的解決方案,今天我們試着自己寫一套解決該問題的方案。
思考
方案一
這裏說到底就是一個任務分發的問題,我們可以建立一個TaskCenter。在每一個異步任務回調中,將異步任務的結果和標記自身的tag。轉發給TaskCenter,TaskCenter根據tag和結果來創建下一個異步任務。並執行。這種方案可能是我們最容易想到的方案,也是代碼實現上最簡單的方案。類似於中介者模式。但是這種方式也有如下缺點
- TaskCenter中承載了所有業務的邏輯,像是一團線中打的結。所有的任務都需要通過它尋找下一個任務節點
- Task類過於多,每一個任務,我們都需要寫一個類。來承載他的邏輯。當業務複雜時,需要創建很多Task類
圖示如下:
方案二
我們上述提到,每一個任務就是一個節點,一個節點執行完去尋找下一個節點。那麼我們可以想到一種常用的數據結構:鏈表。每一個業務場景都是一個任務鏈表,一個節點一個節點執行。直到執行到鏈表的尾端。當我們編寫一個業務場景時,首先確立好有哪幾個步驟,接着創建這樣一個鏈表,最後開始執行。優點:
- 每個業務場景都是一個任務鏈,各場景間不會有代碼邏輯上耦合
- 代碼可讀性增強,很清晰的能看到業務場景的每一步都是做什麼
圖示如下
代碼設計
這裏我們實現方案二,我們需要解決如下幾個問題,代碼也就寫出來了
- 鏈表建立及節點插入
- 啓動鏈表執行第一個節點任務
- 任務結果傳遞給下一個節點並啓動節點任務
- 線程調度
這裏我們給出設計類圖,再依次介紹每個類的作用
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中組件。希望能對大家在解決異步任務串行問題時有所幫助。