SLG手游Java服务器数据管理方案
前言
這一年左右的時間,我參與并完成了一款SLG手游的研發,我負責游戲的服務端研發。這是一款以三國為題材的游戲,除了有三國名將的卡牌養成、多種多樣裝備養成、PVE,玩家競技場等常見玩法外,我們的游戲的主打特色是國戰和軍團戰,目前我們正在進行國戰部分的開發和優化,軍團戰部分仍在策劃中。軍團戰預計是一種以公會形式存在的策略玩法,國戰是全服玩家達到一定等級就可以參與的混戰玩法,玩家的陣營由玩家創建角色時選擇的國家(魏蜀吳)決定,進入國戰地圖之后,服務器會根據玩家選擇的國家分配陣營,然后玩家就可以在世界地圖中進行攻打敵國城池,在世界地圖中,除了三國城池,還有一些資源城池和關隘城池,玩家占領資源城池可以獲得一定的資源收益,而關隘一般是設定在兩國交界處,只有打通了關隘城池,國家內的人才能通過關隘攻打帝國,這一系列的玩法都非常具有策略性,需要同國玩家之間的相互協作才能共同立足于三國之中。國戰的過程中,玩家在混戰的同時也能獲得一部分的收益。整個國戰的玩法充分體現了SLG的策略性。
 ?
前文主要講解我們的游戲的功能和玩法,大家應該都知道,一款游戲里,處處都充滿了“數據”,有關卡配置數據,有人物屬性數據,有玩家基礎數據等,正是這些各種各樣的數據,才讓我們在游戲中更加數字化的感知游戲的樂趣,本文就從游戲服務器的“數據”角度,來分享我們的游戲的數據管理方案。本文的內容只針對我們的游戲的一種解決方案,不包括其他游戲的解決方案,相信不同的游戲中,架構師應該會對數據管理有不同的解決方案,但萬變不離其宗,我們只需掌握數據管理中的核心概念,就可以輕松自如的應對各種應用場景下的數據管理。
我們的游戲服務器的技術架構是用Java來做的,使用Java作為游戲服務器開發語言的成功案例已經越來越多了,Java的網絡應用技術也是越來越成熟,不說太多的語言之爭,總之最后,我們選用了Java作為了整個游戲服務器的技術支撐,其中網絡通信采用Netty,數據與緩存分別選用了MySQL,Memcache和Redis。本文主要講解數據管理,因此重點也就是講解MySQL。Memcache和Redis是如何在我們的服務器架構中應用的。
數據分析
在開始講解如何具體實施之間,我們可以先對游戲中的數據進行一個簡單的分析。相信大家一定都常玩游戲,一定可以感覺到,游戲中的雖然處處是數據,但各種數據的性質又不完全一樣,比如怪物的攻擊力,防御力等戰斗屬性,一般情況是一組靜態數據,這部分數據可以由策劃在配置關卡時提前配置好,而玩家的戰斗屬性,卻是玩家在游戲中一系列養成的綜合因素所決定,這是一組動態的數據。按照宏觀上分,游戲數據大致就可以分為靜態數據和動態數據,靜態數據,如簽到獎勵,戰斗掉落,抽卡概率等內容,一般情況下均由策劃配置好靜態表,服務器啟動時直接讀取靜態表,將表中內容加載到服務器內存,使用時直接從內存讀取,而動態數據,是根據玩家的游戲進度所決定的,因此這部分數據就需要一個數據管理系統來統一管理,也就是我們常用的數據庫,這部分的數據我又細分為熱數據和冷數據,熱數據指游戲中操作頻繁的數據,如體力恢復時間,抽卡免費次數,國戰城池狀態等,這部分的數據的特點,就是讀寫頻繁,并且其數據結構也是復雜多變的,玩家的每一步操作,都可能涉及到這部分數據的讀取或更新。相反,冷數據,則泛指游戲中更新并不頻繁,卻仍然十分重要的數據,如君主信息,卡牌信息,裝備信息等,這部分的數據可能只有特定的操作才會觸發這部分數據的變化,比如卡牌的星級,只有當玩家進行了卡牌升星操作,卡牌的星級才有可能發生變化,這部分的數據,更新不頻繁,且數據結構穩定,一般在一開始就已經設計好,這部分數據,非常易于管理。基于以上總結,可以得出如下分類圖:
 ?
技術選型
靜態數據
游戲中的靜態數據,一般由策劃進行配置,一般情況下是xml文件或csv文件,在我們的游戲中,我們采用了讀取策劃配置的csv文件來獲取游戲中的靜態數據,服務器啟動時加載游戲中的csv靜態數據,把所有的靜態數據加載到Java的內存中,用Map進行管理。我們的游戲中,使用靜態數據的步驟如下:
1.讀取csv文件
2.按一定格式對文件內容進行解析
3.將內容封裝到JavaBean
4.存放數據到Map中
5.從Map中取出數據使用(使用時)
當然讀取靜態數據,也有其他的形式,比如配置xml表,從xml表中讀取數據,這里說的只是我們的采用的其中一種形式。
熱數據
一說到頻繁交互,大家第一反應一定是內存,是的,Redis就是一款基于內存的數據庫,網上有很多帖子說Redis會如何如何吃內存,在大數據量下的效率如何低下,其實我認為,Redis,它只是一款內存數據庫,至于它會發揮怎樣的功效,還是得看我們如何去使用它,有一把好槍不代表我們就有了好槍法,還是得根據我們自己的應用場景,合理運用。我也說說我為什么選擇Redis的原因吧:
1.Redis是內存數據,其操作均基于內存,可以滿足我的“頻繁”需求
2.雖然是內存數據庫,但同時它也具有RDB和AOF兩種持久化功能,可以滿足我的“存儲”需求
3.Redis支持String,Set,List,Sorted Set和Hash五種數據結構,豐富的數據結構,可以滿足我的“數據結構復雜多變”需求
4.Redis可以很容易實現cluster以及主從等分布式擴展,可以滿足我的“數據擴展性”需求
當然,Redis的優點遠遠不止以上四點,但從這四點來說,我覺得Redis就非常適合作為我的游戲動態數據中的熱數據的存儲。
冷數據
前文分析到這部分數據的特點是更新不頻繁且數據結構穩定,我們采用MySQL來存儲這部分數據,MySQL不僅結構清晰,更是方便數據管理。MySQL也有許多很適合我們這部分數據的特點:
1.關系型數據庫,支持標準的SQL語言,適合結構化的數據
2.支持多種列類型,能存儲各種數據格式
3.多線程運行,高并發環境下高效穩定
4.支持事務控制
以上我只列舉了我認為比較重要的四點,MySQL已經足以滿足我對冷數據的所有存儲需求。
緩存數據
這部分數據是在前文在數據分析中沒有提到的,因為這部分數據,只是臨時的,嚴格的說,如果不需要考慮服務器的效率問題,沒有緩存也是可以照常運行的。緩存的出現是為了減輕服務器的負載,我們可以把客戶端的請求全部交給緩存去做,緩存再通過一定的策略與數據庫同步,這樣,我們就不必要重復的查找或插入同一條數據。這就相當于,在客戶端和數據庫中間,添加一個擋板,這個擋板先行對數據進行過濾處理,而緩存就是這個擋板。我認為,在服務器中,緩存是一門藝術,用好了緩存,可以讓整個架構都看起來賞心悅目。在緩存技術上,我選擇Memcache,Memcache作為內存數據庫,在高并發及大數據下的性能都是不錯的,Memcache采用一致性Hash算法,并且具有數據分布式的特點。
技術實現
Redis存儲數據
對于我們游戲中需要使用Redis存儲的部分熱數據,我做了如下總結:
 ?
使用Redis,除了要理解Redis的內存模型原理外,首先還得了解Redis的五種基本數據類型,每一種數據類型都對應不同的Redis操作API,在Java中使用Redis可以使用官方提供的Jedis客戶端,Jedis客戶端中包含了各種數據類型的操作。Redis既可以作為單服務器使用,也可以做cluster或主從的集群擴展,方便日益龐大的游戲數據的擴展。
MySQL存儲數據
在游戲數據中,我對游戲中的冷數據做了一個總結,如下圖所示:
 ?
完成要存儲的游戲數據的分析之后,我們就可以進行具體建模建表的工作,完成對數據的設計。由于在游戲服務器的數據存儲中,數據庫基本上只是一個玩家下線后的游戲數據臨時存放的地方,所以游戲數據表中的關聯性并不是特別強,不需要特別嚴密的數據庫設計,只需簡單的將玩家所有的數據按照一個userid進行關聯即可。
我們使用Druid和Hibernate來管理數據庫的連接以及進行增刪改查的操作。在使用Hibernate的時候,我們使用了Hibernate4,我們只需將需要存儲的Model寫成JavaBean,并加上作為數據Model的注解,在啟動時,Hibernate掃描到JavaBean會自動為我們創建或更新表結構。
Druid數據庫連接池
游戲服務器運行中經常是多個玩家同時在線的,可想而知,如果同時進行某一項涉及數據庫的操作時,會并發請求數據庫,多個數據庫請求就需要我們對多個數據庫連接進行有效的管理,當然,我們可以自己寫一個數據庫連接池來進行數據庫管理,但好在前輩們為我們做足了工作,有很多成型的開源數據庫連接池可供我們選擇,常見的有c3p0、dbcp、proxool和driud等,這里我們使用阿里巴巴公司的開源產品Druid,這是我個人認為最好用的數據庫連接池,它不僅提供了數據庫連接池應有的功能,更是提供了良好的數據庫監控性能,這是我們作為開發人員在遇到性能瓶頸時最需要的東西,感興趣的朋友可以參考下官方github,根據官方wiki配置一個Druid的數據監控系統,通過系統可以查看數據庫的各種性能指標。
Druid在github中的地址是:GitHub - alibaba/druid: 阿里云計算平臺DataWorks(https://help.aliyun.com/document_detail/137663.html) 團隊出品,為監控而生的數據庫連接池
Hibernate
使用Hibernate作為Mysql數據庫的ORM框架,主要是因為其良好的封裝,首先我個人認為Hibernate的性能是不足與和原生JDBC以及MyBatis這樣的框架所匹敵的,封裝的更好卻帶來了更多的性能損失,但我使用他也是看中他良好的封裝性,因為我對性能的需求還沒有達到很高的級別;其次,Hibernate不適用于寫復雜的SQL查詢,而MyBatis可以寫出一些復雜的SQL。但在我的設計中,我不需要太復雜的查詢,基本上我所有的SQL語句的where條件都是”where userid=?”,因此在性能需求上以及易用的對比上,我選擇了Hibernate。
Memcache緩存數據
Memcache作為一種內存數據庫,經常用作應用系統的緩存系統,在我們的游戲服務器中,我使用Memcache應用在三處地方。
1.MySQL的數據表主鍵自增ID的生成
2.MySQL的查詢結果集的緩存
3.國戰數據緩存
MySQL數據表ID生成器
在數據庫的設計中,我們的主鍵ID是自增,但由于自增ID也是會消耗一定的MySQL性能的,因此我使用Memcache封裝了一個QQ靚號買號平臺ID生成器,其原理就是利用Memcache的incr方法實現ID的自增,Memcache的incr方法是并發安全的,能保證在多線程環境下,MySQL的數據表ID的唯一。
MySQL查詢結果集的緩存
我在將Memcache引入到項目作為Mysql數據結果集的緩存系統的過程中,曾進行了多種緩存方案的嘗試,具體有以下幾種緩存模型:
1.無緩存
這種方式不使用Memcache緩存,游戲服務器的操作直接穿透到Mysql中,這種方式在高并發環境下容易引起Mysql服務器高負載情況。如下圖所示:
2.查詢使用緩存,更新穿透到數據庫,數據庫同步數據到緩存
這種方式在客戶端表現來看可以提高一部分速度,因為查詢操作都是基于緩存的,但實際上Mysql的負擔反而加大了,因為每一個更新請求,都需要Mysql同步最新的查詢結果集給Memcache,因為每一個更新操作都會帶來一個查詢操作,當然這個同步過程可以是異步的,但是就算我們感受不到這個同步的過程,在實際上也是加大了數據庫的負擔。如下圖所示:
 ?
3.更新和查詢都使用緩存,緩存按策略與數據庫進行同步
這種方式是比較好的方式,因為客戶端的所有操作都是被緩存給攔截下來了,所有操作均是基于緩存,不會穿透到數據庫,而緩存與數據庫之間可以按照一定策略進行同步,如每5分鐘同步一次數據到數據庫等,具體同步策略可根據情況具體調整,當然這種方式的缺陷就是一旦服務器宕機,那么在上次同步到宕機這段時間之間的數據都會丟失。如下圖所示:
 ?
4.更新和查詢都是用緩存,更新操作同時穿透到數據庫,數據庫同步緩存的查詢
這種方式是我最終使用的方式,雖然更新操作穿透到數據庫,但是我可以在保證查詢效率的同時,也保證數據的安全穩定性,因為每一步更新操作都是要進行數據庫存儲的,并且所有的查詢操作可以直接在緩存中進行。如下圖所示:
 ?
國戰數據緩存
在我們的數據管理中,前文提到的所有數據均是基于玩家的個人數據,游戲服務器與Web服務器最大的不同,就是游戲服務器中存在一個“游戲世界”,在這個游戲世界中,多個玩家均操作同一套數據,稍有經驗的后端開發者就知道,在多線程環境下操作共享資源,需要處理好共享數據的安全同步問題。在我們的國戰玩法中,共享資源就是世界地圖上的城池,以及各個玩家的國戰信息(玩家被打敗后會更改玩家的占領城池信息)。
我們的國戰數據是存儲在Redis中的,但如果我們不做任何安全問題的保護,就會出現數據的混亂,舉個例子,當玩家A和玩家B同時操作一座城池的信息時,假設有以下步驟:
1.玩家A從Redis讀取城池信息
2.玩家A對城池信息進行更改
3.玩家A將城池數據入庫
4.玩家B從Redis讀取城池信息
5.玩家B對城池信息進行更改
6.玩家B將城池數據入庫
如下圖所示:
 ?
以上是一種A和B按順序分別對城池信息操作的正常情況,在這種情況下,城池信息的數據是不會有異常的,可如果A和B的的操作順序變成了如下這樣,就會產生數據的安全問題了:
1.玩家A從Redis讀取城池信息
2.玩家B從Redis讀取城池信息
3.玩家A對城池信息進行更改
5.玩家A將城池數據入庫
6.玩家B將城池數據入庫
如下圖所示:
 ?
可以假象以下,如果兩個玩家按照如上步驟對城池數據進行操作,那么玩家B的操作將完全覆蓋玩家A的操作,其最后的結果肯定是不允許的,這種需要將一系列操作執行完的叫做事務,在一個線程對當前的共享數據進行事務操作的過程中,其他線程是不允許操作這個共享數據的。Redis中,是提供了這樣的事務操作API的,Redis的multi和exec函數,可以保證如果玩家A正在操作數據,玩家B的操作無效。而在我們的服務器中,我使用了Memcache提供的CAS原子操作來保證同步數據安全。因為國戰數據的操作頻繁,且大部分數據需要保證同步安全,因此我在客戶端和Redis數據庫的中間,用了Memcache作為緩存,既保證了國戰數據的操作效率,也保證了其事務的特性。
所謂CAS(check and set),即在寫操作時,先檢查數據是否被別的線程修改過。其基本原理就是對每次存儲的對象分配一個版本號(casUnique)。客戶端每次讀取數據時,調用gets,Memcache返回當前數據的casUnique,在客戶端提交數據的時候,客戶端將此casUnique一起提交,Memcache會判斷此casUnique是否是當前版本最新的casUnique,如果不是,則本次操作失效,如果是,則操作成功。對于操作失效的情況,我的處理是讓此請求重復執行一定的次數,如果執行完這個次數之后仍未成功,則返回“系統繁忙,請稍后再試”。其操作過程如下圖所示:
 ?
如圖所示,通過如上圖的流程,A和B的操作流程如下:
1.玩家A讀取城池信息,并獲取到CAS-ID1;
2.玩家B讀取城池信息,并獲取到CAS-ID2;
3.玩家A對城池信息進行更改
4.玩家B對城池信息進行更改
5.玩家B將城池數據返回給Memcache,在寫入緩存前,檢查CAS-ID與緩存空間中該數據的CAS-ID是否一致。結果是“一致”,就將修改后的帶有CAS-ID2的X寫入到緩存。
6.玩家A將城池數據返回給Memcache,在寫入緩存前,檢查CAS-ID與緩存空間中該數據的CAS-ID是否一致。結果是“不一致”,則拒絕寫入,返回存儲失敗。
通過Memcache作為Redis前面的中間層,既提高了讀寫效率,又保證了多線程環境下的同步數據的安全。
總結
本文講解了我們的游戲服務器的游戲管理方案,通過使用MySQL、Redis和Memcache,使它們各司其職,各自承擔不同的功能,發揮它們的最大功效,如今我們的游戲仍在緊張的新版本開發中,目前已經登錄一些平臺進行內測。本文僅作為參考,其中的所有內容是我在對我們當前游戲的開發中所總結出的一點經驗,或許并不適用于其他的數據管理方案,但應該可以幫助一些人填坑,這些也是我曾經爬過的坑。本人學藝不精,文中難免有不嚴謹之處,希望發現的同學給提出來,不要誤導了大家。感謝大家欣賞!
總結
以上是生活随笔為你收集整理的SLG手游Java服务器数据管理方案的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 游戏编程新手教程:怪物AI设计简述
- 下一篇: 以CSGO为例 分析不同网络延时下FPS
