鎖機制一
鎖機制是計算機系統中解決并發沖突的核心工具,其存在和應用場景源于一個根本問題:當多個執行單元(線程、進程、分布式節點)同時訪問或修改同一份共享資源時,如何保證數據的正確性、一致性和系統可靠性?
一、為什么需要鎖?
想象以下場景,如果沒有鎖會發生什么:
- 銀行存款取款(數據競爭):
- 線程A讀取賬戶余額:100元。
- 線程B讀取賬戶余額:100元。
- 線程A存入50元,計算新余額 100 + 50 = 150,寫入150。
- 線程B取出30元,計算新余額 100 - 30 = 70,寫入70。
- 結果: 最終余額是70元,而不是正確的120元 (100+50-30)。線程B的操作覆蓋了線程A的操作,因為兩者都基于舊的余額100元進行計算。這破壞了數據一致性。
- 訂票系統(超賣問題):
- 剩余票數:1張。
- 用戶A和用戶B同時點擊購買。
- 服務器進程A讀取票數:1。
- 服務器進程B讀取票數:1。
- 進程A判斷有票,執行扣減:1-1=0,更新為0,出票成功。
- 進程B判斷有票,執行扣減:1-1=0,更新為0,出票成功。
- 結果: 1張票賣給了兩個人。這破壞了業務規則。
- 鏈表插入操作(數據結構損壞):
- 鏈表: Node1 -> Node2。
- 線程A要在Node1和Node2之間插入NodeX。
- 線程B要刪除Node2。
- 如果沒有鎖控制時序,可能導致:
- 線程A剛讓Node1指向NodeX(但NodeX還沒指向Node2)。
- 線程B刪除Node2,讓Node1(或前一個節點)指向Node2的下一個節點(可能是NULL)。
- 結果:NodeX懸空或鏈表斷裂。這破壞了數據結構完整性。
這些問題的根源在于:
- 非原子操作: 像“讀取-修改-寫入”這樣的復合操作,如果中間被其他操作打斷,就會導致結果錯誤。
- 操作交錯的不可預測性: 多個操作以不同的順序和時機交織執行(交錯),會產生各種預期之外的結果。
- 內存/緩存可見性問題: 一個線程對共享變量的修改可能不會立即被其他線程看到(由于CPU緩存的存在)。
鎖就是解決這些問題的“協調員”:
- 實現互斥: 鎖確保在同一時刻,只有一個執行單元能進入受保護的代碼區域(臨界區)訪問或修改特定的共享資源。其他執行單元必須等待鎖釋放。
- 保證原子性: 對于復合操作(如余額增減、票數扣減、鏈表節點指針修改),鎖可以將它們包裝成一個不可分割的操作單元,在執行過程中不會被其他操作打斷。
- 保障可見性: 在釋放鎖時,通常會強制將修改刷新到主內存;在獲取鎖時,通常會強制從主內存重新加載最新值。這確保了臨界區內修改的結果對后續獲得鎖的執行單元是可見的。
- 維持順序: 鎖隱式地建立了操作的先后順序,避免了破壞性交錯的產生。
二、鎖有哪些應用場景?
鎖的應用極其廣泛,存在于計算機系統的各個層面:
- 操作系統內核:
- 保護內核數據結構: 進程表、文件描述符表、內存管理結構(如頁表、空閑列表)、設備驅動狀態等。多個CPU核心上的線程或中斷處理程序都需要安全地訪問這些全局結構。
- 同步原語實現: 信號量、條件變量、屏障等同步機制本身就需要鎖(通常是自旋鎖)來保護其內部狀態。
- 設備訪問: 確保同一時間只有一個進程/線程能向特定硬件設備(如打印機、特定端口)發送命令或數據。
- 多線程應用程序:
- 保護共享內存變量: 計數器、標志位、配置參數等。
- 保護復雜數據結構: 鏈表、哈希表、樹、隊列等。插入、刪除、查找(如果查找可能觸發修改)都需要鎖來防止結構損壞。
- 單例模式實現: 確保在多線程環境下,一個類只有一個實例被創建(通常使用互斥鎖+雙重檢查鎖定)。
- 線程池任務隊列: 多個工作線程從任務隊列取任務,生產者線程向隊列添加任務。隊列本身需要鎖保護。
- 資源池管理: 如數據庫連接池、線程池。分配和回收資源需要互斥操作。
- 緩存同步: 更新和讀取共享緩存數據時。
- 數據庫管理系統:
- 事務并發控制: 這是數據庫鎖最核心的應用。數據庫使用各種粒度的鎖(行鎖、頁鎖、表鎖、意向鎖)和不同模式的鎖(共享鎖/S鎖、排他鎖/X鎖)來實現不同的事務隔離級別,保證ACID特性(特別是隔離性I和一致性C)。
- 行鎖/記錄鎖: 防止兩個事務同時修改同一條記錄。
- 間隙鎖: 防止幻讀(在范圍查詢中插入新記錄)。
- 表鎖: 在特定操作(如ALTER TABLE)或行鎖沖突升級時使用。
- 死鎖檢測與處理: 數據庫有專門的機制來處理事務間因循環等待鎖而導致的死鎖。
- 索引維護: 對B+樹等索引結構進行分裂、合并等操作時需要鎖保護。
- 緩存管理: 數據庫緩沖池(Buffer Pool)的管理也需要鎖機制。
- 事務并發控制: 這是數據庫鎖最核心的應用。數據庫使用各種粒度的鎖(行鎖、頁鎖、表鎖、意向鎖)和不同模式的鎖(共享鎖/S鎖、排他鎖/X鎖)來實現不同的事務隔離級別,保證ACID特性(特別是隔離性I和一致性C)。
- 文件系統:
- 文件寫入: 防止多個進程同時寫入同一個文件導致內容混亂。通常使用文件鎖(如
flock
,fcntl
)。 - 元數據更新: 修改文件的屬性(如大小、權限、時間戳)、目錄結構(創建、刪除、重命名文件/目錄)時需要鎖保護,避免元數據不一致。
- 文件寫入: 防止多個進程同時寫入同一個文件導致內容混亂。通常使用文件鎖(如
- 分布式系統:
- 分布式鎖: 在多個獨立的服務器或進程之間協調對共享資源的訪問。例如:
- 防止多個節點同時執行同一個定時任務。
- 確保在分布式環境中對某個全局配置項的修改是互斥的。
- 實現分布式環境下的選主(Leader Election)。
- 控制對共享存儲(如分布式文件系統中的一個文件)的并發寫入。
- 常用實現: ZooKeeper, Redis (RedLock), etcd, Consul 等提供的分布式鎖服務。這比單機鎖復雜得多,需要處理網絡延遲、節點故障、時鐘漂移等問題。
- 分布式鎖: 在多個獨立的服務器或進程之間協調對共享資源的訪問。例如:
三、常見的鎖類型
-
互斥鎖:
- 特點: 最基本的鎖類型。嚴格互斥,一次只允許一個持有者。
- 行為: 如果一個線程試圖獲取已被持有的互斥鎖,它將被阻塞(進入睡眠狀態),直到鎖被釋放。操作系統會進行線程調度。
- 用途: 保護需要絕對互斥訪問的臨界區。
- 例子:
pthread_mutex_t
(POSIX),std::mutex
(C++),synchronized
關鍵字修飾的方法或代碼塊 (Java 內部使用互斥鎖),Lock
(數據庫中的排他鎖)。
-
自旋鎖:
- 特點: 當嘗試獲取鎖失敗時,線程不會立即阻塞,而是在一個循環中不斷檢查鎖的狀態(“自旋”)。
- 優點: 避免上下文切換的開銷,對于預期等待時間非常短的場景效率高。
- 缺點: 浪費CPU周期(忙等待),如果等待時間長,效率極低。
- 用途: 多處理器系統,臨界區非常小且執行時間極短,且持有鎖的線程不太可能被搶占的場景(如內核中斷處理)。
- 例子:
pthread_spinlock_t
(POSIX),std::atomic_flag
(C++ 可用于實現自旋鎖), 底層硬件指令如test-and-set
,compare-and-swap
。
-
讀寫鎖:
- 特點: 區分讀操作和寫操作。
- 讀鎖: 允許多個讀者同時持有。只要沒有寫者,讀者就可以并發訪問。
- 寫鎖: 是排他的。一個寫鎖被持有時,不能有其他讀者或寫者。獲取寫鎖通常需要等待所有現有的讀者釋放讀鎖。
- 優點: 大幅提高讀多寫少場景的并發性能。
- 缺點: 實現比互斥鎖復雜;如果寫操作頻繁,可能導致讀者或寫者“餓死”(需要公平策略)。
- 用途: 保護經常被讀取但較少被修改的數據結構(如配置信息、緩存)。
- 例子:
pthread_rwlock_t
(POSIX),std::shared_mutex
(C++17),ReentrantReadWriteLock
(Java),LOCK IN SHARE MODE
/FOR SHARE
(數據庫共享鎖),FOR UPDATE
(數據庫排他鎖)。
- 特點: 區分讀操作和寫操作。
-
?悲觀鎖 vs 樂觀鎖?
?類型? ?機制? ?適用場景? ?悲觀鎖? 默認資源會被修改,訪問前強制加鎖(如行鎖、表鎖) 寫操作頻繁的高沖突場景? ?樂觀鎖? 通過版本號或CAS算法檢測沖突,提交時校驗 讀多寫少的低沖突場景(如電商庫存)?
CAS(Compare-And-Swap) 是一種關鍵的無鎖(Lock-Free)編程原子操作,也是實現樂觀并發控制的核心。它直接由大多數現代CPU提供硬件支持(通過特定的機器指令),用于在多線程/多處理器環境下安全地更新共享變量,而無需使用傳統的互斥鎖。
工作流程(從線程視角)
- 讀取: 線程讀取共享變量
V
的當前值,記為current_value
。 - 計算: 線程基于
current_value
計算出希望更新的新值new_value
。 - CAS嘗試: 線程執行
CAS(V, current_value, new_value)
。- 成功: 如果
V
的當前值仍然等于current_value
(意味著在此期間沒有其他線程修改過V
),則V
被原子地設置為new_value
,返回true
。線程的更新操作完成。 - 失敗: 如果
V
的當前值不等于current_value
(意味著在此期間有其他線程搶先修改了V
),則CAS
什么也不做(不更新V
),返回false
。
- 成功: 如果
- 失敗處理: 如果
CAS
失敗,線程通常不會阻塞,而是選擇:- 放棄: 如果操作允許失敗。
- 重試(自旋): 最常見的方式。線程回到步驟1,重新讀取
V
的最新值作為新的current_value
,重新計算new_value
,然后再次嘗試CAS
。這個循環(讀取 -> 計算 -> CAS)會一直持續,直到CAS
成功或達到某種退出條件(如重試次數上限)。
四、golang中的鎖機制
在Go語言中,處理并發問題時通常優先考慮使用信道(channel),但在某些情況下,當信道無法解決問題時,就需要使用鎖機制來處理共享內存的并發訪問。Go語言提供了兩種主要的鎖類型:互斥鎖(Mutex)和讀寫鎖(RWMutex)。
1. 互斥鎖(sync.Mutex)
- 作用:確保同一時間只有一個goroutine訪問共享資源。
- 方法:
Lock()
:獲取鎖(若鎖被占用,則阻塞當前goroutine)Unlock()
:釋放鎖
- 特點:
- 不可重入:同一goroutine重復加鎖會導致死鎖。
- 零值可用:未初始化的
Mutex
可直接使用。
var mu sync.Mutex
var counter intfunc increment() {mu.Lock() // 加鎖defer mu.Unlock() // 確保解鎖(推薦用defer避免忘記解鎖)counter++
}
2. 讀寫鎖(sync.RWMutex)
- 適用場景:讀多寫少(如緩存系統)。
- 方法:
RLock()
:獲取讀鎖(允許多個讀并發)RUnlock()
:釋放讀鎖Lock()
:獲取寫鎖(獨占,與讀/寫互斥)Unlock()
:釋放寫鎖
- 規則:
- 寫鎖優先級高于讀鎖(防止讀鎖餓死寫鎖)
- 持有讀鎖時無法升級為寫鎖
var rwMu sync.RWMutex
var data map[string]stringfunc read(key string) string {rwMu.RLock() // 讀鎖defer rwMu.RUnlock()return data[key]
}func write(key, value string) {rwMu.Lock() // 寫鎖defer rwMu.Unlock()data[key] = value
}
3. Mutex的優化機制
- 自旋嘗試:
當鎖被短期持有時,等待的goroutine會在用戶態自旋嘗試(約4次),避免立即進入休眠(減少上下文切換開銷)。 - 饑餓模式:
若某個goroutine等待超過1ms,鎖會進入饑餓模式——新來的goroutine直接排隊(不搶鎖),確保公平性。
4. RWMutex的設計
- 讀鎖計數:
通過readerCount
跟蹤當前讀鎖數量(正數表示讀鎖,負數表示有寫鎖等待)。 - 寫鎖優先:
當有寫鎖等待時,新來的讀鎖會被阻塞,防止寫鎖被餓死。
五、mysql的鎖機制
1. 全局鎖(Global Lock)
- 作用:鎖定整個數據庫實例,使所有表處于只讀狀態。
- 命令:
FLUSH TABLES WITH READ LOCK
(FTWRL)25。 - 場景:全庫邏輯備份(如
mysqldump
)時確保數據一致性。 - 風險:阻塞所有寫操作,長時間鎖定會導致業務癱瘓。推薦事務引擎使用
–single-transaction
參數(基于MVCC備份,不阻塞寫)24。
2. 表級鎖(Table Lock)
- 類型:
- 普通表鎖:通過
LOCK TABLES ... READ/WRITE
手動加鎖,讀鎖允許多會話并發讀但阻塞寫,寫鎖獨占36。 - 元數據鎖(MDL):自動加鎖,保護表結構。DML操作(如
SELECT
)加MDL讀鎖,DDL操作(如ALTER TABLE
)加MDL寫鎖,讀寫互斥。長事務會阻塞DDL,導致雪崩24。 - 意向鎖(Intention Lock):表級鎖,分為意向共享鎖(IS)和意向排他鎖(IX),用于快速判斷表中是否有行鎖沖突48。
- 普通表鎖:通過
- 適用引擎:MyISAM默認表鎖;InnoDB顯式支持。
- 特點:開銷小、加鎖快、無死鎖,但并發度低39。
3. 行級鎖(Row Lock)
- 適用引擎:僅InnoDB支持,細粒度鎖定單行數據。
- 特點:開銷大、加鎖慢、可能出現死鎖,但并發度高16。
- 加鎖條件:必須通過索引檢索數據,否則退化為表鎖310。
4.共享鎖(S鎖)
共享鎖(S鎖):SELECT ... LOCK IN SHARE MODE
,允許多事務讀,阻塞寫。
允許多事務?并發讀取同一數據?,但阻止任何事務獲取排他鎖進行修改?
鎖兼容性:多個S鎖可共存,但S鎖與X鎖互斥?
5.排他鎖(X鎖)
排他鎖(X鎖):SELECT ... FOR UPDATE
或自動加鎖(如UPDATE
),獨占數據
事務持有X鎖時,?禁止其他事務加任何鎖?(包括S鎖和X鎖),實現獨占讀寫?
自動應用于寫操作:UPDATE
、DELETE
、INSERT
語句默認加X鎖?
6.悲觀鎖(Pessimistic Lock)
- ?實現機制?
- 基于?數據庫原生鎖機制?(S鎖/X鎖),操作前先加鎖,假設高并發沖突概率?。
- 典型語句:
SELECT ... FOR UPDATE
(X鎖)、SELECT ... LOCK IN SHARE MODE
(S鎖)?。
- ?適用場景?
- 寫密集型操作(如庫存扣減、支付交易)?。
- 需強一致性的金融系統,容忍鎖開銷換取安全性?。
7.樂觀鎖(Optimistic Lock)
-
?實現機制?
-
?無鎖設計?:通過業務層邏輯(版本號/時間戳)檢測沖突,提交時校驗數據一致性?89。
-
偽代碼邏輯:
sql CodeUPDATE products SET stock = new_stock, version = version + 1 WHERE id = 10 AND version = current_version; -- 校驗版本號
-
-
?適用場景?
- 讀多寫少場景(如評論計數更新)?。
- 分布式系統,減少數據庫鎖競爭開銷?。