PHP讀寫(xiě)文件高并發(fā)處理操作實(shí)例詳解
本文實(shí)例講述了PHP讀寫(xiě)文件高并發(fā)處理操作。分享給大家供大家參考,具體如下:
背景:
最近公司游戲開(kāi)發(fā)需要知道游戲加載的流失率。因?yàn)?,我們做的是網(wǎng)頁(yè)游戲。玩過(guò)網(wǎng)頁(yè)游戲的人都知道,進(jìn)入游戲前要加載一些資源。最后才能到達(dá)創(chuàng)建角色的游戲界面。我們有一個(gè)需求就是要統(tǒng)計(jì)在加載過(guò)程中還未到達(dá)角色創(chuàng)建界面而流失的用戶(hù)數(shù)量。
我們?cè)诩虞d開(kāi)始就進(jìn)行統(tǒng)計(jì)人數(shù),加載完成之后再記錄人數(shù)。這樣,通過(guò)用加載前的人數(shù)減去成功加載后的人數(shù)。就知道了加載的流失率。就可以知道游戲是否還要繼續(xù)優(yōu)化加載過(guò)程,降低用戶(hù)加載游戲率。
由于,我們的量都是從*主流的合作媒體進(jìn)行導(dǎo)量過(guò)來(lái)。所以,并發(fā)非常高,據(jù)粗略計(jì)算應(yīng)該能達(dá)到每秒1000左右的并發(fā)數(shù)量。
加載前的人數(shù)本來(lái)想放到游戲內(nèi)部的緩存平臺(tái)。但是,游戲后端的同事?lián)牟l(fā)太高,導(dǎo)致資源無(wú)故浪費(fèi)。因?yàn)?,?nèi)存的釋放并不是實(shí)時(shí)響應(yīng)的。所以,將統(tǒng)計(jì)的人數(shù)放到在另外一臺(tái)服務(wù)器:統(tǒng)計(jì)服務(wù)器。
我剛開(kāi)始采用的方案如下:
通過(guò)php的file_get_contents()
與file_put_contents()
進(jìn)行讀取與寫(xiě)入。第一次讀寫(xiě)就向文件寫(xiě)入1,第二次加載就在原來(lái)的基礎(chǔ)上加1.以此類(lèi)推.這種順序的思想完全不存在任何問(wèn)題。問(wèn)題就出在,我們的服務(wù)器不可能是順序形式的。
準(zhǔn)確的說(shuō),并發(fā)的訪(fǎng)問(wèn)不是順序的。當(dāng)A玩家加載游戲讀取到文件里面的數(shù)字100(假如這時(shí)是100),B玩家讀取到的也是100,這時(shí),處理A玩家的線(xiàn)程就是在100的基礎(chǔ)上加1,得到101,就會(huì)向文件寫(xiě)入101。
處理B玩家的線(xiàn)程也得到相同的結(jié)果,將101寫(xiě)入文件。這時(shí),問(wèn)題就出現(xiàn)了?B玩家是在A玩家之后加載游戲的,理應(yīng)得到102的計(jì)算結(jié)果。
這就是并發(fā)導(dǎo)致的問(wèn)題。這個(gè)時(shí)候,我想到了采用fopen()
打開(kāi)文件,并用flock()
加一個(gè)寫(xiě)入鎖。大家一定會(huì)認(rèn)為,這種方式有了鎖定,那么就不會(huì)造成問(wèn)題了。其實(shí),也是錯(cuò)的。
因?yàn)?,我們的?wèn)題不是出在寫(xiě)入上面。而是讀取的時(shí)候造成數(shù)據(jù)的不同步。OK。到這里,我實(shí)在百度谷歌都搞不定了。
當(dāng)希望寄托在PHP函數(shù)本身而夢(mèng)碎的時(shí)候,我只能另尋它法。脫離它。于是,我想到了*語(yǔ)言的Map映射的機(jī)制。類(lèi)似于我們的PHP數(shù)組,每加載一次就我往數(shù)組添加一個(gè)元素。這樣,到最后我只需要count()
一下數(shù)組就知道了有多少玩家加載了游戲。
但是,用數(shù)組的話(huà),也存在一個(gè)問(wèn)題。就是PHP的變量還是常量,在腳本執(zhí)行完畢之后都會(huì)自己清掉。于是,我想到了文件保存的方式。
最終的可行方案思路如下:
用fopen打開(kāi)一個(gè)文件,以只寫(xiě)的方式。然后寫(xiě)鎖定。玩家每加載一次我就向文件里面寫(xiě)入一個(gè)數(shù)字1,最后得到的文件內(nèi)容通過(guò)file_get_contents()
一次性讀取出來(lái),再用strlen()
計(jì)算一下長(zhǎng)度即知道了有多少玩家加載了游戲。
聽(tīng)聞flock()
函數(shù)會(huì)鎖定會(huì)造成系統(tǒng)資源在很多時(shí)間升高。所以,我采用大家所使用的方式,用微秒超時(shí)的技術(shù)解決這個(gè)問(wèn)題。如果,走出這個(gè)時(shí)間我就*掉它。具體的代碼如下:
// loadcount.func.php 函數(shù)文件。 /** * 獲取某來(lái)源和某服務(wù)器ID的游戲加載次數(shù)。 * * @param string $fromid 來(lái)源標(biāo)識(shí)。 * @param int $serverid 服務(wù)器ID編號(hào)。 * * @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); } /** * 獲取某來(lái)源所有服務(wù)器的游戲加載次數(shù)。 * * @param string $fromid 來(lái)源標(biāo)識(shí)。 * * @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; } /** * 清空所有的加載數(shù)據(jù)。 * * @return void */ function clearLoadCount() { foreach (glob("*.txt") as $filename) { unlink($filename); } return true; } /** * 延遲更新游戲加載次數(shù)中間件。 * * 使用此函數(shù)來(lái)延遲更新數(shù)據(jù),原理:當(dāng)不足1000次的時(shí)候,不更新數(shù)據(jù)庫(kù),超過(guò)1000就更新到數(shù)據(jù)庫(kù)里面去。 * * @param string $fromid 來(lái)源標(biāo)識(shí)。 * @param int $serverid 服務(wù)器ID編號(hào)。 */ function delayAddLoadCount($fromid, $serverid) { // 使用MD5生成文件名記錄緩存次數(shù)。 $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; }
以下是我調(diào)用以上方法的文件:
< ?php /** * @describe 平臺(tái)用戶(hù)加載游戲次數(shù)統(tǒng)計(jì)接口入口。 * @date 2012.12.17 */ include_once './loadcount.func.php'; // 測(cè)試用。 // $_GET['fromid'] = '4399'; // $_GET['serverid'] = mt_rand(0, 5); // 添加加載次數(shù)。 if ( $_GET['action'] == 'addcount' ) { $fromid = $_GET['fromid']; // 來(lái)源標(biāo)識(shí)。 $serverid = $_GET['serverid']; // 服務(wù)器ID編號(hào)。 $return = delayAddLoadCount($fromid, $serverid); $return = $return ? 1 : 0; ob_clean(); echo json_encode($return); exit; } // 取加載次數(shù)。 elseif ( $_GET['action'] == 'getcount' ) { $fromid = $_GET['fromid']; // 來(lái)源標(biāo)識(shí)。 if ( !isset( $_GET['serverid'] ) ) // 有服務(wù)器編號(hào) ID則取來(lái)源對(duì)應(yīng)的服務(wù)器加載次數(shù)。 { $count = getAllLoadCount($fromid); } else // 加載對(duì)應(yīng)來(lái)源的次數(shù)。 { $serverid = $_GET['serverid']; // 服務(wù)器ID編號(hào)。 $count = getLoadCount($fromid, $serverid); } ob_clean(); header('Content-Type:text/html;charset=UTF-8'); $serverid = strlen($serverid) ? $serverid : '無(wú)'; echo "來(lái)源:{$fromid},服務(wù)器ID:{$serverid},游戲加載次數(shù):" . $count; exit; } // 清除加載次數(shù)。 elseif ( $_GET['action'] == 'clearcount' ) { header('Content-Type:text/html;charset=UTF-8'); $return = clearLoadCount(); if ($return) { echo "清除成功!"; } else { echo "清除失??!"; } }
這是血的教訓(xùn),所以,我不得不將它記錄下來(lái)。以備以后讓他人借鑒。
本文是作者寒冰一年前在4399游戲工作室負(fù)責(zé)做數(shù)據(jù)分析的時(shí)候?qū)懙拇a。希望對(duì)大家有所幫助。
PHP數(shù)據(jù)庫(kù)操作之高并發(fā)實(shí)例
高并發(fā)下PHP寫(xiě)文件日志丟失
<?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) { //以防一直添加失敗造成死循環(huán) 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); } } } } }
更多關(guān)于PHP相關(guān)內(nèi)容感興趣的讀者可查看本站專(zhuān)題:《php文件操作總結(jié)》、《php緩存技術(shù)總結(jié)》、《PHP運(yùn)算與運(yùn)算符用法總結(jié)》、《PHP基本語(yǔ)法入門(mén)教程》、《php面向?qū)ο蟪绦蛟O(shè)計(jì)入門(mén)教程》、《php字符串(string)用法總結(jié)》、《php+mysql數(shù)據(jù)庫(kù)操作入門(mén)教程》及《php常見(jiàn)數(shù)據(jù)庫(kù)操作技巧匯總》
希望本文所述對(duì)大家PHP程序設(shè)計(jì)有所幫助。
相關(guān)文章
Warning:?require():?open_basedir?restriction?in?effect,
在linux服務(wù)器部署thinkphp5的時(shí)候PHP報(bào)了Warning:?require():?open_basedir?restriction?in?effect這個(gè)錯(cuò)誤,是因?yàn)榫W(wǎng)站目錄配置錯(cuò)誤,PHP不能引入其授權(quán)目錄上級(jí)及其以上的文件。下面詳細(xì)講解如何處理這個(gè)問(wèn)題,需要的朋友可以參考下2022-11-11php和html的區(qū)別點(diǎn)詳細(xì)總結(jié)
在本篇文章里小編給大家整理了關(guān)于php和html的區(qū)別點(diǎn),有需要的朋友們可以參考下。2019-09-09PHP使用redis實(shí)現(xiàn)分布式鎖的示例詳解
分布式鎖是控制分布式系統(tǒng)之間同步訪(fǎng)問(wèn)共享資源的一種方式。實(shí)現(xiàn)分布式鎖的原理很簡(jiǎn)單,本文就將利用redis實(shí)現(xiàn)分布式鎖,感興趣的可以了解一下2022-11-11基于php雙引號(hào)中訪(fǎng)問(wèn)數(shù)組元素報(bào)錯(cuò)的解決方法
下面小編就為大家分享一篇基于php雙引號(hào)中訪(fǎng)問(wèn)數(shù)組元素報(bào)錯(cuò)的解決方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-02-02mysql_connect localhost和127.0.0.1的區(qū)別(網(wǎng)絡(luò)層闡述)
這篇文章主要介紹了mysql_connect localhost和127.0.0.1的區(qū)別(網(wǎng)絡(luò)層闡述),本文從網(wǎng)絡(luò)通信層面講解了它們的不同,需要的朋友可以參考下2015-03-03PHP實(shí)現(xiàn)通用alert函數(shù)的方法
這篇文章主要介紹了PHP實(shí)現(xiàn)通用alert函數(shù)的方法,實(shí)例分析了php自定義alert函數(shù)實(shí)現(xiàn)提示信息的技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-03-03