基於Redis實現分佈式消息隊列(2)

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的契合。

發佈了20 篇原創文章 · 獲贊 13 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章