背景:php
最近公司遊戲開發須要知道遊戲加載的流失率。由於,咱們作的是網頁遊戲。玩過網頁遊戲的人都知道,進入遊戲前要加載一些資源。最後才能到達建立角色的遊戲界面。咱們有一個需求就是要統計在加載過程當中還未到達角色建立界面而流失的用戶數量。html
咱們在加載開始就進行統計人數,加載完成以後再記錄人數。這樣,經過用加載前的人數減去成功加載後的人數。就知道了加載的流失率。就能夠知道遊戲是否還要繼續優化加載過程,下降用戶加載遊戲率。數據庫
因爲,咱們的量都是從*主流的合做媒體進行導量過來。因此,併發很是高,據粗略計算應該能達到每秒1000左右的併發數量。json
加載前的人數原本想放到遊戲內部的緩存平臺。可是,遊戲後端的同事擔憂併發過高,致使資源無端浪費。由於,內存的釋放並非實時響應的。因此,將統計的人數放到在另一臺服務器:統計服務器。後端
我剛開始採用的方案以下:
經過php的file_get_contents()與file_put_contents()進行讀取與寫入。第一次讀寫就向文件寫入1,第二次加載就在原來的基礎上加1.以此類推.這種順序的思想徹底不存在任何問題。問題就出在,咱們的服務器不多是順序形式的。數組
準確的說,併發的訪問不是順序的。當A玩家加載遊戲讀取到文件裏面的數字100(假如這時是100),B玩家讀取到的也是100,這時,處理A玩家的線程就是在100的基礎上加1,獲得101,就會向文件寫入101。緩存
處理B玩家的線程也獲得相同的結果,將101寫入文件。這時,問題就出現了?B玩家是在A玩家以後加載遊戲的,理應獲得102的計算結果。服務器
這就是併發致使的問題。這個時候,我想到了採用fopen()打開文件,並用flock()加一個寫入鎖。你們必定會認爲,這種方式有了鎖定,那麼就不會形成問題了。其實,也是錯的。併發
由於,咱們的問題不是出在寫入上面。而是讀取的時候形成數據的不一樣步。OK。到這裏,我實在百度谷歌都搞不定了。app
當但願寄託在PHP函數自己而夢碎的時候,我只能另尋它法。脫離它。因而,我想到了*語言的Map映射的機制。相似於咱們的PHP數組,每加載一次就我往數組添加一個元素。這樣,到最後我只須要count()一下數組就知道了有多少玩家加載了遊戲。
可是,用數組的話,也存在一個問題。就是PHP的變量仍是常量,在腳本執行完畢以後都會本身清掉。因而,我想到了文件保存的方式。
最終的可行方案思路以下:
用fopen打開一個文件,以只寫的方式。而後寫鎖定。玩家每加載一次我就向文件裏面寫入一個數字1,最後獲得的文件內容經過file_get_contents()一次性讀取出來,再用strlen()計算一下長度即知道了有多少玩家加載了遊戲。
聽聞flock()函數會鎖定會形成系統資源在不少時間升高。因此,我採用你們所使用的方式,用微秒超時的技術解決這個問題。若是,走出這個時間我就*掉它。具體的代碼以下:
// loadcount.func.php 函數文件。 /** * 獲取某來源和某服務器ID的遊戲加載次數。 * * @param string $fromid 來源標識。 * @param int $serverid 服務器ID編號。 * * @return int */ function getLoadCount($fromid, $serverid) { global $g_global; $serverid = (int) $serverid; $fromid = md5($fromid); $filename = $fromid . $serverid . '.txt'; $data = file_get_contents($filename); return strlen($data); } /** * 獲取某來源全部服務器的遊戲加載次數。 * * @param string $fromid 來源標識。 * * @return int */ function getAllLoadCount($fromid) { global $g_global; $fromid = md5($fromid); $count = 0; foreach (glob("{$fromid}*.txt") as $filename) { $file_content = file_get_contents($filename); $count += strlen($file_content); } return $count; } /** * 清空全部的加載數據。 * * @return void */ function clearLoadCount() { foreach (glob("*.txt") as $filename) { unlink($filename); } return true; } /** * 延遲更新遊戲加載次數中間件。 * * 使用此函數來延遲更新數據,原理:當不足1000次的時候,不更新數據庫,超過1000就更新到數據庫裏面去。 * * @param string $fromid 來源標識。 * @param int $serverid 服務器ID編號。 */ function delayAddLoadCount($fromid, $serverid) { // 使用MD5生成文件名記錄緩存次數。 $fromid = md5($fromid); $filename = $fromid . $serverid . '.txt'; if($fp = fopen($filename, 'a')) { $startTime = microtime(); do { $canWrite = flock($fp, LOCK_EX); if(!$canWrite) { usleep(round(mt_rand(0, 100)*1000)); } } while ( ( !$canWrite ) && ( ( microtime()- $startTime ) < 1000 ) ); if ($canWrite) { fwrite($fp, "1"); } fclose($fp); } return true; }
如下是我調用以上方法的文件:
< ?php /** * @describe 平臺用戶加載遊戲次數統計接口入口。 * @date 2012.12.17 */ include_once './loadcount.func.php'; // 測試用。 // $_GET['fromid'] = '4399'; // $_GET['serverid'] = mt_rand(0, 5); // 添加加載次數。 if ( $_GET['action'] == 'addcount' ) { $fromid = $_GET['fromid']; // 來源標識。 $serverid = $_GET['serverid']; // 服務器ID編號。 $return = delayAddLoadCount($fromid, $serverid); $return = $return ? 1 : 0; ob_clean(); echo json_encode($return); exit; } // 取加載次數。 elseif ( $_GET['action'] == 'getcount' ) { $fromid = $_GET['fromid']; // 來源標識。 if ( !isset( $_GET['serverid'] ) ) // 有服務器編號 ID則取來源對應的服務器加載次數。 { $count = getAllLoadCount($fromid); } else // 加載對應來源的次數。 { $serverid = $_GET['serverid']; // 服務器ID編號。 $count = getLoadCount($fromid, $serverid); } ob_clean(); header('Content-Type:text/html;charset=UTF-8'); $serverid = strlen($serverid) ? $serverid : '無'; echo "來源:{$fromid},服務器ID:{$serverid},遊戲加載次數:" . $count; exit; } // 清除加載次數。 elseif ( $_GET['action'] == 'clearcount' ) { header('Content-Type:text/html;charset=UTF-8'); $return = clearLoadCount(); if ($return) { echo "清除成功!"; } else { echo "清除失敗!"; } }
這是血的教訓,因此,我不得不將它記錄下來。以備之後讓他人借鑑。
本文是做者寒冰一年前在4399遊戲工做室負責作數據分析的時候寫的代碼。但願對你們有所幫助。不知道這算不算泄漏機密?
原文地址:http://blog.aizhet.com/PHP/8350.html
<?php /** * Created by PhpStorm. * User: andyfeng * Date: 2015/6/24 * Time: 13:31 */ class LogFileUtil { public static $fileHandlerCache; private static $initFlag = false; private static $MAX_LOOP_COUNT = 3; private static function init() { self::$initFlag = true; register_shutdown_function(array("LogFileUtil", "shutdown_func")); } /** * 輸出到文件日誌 * @param $filePath 文件路徑 * @param $msg 日誌信息 * @return int */ public static function out($filePath, $msg) { if (!self::$initFlag) { self::init(); } return self::internalOut($filePath, $msg); } /** * @param $filePath * @param $msg * @param $loop * @return int */ private static function internalOut($filePath, $msg, $loop = 0) { //以防一直添加失敗形成死循環 if ($loop > self::$MAX_LOOP_COUNT) { $result = 0; } else { $loop++; $fp = self::$fileHandlerCache["$filePath"]; if (empty($fp)) { $fp = fopen($filePath, "a+"); self::$fileHandlerCache[$filePath] = $fp; } if (flock($fp, LOCK_EX)) { $result = fwrite($fp, $msg); flock($fp, LOCK_UN); } else { $result = self::internalOut($filePath, $msg, $loop); } } return $result; } function shutdown_func() { if (!empty(LogFileUtil::$fileHandlerCache)) { if (is_array(LogFileUtil::$fileHandlerCache)) { foreach (LogFileUtil::$fileHandlerCache as $k => $v) { if (is_resource($v)) //file_put_contents("close.txt",$k); fclose($v); } } } } }