1、消息隊列需提供哪些功能?
在功能設計上,我崇尚奧卡姆剃刀法則。
對於消息隊列,只需要兩個方法: 生產 和 消費。
具體的業務場景是任務隊列,代碼設計如下:
public abstract class TaskQueue{
private final String name ;
public String getName(){return this.name;}
public abstract void addTask(Serializable taskId);
public abstract Serializable popTask();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
同時支持多個隊列,每個隊列都應該有個名字。final確保TaskQueue是線程安全的。TaskQueue的實現類也應該確保線程安全。
addTask向隊列中添加一個任務。隊列中僅保存任務的id,不存儲任務的業務數據。
popTask從隊列中取出一個任務來執行。
這種設計不是特別友好,因爲她需要調用者自行保證任務執行成功,如果執行失敗,自行確保重新把任務放回隊列。 無論如何,這種機制是可以工作的。想想奧卡姆剃刀法則,我們先按照這個設計實現出來看看。
如果調用者把業務數據存在數據庫中,業務數據中包含“狀態“列,標識任務是否被執行,調用者需要自行管理這個狀態,並控制事務。
popTask採用阻塞方式,還是非阻塞方式呢?
如果採用阻塞方式,隊列中沒任務的時候,客戶端不會斷開連接,只是等。
一般情況下,客戶端會有多個worker搶着幹活兒,幾條狼一起等一個肉包子,畫面太美。連接是重要資源,如果一直沒活兒幹,先放回池裏,也不錯。
先採用非阻塞的方式吧,如果隊列是空的,popTask返回null,立即返回。
2、後續可能提供的功能
2.1、引入Task生命週期概念
應用場景不同,需求也不同。
在嚴格的應用場景中,需要確保每個Task執行“成功“了。
對於上面提到的popTask後不管的“模式“,這是另外一種“運行模式“,兩種模式可以並行存在。
在這種新模式下,Task狀態有3種:新創建(new,剛調用addTask加到隊列中)、正在執行(in-process,調用popTask後,調用finish前)、完成(done,執行OK了,調用finishTask後)。
調整後的代碼如下:
public abstract class TaskQueue{
private final String name ;
public String getName(){return this.name;}
public abstract int getMode();
public abstract void addTask(Serializable taskId);
public abstract Serializable popTask();
public abstract void finishTask(Serializable taskId);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
2.2、增加批量取出任務的功能
popTask()一次取出一個任務,太磨嘰了。
好比我們要買5瓶水,開車去超市買,每去一次買1瓶,有點兒啥。
我們需要一個一次取多個任務的方法。
public abstract class TaskQueue{
... ...
public abstract Serializable[] popTasks(long cnt);
}
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
2.3、增加阻塞等待機制
想象一種場景:
小明同學,取出一個任務,發現幹不了,放回隊列,再去取,取出來發現還是幹不了,又放回去。反反覆覆。
小明童鞋腫麼了?可能是他幹活需要網絡,網絡斷了。可能是他做任務需要寫磁盤,磁盤滿了。
如果小明像鄰居家的孩子一樣優秀,當他發現哪裏不對的時候,他應該冷靜下來,歇會兒。
但他萬一不是呢?只有我們能幫他了。
假如隊列中有10000個待辦任務。
這時候小明來了。他失敗100次後,我們應該攔他嗎?不應該,除非他主動要求(在系統參數中配置)。5000次後呢?也不應該,除非他主動要求。我們的原則是:我們做的所有事情,對於調用者,都是可以預期的。
我們可以在系統參數中要求調用者設置一個閥值N,如果不設置,默認爲100。連續失敗N次後,讓調用者睡一會兒,睡多長時間,讓調用者配置。
假如我們的底層實現中包含待辦子隊列、重做子隊列和完成子隊列(這種設計好複雜!pop的時候先pop重做,還是先pop待辦,複雜死了!但願不需要這樣)。
待辦子隊列中有10000個任務。
在小明失敗10000次後,所有的任務都在重做子隊列了。這時候我們應該攔他嗎?
重做子隊列要不要設置大小,超過之後,讓下一個訪問者等。
等的話就會涉及超時,超時後,任務也不能丟棄。
太複雜 了!設置一個連續失敗次數的限制就夠了!
2.4、考慮增加Task類
不保存任務的相關數據是基本原則,絕對不動搖。
增加Task類可以管理下生命週期,更有用的是,可以把Task本身設計成Listener,代碼大概時這樣的:
public abstract class Task{
public Serializable getId();
public int getState();
pubic void doTask();
public void whenAdded(final TaskQueue tq);
public void whenPoped(final TaskQueue tq);
// public void whenFaild(final TaskQueue tq);
public void whenFinished(final TaskQueue tq);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
通過Task接口,我們可以對調用過程進行更強勢的管理(如進行事務控制),對調用者施加更強的控制,用戶也可以獲得更多的交互機會,同TaskQueue有更好的交互(如在whenFinished中做持久化工作)。
但這些真的有必要嗎?是不是太侵入了?註解的方式會好些嗎?
再考慮吧。
2.5、增加系統參數
貌似需要個Config類了,不爽!
本來想做一個很小很精緻的小東西的,如果必須再加吧。
如果做的話,需要支持properties、註解設置、api方式設置、spring注入式設置,煩。
次回預告:Redis本身機制和TaskQueue的契合。