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


Posted in PHP onOctober 25, 2013

对于日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()函数,也同样会有很好的解决方案来解决我们的问题。经过我个人的搜集和总结,大致归纳了解决方案有如下几种。
方案一:对文件进行加锁时,设置一个超时时间。大致实现如下:
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)rename();重命名一个文件或一个目录,该函数其实更像linux里的mv。更新文件或者目录的路径或名字很方便。但当我在window测试上面代码时,如果新文件名已经存在,会给出一个notice,说当前文件已经存在。但在linux下工作的很好。
(2)clearstatcache();清除文件的状态.php将缓存所有文件属性信息,以提供更高的性能,但有时,多进程在对文件进行删除或者更新操作时,php没来得及更新缓存里的文件属性,容易导致访问到最后更新时间不是真实的数据。所以这里需要使用该函数对已保存的缓存进行清除。

方案三:对操作的文件进行随机读写,以降低并发的可能性。
在对用户访问日志进行记录时,这种方案似乎被采用的比较多。先前需要定义一个随机空间,空间越大,并发的的可能性就越小,这里假设随机读写空间为[1-500],那么我们的日志文件的分布就为log1~到log500不等。每一次用户访问,都将数据随机写到log1~log500之间的任一文件。在同一时刻,有2个进程进行记录日志,A进程可能是更新的log32文件,而B进程呢?则此时更新的可能就为log399.要知道,如果要让B进程也操作log32,概率基本上为1/500,差不多约等于零。在需要对访问日志进行分析时,这里我们只需要先将这些日志合并,再进行分析即可。使用这种方案来记录日志的一个好处时,进程操作排队的可能性比较小,可以使进程很迅速的完成每一次操作。

方案四:将所有要操作的进程放入一个队列中。然后专门放一个服务完成文件操作。队列中的每一个排除的进程相当于第一个具体的操作,所以第一次我们的服务只需要从队列中取得相当于具体操作事项就可以了,如果这里还有大量的文件操作进程,没关系,排到我们的队列后面即可,只要愿意排,队列的多长都没关系。

对于以前几种方案,各有各的好处!大致可能归纳为两类:
(1)需要排队(影响慢)比如方案一、二、四
(2)不需要排队。(影响快)方案三
在设计缓存系统时,一般我们不会采用方案三。因为方案三的分析程序和写入程序是不同步的,在写的时间,完全不考虑到时候分析的难度,只管写的行了。试想一下,如我们在更新一个缓存时,如果也采用随机文件读写法,那么在读缓存时似乎会增加很多流程。但采取方案一、二就完全不一样,虽然写的时间需要等待(当获取锁不成功时,会反复获取),但读文件是很方便的。添加缓存的目的就是要减少数据读取瓶颈,从而提高系统性能。
从上为个人经验和一些资料的总结,有什么不对的地方,或者没有谈到的地方,欢迎各位同行指正。

PHP 相关文章推荐
substr()函数中文版
Oct 09 PHP
一个从别的网站抓取信息的例子(域名查询)
Oct 09 PHP
php入门学习知识点二 PHP简单的分页过程与原理
Jul 14 PHP
php中json_encode中文编码问题分析
Sep 13 PHP
php中JSON的使用与转换
Jan 14 PHP
php微信公众平台开发类实例
Apr 01 PHP
php实现微信公众号主动推送消息
Dec 31 PHP
php实现和c#一致的DES加密解密实例
Jul 24 PHP
laravel中短信发送验证码的实现方法
Apr 25 PHP
PHP实现微信申请退款功能
Oct 01 PHP
PHP单例模式实例分析【防继承,防克隆操作】
May 22 PHP
apache集成php7.3.5的详细步骤
Jun 20 PHP
ajax返回值中有回车换行、空格的解决方法分享
Oct 24 #PHP
学习php分页代码实例
Oct 24 #PHP
获取URL文件名后缀
Oct 24 #PHP
关于php程序报date()警告的处理(date_default_timezone_set)
Oct 22 #PHP
php 修改、增加xml结点属性的实现代码
Oct 22 #PHP
PHP的构造方法,析构方法和this关键字详细介绍
Oct 22 #PHP
实用的简单PHP分页集合包括使用方法
Oct 21 #PHP
You might like
一首老MP3,致敬WAR3经典
2021/03/08 魔兽争霸
给php新手谈谈我的学习心得
2007/02/25 PHP
PHP5.3.1 不再支持ISAPI
2010/01/08 PHP
php环境配置之CGI、FastCGI、PHP-CGI、PHP-FPM、Spawn-FCGI比较?
2011/10/17 PHP
php实现的rc4加密解密类定义与用法示例
2018/08/16 PHP
Laravel 类和接口注入相关的代码
2019/10/15 PHP
简易js代码实现计算器操作
2013/04/15 Javascript
实测jquery data()如何存值
2013/08/18 Javascript
JS网页播放声音实现代码兼容各种浏览器
2013/09/22 Javascript
NodeJs基本语法和类型
2015/02/13 NodeJs
JS动态添加iframe的代码
2015/09/14 Javascript
javascript巧用eval函数组装表单输入项为json对象的方法
2015/11/25 Javascript
详解原生JavaScript实现jQuery中AJAX处理的方法
2016/05/10 Javascript
基于原生JS实现图片裁剪
2016/08/01 Javascript
jQuery实现滚动效果
2017/11/17 jQuery
详解ES6通过WeakMap解决内存泄漏问题
2018/03/09 Javascript
微信小程序下拉加载和上拉刷新两种实现方法详解
2019/09/05 Javascript
VUE前端从后台请求过来的数据进行转换数据结构操作
2020/11/11 Javascript
Python元组操作实例分析【创建、赋值、更新、删除等】
2017/07/24 Python
python机器学习理论与实战(六)支持向量机
2018/01/19 Python
Python 堆叠柱状图绘制方法
2019/07/29 Python
浅谈cv2.imread()和keras.preprocessing中的image.load_img()区别
2020/06/12 Python
html svg生成环形进度条的实现方法
2019/09/23 HTML / CSS
澳大利亚吉他在线:Artist Guitars
2017/03/30 全球购物
Exoticca英国:以最优惠的价格提供豪华异国情调旅行
2018/10/18 全球购物
荷兰睡眠专家:Beter Bed
2020/11/23 全球购物
办公室主任职责范文
2013/11/08 职场文书
2014迎新年晚会策划方案
2014/02/23 职场文书
受伤赔偿协议书
2014/09/24 职场文书
2014年安全员工作总结
2014/11/13 职场文书
导师对论文的学术评语
2015/01/04 职场文书
酒店仓管员岗位职责
2015/04/01 职场文书
2015年反腐倡廉工作总结
2015/05/14 职场文书
vue组件的路由高亮问题解决方法
2021/05/11 Vue.js
5种方法告诉你如何使JavaScript 代码库更干净
2021/09/15 Javascript
纯 CSS 自定义多行省略的问题(从原理到实现)
2021/11/11 HTML / CSS