本文實例講述了PHP讀寫文件高并發處理操作。分享給大家供大家參考,具體如下:
背景:
最近公司游戲開發需要知道游戲加載的流失率。因為,我們做的是網頁游戲。玩過網頁游戲的人都知道,進入游戲前要加載一些資源。最后才能到達創建角色的游戲界面。我們有一個需求就是要統計在加載過程中還未到達角色創建界面而流失的用戶數量。
我們在加載開始就進行統計人數,加載完成之后再記錄人數。這樣,通過用加載前的人數減去成功加載后的人數。就知道了加載的流失率。就可以知道游戲是否還要繼續優化加載過程,降低用戶加載游戲率。
由于,我們的量都是從*主流的合作媒體進行導量過來。所以,并發非常高,據粗略計算應該能達到每秒1000左右的并發數量。
加載前的人數本來想放到游戲內部的緩存平臺。但是,游戲后端的同事擔心并發太高,導致資源無故浪費。因為,內存的釋放并不是實時響應的。所以,將統計的人數放到在另外一臺服務器:統計服務器。
我剛開始采用的方案如下:
通過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。到這里,我實在百度谷歌都搞不定了。
當希望寄托在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游戲工作室負責做數據分析的時候寫的代碼。希望對大家有所幫助。
PHP數據庫操作之高并發實例
高并發下PHP寫文件日志丟失
?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);
}
}
}
}
}
更多關于PHP相關內容感興趣的讀者可查看本站專題:《php文件操作總結》、《php緩存技術總結》、《PHP運算與運算符用法總結》、《PHP基本語法入門教程》、《php面向對象程序設計入門教程》、《php字符串(string)用法總結》、《php+mysql數據庫操作入門教程》及《php常見數據庫操作技巧匯總》
希望本文所述對大家PHP程序設計有所幫助。
您可能感興趣的文章:- PHP文件讀寫操作相關函數總結
- php中讀寫文件與讀寫數據庫的效率比較分享
- php格式文件打開的四種方法
- php實現以只讀方式打開文件的方法
- PHP打開和關閉文件操作函數總結
- php打開文件fopen函數的使用說明
- PHP 處理TXT文件(打開/關閉/檢查/讀取)
- php文件怎么打開 如何執行php文件
- PHP文件打開、關閉、寫入的判斷與執行代碼
- PHP文件打開關閉及讀寫操作示例解析