繼上篇《GGTalk 開源即時通訊系統原始碼剖析之:伺服器端全域性快取》詳細介紹了 GGTalk 對需要頻繁查詢資料庫的資料做了伺服器端全域性快取處理,以降低資料庫的讀取壓力以及加快使用者端請求的響應,接下來我們將進入GGTalk伺服器端的虛擬資料庫。
GGTalk V8.0 除了支援真實的資料庫外,還內建了虛擬的資料庫,僅僅通過一行設定便可以啟動虛擬的資料庫,無需部署真實資料庫便能體驗GGTalk的全部功能。若只是需要做簡單的演示,這將極大地簡化伺服器端的部署過程,使得伺服器端能立即執行起來。
這篇文章將會詳細的介紹GGTalk虛擬資料庫的設計和實現。還沒有GGTalk原始碼的朋友,可以到 GGTalk原始碼下載中心 下載。
為了方便大家能夠快速、零成本地將 GGTalk 執行起來,並且體驗GGTalk的全部功能,GGTalk伺服器端在記憶體中內建了一個虛擬的資料庫可以替代真實的資料庫以方便測試。接下來將會介紹如何啟用虛擬資料庫執行GGTalk。
首先找到 GGTalk.Server 目錄下的 App.config 檔案。
在 App.config 組態檔中,找到關於UserVirtualDB
的設定,將其值修改為true
,如上圖所示。
在修改完伺服器端組態檔後,啟動伺服器端程式,如此,伺服器端使用的就是記憶體中的虛擬資料庫。
若能看到這個視窗彈出,則代表伺服器端程式執行成功。
注意:由於伺服器端使用的是在記憶體中模擬出來的虛擬資料庫,故伺服器端退出時,記憶體將被釋放,虛擬資料庫中的一切資料都會被清除。
在電腦科學中有一句經典名言:
電腦科學領域內的任何問題,都可以通過增加一個間接的中間層來解決。
在物件導向設計(OOP)中,這句名言所表達的含義通常是通過抽象出一個介面(interface)來完成的。
基於這份瞭解,為了能切換真實資料庫與虛擬資料庫,我們將資料庫存取層抽象為一個介面 IDBPersister,在伺服器端所有存取資料庫的地方都通過呼叫 IDBPersister 介面來實現。
真實資料庫存取 DBPersister 和虛擬資料庫 MemoryPersister 都實現 IDBPersister 介面,這樣一來,在程式啟動的時候,就可以自由決定是使用 DBPersister 還是 MemoryPersister 了。
關於這部分的程式碼位於
GGTalk/GGTalk.Server/MemoryPersister.cs
。
虛擬資料庫的設計原理很簡單,接下來我們看看其具體是如何實現的。
MemoryPersister 類實現了GGTalk伺服器端中的虛擬資料庫,讓我們來看看它到底是如何實現的吧。
public class MemoryPersister : OfflineMemoryCache, IDBPersisterExtend {
//...
private ObjectManager<string, GGUser> userManager = new ObjectManager<string, GGUser>();
private ObjectManager<string, GGGroup> groupManager = new ObjectManager<string, GGGroup>();
//string : requesterID + "-" + accepterID
private ObjectManager<string, AddFriendRequest> addFriendRequestManager = new ObjectManager<string, AddFriendRequest>();
//string : requesterID + "-" + groupID
private ObjectManager<string, AddGroupRequest> addGroupRequestManager = new ObjectManager<string, AddGroupRequest>();
//string : groupID + "-" + userID
private ObjectManager<string, GroupBan> groupBanManager = new ObjectManager<string, GroupBan>();
//...
}
以上是就MemoryPersister類的部分定義,也是實現虛擬資料庫的核心內容。可以觀察到,這個類繼承了OfflineMemoryCache類,同時還實現了IDBPersisterExtend介面。OfflineMemoryCache類的作用是用於在記憶體中儲存離線訊息和離線檔案條目,這個不是本篇文章關注的重點,我們重點來看一下IDBPersisterExtend介面,以下是關於這個介面的定義:
public interface IDBPersisterExtend: IDBPersister<GGUser, GGGroup> {
/// <summary>
/// 根據使用者ID獲取其手機號
/// </summary>
/// <param name="userID"></param>
/// <returns></returns>
string GetPhone4UserID(string userID);
/// <summary>
/// 更新使用者手機號
/// </summary>
/// <param name="userID"></param>
/// <param name="phone"></param>
void UpdateUserPhone(string userID, string phone, int version);
}
我們可以發現,這個介面實現了IDBPersister<GGUser, GGGroup>
介面,再分析一下這個介面的命名,我們很容易就知道這個介面僅僅是對IDBPersister介面的一個拓展,因此我們繼續分析IDBPersister介面,這個介面定義了大量運算元據庫的方法。現在讓我們把思緒收回來,也就是說MemoryPersister類最終實現了IDBPersister<GGUser, GGGroup>介面。因此,在MemoryPersister類也將會存在大量運算元據庫的方法,如下圖所示:
結果很顯然,MemoryPersister 類中實現了很多運算元據庫的方法,等等,到這裡只能說明 MemoryPersister 類僅僅只是實現了IDBPersister<GGUser, GGGroup>介面
,僅僅只是約定了方法的名字、方法引數和返回值,內部的實現一定是運算元據庫嗎?接下來讓我們隨便點開一個方法看看具體實現:
很容易看出,這是一個更新使用者資訊的方法,方法接收一系列和使用者有關的欄位,然後從userManager上呼叫 Get 方法,似乎返回了一個包含使用者資訊的物件,型別是 GGUser。然後更新這個物件的資訊。很好,現在讓我們把關注點放在這個userManager
上,明白了它代表什麼,那麼就能知道這個方法究竟是不是在運算元據庫了。來到它的定義的地方:
可以發現,它是MemoryPersister類內部的一個私有欄位,型別為ObjectManager<string, GGUser>
,ObjectManager 是對 Dictionary 的二次封裝,支援多執行緒安全,相比Dictionary,使用起來也更方便。
到這裡,首先我們明白,上述的UpdateUserInfo
方法和資料庫一點關係都沒有。而這個類的作用是將資料儲存到記憶體中,如果你有了解《GGTalk 開源即時通訊系統原始碼剖析之:伺服器端全域性快取》的話,你會發現 ObjectManager 也正是實現伺服器端快取的那個類。而GGTalk虛擬資料庫中的資料也是通過這個類的範例來儲存的。
而這裡userManager
其實就是用來儲存使用者資料和操作使用者資料。一個東西,它既能儲存資料,也能運算元據,那這個東西的作用是不是和資料庫很類似呢?沒錯,這正是GGTalk虛擬資料庫的設計,將資料儲存在記憶體中,並且定義了一系列能夠運算元據的方法(有沒有感覺像sql語句)。
現在讓我們再來回顧 MemoryPersister 類的定義:
public class MemoryPersister : OfflineMemoryCache, IDBPersisterExtend {
//...
private ObjectManager<string, GGUser> userManager = new ObjectManager<string, GGUser>();
private ObjectManager<string, GGGroup> groupManager = new ObjectManager<string, GGGroup>();
//string : requesterID + "-" + accepterID
private ObjectManager<string, AddFriendRequest> addFriendRequestManager = new ObjectManager<string, AddFriendRequest>();
//string : requesterID + "-" + groupID
private ObjectManager<string, AddGroupRequest> addGroupRequestManager = new ObjectManager<string, AddGroupRequest>();
//string : groupID + "-" + userID
private ObjectManager<string, GroupBan> groupBanManager = new ObjectManager<string, GroupBan>();
//...
}
在瞭解GGTalk虛擬資料庫的設計後,現在再來看就很清晰了,各個欄位的作用如下所示:
userManager
:用於管理使用者資料的範例物件,內含資料和運算元據的方法;groupManager
:用於管理群組資料的範例物件,內含資料和運算元據的方法;addFriendRequestManager
:用於管理新增好友請求資料的範例物件,內含資料和運算元據的方法;addGroupRequestManager
:用於管理新增群組請求資料的範例物件,內含資料和運算元據的方法;groupBanManager
:用於管理群組禁言資料的範例物件,內含資料和運算元據的方法。這些欄位便是GGTalk虛擬資料庫的核心實現了。
在瞭解完GGTalk虛擬資料庫的核心實現後,現在我們來看一看GGTalk虛擬資料庫是在哪裡進行初始化的:
在MemoryPersister類別建構函式中,我們可以發現,userManager欄位中被插入了11個使用者的資料,groupManager欄位中被插入了兩個群組的資料。接下來我們再來到MemoryPersister範例化的地方:
我們分析這段程式,首先定義了一個persister 變數
,然後去讀取專案的App.config組態檔中的UseVirtualDB 設定
,若是為true,則將persister變數賦值為MemoryPersister類的範例。
其實這段程式的意思就是通過讀取組態檔來決定是否啟用虛擬資料庫,而這個 UseVirtualDB設定 則是是否啟用虛擬資料庫的關鍵設定,也是文章開頭為了啟用虛擬資料庫而修改的那個設定。到了這裡,是不是豁然開朗了呢。
本篇建議結合 GGTalk的原始碼 進行閱讀,為了方便大家理解GGTalk虛擬資料庫的設計思路,本文刻意沒有將比較複雜的部分進行展開講解,同時也是為了控制文章篇幅,例如 MemoryPersister 類中的 messageRecordMemoryCache 欄位,此欄位的作用是在記憶體中儲存聊天記錄,對其有興趣的可以看看它的設計和實現。最後,希望本篇文章對你有所幫助。
在接下來的一篇我們將介紹GGTalk使用者端的全域性快取以及使用者端本地儲存。
敬請期待:《GGTalk 開源即時通訊系統原始碼剖析之:使用者端全域性快取及本地儲存》