開始這個話題前,離上篇開發筆記已經有一週多了。我是打算一直把開發筆記寫下去的,而開發過程當中必定不會一路順風,各類技術的抉擇,放棄,均可能有反覆。公開記錄這個歷程,便是對思路的持久化,又是一種自我督促。不輕易陷入到技術細節中而丟失了產品開發進度。並且有一天,當咱們的項目完成了後,我能夠對全部人說,看,咱們的東西就是這樣一步步作出來的。每一個點滴都凝聚了叫得上名字的開發人員這麼多個月的心血。 html
技術方案的爭議在咱們幾我的內部是很激烈的。讓本身的想法說服每一個人是很困難的。有下面這個話題,是源於咱們將來的服務器的數據流究竟是怎樣的。 算法
我但願數據和邏輯能夠分離,有物理上獨立的點能夠存取數據。而且有單獨的 agent 實體爲每一個外部鏈接服務。這使得進程間通信的代價變得很頻繁。對於一個及時戰鬥的遊戲,咱們又但願對象實體之間的交互速度足夠快。因此對於這個看似挺漂亮的方案,可能面臨實現出來性能不達要求的結果。這也是爭議的焦點之一。 數據庫
我我的比較有信心解決高性能的進程間數據共享問題。上一篇 談的其實也是這個問題,只是此次更進一步。 api
核心問題在於,每一個 PC (玩家) 以及有可能的話也包括 NPC 相互在不一樣的實體中(我沒有有進程,由於不想被理解成 OS 的進程),他們在互動時,邏輯代碼會讀寫別的對象的數據。最終有一個實體來保有和維護一個對象的全部數據,它提供一個 RPC 接口來操控數據當然是必須的。由於整個虛擬世界會搭建在多臺物理機上,因此 RPC 是惟一的途徑。這裏能夠理解成,每一個實體是一個數據庫,保存了實體的全部數據,開放一個 RPC 接口讓外部來讀寫內部的這些數據。 服務器
可是,在高頻的熱點數據交互時,不管怎麼優化協議和實現,可能都很難把性能提高到須要的水平。至少很難達到讓這些數據都在一個進程中處理的性能。 數據結構
這樣,除了 RPC 接口,我但願再提供一個更直接的 api 採用共享狀態的方式來操控數據。若是咱們認爲兩個實體的數據交互很頻繁,就能夠想辦法把這兩個實體的運行流程遷移到同一臺物理機上,讓同時處理這兩個對象的進程能夠同時用共享內存的方式讀寫二者的數據,性能能夠作到理論上的上限。 性能
ok, 這就涉及到了,如何讓一塊帶結構的數據被多個進程共享訪問的問題。結構化是其中的難點。 優化
方案以下: google
咱們認爲,須要交互和共享的數據,就是最終須要持久化到外存中的數據。總體上看,它好像一個小型內存數據庫。它必定能夠經過相似 google protocol buffers 的協議來序列化爲二進制流。它和內存數據結構是有區別的。主要是一些約束條件,讓這件事情能夠簡單點解決,又能知足能想到的各類需求。 lua
數據類型是有限的:
以上 6 種類型足夠描述全部的需求,這在 lua 中已獲得了證明。不過這裏把 lua 的 table 拆分爲 map 和 array 是對 protobuf 的一種借鑑。這裏的 map 是有 data scheme 的,而不是隨意的字典。即 key 必定是事先定義好的原子,在儲存上實際上是一個整數 id ,而 value 則能夠是其它全部類型。
array 則必定是同類型數據的簡單集合,且不存在 array 的 array 。這種方式的可行性在 protobuf 的應用中也獲得了證明。
本質上,任何一個實體的全部數據,均可以描述爲一個 map 。也就是若干 key-value 對的集合。array 只是相同 key 的重複(至關於 protobuf 裏的 repeated)。
這裏能夠看出,除了 string 外,全部的 value 均可以是等長的,適合在 C 裏統一儲存。每一個條目就是 id - type - value - brother 的一組記錄而已。
其中 map 用二叉樹的方式儲存就能夠知足節點的定長需求,左子樹是它的第一個兒子,右子樹是它的兄弟。
咱們用一個固定內存塊來保存整塊數據,裏面都是等長的記錄,map 的記錄中,左右子樹都用保存着全局記錄序號。
string 須要單獨儲存,全部的 string 都額外保存在另外一片內存中(也能夠是同一片內存的另外一端)。在記錄表中,記錄 string 內容在 string pool 中的位置。
這樣作有什麼好處?
因爲數據有 scheme(能夠直接用 protobuf 格式描述),因此數據在每一個層次上的規模是能夠預估的,數據都是以等長記錄保存的,對整個數據塊的修改均可以當作是對局部數據的修改或是對總體的追加。這兩個操做恰巧均可以作成無鎖的操做。
換句話說,每次對整顆樹具體一個節點的修改,都絕對不會損壞其它節點的數據。
有了這塊組織好的數據結構有什麼用呢?首先持久化問題就不是問題,但這只是一個附帶的好處。這塊數據雖然能完整的記錄各類複雜的結構數據,但不利於快速檢索。咱們須要在對這顆樹的訪問點,製做一個索引結構。若是導入到 lua 中,就是一個索引表。當咱們第一次須要訪問這顆樹的特定節點:體現爲讀寫 xxx.yyy.zzz 的形式,咱們遍歷這顆樹,能夠方便的找到節點的位置。大約時間複雜度是 O(N^2) :要遍歷 N 個層次,每一個層次上要遍歷 M 個節點。固然這裏 N 和 M 都很小。
一旦找到節點的位置,咱們就能夠在 lua 中記錄下這個絕對位置。由於每一個節點一旦生成出來,就不會改變位置了。下次訪問時,能夠經過這個位置直接讀寫上面的數據了。
string 怎麼辦呢?個人想法是開一個 double buffer 來保存 string 。string 和節點是一一對應的關係。當節點上的 string 修改時,就新增長一個 string 到 pool 裏,並改變引用關係。當一個 string pool 滿後,能夠很輕易的掃描整個 string pool ,找到那些正在引用的 string ,copy 到另外一個 string pool 中。這個過程比通常的 GC 算法要簡單的多。
最後就是考慮讀寫鎖的問題了。只有一些關鍵的地方須要加鎖,而大部分狀況下均可以無鎖處理。甚至在特定條件下,整個設計均可以是無鎖的。
btw, 這一週剩下來的時間就是實現了。多說無益,快速實現出來最有說服力。按照慣例,應該會開源。