項目地址 : https://github.com/kelin-xycs/ShareMemoryhtml
一個用 C# 實現的 No Sql 數據庫 , 也能夠說是 分佈式 緩存 , 用於做爲 集羣 的 共享內存git
ShareMemory 是 一個用 C# 實現的 No Sql 數據庫 , 也能夠說是 分佈式 緩存 , 用於做爲 集羣 的 共享內存 。github
構建 集羣 的 關鍵是 共享內存 。 ShareMemory 能夠做爲 集羣 的 共享內存 , 幫助 構建 集羣 , 這是 ShareMemory 的 第一 設計目標 。數據庫
事實上 , 在過去的 十幾年間 , 利用 分佈式緩存 來做爲 共享內存 構建 Web 集羣 , 已經成爲 事實上 的 作法 。數組
ShareMemory 設計目標中支持的 集羣 包含 Web 集羣 , 分佈式並行計算集羣 等。緩存
ShareMemory 支持 2 種 數據結構 : 字典(Dictionary) 隊列(Queue) 。安全
支持 6 大類 數據類型 : Value Type , string , Simple Object , Value Type 數組 , string 數組 , Simple Object 數組 。服務器
能夠將這 6 大類 數據類型 存放到 ShareMemory 。數據結構
Simple Object 是指 屬性(Property)和 字段(Field) 類型 是 Value Type , string 的 對象 , 簡單的說 , 不支持 對象嵌套 。 這部分會在下面 序列化 的 部分詳細介紹 。多線程
ShareMemory 提供的 數據操做 都是 原子操做 , 是 線程安全 的 。
解決方案 中 包含 6 個 項目:
Client : 用於 Demo 的 Client
Server : 用於 Demo 的 Server
ShareMemory : ShareMemory 核心庫 , 用於 Server 端
ShareMemory.Client : ShareMemory 客戶端庫 , 用於 Client 端
ShareMemory.Serialization : ShareMemory 序列化庫 , 用於 序列化
Test : 用於測試 ShareMemory.Serialization 的 測試項目
ShareMemory 服務器端 在 App.config 中配置 字典 和 隊列,在 AppSettings 中 經過 「ShareMemory.Dics」 和 「ShareMemory.Queues」 2 個 key 來配置 字典 和 隊列 , 如
add key="ShareMemory.Dics" value="Dic1, Dic2, Dic3"
add key="ShareMemory.Queues" value="Queue1, Queue2, Queue3"
Dic1 Dic2 Dic3 表示要建立的 字典 , Queue1 Queue2 Queue3 表示要建立的 隊列 , 字典名 隊列名之間用 逗號 「,」 隔開 。 這樣 ShareMemory Host 在啓動時會建立 Dic1 Dic2 Dic3 3 個 字典 , 和 Queue1 Queue2 Queue3 3 個 隊列 。
ShareMemory 客戶端 經過 ShareMemory.Client 庫 提供的 Helper 類 , Dic 類 , Q 類 來 訪問 ShareMemory 服務器端 。
Helper類提供 GetDic() 方法 , 返回 Dic 對象 。 和 GetQ() 方法,返回 Q 對象 。
Dic 提供 Set(key, value) 方法 , Get(key) 方法 , TryGet(key out value) 方法 , Remove(key) 方法 。 Set() 方法 新增鍵值對 或者 修改鍵值對的值 , 若是 鍵值對 不存在,則新增鍵值對,若是鍵值對已存在,則更新值 。 Get() 方法從 Dic 取得值 , 對於 引用類型 , 若是 鍵值對 不存在 , 則返回 null 。 TryGet() 方法也是從 Dic 取得值,經過 out value 參數返回, 若 鍵值對 不存在,則 Get() 方法返回值爲 false 。 TryGet() 方法是對 Value Type 設計的 , 由於 Value Type 不能根據返回值爲 null 來判斷鍵值對在 Dic 中是否存在 。 Remove() 方法 移除 鍵值對 , 若是 鍵值對 不存在 , 也不會報錯 。
Q 提供 En() 方法 , De , TryDe(out value) 方法 。 En() 方法將 對象 放入 隊列 , De() 方法從 隊列 取出對象 , 對於 引用類型 , 若返回 null , 表示 隊列 爲空 。 TryDe() 方法也是從 隊列 取出 對象 , 經過 out value 參數返回 , 若 隊列 爲空 , TryDe() 方法返回值爲 false 。 TryDe() 方法是對 Value Type 設計的 , 由於 Value Type 不能根據返回值爲 null 來判斷 隊列 是否爲空 。
接下來 說明 一下 序列化 的 格式 :
序列化由 ShareMemory.Serialization 項目完成 , 序列化格式 是 這樣的 :
好比 , 有一個 Simple Object , 包含有 1 個 int A 屬性 , 1 個 string B 字段 , A = 2 , B = "Hello" , 那麼 , 序列化產生這樣一個字符串 :
「o 1 A1 21 B5 Hello」
把 這個 字符串 經過 Encoding.Utf8 轉成 byte 數組 , 就是 序列化 的 結果 了 。
這個 字符串 的 開頭 是 「o」 , 這表示 Simple Object 對象 , 後面跟着 一個 空格 , 空格 後面 的 「1」 表示 接下來 的 數據長度 是 1 個 字符 。 這個數據就是 後面的 「A」 , 這表示 A 屬性的 屬性名 , 「A」 後面 有一個 「1」 , 這表示 下一項 的 數據長度 是 1 , 這個數據就是 後面的 「2」 , 這是 A 屬性的 值 , 以此類推 , 「2」 後面 緊跟着 的 「1」 是 下一項 的 數據長度 , 這個數據就是 「B」 字符 , 這表示 B 屬性的 屬性名 , 「B」 後面的 「5」 表示 下一項 的 數據長度 , 這個數據就是 「Hello」 。 這樣就完成了 對 這個 Simple Object 的 序列化 。
你們能夠看到 , 對於 int 類型 , 序列化 的 方式 是 ToString() , 對於 string , 就是 string 自己 , 實際上 , 目前 除了 DateTime 外 , 其它的 Value Type 都是 以 ToString() 的方式來 序列化 , DateTime 是 取 Ticks 屬性 , 固然 string 就是 string 自己 。
若是是 單獨 序列化 一個 Value Type 的值 , 好比 int a = 2; 那麼 就是 「1 2」 這樣一個 字符串 , 如上所述 , 「1」 表示 數據長度 , 「2」 表示 數據值 。
對於 數組類型 , 舉個例子 , 假設 有一個 數組 , 放了 2 個 Simple Object 對象 , 這個 Simple Object 對象 就是 上面說的 那個 , 那 序列化 後的 字符串 是 這樣的 :
「a 2 18 o 1 A1 21 B5 Hello18 o 1 A1 21 B5 Hello」
第一個字符 「a」 表示 數組 , 這是 固定的 , 後面跟一個 空格 , 這也是固定的 。 空格後面是 「2」 , 表示 數組長度 , 即 數組元素 的 個數 。 「2」 後面跟一個 空格 , 這也是固定的 , 空格後面是 「18」 , 表示 接下來 的 元素 的 長度 , 「18」 後面跟一個 空格 , 這也是固定的 。 空格以後 就是 元素 的 內容 。 這個內容 , 就是上面咱們講過的 Simple Object 序列化以後的 字符串 , 這個 字符串 長度是 18 , 前面的 「18」 就是指這個 。 以此類推 , 第一個元素結束以後 , 又是一個 「18」 , 這個 18 是指 第二個元素的長度 , 「18」 後面是空格 , 空格 以後 就是 第二個元素 序列化以後的 字符串 。
Value Type 數組 , string 數組 的 原理 都包含在 上述 裏了 , 就不具體舉例了 。
總之 , ShareMemory.Serialization 能夠支持 6 大類 數據類型 的 序列化 : Value Type , string , Simple Object , Walue Type 數組 , string 數組 , Simple Object 數組 。
能夠實際到 項目 裏 運行 看一下 效果 就比較清楚了 。 ^ ^
ShareMemory.Serialization 只會序列化 公有 的 屬性 和 字段 , 而且須要在要序列化的 屬性 和 字段 上 加上 [ S ] 標記 。
ShareMemory.Serialization 並不要求 序列化方 和 反序列化方 的 對象定義 在 語法 上 徹底一致 , 好比 序列化方 的 對象 有一個 A 屬性 , 反序列化方 能夠用一個 A 字段 來 接收 A 屬性 的 值 , 只要 二者 的 名字 相同就行 。
ShareMemory.Serialization 能夠做爲一個 序列化庫 單獨使用 。
ShareMemory 沒有提供 對 數據操做 的 鎖 機制(Lock) , 由於 對 數據 的 鎖機制 邏輯 比較 複雜 。 那麼 , 多個 客戶端 線程 之間 怎麼進行 通訊協做 呢 ? ShareMemory 提供了 與 數據無關 的 鎖機制 。 Helper類 提供了 TryLock(lockName) 方法 和 UnLock(lockName, lockId) 方法 。 TryLock() 用來獲取 鎖 , 參數 lockName 是 鎖 的 名字 , 參與協做 的 線程 間 能夠 約定 一個 鎖 的 名字 來 通訊 。 TryLock() 方法 的 返回值 是 lockId , 用來 標識 1 次 Lock , 由於同一個 名字 的 鎖 可能會屢次 Lock 和 UnLock 。 UnLock 的時候須要 傳入 lockId 參數 。 若是 TryLock() 方法返回的 lockId 是 null , 則表示未成功獲取鎖 , 客戶端 可能須要再次 TryLock() 。
在 分佈式系統 中 , 由於一些緣由 , 可能會發生 鎖 沒有解鎖就被 「遺棄」 的 狀況 , 好比 發起 鎖定 的 客戶端 線程 死掉 或者 掉線 了 , 這樣就會形成 「遺棄」 的 鎖 。 這個 鎖 就一直沒人解 , 就會形成 其它 線程 一直等待而不能正常運行 。 爲了不這種問題 , ShareMemory 規定 鎖 的 有效時間 是 1 分鐘 , 超過 1 分鐘的鎖會被系統 自動解鎖 。 ShareMemory 每 30 秒 執行一次 回收鎖 的 任務 , 因此實際中 鎖 的 最大有效時間 理論上 大約是 1 分 30 秒 。
這就是 ShareMemory 提供的 鎖機制 , 能夠利用這個 鎖機制 來 實現 多個 客戶端 線程 間 的 通訊協做 。 以此爲基礎 , 開發者還能夠實現各類豐富的線程間通訊協做方式 。
關於 鎖 機制 , 能夠在 Client 項目中 查看 Demo 。
接下來 再來 討論 持久化 水平擴展 可用性 數據不丟失性 。
ShareMemory 不提供 持久化 。 持久化 仍然 交給 傳統的 關係數據庫 和 文件系統 。
ShareMemory 不提供 水平擴展 。 水平擴展 會 帶來 性能損耗 。 固然這不是多有理由的理由 , 啊哈哈 。
ShareMemory 不提供 可用性 。 開發者能夠本身想辦法解決 , 好比 準備一臺 備機 。
ShareMemory 不提供 數據不丟失性 。 ShareMemory 至關於 內存 , 因此 不提供 數據不丟失性 。 這好像也不是什麼理由 , 哈哈哈哈 。
對於以上 , 咱們考慮過一些方案 , 比較 簡單經典 的 方案 是 主從熱備 , 但在具體設計的時候 , 發現仍然有一些複雜的狀況 。 好比 主從熱備 , 是 同步備 仍是 異步備 ? 同步備 程序比較簡單 , 但會帶來 性能損耗 , 對 在用主機 的 響應時間 產生影響 。 由於要把 每一筆 數據更新 操做 包含了 主機 和 備機 雙份的 操做 , 主機 備機 2 份操做合在一塊兒 做爲一個 操做 。 而且若是 備機 發生問題 會反過來 影響主機 。
那麼 異步備 呢 ? 異步備 能夠把 數據更新操做 放到 一個 隊列(Queue)裏 , 而後 由 另外一個 線程 來 逐一 讀取 隊列 裏的 操做 對 備機 執行 。 但 問題是順序的執行這些操做 , 這樣才能 還原 主機 上的數據變化 。 這就致使 不能 多線程並行 執行 。 這樣帶來的問題是 , 假如 服務器 的 CPU 是 4 核 , 若是 更新 很頻繁的話 , 那麼可能有 3 個 核 加入了 更新的工做 , 熱備 只有一個 線程 , 最多隻能利用 1 個核 , 那麼 更新記錄 的 增加 會 大於 消費 , 那麼 隊列 裏的 更新記錄 會 愈來愈多 , 堆積起來 。 把 更新記錄 寫到 文件 裏 也沒用 , 文件 也會增加 。 事實上 , Sql Server 的 Always On 也存在 Log 膨脹 的 問題 。
對於 以上的問題 , 若是反過來 , ShareMemory只提供一個內存模型 , 並不 一刀切 的 負責 可用性 數據一致性 數據完整性 , 另外一方面 ,開發者的程序本身實現如下 3 件事:
1 數據什麼時候持久化(哪些數據須要持久化),就像咱們編輯文檔會不按期隨時保存同樣
2 主機掛掉時,新的主機啓動時須要預先加載哪些數據,如經常使用數據,如 User Profile
3 一些重要數據的備份,好比 按期 不按期 的 數據同步 快照
我想,這樣,事情就簡單了。
ShareMemory 提供 讀寫數據 的 API 便可 。
ShareMemory 提供一個內存模型,持久化 仍然 交給 傳統的 關係數據庫 文件系統 等 。
對於 集羣 負載均衡 可用性 數據完整性(不丟失性) , 咱們能夠參考 Windows NLB , Windows 故障轉移集羣 , Sql Server Log Shipping , Sql Server Always On , 然鵝 。
對於 分佈式緩存 分佈式消息 的 集羣 可用性 數據備份 數據同步 數據恢復 , 咱們能夠參考 Redis RabbitMQ , 然鵝 。
根據網上的測評結果 , 固態硬盤 的 連續讀取速度 能夠達到 1800M/s 以上 (參考 http://ssd.zol.com.cn/608/6082302.html ) 。 咱們能夠來假想評估一個 使用場景 。 好比 , 以 門戶網站 的 場景 爲例 , 假設有 100 萬 人同時在線 , 用 ShareMemory 來存儲 User Profile 的話 , 假如每一個用戶的 User Profile 大小 是 1 KB , 那麼 , 100 萬個 用戶的 User Profile 佔用的空間就是 1 G 。 若是有 1 億個用戶的話 , 那佔用的空間就是 100 G 。 在 操做系統 虛擬內存 的 支持 下 , 32 G 內存 + 120 G 固態硬盤 應該會有不錯的表現 。 或者 , 16 G 內存 + 120 G 固態硬盤 , 8 G 內存 + 120 G 固態硬盤 也許表現都會很好 。
ShareMemory 的 遠程通訊 採用 MessageRPC 實現 , MessageRPC 是我寫的另外一個項目 : https://github.com/kelin-xycs/MessageRPC