fopen()
fopen(filename,mode,include_path,context)會(huì)返回一個(gè)文件句柄。
文件句柄其實(shí)就是一個(gè)指針,指針就是指向文件中的某個(gè)位置,mode參數(shù)決定了指針的位置。


fwrite()
fwrite(file,string,length)將字符串寫(xiě)入到文件句柄的指針處。
- 參數(shù)file:必需。fopen()返回的文件句柄
- 參數(shù)string:必需。寫(xiě)入文件的字符串
- 參數(shù)length:可選。規(guī)定寫(xiě)入的最大字節(jié)數(shù)
注意:小于
8k的字符串寫(xiě)入文件時(shí)不是一個(gè)字符一個(gè)字符的寫(xiě)入,而是所有字符串于一次性全部寫(xiě)入文件。為什么小于8k的會(huì)這樣?為什么是一次性寫(xiě)入?這些問(wèn)題下面介紹。
fclose()
fclose(file)關(guān)閉文件句柄
- file:fopen()返回的文件句柄
正常的寫(xiě)入文件代碼如下
<?php
//打開(kāi)文件句柄,并將資源綁定到一個(gè)流上
$status=fopen('./1.txt','a+');
fwrite($status,'88');
fclose($status);
flock()
flock(file,lock,block)為鎖定資源或者釋放資源。也稱作文件鎖。
- file:必需。fopen()返回的文件句柄
- lock:必需。規(guī)定要使用哪種鎖定類型
- block:可選。若設(shè)置為 1 或 true,則當(dāng)進(jìn)行鎖定時(shí)阻擋其他進(jìn)程
lock參數(shù)選項(xiàng)如下
- LOCK_SH:共享鎖定,類似mysql的共享鎖,可讀不可寫(xiě),是阻塞的。
- LOCK_EX:獨(dú)占鎖定,類似mysql的排他鎖,不可讀也不可寫(xiě),是阻塞的,一般使用該類型。
- LOCK_UN:釋放鎖
- LOCK_NB:如果不希望flock()在鎖定時(shí)阻塞,則加上該選項(xiàng)
使用加鎖后的普通代碼如下
<?php
//打開(kāi)文件句柄,并將資源綁定到一個(gè)流上
$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()函數(shù)是把一個(gè)字符串寫(xiě)入到文件中。
與依次調(diào)用fopen(),fwrite(),fclose()功能一樣。
格式為:file_put_contents(file,data,mode,context)
- file:
文件句柄。規(guī)定要寫(xiě)入的文件,文件如果不存在則會(huì)創(chuàng)建。 - data: 寫(xiě)入文件的內(nèi)容,可以是字符串,數(shù)組(不能是多維數(shù)組)或者數(shù)據(jù)流。
- mode:規(guī)定如何打開(kāi)/寫(xiě)入文件,值有下面幾個(gè)選項(xiàng)
- FILE_APPEND:將文件指針指向文件末尾,用于追加內(nèi)容。與fopen()中的a模式相同。
- LOCK_EX:寫(xiě)文件時(shí)加鎖。與flock()中的LOCK_EX相同。
- FILE_USE_INCLUDE_PATH:很少用。
追加內(nèi)容
file_put_contents('./1.txt','ff',FILE_APPEND);
追加內(nèi)容并加鎖
file_put_contents('./1.txt','ff2',FILE_APPEND | LOCK_EX);
要知道:file_put_contents()和fwrite()一樣都是:
小于8k的字符串在寫(xiě)入文件時(shí)不是一個(gè)字符一個(gè)字符的寫(xiě)入,而是所有字符串于一次性全部寫(xiě)入文件。
為什么是一次性寫(xiě)入?
因?yàn)槊看螌?xiě)入文件都是一次io,io是阻塞耗時(shí)的,所以從性能各個(gè)方面肯定不會(huì)將每次字符都寫(xiě)入一次的,所以是一次性寫(xiě)入的,減少了io次數(shù),相應(yīng)的也就提高了性能。
為什么小于8k的會(huì)這樣?
如果寫(xiě)入的內(nèi)容很大很大,且一次性寫(xiě)入時(shí)消耗的性能也是很大的,所以可以分批次寫(xiě)入<=8k的內(nèi)容,這樣消耗可能更低。
使用file_put_contents()寫(xiě)入日志發(fā)生日志錯(cuò)亂
現(xiàn)象
在高并發(fā)情況下,且日志很長(zhǎng)大于8k的情況下,日志會(huì)發(fā)生錯(cuò)亂的現(xiàn)象。
我們可以注意到倆個(gè)關(guān)鍵字,高并發(fā)和日志很長(zhǎng)。寫(xiě)入內(nèi)容大于8k的情況下,內(nèi)容會(huì)分批次寫(xiě)入,也就保證不了原子性了,如果現(xiàn)在有并發(fā)情況,就可能在分批次寫(xiě)入這里發(fā)生日志錯(cuò)亂的情況。
寫(xiě)入內(nèi)容小于8k的情況下會(huì)一次性寫(xiě)入,這也就說(shuō)明了保證了原子性,所以在高并發(fā)的情況下不會(huì)發(fā)生錯(cuò)亂的情況。
查看file_put_contents()源碼
- 腳本服務(wù)寫(xiě)入日志代碼如下:
if ($this->isCli == true) {
return file_put_contents($messageLogFile, $strLogMsg, FILE_APPEND);
}
- 查看file_put_contents 的源碼實(shí)現(xiàn),最終寫(xiě)文件會(huì)執(zhí)行到_php_stream_write_buffer 函數(shù),里面有這樣一處代碼:

明確幾個(gè)變量的含義:
count:需寫(xiě)入文件的字符串長(zhǎng)度
stream->chunk_size :默認(rèn)為8192 (8k)
從上面代碼可以看出,當(dāng)寫(xiě)入的字符串長(zhǎng)度 大于8192時(shí),則拆為多次<=8192的字符串,分批次寫(xiě)入,打個(gè)比方,如果寫(xiě)入的內(nèi)容是23k,就分三次寫(xiě)入,第一次寫(xiě)入8k,第二次寫(xiě)入8k,第三次7k。
然后調(diào)用php_stdiop_write函數(shù)寫(xiě)入文件。什么意思呢?
- php_stdiop_write函數(shù)實(shí)現(xiàn)如下:
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 則調(diào)用的 write函數(shù) 寫(xiě)入文件;write函數(shù)是能保證一次寫(xiě)入的完整的。
所以日志寫(xiě)串的原因也就能分析出來(lái)了,調(diào)用鏈接為:file_put_contents ->_php_stream_write_buffer ->php_stdiop_write(多次調(diào)用,每次最多寫(xiě)入8192字節(jié)) ->write(),是在 多次調(diào)用php_stdiop_write 函數(shù)時(shí)出的問(wèn)題;第一次寫(xiě)完,緊接著在高并發(fā)的情況下,被其他進(jìn)程的 write 函數(shù)追著寫(xiě),此時(shí)就出現(xiàn)寫(xiě)串,也就是前面示例中日志;
總結(jié):寫(xiě)入內(nèi)容小于8k時(shí)是原子性操作,不用加鎖,反之需要。這個(gè)8k的限制可以在php中修改的。
加鎖代碼
由于加鎖是阻塞的,在并發(fā)時(shí)會(huì)影響性能,所以寫(xiě)入內(nèi)容時(shí)最好判斷下大小是否超過(guò)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()的效果是一樣的。
加鎖后會(huì)有死鎖的問(wèn)題嗎?
這個(gè)鎖并沒(méi)有設(shè)定過(guò)期時(shí)間,那么會(huì)不會(huì)有死鎖的情況呢?比如在執(zhí)行完加鎖還沒(méi)有到解鎖的時(shí)候機(jī)器宕機(jī),該文件會(huì)不會(huì)被鎖死?
答案是:進(jìn)程重啟或者kill掉該進(jìn)程后,系統(tǒng)會(huì)自動(dòng)釋放這個(gè)文件鎖。在沒(méi)重啟或者沒(méi)kill掉進(jìn)程之前,該文件會(huì)被死鎖
在多進(jìn)程模式下,使用file_put_contents()會(huì)影響并發(fā)嗎?
分倆種情況
- 不加鎖的情況
首先f(wàn)ile_put_contents()就是一個(gè)阻塞io,所以肯定會(huì)阻塞進(jìn)程的,這點(diǎn)毋庸置疑。
比如php-fpm一共有10個(gè)進(jìn)程,執(zhí)行file_put_contents()時(shí)會(huì)阻塞1s,那么此時(shí)最高的qps也就是10/s。只有進(jìn)程空閑后才會(huì)繼續(xù)處理別的請(qǐng)求。 - 加鎖的情況
$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個(gè)進(jìn)程,在寫(xiě)入數(shù)據(jù)時(shí)會(huì)阻塞1s,而且該文件還被加鎖。
第一個(gè)請(qǐng)求在寫(xiě)入阻塞了1s,且該文件已加鎖。第二個(gè)并發(fā)請(qǐng)求寫(xiě)入時(shí)需要等待第一個(gè)請(qǐng)求鎖釋放才能寫(xiě)入,一次類推,此時(shí)qps也就是1/s。
如果前一個(gè)請(qǐng)求沒(méi)有釋放文件鎖就會(huì)導(dǎo)致后面的請(qǐng)求無(wú)法獲得鎖,卡死在獲取鎖的這一步。如果php-fpm一共10個(gè)進(jìn)程,此時(shí)系統(tǒng)最多能處理10個(gè)請(qǐng)求,且這10個(gè)請(qǐng)求都是阻塞狀態(tài)。說(shuō)白了都在阻塞造成的問(wèn)題,
所以在必須加鎖的情況下,我們必須加上
LOCK_NB,它可以避免阻塞,也就是說(shuō)此時(shí)的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);
?>