背景:

最近公司游戏开发需要知道游戏加载的流失率。因为,我们做的是网页游戏。玩过网页游戏的人都知道,进入游戏前要加载一些资源。最后才能到达创建角色的游戏界面。我们有一个需求就是要统计在加载过程中还未到达角色创建界面而流失的用户数量。

我们在加载开始就进行统计人数,加载完成之后再记录人数。这样,通过用加载前的人数减去成功加载后的人数。就知道了加载的流失率。就可以知道游戏是否还要继续优化加载过程,降低用户加载游戏率。

由于,我们的量都是从*主流的合作媒体进行导量过来。所以,并发非常高,据粗略计算应该能达到每秒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游戏工作室负责做数据分析的时候写的代码。希望对大家有所帮助。不知道这算不算泄漏机密?

原文地址:http://blog.aizhet.com/PHP/8350.html

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读写文件高并发处理实例-转的更多相关文章

  1. [WinAPI] API 10 [创建、打开、读写文件,获取文件大小]

    在Windows系统中,创建和打开文件都是使用API函数CreateFile,CreateFile通过指定不同的参数来表示是新建一个文件,打开已经存在的文件,还是重新建立文件等.读写文件最为直接的方式 ...

  2. 你好,C++(5)如何输出数据到屏幕、从屏幕输入数据与读写文件?

    2.2  基本输入/输出流 听过HelloWorld.exe的自我介绍之后,大家已经知道了一个C++程序的任务就是描述数据和处理数据.这两大任务的对象都是数据,可现在的问题是,数据不可能无中生有地产生 ...

  3. akka框架——异步非阻塞高并发处理框架

    akka actor, akka cluster akka是一系列框架,包括akka-actor, akka-remote, akka-cluster, akka-stream等,分别具有高并发处理模 ...

  4. Python读写文件

    Python读写文件1.open使用open打开文件后一定要记得调用文件对象的close()方法.比如可以用try/finally语句来确保最后能关闭文件. file_object = open('t ...

  5. php中并发读写文件冲突的解决方案

    在这里提供4种高并发读写文件的方案,各有优点,可以根据自己的情况解决php并发读写文件冲突的问题. 对于日IP不高或者说并发数不是很大的应用,一般不用考虑这些!用一般的文件操作方法完全没有问题.但如果 ...

  6. 161228、Java IO流读写文件的几个注意点

    平时写IO相关代码机会挺少的,但却都知道使用BufferedXXXX来读写效率高,没想到里面还有这么多陷阱,这两天突然被其中一个陷阱折腾一下:读一个文件,然后写到另外一个文件,前后两个文件居然不一样? ...

  7. C#常用IO流与读写文件

    .文件系统 ()文件系统类的介绍 文件操作类大都在System.IO命名空间里.FileSystemInfo类是任何文件系统类的基类:FileInfo与File表示文件系统中的文件:Directory ...

  8. php中并发读写文件冲突的解决方案(文件锁应用示例)

    PHP(外文名: Hypertext Preprocessor,中文名:“超文本预处理器”)是一种通用开源脚本语言.语法吸收了C语言.Java和Perl的特点,入门门槛较低,易于学习,使用广泛,主要适 ...

  9. 161108、Java IO流读写文件的几个注意点

    平时写IO相关代码机会挺少的,但却都知道使用BufferedXXXX来读写效率高,没想到里面还有这么多陷阱,这两天突然被其中一个陷阱折腾一下:读一个文件,然后写到另外一个文件,前后两个文件居然不一样? ...

随机推荐

  1. Frugalware Linux 1.9 RC1 发布

    Frugalware Linux 1.9 RC1 发布了,下载地址:fvbe-1.9rc1-full-x86_64.iso (1,874MB, SHA1). 发行通知:http://www.fruga ...

  2. 从“程序员转行卖烧饼”想到IT人创业

    我的一个朋友最近总在跟我念叨着“我不想做开发了,整天累死累活写程序,也攒不下几个钱.我想辞职搞点啥!” 我问他:“你想搞点啥?”. 他说:“搞啥都比做开发强,做个网站赚广告费,接私活……实在不行我去卖 ...

  3. C语言 稀疏矩阵 压缩 实现

    稀疏矩阵压缩存储的C语言实现 (GCC编译). /** * @brief C语言 稀疏矩阵 压缩 实现 * @author wid * @date 2013-11-04 * * @note 若代码存在 ...

  4. 解读SQL Server 2014可更新列存储索引——存储机制

    概述 SQL Server 2014被号称是微软数据库的一个革命性版本,其性能的提升的幅度是有史以来之最. 可更新的列存储索引作为SQL Server 2014的一个关键功能之一,在提升数据库的查询性 ...

  5. Linux:Shell 常用通配符

    字符 含义 * 匹配 0 或多个字符 ? 匹配任意一个字符 [list] 匹配 list 中的任意单一字符 [!list] 匹配 除list 中的任意单一字符以外的字符 [c1-c2] 匹配 c1-c ...

  6. Minifying Angular应用时产生的问题

    一.产生的问题 如果你正在进行AngularJS的项目开发,生产时Minified JS文件有没有遇到下面问题: angular.module("myApp", []) .cont ...

  7. JAVA软件开发职责

    1.了解OO,AOP,SOA设计模式.J2EE的核心设计模式.应用架构模式和应用集成模式:2.熟练使用Spring.Hibernate/ibatis.Struts等通用性开源框架,并对其原理有深刻的理 ...

  8. javascript中重要概念-闭包-深入理解

    在上次的分享中javascript--函数参数与闭包--详解,对闭包的解释不够深入.本人经过一段时间的学习,对闭包的概念又有了新的理解.于是便把学习的过程整理成文章,一是为了加深自己闭包的理解,二是给 ...

  9. 【Android】Android内存机制,了解Android堆和栈

    1.dalvik的Heap和Stack 这里说的只是dalvik java部分的内存,实际上除了dalvik部分,还有native.     下面针对上面列出的数据类型进行说明,只有了解了我们申请的数 ...

  10. Liferay7 BPM门户开发之34: liferay7对外服务类生成(RestService Get Url)

    在liferay7中开发不依赖Service Builder的对外服务类,非常简洁,只需要2点注解: 在类的前部定义: @ApplicationPath("/PathXXX") 方 ...