以下內容為目前自己理解的總結,如有錯誤請大家指正。
什么是鎖
-
在單進程的系統中,當存在多個線程可以同時改變某個變量(可變共享變量)時,就需要對變量或代碼塊做同步,使其在修改這種變量時能夠線性執行消除並發修改變量。
-
而同步的本質是通過鎖來實現的。為了實現多個線程在一個時刻同一個代碼塊只能有一個線程可執行,那么需要在某個地方做個標記,這個標記必須每個線程都能看到,當標記不存在時可以設置該標記,其余后續線程發現已經有標記了則等待擁有標記的線程結束同步代碼塊取消標記后再去嘗試設置標記。這個標記可以理解為鎖。
-
不同地方實現鎖的方式也不一樣,只要能滿足所有線程都能看得到標記即可。如java中synchronize是在對象頭設置標記,Lock接口的實現類基本上都只是某一個volitile修飾的int型變量其保證滅個線程都能擁有對該int的可見性和原子修改,linux內核中也是利用互斥量或信號量等內存數據做標記。
-
除了利用內存數據做鎖其實任何互斥的都能做鎖(只考慮互斥情況),如流水表中流水號與時間結合做冪等校驗可以看作是一個不會釋放的鎖,或者使用某個文件是否存在作為鎖等。只需要滿足在對標記進行修改能保證原子性和內存可見性即可。
分布式
分布式情況
此處主要指集群模式下,多個相同服務同時開啟.
- 分布式與單機情況下最大的不同在於其不是多線程而是多進程。
- 多線程由於可以共享堆內存,因此可以簡單的采取內存作為標記存儲位置。而進程之間甚至可能都不在同一台物理機上,因此需要將標記存儲在一個所有進程都能看到的地方。
分布式鎖
-
當在分布式模型下,數據只有一份(或有限制),此時需要利用鎖的技術控制某一時刻修改數據的進程數。
-
與單機模式下的鎖不僅需要保證進程可見,還需要考慮進程與鎖之間的網絡問題。(我覺得分布式情況下之所以問題變得復雜,主要就是需要考慮到網絡的延時和不可靠。。。一個大坑)
-
分布式鎖還是可以將標記存在內存,只是該內存不是某個進程分配的內存而是公共內存如Redis、Memcache。至於利用數據庫、文件等做鎖與單機的實現是一樣的,只要保證標記能互斥就行。
單機Redis鎖
基本鎖
- 原理:利用Redis的setnx如果不存在某個key則設置值,設置成功則表示取得鎖成功。
- 缺點:如果獲取鎖后的進程,在還沒執行完的時候掛調了,則鎖永遠不會釋放。
改進型
- 改進:在基本型是鎖上的setnx后設置expire,保證即使獲取鎖的進程不主動釋放鎖,過一段時間后也能自動釋放。
- 缺點:
- setnx與expire不是一個原子操作,可能執行完setnx該進程就掛了。
- 當鎖過期后,該進程還沒執行完,可能造成同時多個進程取得鎖。(貌似這個問題目前還沒有很優雅的解決方案)
再改進
- 改進:利用Lua腳本,將setnx與expire變成一個原子操作,可解決一部分問題。
- 缺點:還是鎖過期的問題。
步驟
1. 直接調用Lua腳本原子setnx同時expire,設置一個隨機值。
2. 獲取到鎖則執行同步代碼塊,沒獲取則根據業務場景可以選擇自旋、休眠、或做一個等待隊列等擁有鎖進程來喚醒(類似Synchronize的同步隊列)。
3. 當同步代碼塊執行完成,先判斷鎖的key是否是自己設置的,如果是則刪除key(可利用Lua做成原子操作),不是則表明自己的鎖已經過期,不需要刪除。(這時候就出現了多進程同時有鎖的問題了)
總結
一般情況下直接用setnx加expire就夠了,但從安全性的角度看還是存在一下幾個問題:
- 單點問題。單機Redis只在單機上,如果單機down了,那么所有需要用分布式鎖的地方均獲取不到鎖,全部阻塞。需要做好降級的處理。
- 可能出現多進程同時擁有鎖。
Redlock
Redlock是Redis的作者antirez給出的集群模式的Redis分布式鎖,它基於N個完全獨立的Redis節點(通常情況下N可以設置成5)。
步驟
1. 獲取當前時間(毫秒數)。
2. 按順序依次向N個Redis節點執行獲取鎖的操作。獲取鎖的操作與單機鎖一樣。
3. 如果獲取鎖成功的節點數>=N/2+1,則再計算獲取鎖的時間有沒有超過鎖過期時間(可考慮設置一個必須留多長的時間給代碼塊執行),如果超過了則認為取鎖失敗。
4. 如果取鎖失敗則應該對所有節點進行釋放鎖的操作。
優化
- 當有5個節點,某次上鎖對a,b,c三個節點上鎖成功,而后c馬上down了,此時還沒通過AOF或RDB寫入磁盤。而后c又馬上恢復,此時c沒有上鎖數據,因此此時可能出現c,d,e三個節點被別的進程上鎖。所以在節點恢復時應該延時起碼一個鎖的過期時間。
Zookeeper鎖
zookeeper鎖相關基礎知識
- zk一般由多個節點構成(單數),采用zab一致性協議。因此可以將zk看成一個單點結構,對其修改數據其內部自動將所有節點數據進行修改而后才提供查詢服務。
- zk的數據以目錄樹的形式,每個目錄稱為 znode, znode中可存儲數據(一般不超過1M),還可以在其中增加子節點。
- 子節點有三種類型。序列化節點,每在該節點下增加一個節點自動給該節點的名稱上自增。臨時節點,一旦創建這個 znode 的客戶端與服務器失去聯系,這個 znode 也將自動刪除。最后就是普通節點。
- Watch機制,client可以監控每個節點的變化,當產生變化會給client產生一個事件。
zk基本鎖
-
原理:利用臨時節點與watch機制。每個鎖占用一個普通節點/lock,當需要獲取鎖時在/lock下創建一個臨時節點,創建成功則表示獲取鎖成功,失敗則watch/lock節點,有刪除操作后再去爭鎖。臨時節點好處在於當進程掛掉后能自動上鎖的節點自動刪除即取消鎖。
-
缺點:所有取鎖失敗的進程都監聽父節點,很容易發生羊群效應,即當釋放鎖后所有等待進程一起來創建節點,並發量很大。
zk鎖 優化
-
原理:上鎖改為創建臨時有序節點,每個上鎖的節點均能創建節點成功,知識其序號不同。只有序號最小的可以擁有鎖,當需要不是最小的則watch序號排在前面的一個節點(公平鎖)。
-
步驟:
1. 在/lock節點下創建一個有序臨時節點(EPHEMERAL_SEQUENTIAL)。
2. 判斷創建的節點序號是否最小,如果是最小則獲取鎖成功。不是則取鎖失敗,然后watch序號比本身小的前一個節點。
3. 當取鎖失敗,設置watch后則等待watch事件到來后,再次判斷是否序號最小。
4. 取鎖成功則執行代碼,最后刪除本身節點,釋放了鎖。
分布式鎖總結
分布式鎖存在的問題
- 均可能存在多進程擁有鎖的情況。redis鎖主要是expire時間與代碼執行時間的問題,zk鎖的問題在於zk是通過心跳監控進程存活狀態,如果進程進行GC pause或者因為網絡原因導致很長時間沒與zk聯系,則將導致zk認為進程已掛,而后鎖自動釋放,而此時進程並未掛任然在執行。
- Redlock鎖的時間問題。由於redis的expire的實現是通過pexpireat,如果某個節點發生時鍾跳躍,則該節點可能過早釋放鎖導致一系列問題。
解決方案
- 獲取鎖時提供一個fencing token(兩種說法,一種說需要有序,一種說隨機值就可以,我覺得隨機值就可以),在進程獲取鎖后對數據進行操作時,數據所在的資源服務器需要去鎖中查看當前token,如果token對的才執行,不對則放棄執行。
- 我覺得對於放棄執行的應該在我們的代碼塊中增加類似事物的rollback的操作。因此如果資源服務器拒絕了我們的操作則表明此時起碼已經存在了另外一個進程擁有鎖了,為了保證數據安全性不能繼續執行,因此需要回滾到執行代碼塊之前而繼續去競爭鎖。
- 至於Redis鎖的時間問題,Antirez說在運維層面是可以控制時鍾跳躍的區間的,只要能控制跳躍區間與expire的比例就沒問題,詳細可看《基於Redis的分布式鎖真的安全嗎?》
總結
- 大多數時候采用zk鎖就好了,沒必要再考慮安全性的問題。其實也可以通過zk鎖+冪等校驗來達到雙層保障。
- fencing 機制需要對數據服務進行修改適配,個人覺得沒這個必要吧。。。
目前就這些了。。。。后面想到再補充吧。
引用:基於Redis的分布式鎖真的安全嗎?
基於Redis的分布式鎖真的安全嗎?上
基於Redis的分布式鎖真的安全嗎?下