Xgame 合服工具技術設計文檔
這是一篇介紹“合服工具”的技術文檔;合服工具代碼可以參考:https://git.oschina.net /afrxprojs/xgame-php_tool,注意:合服工具使用 PHP 語言開發!為什么使用 PHP,我后面會簡要說明;該文檔的主要目的是說明開發“合服工具”的思維過程,而不僅僅是使用說明!技術文檔的價值在于過程而不是結論;我假定你對游戲 行業的技術崗位工作有所了解。如果你親身參與過游戲合服工作,這篇文檔閱讀起來可能會比較輕松;這是一篇不合規范的文檔,所有符合規范的文檔,沒有人喜歡 讀……該文檔以“輕松、易讀、實用”作為規范;
如何使用
-
需要安裝 PHP 5.5+ 環境;
</li> -
需要安裝 php-pdo,php-mysql;
</li> -
修改 combine_tool/etc/dbConfig.php 文件,修改狼服和羊服的配置。具體配置內容,可以參考:dbConfig.php.template 文件;
</li> -
回到 combine_tool 目錄執行:
</li> </ul>php App_Combine.php
首先
-
這是一篇介紹“合服工具”的技術文檔;
</li> -
合服工具代碼可以參考:https://git.oschina.net/afrxprojs/xgame-php_tool,
</li> -
注意:合服工具使用 PHP 語言開發!為什么使用 PHP,我后面會簡要說明;
</li> -
該文檔的主要目的是說明開發“合服工具”的思維過程,而不僅僅是使用說明!技術文檔的價值在于過程而不是結論;
</li> -
我假定你對游戲行業的技術崗位工作有所了解。如果你親身參與過游戲合服工作,這篇文檔閱讀起來可能會比較輕松;
</li> -
這是一篇不合規范的文檔,所有符合規范的文檔,沒有人喜歡讀……該文檔以“輕松、易讀、實用”作為規范;
</li> </ol>在內容開始之前,我先簡單的交代一下背景……
背景
-
假設我們的游戲服采用的是單服架構,即每個游戲服都有自己的數據庫;
</li> -
數據庫我們使用的是 MySQL;
</li> -
我們的合服工具必須能夠一次合并 10 個游戲服,即,其中 1 個服務器吃掉另外 9 個;
</li> -
但是運營給我們的時間最多只有 6 個小時,這是極限值。一般是在 4 個小時之內完成;
</li> -
合服時,將服務器分為“狼服”和“羊服”。例如將 S2 服數據合并到 S1 服,即,S1 吃掉 S2。那么 S1 稱作狼服,S2 稱作羊服;
</li> </ol>
我所使用的最早的合服工具,一次只能合并 2 個游戲服(1 個狼服吃掉 1 個羊服)。而且合服時間最長的一次用了將近 8 個小時……隨著新項目的迅速擴大以及運營需求標準的提高,早期的合服工具已經無法勝任,所以只好另行開發新工具。舊的合服工具在合并數據時,所采用的辦法是從羊服讀出單條數據,再將這條數據寫入到狼服數據庫。這種辦法的確比較直觀,但是效率實在太低了,而且合服過程一旦中斷,是不能重啟并接著合服的,否則數據就亂了。
我們必須改變原有思路,找到更快、更可靠的辦法!核心實現
經過反復思考,我決定利用 MySQL 的“insert into ... select ...”語法來批量插入數據,這樣做的效率非常高,基本可以應對時間約束。這樣一來我們也就可以確定一個核心思路:
利用批量插入!如果遇到問題,則盡量往這個思路上靠……
“利用批量插入!“,這句話很好理解。那么,“如果遇到問題,則盡量往這個思路上靠……”這句話該怎么理解呢?我曾經和一個同事打過一盤臺球,他告訴我一個打臺球的小技巧:如果當你沒把握一桿就把球打進洞的時候,你就想辦法讓球慢慢往洞口靠攏。等球距離洞口很近了,你也就有把握了。技術實現又何嘗不是這樣呢?如果我們沒有把握一下子搞定一個大系統,那么我們就想辦法把這個大系統拆成若干個可以搞定的小系統,逐個擊破不就可以了么……技術源于生活,卻又高于生活。
在接下來的文字中,我們一點點親歷這樣的過程。分析
上一段文字中,我們確定了核心實現。接下來,我們對幾個具體的業務模塊進行分析。因為篇幅有限,所以我們只看 3 個業務模塊:角色數據、單人副本、競技場。之所以選這 3 個業務模塊,是因為這幾個模塊比較有代表性,可以基本說明合服過程的設計思路。
接下來,我們分別對這 3 各個業務模塊進行分析,從具體到抽象,提取出整個合服流程的設計思路。角色數據
對于角色數據,在合服的時候大概要經歷如下過程:
(圖1)角色數據的合服流程-
清理無用數據,就是把 30 天沒登錄的,并且角色等級小于指定等級的,并且沒有沖過錢的,再并且不是軍團長的角色數據給刪除掉;
</li> -
合并數據,就是把羊服數據合并到狼服,完成狼吃羊的過程。這一步我們就可以使用 insert into … select … 這個語法,進行批量插入;
</li> </ul>
這里需要注意的一個地方是,我們事先將無用數據清理完,再執行合并數據。而不是把有用的數據撿出來一條一條的合并過去……這符合我剛提到的思路:利用批量插入!單人副本
對于單人副本這個業務模塊,在合服的時候大概要經歷如下過程:
(圖2)單人副本的合服流程
單人副本的合服流程跟“角色數據”那個基本一樣,甚至可以說是完全一樣!單人副本模塊,也需要事先清理無用數據,之后再合并數據。到這里,我們算是找到了兩個模塊在合服過程中的一些共同點。可以總結出如下流程圖:
(圖3)合服流程總覽
但是這里面有一些問題,可能需要我們思考一下。
單人副本執行清理無用數據這一步,這“無用數據”指的是什么?應該是“角色數據”那一步被清理掉的數據!如果角色數據都已經不存在了,那么這個角色對應的單人副本數據無任何意義,所以也需要刪除掉。那么清理工作執行到單人副本這一步的時候,如何知道有哪些角色數據被刪除掉了呢?我們可以在執行“角色數據”清理的時候,使用一個數組變量把已刪除的角色 Id 都記錄下來。在清理單人副本的時候,從數組變量中取出角色 Id,用這個值作為單人副本數據是否需要刪除的判斷條件,這樣就可以了……
這么做確實沒錯,但是存在兩個問題:-
這樣做的效率比較低,而且需要額外占用內存;
</li> -
如果第 1 點你不完全同意的話,那么如果應用程序中途出現異常被中斷,這個保存著被刪除角色的數組變量也就不復存在了。這無法保證程序的可靠性;
</li> </ol>
其實,技術設計不是需求驅動的,而是質疑驅動的。技術設計的一個根本方法是:舉反例。在你想出一個設計方案之后,必須在一定范圍之內禁得起質疑。我們可以使用窮舉法盡可能找出這個方案的反例。不管思路有多么精妙,只要在一定范圍內存在反例,那么設計就會被擊翻。這種倒地擊翻在設計階段并不可怕,如果是在編碼實現階段才出現,那么所有人的體力勞動都會白費……其實,由于設計缺陷導致的代碼完全推翻重做,并不多見。更為常見的情況是,維護或修復一個簡單的功能或 Bug 也能讓你花費好幾天時間。
設計和科學一樣,科學應該是可證偽的。
為了解決第 1 個問題,我們可以采用 MySQL 的 where … in … 的查詢方式,而第 2 個問題,應用程序不是怕中斷么?我就把變量寫到磁盤上!這樣,即便是中途宕機或者斷電,我還是能知道有哪些角色被刪除掉了。因為我把數據存在文件里了……讓我們再仔細想想,有現成的 MySQL 數據庫,往這里面存更方便!干嘛還要寫到文件里呢?MySQL 數據庫不是也可以看作是一個“文件”么?而且這樣,我們使用 in 查詢還更方便了。
為此,我們可以在羊服數據庫中建立一張臨時表,專門用來記錄將要被刪除的角色 Id,這個表只有一個字段,就是“角色 Id”。為了讓整個過程更可靠,我們還可以把角色數據的清理過程放在最后,也就是說,先執行單人副本模塊的數據清理過程,然后再執行角色數據的清理過程。
(圖4)清理無用數據,調換單人副本和角色數據的執行順序
我們的核心思路,經歷了一次小的考驗,倒沒有太大問題,值得慶幸。不過,接下來的競技場模塊,就沒這么幸運了……競技場
競技場模塊的合服流程也可以總結為“刪除無用數據”及“合并數據”,流程圖沒什么兩樣,所以就不在這里畫了。但是競技場有個排名問題,這個問題非常棘手。我來具體說明一下:
假設,在 S1 服務器中的競技場數據如下:
(表1)S1 服競技場數據
再假設,在 S2 服務器中的競技場數據如下:
(表2)S2 服競技場數據
名字起的有點俗,能說明問題就好。在合服時,我們不能把競技場數據全部清掉,讓玩家重新來過。這樣的結果運營是無法接受的。競技場會根據排名,每天給玩家發一些獎勵,所以清掉競技場數據,眾玩家也是不會答應的。比較合理的結果類似下面這樣:-
S1 服的第 1 名保持不變還排在第 1 的位置;
</li> -
S2 服的第 1 名,排第 2;
</li> -
S1 服的第 2 名,排第 3;
</li> -
S2 服的第 2 名,排第 4;
</li> </ul>結果就像這樣:
(表3)S1、S2 合服結果,S1 為狼服,S2 為羊服
這是大家還都能接受的一個方案。為此,我們需要寫一個算法,把 S1 的競技場數據全部讀出,把 S2 競技場的數據也全部讀出,然后排序,最后回寫到 S1 服。這么做可以,但這實在是太麻煩了……
通過算法方式,并不符合我們最開始的核心思路:利用批量插入!所以,這個時候,我們執行第二套方針,如果遇到問題,則盡量往這個思路上靠……
我們試試,如果讓 S1 服的排名數值都乘以 1.5 并取整,而讓 S2 服的排名數值都乘以 2,最后再利用批量插入合并數據。怎么樣?這真是一個很牛 B 的想法!
但這個想法實在是過于幼稚!我舉一個反例,假設,S2 的排名數值實際上是不連續的,就像這樣:
(表4)S2 服競技場數據,帶木樁
S2 服排行榜中,第 4 到第 8 名,都是木樁(假人)。這些木樁是為了保障競技場模塊剛開放給玩家的時候,不會讓玩家一下子沖到第 1 名所設置的,而這些木樁數據,是不能參與合服的。也就是說最終的合服結果應該如下:
(表5)S1、S2 合服結果,S1 為狼服,S2 為羊服。S2 服的木樁數據不應被合并
所以修改排名數值再合服的方法是行不通的。如果競技場里存在被清掉的角色,也會發生排名不連續的情況。如果我們通過算法來解決這個問題,那么算法的難度會相當高。
完了,遇到大問題了,框架流程到此打結了,無法繼續進行了……逆向思考
如果一個問題正向解決,難度太高了,那么就試試倒過來解決。其實前面的思路方式,已經是很大進步了。對于競技場數據,我們就把數據先一股腦的合并過來,然后再對排名數值進行整理,又能怎么樣呢?反正我們也有原數據,大不了回滾唄。數據合并完成之后,已經是在同一個 DB 中了,所以整理過程也會比較高效。即便算法在復雜,性能應該也不會差到哪去?
說到這個排名數值的算法,本質上不就是把每條數據按順序讀出來,更新一下排名數值,然后再更新到數據庫。這不就是一個打標簽的過程么,多么簡單啊……
簡單是簡單,但是回想一下,還是需要寫一些代碼的。怎么著也得寫個 for 循環,來更新每一條數據吧。有沒有效率更高,更簡單的辦法呢?我們沒法一下子做到的事情,我們可以用兩下子做到。我們是不是可以建立一張臨時表來存放新的排名數據?例如,我們建立這樣的一張表:create table `臨時表` ( `角色 Id` bigint, `排名` int not null auto_increment, primary key ( `角色 Id` ), unique key ( `排名` ) );
然后通過 SQL 語句把角色 Id 插入到這張臨時表里:insert into `臨時表` ( `角色 Id` ) select X.`角色 Id` from `排行榜` as X order by X.`排名` asc, X.`服務器名` asc;
因為臨時表中的排名字段是自增的,所以數據插入之后,自然就會形成連續的排名。我們再通過一條 SQL 語句將臨時表中的數據,回寫到競技場:update `競技場` as A, `臨時表` as B set A.`排名` = B.`排名` where A.`角色 Id` = B.`角色 Id`;
得益于 MySQL 在 update 語法上支持關聯查詢,給我們省去了很大的工作量。至此,我們的核心思路再一次經受住考驗。
總結
我們的整個框架,可以總結為這樣三步:
(圖5)
每個功能模塊都可以通過這 3 步合服,我們還可以找更多的系統模塊來驗證,看看是否有反例。由于篇幅限制,我們就不在這里繼續列舉了。我們是否可以設計這樣的一個框架,就由這 3 大步來組成,上層的控制模塊用來控制該執行哪個大步驟。而且上層控制邏輯是不關心每個業務模塊具體是怎么操作的,它只需要知道工作的流程。而每個具體的業務模塊,只需要知道具體該怎么做,但是并不需要關心什么時候做?以后有新增的功能模塊,也是按照這 3 大步來做,只要做好自己的本職工作就可以了……想想富士康手機生產線上的線工
這就是典型的執行過程與具體實現的分離,它其實是一個工作流(Work Flow)!
我們把系統的運行過程想象成一道洪流,洪流流到“清理無用數據”這一關,每個業務模塊按照事先規定好的順序,一個接一個的清理自己的數據。當最后一個業務模塊清理完數據之后,系統的洪流向下流動,流到合并數據這一關。這時,每個業務模塊又開始忙碌起來,按照順序合并自己的數據。系統的洪流繼續向下流動,流動到“數據整理”這一關,各個業務模塊繼續忙碌,直到全部結束……具體實現
我們可以做出如下的類定義:
(圖6)合服工具類定義
在合服工具的主控類中,規定了調用順序。即,先調用“清理無用數據”類,再調用“合并數據”,最后調用“整理數據”。注意:這 3 個類都是抽象類,需要具體的實現!具體實現如下定義:
(圖7)具體實現類
這一步也很好理解。我們可以做一步整理,比如把“清理無用數據”、“合并數據”、“整理數據”這三步的函數名都改成一樣的!如圖 8 所示:
(圖8)將所有的函數名改為相同的名字
這一步只有目的的,之后,我們可以將“工作”函數提取到一個接口里。如圖 9 所示:
(圖9)提取“工作節點”接口
“合服工具主控類”,現在不再依賴“清理無用數據”、“合并數據”、“整理數據”這 3 個抽象類了。而是直接依賴“工作節點”接口!而且,這個接口只提供一個函數,就是“工作”。這樣做的意義在于高度抽象,讓主控類徹底擺脫具體實現!我們甚至可以干掉這 3 個抽象類,讓具體類直接實現“工作節點”接口。如圖 10 所示:
(圖10)令具體實現類直接繼承“工作節點”接口
這樣,主控類無需知道有多少個具體實現,更無需知道他們具體是怎么實現的?最后我們要做的,就是把這些規整到不同目錄下去,如圖 11 所示:
(圖11)強具體實現類規整到不同目錄中
主控類,只知道調用“Clear”、“Combine”、“Order”這 3 個目錄里的“工作節點”實現類,而這些實現類都只有一個統一的命名的方法:“工作”。這樣主控程序省了很大心。而對于每個具體的實現類,他們只要保證自己的工作是正確無誤的就可以了……
看著圖,回想一下之前提到的工作流(Work Flow),這就是一個具體實現過程!這是也是設計中的責任鏈模式。是用責任鏈模式的一大好處就是,我可以任意添加和刪除工作節點,甚至是修改工作節點的鏈接順序,但又不用修改大量代碼。例如,我想在“Clear”和“Combine”這兩大關之間增加一個“Fixed”用于修復一些錯誤的數據,那么,只要增加一個新目錄,并增加“工作節點”的實現類,就可以了。其它代碼根本不用動……當然,主控類還要稍微修改一下。PHP
最后,再提一下 PHP 語言。合服工具使用 PHP 語言開發,不是因為 PHP 是世界上最好的語言(這話扔出來,不知道能不能拉點仇恨吸點流量)。而是因為PHP 語言的動態性可以滿足易擴展的需求,它不像編譯語言那樣,每次更新的時候都需要編譯和打包。PHP 可以直接修改代碼。利用 PHP 的動態性和文件系統的目錄結構,可以很輕松的實現合服框架代碼。
</div>
來自:http://my.oschina.net/hjj2017/blog/492450
-
-
-
-
-