背景: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); } } } } }