fopen()
fopen(filename,mode,include_path,context)会返回一个文件句柄
。
文件句柄其实就是一个指针
,指针就是指向文件中的某个位置,mode
参数决定了指针的位置。
fwrite()
fwrite(file,string,length)将字符串
写入到文件句柄的指针
处。
- 参数file:必需。fopen()返回的文件句柄
- 参数string:必需。写入文件的字符串
- 参数length:可选。规定写入的最大字节数
注意:小于
8k
的字符串写入文件时不是一个字符一个字符的写入,而是所有字符串
于一次性
全部写入文件。为什么小于8k的会这样?为什么是一次性写入?这些问题下面介绍。
fclose()
fclose(file)关闭文件句柄
- file:fopen()返回的文件句柄
正常的写入文件代码如下
flock()
flock(file,lock,block)为锁定资源或者释放资源。也称作文件锁
。
- file:必需。fopen()返回的文件句柄
- lock:必需。规定要使用哪种锁定类型
- block:可选。若设置为 1 或 true,则当进行锁定时阻挡其他进程
lock参数选项如下
- LOCK_SH:共享锁定,类似mysql的共享锁,可读不可写,是阻塞的。
- LOCK_EX:独占锁定,类似mysql的排他锁,不可读也不可写,是阻塞的,一般使用该类型。
- LOCK_UN:释放锁
- LOCK_NB:如果不希望flock()在锁定时阻塞,则加上该选项
使用加锁后的普通代码如下
file_put_contents()
file_put_contents()函数是把一个字符串写入到文件中。
与依次调用fopen(),fwrite(),fclose()功能一样。
格式为:file_put_contents(file,data,mode,context)
- file:
文件句柄
。规定要写入的文件,文件如果不存在则会创建。 - data: 写入文件的内容,可以是字符串,数组(不能是多维数组)或者数据流。
- mode:规定如何打开/写入文件,值有下面几个选项
- FILE_APPEND:将文件指针指向文件末尾,用于追加内容。与fopen()中的a模式相同。
- LOCK_EX:写文件时加锁。与flock()中的LOCK_EX相同。
- FILE_USE_INCLUDE_PATH:很少用。
追加内容
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);
}
- 查看file_put_contents 的源码实现,最终写文件会执行到_php_stream_write_buffer 函数,里面有这样一处代码:
明确几个变量的含义:
count:需写入文件的字符串长度
stream->chunk_size :默认为8192 (8k)
从上面代码可以看出,当写入的字符串长度 大于8192时,则拆为多次<=8192的字符串,分批次写入,打个比方,如果写入的内容是23k,就分三次写入,第一次写入8k,第二次写入8k,第三次7k。
然后调用php_stdiop_write函数写入文件。什么意思呢?
- 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,代码如下
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()会影响并发吗?
分俩种情况
- 不加锁的情况
首先file_put_contents()就是一个阻塞io,所以肯定会阻塞进程的,这点毋庸置疑。
比如php-fpm一共有10个进程,执行file_put_contents()时会阻塞1s,那么此时最高的qps也就是10/s。只有进程空闲后才会继续处理别的请求。 - 加锁的情况
$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。