php文件句柄,file_put_contents相关问题以及日

2020-01-13  本文已影响0人  PENG先森_晓宇

fopen()

fopen(filename,mode,include_path,context)会返回一个文件句柄
文件句柄其实就是一个指针,指针就是指向文件中的某个位置mode参数决定了指针的位置。

fwrite()

fwrite(file,string,length)将字符串写入到文件句柄的指针处。

注意:小于8k的字符串写入文件时不是一个字符一个字符的写入,而是所有字符串一次性全部写入文件。为什么小于8k的会这样?为什么是一次性写入?这些问题下面介绍。

fclose()

fclose(file)关闭文件句柄

正常的写入文件代码如下

<?php
//打开文件句柄,并将资源绑定到一个流上
$status=fopen('./1.txt','a+');
fwrite($status,'88'); 
fclose($status);

flock()

flock(file,lock,block)为锁定资源或者释放资源。也称作文件锁

lock参数选项如下

使用加锁后的普通代码如下

<?php
//打开文件句柄,并将资源绑定到一个流上
$status=fopen('./1.txt','a+');
$lock=flock($status,LOCK_EX);
if($lock){
    fwrite($status,'88');
    flock($status,LOCK_UN);
}
fclose($status);

file_put_contents()

file_put_contents()函数是把一个字符串写入到文件中。
与依次调用fopen(),fwrite(),fclose()功能一样。

格式为:file_put_contents(file,data,mode,context)

追加内容

file_put_contents('./1.txt','ff',FILE_APPEND);

追加内容并加锁

file_put_contents('./1.txt','ff2',FILE_APPEND | LOCK_EX);

要知道:file_put_contents()和fwrite()一样都是:小于8k的字符串在写入文件时不是一个字符一个字符的写入,而是所有字符串于一次性全部写入文件
为什么是一次性写入?
因为每次写入文件都是一次io,io是阻塞耗时的,所以从性能各个方面肯定不会将每次字符都写入一次的,所以是一次性写入的,减少了io次数,相应的也就提高了性能。
为什么小于8k的会这样?
如果写入的内容很大很大,且一次性写入时消耗的性能也是很大的,所以可以分批次写入<=8k的内容,这样消耗可能更低。

使用file_put_contents()写入日志发生日志错乱

现象

高并发情况下,且日志很长大于8k的情况下,日志会发生错乱的现象。
我们可以注意到俩个关键字,高并发日志很长。写入内容大于8k的情况下,内容会分批次写入,也就保证不了原子性了,如果现在有并发情况,就可能在分批次写入这里发生日志错乱的情况

写入内容小于8k的情况下会一次性写入,这也就说明了保证了原子性,所以在高并发的情况下不会发生错乱的情况。

查看file_put_contents()源码

if ($this->isCli == true) {
    return file_put_contents($messageLogFile, $strLogMsg, FILE_APPEND);
}

明确几个变量的含义:
count:需写入文件的字符串长度
stream->chunk_size :默认为8192 (8k)

从上面代码可以看出,当写入的字符串长度 大于8192时,则拆为多次<=8192的字符串,分批次写入,打个比方,如果写入的内容是23k,就分三次写入,第一次写入8k,第二次写入8k,第三次7k。

然后调用php_stdiop_write函数写入文件。什么意思呢?

static size_t php_stdiop_write(php_stream *stream, const char *buf, size_t count)
{
    php_stdio_stream_data *data = (php_stdio_stream_data*)stream->abstract;

    assert(data != NULL);

    if (data->fd >= 0) {
#ifdef PHP_WIN32
        int bytes_written;
        if (ZEND_SIZE_T_UINT_OVFL(count)) {
            count = UINT_MAX;
        }
        bytes_written = _write(data->fd, buf, (unsigned int)count);
#else
        int bytes_written = write(data->fd, buf, count);
#endif
        if (bytes_written < 0) return 0;
        return (size_t) bytes_written;
    } else {

#if HAVE_FLUSHIO
        if (!data->is_pipe && data->last_op == 'r') {
            zend_fseek(data->file, 0, SEEK_CUR);
        }
        data->last_op = 'w';
#endif

        return fwrite(buf, 1, count, data->file);
    }
}

php_stdiop_write 则调用的 write函数 写入文件;write函数是能保证一次写入的完整的

所以日志写串的原因也就能分析出来了,调用链接为:file_put_contents ->_php_stream_write_buffer ->php_stdiop_write(多次调用,每次最多写入8192字节) ->write(),是在 多次调用php_stdiop_write 函数时出的问题;第一次写完,紧接着在高并发的情况下,被其他进程的 write 函数追着写,此时就出现写串,也就是前面示例中日志;

总结:写入内容小于8k时是原子性操作,不用加锁,反之需要。这个8k的限制可以在php中修改的。

加锁代码

由于加锁是阻塞的,在并发时会影响性能,所以写入内容时最好判断下大小是否超过8k,代码如下

<?php

$str='xxx';
$strlen=strlen($str);
if($strlen > 8192){
    file_put_contents('./1.txt',$str,FILE_APPEND | LOCK_EX);
}else{
    file_put_contents('./1.txt',$str,FILE_APPEND);
}

file_put_contents()中的LOCK_EX和flock()的效果是一样的。

加锁后会有死锁的问题吗?

这个锁并没有设定过期时间,那么会不会有死锁的情况呢?比如在执行完加锁还没有到解锁的时候机器宕机,该文件会不会被锁死?
答案是:进程重启或者kill掉该进程后,系统会自动释放这个文件锁。在没重启或者没kill掉进程之前,该文件会被死锁

在多进程模式下,使用file_put_contents()会影响并发吗?

分俩种情况

$fp = fopen("/home/guoxinhua/php.log", "a+");
if (flock($fp, LOCK_EX)) {  //给日志文件加锁
   //do something
    fwrite($fp, "the huge string\n");
    flock($fp, LOCK_UN);    // 释放锁定
}

或者

file_put_contents("/home/guoxinhua/php.log",'111',FILE_APPEND | LOCK_EX);

比如php-fpm有10个进程,在写入数据时会阻塞1s,而且该文件还被加锁。
第一个请求在写入阻塞了1s,且该文件已加锁。第二个并发请求写入时需要等待第一个请求锁释放才能写入,一次类推,此时qps也就是1/s。

如果前一个请求没有释放文件锁就会导致后面的请求无法获得锁,卡死在获取锁的这一步。如果php-fpm一共10个进程,此时系统最多能处理10个请求,且这10个请求都是阻塞状态。说白了都在阻塞造成的问题,

所以在必须加锁的情况下,我们必须加上LOCK_NB,它可以避免阻塞,也就是说此时的qps也是10/s。

<?php
$fp = fopen('/tmp/lock.txt', 'r+');
if(!flock($fp, LOCK_EX | LOCK_NB)) {
    echo 'Unable to obtain lock';
    exit(-1);
}
fclose($fp);
?>
上一篇 下一篇

猜你喜欢

热点阅读