对于日IP不多或者并发不高的应用,就不用考虑这些了!!一般的文件操作方式是没有问题的。但如果并发很高的话,我们读写文件的时候,很有可能会有多个进程对同一个文件进行操作,如果不对文件的访问进行相应的独占,就很容易出现数据丢失的情况。举个例子:在一个在线聊天室里(这里假设聊天内容是写入文件的),同一时刻,用户A和用户B都在操作数据保存文件。首先,A打开文件并且更新了里面的数据,但是这里B也恰好打开了同一个文件并且也准备更新里面的数据。当A保存写入的文件的时候,B其实已经打开了这个文件。但是当B再把文件保存回去的时候,数据丢失已经发生了,因为用户B根本不知道当他修改自己打开的文件的时候,用户A也修改了这个文件,所以当用户B保存修改的时候,用户A的更新就会丢失。对于这样的问题,一般的解决办法是,当一个进程操作一个文件时,它首先锁定其他进程,这意味着只有这个进程才有权读取该文件。如果现在其他进程读取它,完全没有问题,但如果此时有一个进程试图更新它,操作就会被拒绝。如果之前锁定该文件的进程完成了对该文件的更新操作,独占标志就会被释放,文件就会恢复到可修改的状态。同样,如果进程在操作文件时文件没有被锁定,它就可以安全地锁定该文件并单独使用它。所以一般的解决办法是:
$fp = fopen ( "/tmp/lock.txt", "w+" ); if (flock ( $fp, LOCK_EX )) { fwrite ( $fp, "Write something here\n" ); flock ( $fp, LOCK_UN ); } else { echo "Couldn't lock the file !"; } fclose ( $fp );
但是在PHP中,flock好像就没那么好用了!在多并发的情况下,好像经常会独占资源不立刻释放,或者根本不释放,从而造成死锁,使得服务器的CPU占用率很高,有时候甚至会让服务器彻底死掉。好像在很多Linux/Unix系统中都会出现这种情况。所以在使用flock之前,一定要慎重考虑。那么是不是就没有解决办法了呢?其实不然,如果我们合理使用flock(),是完全可以解决死锁问题的。当然,如果我们不考虑使用flock()函数,也会有很好的方案来解决我们的问题。经过我个人的收集和总结,我总结了以下几种解决办法。
解决方案 1:锁定文件时设置超时时间。
if ($fp = fopen ( $fileName, 'a' )) { $startTime = microtime (); do { $canWrite = flock ( $fp, LOCK_EX ); if (! $canWrite) usleep ( round ( rand ( 0, 100 ) * 1000 ) ); } while ( (! $canWrite) && ((microtime () - $startTime) < 1000) ); if ($canWrite) { fwrite ( $fp, $dataToSave ); } fclose ( $fp ); }
超时时间设置为1ms,如果在这个时间内没有获取到锁,那么就会重复获取,直到直接获取到文件操作权,当然如果到达超时限制,那么就必须立刻退出,并放弃锁,让其他进程进行操作。
解决方法二:不使用flock功能,使用临时文件解决读写冲突问题。
总体原则如下:
1、考虑将需要更新的文件拷贝一份到我们的临时文件目录下,将该文件的最后修改时间保存到一个变量中,并赋予这个临时文件一个随机的、不重复的文件名。
2、更新临时文件后,检查原文件最后更新时间与之前保存的时间是否一致。
3、若最后修改时间相同,则将修改后的临时文件重命名为原文件。为了保证文件状态同步更新,需要清除文件状态。
4.但如果最后修改时间和之前保存的时间一致,说明这段时间内原文件被修改过,此时需要删除临时文件并返回false,表示此时该文件正在被其他进程操作。
大致的实现代码如下:
$dir_fileopen = "tmp"; function randomid() { return time () . substr ( md5 ( microtime () ), 0, rand ( 5, 12 ) ); } function cfopen($filename, $mode) { global $dir_fileopen; clearstatcache (); do { $id = md5 ( randomid ( rand (), TRUE ) ); $tempfilename = $dir_fileopen . "/" . $id . md5 ( $filename ); } while ( file_exists ( $tempfilename ) ); if (file_exists ( $filename )) { $newfile = false; copy ( $filename, $tempfilename ); } else { $newfile = true; } $fp = fopen ( $tempfilename, $mode ); return $fp ? array ($fp, $filename, $id, @filemtime ( $filename ) ) : false; } function cfwrite($fp, $string) { return fwrite ( $fp [0], $string ); } function cfclose($fp, $debug = "off") { global $dir_fileopen; $success = fclose ( $fp [0] ); clearstatcache (); $tempfilename = $dir_fileopen . "/" . $fp [2] . md5 ( $fp [1] ); if ((@filemtime ( $fp [1] ) == $fp [3]) || ($fp [4] == true && ! file_exists ( $fp [1] )) || $fp [5] == true) { rename ( $tempfilename, $fp [1] ); } else { unlink ( $tempfilename ); //说明有其它进程 在操作目标文件,当前进程被拒绝 $success = false; } return $success; } $fp = cfopen ( 'lock.txt', 'a+' ); cfwrite ( $fp, "welcome to beijing.\n" ); fclose ( $fp, 'on' );
对于上述代码用到的函数,需要解释一下:
1.();重命名文件或目录。该函数比较像Linux中的mv,可以很方便的更新文件或目录的路径或名称。
但是当我测试上面的代码时,如果新文件名已经存在,它会给出一个错误,说当前文件已经存在。但它在Linux上运行良好。
2.();清除文件状态。PHP 会缓存所有文件属性信息以提供更高的性能,但有时候,当多个进程删除或更新文件时,PHP 没有来得及更新缓存中的文件属性,这可能会导致访问到的最后更新时间不是真实的数据。所以这里需要使用这个函数来清除保存的缓存。
解决方案三:对需要操作的文件进行随机读写,减少并发的可能性。
这个方案好像在记录用户访问日志的时候用的比较多。
前面我们需要定义一个随机空间,这个空间越大,并发的可能性就越小,这里我们假设随机读写空间为[1-500],那么我们的日志文件的分布就会在log1~~之间,每次用户访问的时候,数据都会被随机写入到log1~~之间的任意一个文件中。
同时有两个进程在记录日志,进程A可能更新log32文件,那进程B呢?这时候更新的可能性就是。要知道如果进程B要操作log32的话,这个概率基本就是1/500,几乎为零。
当我们需要分析访问日志的时候,只需要先将日志合并,然后进行分析即可。
采用这种方案记录日志的一个好处是,进程操作排队的可能性比较小,使得进程能够非常快速的完成每个操作。
方案四:把需要操作的所有进程放入一个队列,然后放入一个专门的服务来完成文件操作。
队列中每一个被排除的进程都相当于第一个具体的操作,所以第一次我们的服务只需要从队列中获取相当于具体操作项即可。如果这里还有大量的文件操作进程,那也没关系,直接放到我们队列后面就行了。只要你愿意排队,队列多长都无所谓。
对于前面几种方案,各有各的优点!大致可以归纳为两类:
1.需要排队(影响缓慢)如方案1、2、4
2. 无需排队。(效果立竿见影)选项 3
在设计缓存系统的时候,我们一般不采用方案三,这是因为方案三的分析程序和写入程序是不同步的,写入的时候完全不考虑分析的难度,只做写入。试想一下,当我们更新一个缓存的时候,如果也采用随机文件读写的方式,读缓存的时候好像会增加很多进程。但是方案一和方案二就完全不一样了,虽然写入的时候需要等待(没有成功获取锁的时候会重复获取),但是读取文件的时候却非常方便。增加缓存的目的是为了减少数据读取的瓶颈,从而提高系统性能。
以上是我个人经验总结和一些信息,如果有错误或者没有提到的地方,欢迎大家指出。
扫一扫在手机端查看
-
Tags : php fwrite 并发
我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求,请立即点击咨询我们或拨打咨询热线: 13761152229,我们会详细为你一一解答你心中的疑难。