商業軟體的開發,大部分都需要有一些為其它模組提供服務的底層模組。這些底層模組由於實現的是一些通用功能,需要同時為幾個高層模組提供功能,因此通常被設計成一種基於訊息佇列的架構。任何需要訪問這些通用功能的高層模組,都可以通過發送訊息並接受傳回值來得到需要的服務。
這種構架的設計,一般是圍繞訊息佇列來展開的:首先有一個訊息佇列,並對外暴露發送訊息的API;然後實現一個負責維護並調度該訊息佇列的線程,該線程負責維護訊息佇列,並分發訊息;最後是一系列處理特定訊息的功能模組。訊息由暴露給外層模組的API發送到訊息佇列,由調度線程接受訊息,並分發給訊息處理模組,然後由處理模組對不同訊息進行處理,將處理結果返回給高層模組,這就是一個完善的基於訊息佇列的公用模組。為了實現這種模組的可擴充性,訊息處理模組一般採取一種基於註冊的設計,允許使用者註冊特定訊息的訊息處理函數。
這種構架從結構上看,是非常完善的,模組功能也比較好劃分,因此應用廣泛。但是根據個人經驗,大部分這種模組的實現,在細節上都會有一些細微的瑕疵,給後期代碼的維護和擴充帶來麻煩,一下是個人遇到過的3中情況。
1. 訊息的定義沒有做到“原子化”。單一的功能被認為拆分成好幾個訊息。比如在一個為其它模組提供資料庫訪問服務的模組,將開啟記錄集,擷取記錄集,關閉記錄集分成了3個訊息,由於這3個訊息在邏輯上有著循序關聯性,而訊息處理並不能很好的保證這種循序關聯性,從而導致擷取記錄集時,記錄集已經被關閉等等類似情況的出現(由於記錄集可能很大,需要分多次擷取,這種設計有時候也是不可避免)。
要處理這種情況,唯一的辦法是保證想辦法保證訊息處理的順序性,比如使用同步訊息。由用戶端確保上一條訊息已經被處理,結果已經被返回的情況下才發送第二條訊息。
2. 主線程接受到退出訊息,開始清理記憶體資源,而訊息處理線程仍然在處理訊息。這種情況的發生一般會導致軟體報非法記憶體訪問錯誤,而且一般是crash問題。
處理這種情況,必須確保主線程wait所有其他線程已經退出後,才開始做清理工作。要做到這一點有時候相當困難。個人遇到過的一種情況是,訊息處理線程由公司其它team的人開發,他們在訊息佇列建立的時候建立一個線程池,該線程池自動調用我傳遞過去的一個回呼函數處理訊息。而且線程池的銷毀工作也在該模組中實現,其結果是在我的模組中,收到系統退出訊息以後,無法準確探測是否所有訊息處理線程已經全部中止,是否已經可以開始做資源清理工作。
實現基於訊息佇列的架構時,必須在設計初期處理好多個線程的調度關係,以及邏輯順序。
3. 如果基於訊息佇列的模組需要訪問資料庫,那麼還有一種更為討厭的情況。在某些資料庫系統中(MS SQL Server),記錄集與交易處理之間的嵌套會導致記錄集被破壞。考慮一種情況,高層模組有如下需求:
a.) 查詢一個資料表。b.) 刪除另外一個表中的2條記錄。
第一個需求可以查分成3個訊息:開啟表,擷取記錄集,關閉表,由於不改變資料庫,不必使用事務。
第二個需求可以簡單打包成一個訊息,由於更改資料庫,需要使用事務機制。
由於訊息處理不能保證順序性,如果以上4條訊息處理順序如下:
開啟表1---- 刪除表2的兩條記錄----擷取表1記錄集中的記錄----關閉表1
第一步操作和第二步操作沒有問題,但是第三步操作,擷取表1記錄集時,某些資料庫將報錯。因為第二步操作使用了事務,而事務會銷毀同一個資料庫連接上以前開啟的記錄集,具體可以參考微軟的文檔:
http://support.microsoft.com/default.aspx?scid=kb;en-us;187942
處理這種情況,一種解決方案是使用微軟建議的方式,使用client side cursor,不過這會大大降低資料庫訪問效能。
另一種解決方案是讓查詢和事務操作使用不同的資料庫連接,這需要在設計初期進行明確的定義。