題目是golang下文件鎖的使用,但本文的目的其實是通過golang下的文件鎖的使用方法,來一窺文件鎖背後的機制。
為什麼需要文件鎖只有多線程/多進程這種並發場景下讀寫文件,才需要加鎖,
場景1-讀寫並發
讀寫並發場景下,如果不加鎖,就會出現讀到髒數據的情況。想像一下,讀文件的進程,讀到第500位元組,有其它進程以覆蓋寫的方式向文件中寫入1000位元組,那讀進程讀到的後500位元組就是髒數據。
場景2-寫寫並發
寫寫並發場景下,如果不加鎖,假設A進程先寫0-1000位元組,B進程寫0-900位元組,以此類推,最後一個進程寫0-100位元組,那最終的文件內容就是每個進程前100個字節拼接起來的錯亂的內容了。
文件鎖的幾個概念共享鎖
共享鎖,也叫讀鎖。某個進程首次獲取共享鎖後,會生成一個鎖類型的變量L,類型標記為共享鎖。其它進程獲取讀鎖的時候,L中的計數器加1,表示又有一個進程獲取到了共享鎖。這個時候如果有進程來獲取排它鎖,會獲取失敗。
排它鎖
排它鎖,也叫寫鎖。某個進程首次獲取排他鎖後,會生成一個鎖類型的變量L,類型標記為排他鎖。其它進程獲取任何類型的鎖的時候,都會獲取失敗。
阻塞
阻塞的意思是說,新的進程發現當前的文件(數據)被加鎖後,會一直處於等待狀態,直到鎖被釋放,才會繼續下一步的行為。
非阻塞
非阻塞的意思是說,新的進程發現當前的文件(數據)被加鎖後,立即返回異常。業務上需要根據具體的業務場景對該異常進行處理。
阻塞和非阻塞其實是進程遇到鎖的時候的兩種處理模式。
golang下如何使用文件鎖基本使用package main
import ( "log" "os" "syscall")
func main() { f, err := os.Create("example.txt") if err != nil { log.Println("create file example.txt failed", err) } defer f.Close() if err := syscall.Flock(int(f.Fd()), syscall.LOCK_SH|syscall.LOCK_NB); err != nil { log.Println("add share lock in no block failed", err) }
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil { log.Println("unlock share lock failed", err) }
return}示例中 LOCK_SH 表示當前獲取的是共享鎖,如果是 LOCK_EX,則表示獲取的是排他鎖。而 LOCK_NB 表示當前獲取鎖的模式是非阻塞模式,如果需要阻塞模式,不加這個參數即可。LOCK_UN 則表示解鎖,即釋放鎖。
golang 下這種文件鎖的使用方式其實是Linux下的系統級調用,使用的是Linux的原生的文件鎖的相關能力。
使用flock的幾個注意點1、只要fd指向的是同一個文件指針,那麼加鎖解鎖的行為都是繼承和覆蓋的(這個可以看最後的解釋)。
2、flock這種方式加的是建議性鎖,也就是說新的進程一上來不管三七二十一,不去通過flock獲取鎖,就對文件各種操作,也是可以正常生效的。
說一說Linux下面的flock和fcntl和flock一樣,fcntl也是系統級調用,但是在具體的使用上卻有很大不用,並且兩種鎖互不幹擾,用flock加鎖,fcntl無法感知,反之也一樣。
建議性鎖和強制鎖flock加的是建議性鎖,而fcntl加的是強制性鎖。
建議性鎖,本質是一種協議,約定讀寫操作前都去檢查一下該文件是否有被其它進程加鎖。如果不遵守該協議,一上來就對文件進行操作,不檢查有沒有鎖,程序執行上是沒有任何問題的,能執行成功。
強制性鎖,才更像真正意義上的鎖。只要加了鎖,其它進程是無法執行非允許的操作的。
其實一些利用redis做的分布式鎖,都是建議性鎖。鎖機的機制要生效,需要大家共同遵守這個約定才行。
全局鎖和局部鎖對於一個文件,flock加鎖的範圍是整個文件內容,而fcntl能對文件的任意部分加鎖。
鎖的持有者問題flock認為,鎖的持有者是文件表(可以理解為文件指針),所以對於fork和dup操作,他們都對應同一個文件指針,所有的操作都會作用到這個文件上。具體表現:
fcntl 認為,鎖的持有者是進程。加鎖和解鎖的行為都是跟著進程走,具體表現為:
參考[1] 被遺忘的桃源——flock 文件鎖
[2] Linux文件鎖學習-flock, lockf, fcntl