天天看點

基于現有遊戲服務端(java)的資料架構調整思路

一、

現狀

1.現有架構

圖檔

基于現有遊戲服務端(java)的資料架構調整思路

2.基于現有部分實作的一些解讀

現有的架構基本屬于中規中矩型的,感覺比較适合業務不是特别複雜的情況。

優勢:1.自我管理的變量寫起來比較靈活,快速。有什麼需要存的直接搞個map就完事。

2.DBserver存在請求隊列的設計,也有線程池和資料庫連接配接池,分别由自己和hibernate來管理。能在一定程度上緩解資料更新請求的壓力。java鎖降低了出現髒讀的機率。

  3.緩存和hibernate均使用對象的形式來交換資料,包括别的server傳輸也使用對象。基本做到了面向對象來處理大部分更新和查詢問題。另外也做到了不受資料庫類型制約。
           

二、問題

該架構目前存在的問題有以下幾點(基于我的了解和了解):

1.hibernate相較于mybatis和一些直接操作JDBC的架構在性能上無太大優勢,好處是面向對象,封裝後常見的增删改查可以較為簡單應對。但對象存取無法應對較為複雜的關聯查詢,這裡說的隻是較為複雜,特别複雜的關系表目前業務還沒有需求(有的話根本無法應對),關系型資料庫的查詢優勢沒有利用起來(當然理論上也可以做hibernate的關系模型)。另外關于對象操作的api也相對來說較少,一些比較常見的方法目前也沒有(比如說查個count),查詢無法查or 的情況。

2.目前dbServer的緩存 所有通路都基于playerId進行,對PlayerData是單線程的,對全局是并發的。也就是說讀的請求和寫的請求會同時在隊列中,有的人在讀,有的人在寫,線上程資源和連接配接資源飽和的情況下,有可能讀的操作要等寫的操作進行。而事實上寫的很多資料,有一些資料常年是沒有人在用的,比如log. 很多頻繁讀的資料,比如使用者名又是基本不會發生變化的,(當然用戶端也是可以做緩存政策,這個不展開說)。簡單的說目前是沒有實作讀寫分離的,性能無需求沒事,性能有要求的話應該是撐不住了,在大規模請求到來時會不會崩,什麼時候崩還有待進行性能測試,但是從理論上來說,慢應該是會的。也就是架構的并發處理能力和吞吐量并沒有優化到最好。

3.伺服器運作中不需要持久化又經常變化的資料(比如玩家的狀态,好友的數量)沒有規範化的管理,有理論上記憶體洩漏的風險。并且沒辦法做到全局通用,目前還不具備分布式共享的能力。

4.其他未知的問題。

三、改進方案(基于網上的一些論點經驗和我個人的偏好,待讨論)

優化目的:

實作讀寫分離,提升伺服器處理能力,為分布式做架構擴充準備。

實作讀寫分離常見的有兩種手段:

l緩存架構 的介入

緩存架構是目前比較流行的做法,因為有成熟的開源架構和成功案例的先例。經過對市面上大部分的架構的對比。有以下幾個候選:

ØMemcached:多線程、不做持久化。Key_value形式存儲

ØRedis:單線程、持久化。多種存取形式。

ØMongoDB:海量資料的通路效率比較強勁。

Ø其他緩存架構如:Ehcache,輕量級緩存架構,易用,輕便,但有一定的局限性。不支援的功能較多。

結合以上看法,可以看出如果考慮到資料安全性持久化方面,Redis要更穩一些。另外性能也不比memcashed差多少。(實際上不到一定的量根本看不出差别)

而MongoDB更像是另外一種資料庫,适合存儲海量的資料。可以考慮戰鬥日志,部分業務資料在後期拆分過來。也可以考慮作為LoginServer的專屬資料庫來做驗證。

另外後期處理一些高并發的資料:比如類似秒殺/聚劃算鄧。從網上看,直接使用mongodb作為主資料庫的遊戲後端案例還不少,應該都是看上了mongodb的海量處理能力。不過從目前我們的項目來講,這個嘗試可以放到下一步來進行。先不做深層次的研究。

其他的架構暫時不考慮。

l資料庫本身層面的優化

包括SQLServer、mysql 和orcale都常用資料庫都提供了主從複制的功能。如果優化的足夠好的話甚至不需要其他的緩存架構。但是需要不斷的調整和優化。另外對資料庫本身的特性調優需要一定的經驗。本身層面的優化有很多地方都可以提高,和使用緩存是不沖突的,這裡不展開讨論。

值得一提的是如果後期采用了分表分庫的做法來提升性能時,需要相應的對緩存架構進行處理,如果處理不當,工作量有可能會較大。

lRedis

ØRedis 的特點:單線程單核cpu(需要用戶端來保障送出的一緻性),很好的持久化能力,分為AOF和RDB.簡單的說RDB性能更快,AOF更安全。我認為不同的資料形式适合不同的存儲形式。(Redis可以建立多個庫和多個執行個體來實作不同的緩存形式)。

Ø資料類型:字元串(string),清單(list),哈希(hash),集合(set)和有序集合(sorted set)

Ø提供了事務的管理

Ø管道(pipeline),簡單的講,在面臨一大堆請求的時候,會合并請求一起傳回結果給用戶端。

比如用戶端發起請求 a字段修改成 1,緊接着又發送請求 修改成 2,緊接着又發送請求修改成3

一般的處理會回複他3次結果,但pipeline形式會等他全部請求玩直接傳回3給用戶端。感覺這個功能比較适合處理count的累加。當然管道特性的使用與否要開發者自己控制。

l緩存處理的幾種具體形式

參看http://coolshell.cn/articles/17416.html

Ø形式1:Cache Aside Pattern

最常用的形式,也是我們目前架構使用的形式。

失效:應用程式先從cache取資料,沒有得到,則從資料庫中取資料,成功後,放到緩存中。

命中:應用程式從cache中取資料,取到後傳回。

更新:先把資料存到資料庫中,成功後,再讓緩存失效。

這裡我認為網上的這個論點是有道理的即:

先删除緩存,然後再更新資料庫,而後續的操作會把資料再裝載的緩存中。然而,這個是邏輯是錯誤的。試想,兩個并發操作,一個是更新操作,另一個是查詢操作,更新操作删除緩存後,查詢操作沒有命中緩存,先把老資料讀出來後放到緩存中,然後更新操作更新了資料庫。于是,在緩存中的資料還是老的資料,導緻緩存中的資料是髒的,而且還一直這樣髒下去了。

Facebook就是這種政策。

但是值得一提的是網上一些其他的使用Cache Aside Pattern政策的支援貌似都無視了這個問題,我猜想有可能是這種問題發生的可能性太小了,或者可以通過别的手段來避免。因為這個問題相較于資料庫和緩存的一緻性處理方式顯得不是那麼重要了。

Ø形式2:Read/Write Through Pattern

Read Through 套路就是在查詢操作中更新緩存,也就是說,當緩存失效的時候(過期或LRU換出),Cache Aside是由調用方負責把資料加載入緩存,而Read Through則用緩存服務自己來加載,進而對應用方是透明的。

Write Through 套路和Read Through相仿,不過是在更新資料時發生。當有資料更新的時候,如果沒有命中緩存,直接更新資料庫,然後傳回。如果命中了緩存,則更新緩存,然後再由Cache自己更新資料庫(這是一個同步操作)

這兩種形式的意思我明白,但具體如何實作并沒有太多的案例可循,是以初步考慮放棄。

Ø形式3:Write Behind Caching Pattern

Write Back套路,一句說就是,在更新資料的時候,隻更新緩存,不更新資料庫,而我們的緩存會異步地批量更新資料庫。這個設計的好處就是讓資料的I/O操作飛快無比(因為直接操作記憶體 ),因為異步,write backg還可以合并對同一個資料的多次操作,是以性能的提高是相當可觀的。

但是,其帶來的問題是,資料不是強一緻性的,而且可能會丢失(我們知道Unix/Linux非正常關機會導緻資料丢失,就是因為這個事)。

這種形式的具體實作需要花時間研究一下,初步可以考慮頻繁的戰鬥日志可以放到這裡來處理。由程式來讀取緩存再異步的插入到資料庫中。因為redis自有的持久化功能還是比較靠譜的,是以不用太擔心資料的丢失。就算是崩了,緩存的資料還在,重新開機後重新接着插。或者做個定期去緩存拉資料的程式在背景運作。

Ø小總結:基于以上,我認為可以根據目前的資料類型來選擇處理方式.

經過總結目前主要的資料有以下幾類:

1.多寫不讀或少讀:如戰鬥日志,記錄檔等。直接選用 write behind caching pattern.先搞到緩存裡慢慢寫就是了。這部分的資料存在或多或少的延遲,緩存可以隻考慮儲存近3個月的資料,其他的資料需要放在資料庫中并設定截止日期。定期去緩存拉資料的程式在背景運作。

3個月内讀緩存,包含3個月外的資料直接讀取資料庫。

2.多讀但少寫:這類資料比較典型及數量衆多,比如使用者姓名/頭像/等級/賬戶/密碼。這個就使用模式Cache Aside Pattern。可以考慮開辟單獨的庫和其他手段來徹底釋放Redis的性能。

可以考慮設定比較長的失效期。

3.多讀多寫:比如:擊殺(真人)數/戰鬥完成次數/戰鬥勝利次數/戰鬥平均排名/亂鬥最高連勝次數/經驗值/目前貨币資訊(金币銀币)。這類資料是Redis的主要用武之地。

貨币/經濟系統資料會單獨拉出來一個庫做。涉及到錢/裝備等資訊的資料考慮使用事務/piepine/等手段保證強一緻性。

讀:

查詢:直接查詢緩存。沒有的話查詢資料庫。成功放入緩存

寫:

方式1:

先更新緩存,後更新資料庫。至于如何更新資料庫和什麼時候更新資料庫可以根據情況做政策。

比如:戰鬥勝利次數,每勝利10次儲存一次到資料庫。(但這樣做的話,相當于根據業務定制,如何做到很好的封裝是個問題)

該方式适合對一緻性要求不是很高的資料。

方式2:

先直接更新資料庫,後将緩存失效。緩存設定較短的失效期。

該方式适合對一緻性要求很高的資料。如金錢/裝備。

方式3:

先直接更新資料庫,同步更新至緩存。

該方式比較中規中矩,在大并發下會有髒資料的情況。但此種形式覆寫面會比較大。适合封裝。

4.少讀少寫:

比如遊戲崩潰次數.這種資料建議不放到緩存中。

5.多讀不寫(不需要持久化):玩家的狀态/好友的數量/好友的狀态, 臨時組隊的資訊

此種資料可以根據需要放入緩存中。由于持久化又占用硬碟。但是各個變量的命名方式需要制定一些标準。

Ø關于上面說的第3點中Mysql往緩存同步的實作

目前查到有兩種方式感覺較為靠譜:

1.通過UDF使mysql主動重新整理redis緩存

使用的mysql的User Defined Function功能,mysql_udf_redis是有人實作的同步資料到Redis的功能,弊端:需要學習成本, 二來,第三方的插件不穩定。udf函數是C++寫的。另外需要挨個表建立觸發器,這個很蛋疼。感覺有點low.但是這個好處是較為底層一些,寫法不用太關心應用層怎麼弄了。

參考:http://blog.csdn.net/socho/article/details/52292064

2.使用阿裡的canal

個人感覺這個更為靠譜一些,這東西有人維護,功能也比較強大。簡單的說就是資料庫一有變化就會被canal捕捉到,他直接解析mysql的binlog将變化更新到緩存。另外他的設計模式是消費者/生産者模式,一有變化他就放一個增量變化的更新放到那裡,所有的用戶端可以自由的來擷取更新。這個可能更适合多個gameserver下的主從複制的分布式。擴充性更高。

這個的劣勢是考慮到用久了有可能依賴性比較強,錯誤不好排查。如果挂了,很多緩存資料容易丢失。需要有一定的容錯機制,這個我相信這個設計者都考慮到了,如果确定要用了待深入研究。

l緩存變量的初步命名規則:

Ø持久化通用資料:使用:做分隔符,比如一個人的名稱

可以通過 player:1:name 來擷取.

Ø持久化通用資料:可以使用方法名+參數的形式來做key比如

getPlayerList(155),這種形式的比較好記一些。

Ø持久化特殊資料:一些複雜查詢的結果。使用sql對應的md5加密來作為key

比如查詢一個人a的好友b的頭像imgid

Select p.imgid from player p

inner join player_invitation pi on pi.sender_id=p.id

Where receiver_id= ‘b’

将以上字元串做md5加密後,key為2B6C2BD990C0E76EF1A0ECB8BA9A0B9E ,value 為一個結果集

Key 為 imgid,value為5

即2B6C2BD990C0E76EF1A0ECB8BA9A0B9E:2B6C2BD990C0E76EF1A0ECB8BA9A0B9E_1:imgid =5

考慮到每次寫sql的輕微差異都将造成key的不一緻進而導緻重複無效資料,

java架構中将新增一個生成标準SQL的類來規範sql的寫法,另外表名的縮寫名将按照以下規範:

比如表明為 player_invitation ,縮寫就是pi,全部用小寫表示。

(上述例子不是太合适的例子,該種處理僅适用與比較複雜的SQL.比如查詢一個人的好友中比他在服 務器排名高的好友頭像)

Ø非持久化資料

可以考慮使用服務類型:資料類型:變量名的形式,如線上玩家
           

Game:string:online_player:

資料為:

基于現有遊戲服務端(java)的資料架構調整思路

l關于資料庫操作的方案

原有的方式是直接調用hibernate,由hibernate基于實體類生成SQL調用jdbc來進行對象的操作。

建議在現有的基礎上補充SQL直連JDBC的形式來加快效率(開發效率和運作效率)。在傳輸上依然使用對象傳輸。包括線程池、資料庫連接配接池、對象鎖等等。這裡隻需擴充一個繞過hibernate直連JDBC的補充方法。

需要封裝一個基于key_value的通用類 EntityObject。

需要封裝一個生成SQL的通用類 SQLEntity。該通用類将根據資料庫類型生成不同的語句.

生成一個直接調用JDBC的通用方法 SQLCommand。

效率比較案例(不考慮緩存介入):

基于現有遊戲服務端(java)的資料架構調整思路

需求:

原有做法,将我的所有戰鬥資料全部拉出來,然後使用java程式循環分析他的勝利、排名等等,使用變量累加的形式将各個資料拿出來。

現有做法:

直接編寫SQL得到EntityObject對象,發送SQLEntity對象至DBServer,DBServer拿到SQLEntity對象中生成的SQL得到結果傳回EntityObject。

基于現有遊戲服務端(java)的資料架構調整思路

從開發效率上少了周遊和很多變量的判斷。從性能上直連JDBC ,速度也有保障。唯一不足的是不能利用hibernate的基于id的二級緩存機制。是以常見的單個對象查詢還是可以使用hibernate。實際上我們原本也沒有使用hibernate自身的緩存機制,而是自己寫的Storge這個工具類。

四  新的示意圖
           

圖檔

基于現有遊戲服務端(java)的資料架構調整思路

新的示意圖

五、有可能出現或需要注意的問題

Redis是單線程阻塞的,是以會使用隊列來工作。但是當某個發送過來的請求處理過慢時,會造成後續請求timeout.是以需要防止慢查詢。

參考:http://carlosfu.iteye.com/blog/2254154

由于沒有實踐經驗,考慮先從邊緣業務入手緩存。再逐漸介入核心業務。

要考慮将緩存和DB服務剝離開來,保證緩存架構down了之後依然可以正常運轉,資料不丢。

緩存層資料 丢失/失效 後的資料同步恢複問題。

nosql層做好多節點分布式(一緻性hash),以及節點失效後替代方案(多層hash尋找相鄰替代節點),和資料震蕩恢複了。

理論上如果在應用層和資料通路層之間加個緩存就需要改大量代碼,那麼說明原來寫的就有問題。因為如果應用層隻通過資料通路層通路資料的話,加緩存隻需要修改資料通路層的實作,對上接口是不變的

對資料庫做該有的優化,防止本末倒置。

後續上分布式可以考慮上Redis-Cluster