為了確保操作的有效性和完整性,可以通過(guò)鎖機(jī)制將并發(fā)狀態(tài)轉(zhuǎn)換成串行狀態(tài).作為鎖機(jī)制中的一種,PHP的文件鎖也是為了應(yīng)對(duì)資源競(jìng)爭(zhēng).假設(shè)一個(gè)應(yīng)用場(chǎng)景,在存在較大并發(fā)的情況下,通過(guò)fwrite向文件尾部多次有序的寫(xiě)入數(shù)據(jù),不加鎖的情況下會(huì)發(fā)生什么?多次有序的寫(xiě)入操作相當(dāng)于一個(gè)事務(wù),我們此時(shí)需要保證這個(gè)事務(wù)的完整性.
bool flock ( int handle, int operation [, int &wouldblock] );
flock() 操作的 handle 必須是一個(gè)已經(jīng)打開(kāi)的文件指針.operation 可以是以下值之一:
1.要取得共享鎖定(讀取程序),將 operation 設(shè)為 LOCK_SH(PHP 4.0.1 以前的版本設(shè)置為 1)
2.要取得獨(dú)占鎖定(寫(xiě)入程序),將 operation 設(shè)為 LOCK_EX(PHP 4.0.1 以前的版本中設(shè)置為 2)
3.要釋放鎖定(無(wú)論共享或獨(dú)占),將 operation 設(shè)為 LOCK_UN(PHP 4.0.1 以前的版本中設(shè)置為 3)
4.如果你不希望 flock() 在鎖定時(shí)堵塞,則給 operation 加上 LOCK_NB(PHP 4.0.1 以前的版本中設(shè)置為 4)
建兩個(gè)文件
實(shí)例代碼如下:
(1) a.php
(2) b.php
運(yùn)行 a.php 后,馬上運(yùn)行 b.php ,可以看到輸出:
abc
等 a.php 運(yùn)行完后運(yùn)行 b.php ,可以看到輸出:
abc
123
顯然,當(dāng) a.php 寫(xiě)文件時(shí)數(shù)據(jù)太大,導(dǎo)致時(shí)間比較長(zhǎng)時(shí),這時(shí) b.php 讀取數(shù)據(jù)不完整
修改 b.php 為:
實(shí)例代碼如下:
運(yùn)行 a.php 后,馬上運(yùn)行 b.php ,可以發(fā)現(xiàn) b.php 會(huì)等到 a.php 運(yùn)行完成后(即 10 秒后)才顯示:
讀取數(shù)據(jù)完整,但時(shí)間過(guò)長(zhǎng),他要等待寫(xiě)鎖釋放.修改 b.php 為:
實(shí)例代碼如下:
運(yùn)行 a.php 后,馬上運(yùn)行 b.php ,可以看到輸出:
Lock file failed…
證明可以返回鎖文件失敗狀態(tài),而不是向上面一樣要等很久.
結(jié)論:
建議作文件緩存時(shí),選好相關(guān)的鎖,不然可能導(dǎo)致讀取數(shù)據(jù)不完整,或重復(fù)寫(xiě)入數(shù)據(jù).file_get_contents 好像選擇不了鎖,不知道他默認(rèn)用的什么鎖,反正和不鎖得到的輸出一樣,是不完整的數(shù)據(jù).
我是要做文件緩存,所以只需要知道是否有寫(xiě)鎖存在即可,有的話就查數(shù)據(jù)庫(kù)就可以了.多次同時(shí)執(zhí)行,雖然都寫(xiě)了100行,但是事務(wù)1和事務(wù)2的數(shù)據(jù)交錯(cuò)寫(xiě)入,這并不是我們想要的結(jié)果.我們要的是事務(wù)完整的執(zhí)行,此時(shí)我們需要有個(gè)機(jī)制去保證在第一個(gè)事務(wù)執(zhí)行完后再執(zhí)行第二個(gè).在PHP中,flock函數(shù)完成了這一使命.在事物1和事務(wù)2的循環(huán)前面都加上: flock($fp, LOCK_EX); 就能滿足我們的需求,將兩個(gè)事務(wù)串行.
當(dāng)某一個(gè)事務(wù)執(zhí)行完flock時(shí),因?yàn)槲覀冊(cè)谶@里添加的是LOCK_EX(獨(dú)占鎖定),所以所有對(duì)資源的操作都會(huì)被阻塞,只有當(dāng)事務(wù)執(zhí)行完成后,后面的事務(wù)才會(huì)執(zhí)行.我們可以通過(guò)輸出當(dāng)前的時(shí)間的方法來(lái)確認(rèn)這一點(diǎn).
關(guān)于在尾部追加寫(xiě)入,在unix系統(tǒng)的早期版本中存在一個(gè)并發(fā)寫(xiě)入的問(wèn)題,如果要在尾部追加,需要先lseek位置,再write.當(dāng)多個(gè)進(jìn)程同時(shí)操作時(shí),會(huì)因?yàn)椴l(fā)導(dǎo)致的覆蓋寫(xiě)入的問(wèn)題,即兩個(gè)進(jìn)程同時(shí)獲取尾部的偏移后,先后執(zhí)行write操作,后面的操作會(huì)將前面的操作覆蓋.這個(gè)問(wèn)題在后面以添加打開(kāi)時(shí)的O_APPEND操作而得到解決,它將查找和寫(xiě)入操作變成了一個(gè)原子操作.
在PHP的fopen函數(shù)的實(shí)現(xiàn)中,如果我們使用a參數(shù)在文件的尾部追加內(nèi)容,其調(diào)用open函數(shù)中oflag參數(shù)為 O_CREAT|O_APPEND,即我們使用追加操作不用擔(dān)心并發(fā)追加寫(xiě)入的問(wèn)題.
在PHP的session默認(rèn)存儲(chǔ)實(shí)現(xiàn)中也用到了flock文件鎖,當(dāng)session開(kāi)始時(shí)就調(diào)用PS_READ_FUNC,且以O(shè)_CREAT | O_RDWR | O_BINARY 打開(kāi)session數(shù)據(jù)文件,此時(shí)會(huì)調(diào)用flock加上寫(xiě)鎖,如果此時(shí)有其它進(jìn)程訪問(wèn)此文件(即同一用戶再次發(fā)起對(duì)當(dāng)前文件的請(qǐng)求),就會(huì)顯示頁(yè)面加載中,進(jìn)程被阻塞了.加寫(xiě)鎖其出發(fā)點(diǎn)是為了保證此次會(huì)話中對(duì)session的操作事務(wù)能完整的執(zhí)行,防止其它進(jìn)程的干擾,保證數(shù)據(jù)的一致性.如果一個(gè)頁(yè)面沒(méi)有session修改操作,可以盡早的調(diào)用session_write_close()釋放鎖.
文件鎖是針對(duì)文件的鎖,除了這種釋義,還可以理解為用文件作為鎖.在實(shí)際工作中,有時(shí)為確保單個(gè)進(jìn)程的執(zhí)行,我們會(huì)在程序執(zhí)行前判斷文件是否存在,如果不存在則創(chuàng)建一個(gè)空文件,在進(jìn)程結(jié)束后刪除這個(gè)空文件,如果存在,則不執(zhí)行.
但是什么時(shí)候使用lock_ex什么時(shí)候使用lock_sh呢?
讀的時(shí)候:
如果不想出現(xiàn)dirty數(shù)據(jù),那么最好使用lock_sh共享鎖.可以考慮以下三種情況:
1. 如果讀的時(shí)候沒(méi)有加共享鎖,那么其他程序要寫(xiě)的話(不管這個(gè)寫(xiě)是加鎖還是不加鎖)都會(huì)立即寫(xiě)成功.如果正好讀了一半,然后被其他程序給寫(xiě)了,那么讀的后一半就有可能跟前一半對(duì)不上(前一半是修改前的,后一半是修改后的)
2. 如果讀的時(shí)候加上了共享鎖(因?yàn)橹皇亲x,沒(méi)有必要使用排他鎖),這個(gè)時(shí)候,其他程序開(kāi)始寫(xiě),這個(gè)寫(xiě)程序沒(méi)有使用鎖,那么寫(xiě)程序會(huì)直接修改這個(gè)文件,也會(huì)導(dǎo)致前面一樣的問(wèn)題
3. 最理想的情況是,讀的時(shí)候加鎖(lock_sh),寫(xiě)的時(shí)候也進(jìn)行加鎖(lock_ex),這樣寫(xiě)程序會(huì)等著讀程序完成之后才進(jìn)行操作,而不會(huì)出現(xiàn)貿(mào)然操作的情況
寫(xiě)的時(shí)候:
如果多個(gè)寫(xiě)程序不加鎖同時(shí)對(duì)文件進(jìn)行操作,那么最后的數(shù)據(jù)有可能一部分是a程序?qū)懙?一部分是b程序?qū)懙摹H绻麑?xiě)的時(shí)候加鎖了,這個(gè)時(shí)候有其他的程序來(lái)讀,那么他會(huì)讀到什么東西呢?
1. 如果讀程序沒(méi)有申請(qǐng)共享鎖,那么他會(huì)讀到dirty的數(shù)據(jù).比如寫(xiě)程序要寫(xiě)a,b,c三部分,寫(xiě)完a,這時(shí)候讀讀到的是a,繼續(xù)寫(xiě)b,這時(shí)候讀讀到的是ab,然后寫(xiě)c,這時(shí)候讀到的是abc.
2. 如果讀程序在之前申請(qǐng)了共享鎖,那么讀程序會(huì)等寫(xiě)程序?qū)bc寫(xiě)完并釋放鎖之后才進(jìn)行讀.