雲風開發筆記(2) Redis數據庫結構設計

按照我們一期項目的需求,昨天我簡單設計了數據庫裏的數據格式。數據庫採用的是 Redis ,我把它看成一個遠端的數據結構保存設備。它提供基本的 Key-Value 儲存功能,沒有層級表。如果需要兩層結構,可以在 Value 裏保存一組 Hashes 。

這是我第一次實戰使用 Redis ,沒有什麼經驗。不過類似的設施幾年前自己實現過,區別不大。經過這幾年,有了 Redis 這個開源項目,就不需要重造輪子了。但其模式還是比較熟悉的。也就是說,是按我歷史經驗來使用 Redis 。

一期項目需要比較簡單,不打算把數據拆分到不同的數據服務器上。但爲日後的拆分需求做好設計準備。以後有需要,可以按 Key 的前綴把數據分到不同的位置。例如,account 信息是最可能獨立出去的,因爲它和具體遊戲無關。

用戶系統使用 email 來做用戶名,但在數據庫中的唯一標識是一個 uid 。用戶應該允許修改登陸名(用戶很可能更換 email)。用戶的身份識別是用 id 來定位的。所以,在數據庫中就應該有如下幾組 Key :

  • account:count id
  • account:userlist set(id)
  • account:email:[email] id

這裏,account:userlist 對應的 value 是一個 set ,裏面存放了所有存在的 user id 。用於遍歷所有的 user 。這個暫時可能用不上,而且當用戶量相當大的時候可能有問題。不過暫時不用考慮這麼多問題,等以後改進。

account:count 是一個計數器,可以用來生成唯一 id 。

account:email:[email] 用來標示每個註冊的 account 的登陸名。[email] 指登陸用 email 地址。

這裏,email 內可能也存在符號 ":" ,爲了迴避這個問題,許多對 email 進行編碼。我的方案是,將字母數字 @ . _ 之外的字符編碼爲 %XX 的形式。用 lua 幹這件事情非常簡單:

local function _encode(str)
    return string.format("%%%02X",string.byte(str))
end

function emailEncode(str)
    return string.gsub(str,"([^%w_@.])",_encode)
end

當然,解碼回來也很簡單

local function _decode(str)
    return string.char(tonumber(str,16))
end

function emailDecode(str)
    return string.gsub(str,"%%(%w%w)",_decode)
end

之後,就是 account 下每個 id 的數據:

  • account:[id]:version number
  • account:[id]:email string
  • account:[id]:password string // md5(password..salt)
  • account:[id]:nickname string
  • account:[id]:lastlogin hashes
    • ip string
    • time string
  • account:[id]:history list(string)
  • account:[id]:available enum(open/locked/delete)

其中,密碼不想保存爲明文。因爲任何可能的數據泄露都會導致用戶的損失,我也不想任何人看到用戶的密碼。所以採用 md5(password .. salt) 的風格。

md5 運算前,加一個 salt 後綴,是因爲單純的文本 md5 值也是有數據庫可查的。

lastlogin 下保存了用戶最後一次登陸的信息,使用了一張 hashes 表,因爲這些信息在未來會進一步擴充。

history 保存了用戶登陸的所有歷史記錄,用一個 string 鏈表記錄。

用戶刪除自己的賬戶時,不想把數據從數據庫刪除,只想在 available 下做一個標記。

考慮到數據庫內數據結構有可能發生變化,所以加了 version 域做版本標識。


我不想讓各種服務可以直接讀寫這份數據,所以,會單獨寫一個認證服務器做處理。

認證服務器提供三項服務:

  1. 用戶註冊

  2. 用戶名 密碼 認證 (用於 ssl 連接上的 web 服務)

  3. 用戶名 密碼 挑戰式認證 (用於 client 的認證服務)


下面是基本的場景服務用的數據:

  • account:[id]:avatars set(id)
  • avatar:count id
  • avatar:[id]:version number
  • avatar:[id]:account id
  • avatar:[id]:scene string
  • avatar:[id]:available enum(open/delete)
  • avatar:[id]:data hashes
    • name string
    • figure string
  • world:scene hashes
    • [name] id
  • scene:count id
  • scene:[id]:name string
  • scene:[id]:available enum(open/close/delete)
  • scene:[id]:info hashes
    • time string
    • pc number
  • scene:[id]:pc hashes
    • [id] enum[online/offline]
  • scene:[id]:pc:[id] hashes
    • status string

用戶賬號下可以有許多遊戲角色,列表放在 account:[id]:avatars 下。

每個角色也擁有一個唯一 id 。這個 id 原則上和 account id 是獨立體系,但是爲了人類好區格,avatar:count 的起點和 account:count 不同。

角色所在場景記錄一個字符串的場景名 avatar:[id]:scene ,角色的其它各種數據放在一個 hashes 裏。

所有的場景索引方在 world:scene 下。如果日後有多個世界,可以採用 world:[id]:scene 。但目前不必考慮。

scene 下面的所有 pc 的在線狀態放在 scene:[id]:pc hashes 中,pc 離線也把它的 id 記錄在內,只有 pc 轉移場景才移除。

每個 PC 的位置狀態信息記錄在 scene:[id]:pc:[id] 中,第一個 id 是 scene 的 id ,第二個則是 PC 的 avatar id 。

btw. 這是一份草稿,雖然思考不周,但足夠滿足項目一期的需求。當然許多欠考慮的地方也並非是考慮不到,而是希望儘量簡單,以滿足一期需求爲目的。這個日後修改的代價並不大。


最後吐槽一下 Redis 的 Windows 版。辦公室的 Linux 服務器還沒有裝好,我暫時在 Windows 下做開發。取了一份 google 搜到的 非官方 Redis 的 Windows 版 。爲了圖方便,使用的是 luajit ffi 去調用 hiredis 的 dll 。一開始怎麼都搞不定。建立不了 socket 連接,出錯碼也取不到。

對比了源代碼,發現修改版把 C Struct 結構改了,前面增加了幾個域,而我以 hiredis 官方標準來定義的接口。

改好後,能夠正確取出出錯碼了。發現萬惡的 Windows socks api 需要調用 WSAStartup 纔可以用。而 hiredis 的 Windows 修改版居然沒有去調用。讓我大費周折才改好,前後折騰了一個多小時。

再吐槽一下 hiredis 的 API 設計,居然依賴 C Struct 的佈局。良好的 C 庫的接口設計不會這麼幹的吧。比如 lua ,又比如 zmq 。唉,用這種東西有點小不爽。不過比 C++ 庫還是好太多了。

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