分布式鎖技術
什麼是鎖?
在介紹分布式鎖之前,我們先來聊一聊鎖:
和生活中的鎖是一樣的,都是為了鎖住門或者其他東西而保護某些東西。
在程式中也是一樣的,當出現多個線程對同一個資源進行操作的時候,如果沒有進行合理的控制,那麼就會出現問題,也就是我們常說的并發問題
舉個小例子:
public static void main(String[] args) {
final int[] i = {0};
final ExecutorService pool = Executors.newCachedThreadPool();
for (int j = 0; j < 100; j++) {
pool.submit(new Runnable() {
public void run() {
i[0]++;
}
});
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.HOURS);
System.out.println(i[0]);
}
執行結果會有非100的情況,這就是多線程操作問題
在多線程中,由于不能确定每個線程的操作順序,每個線程都在搶着執行是以最終執行順序是随機的。
那麼,解決這個問題的重點就是對這個執行方法進行加鎖操作
鎖特性
我們多聊一會,在程式中鎖和鎖是還是有一定的差別的,舉個重口味的例子:
你突然肚子疼,廁所隻有一個坑位。
你跑到廁所管理者跟前詢問:“廁所裡還有人麼?”,這時廁所管理者會對你說:“現在還有人,你先在這裡等着”
你剛排好隊恰好有另一個人也過來了,看你在排隊然後跟在了你的後面
明白了麼,這就是公平鎖,就是說加鎖之前會先檢查檢查是否有排隊的線程,優先執行排在前列的線程。
需要注意的一點是:公平鎖并不能保證卻對的公平。
然後我們繼續:
你突然肚子疼,廁所隻有一個坑位。
然後你站在廁所門口等着,這是過來一個人二話不說就要去上廁所。然後你和他進行友好決鬥之後去了廁所
這就是非公平鎖:線程加鎖從不考慮排隊問題,每個線程都在競争鎖資源,哪個線程能夠搶上鎖誰就能執行
你突然肚子疼,廁所隻有一個坑位,恰好被别人給占了
于是你在廁所門口一會問一遍:“你好了沒?”,一直問知道上廁所的人出來
這就是自旋鎖:線程加不上鎖就循環轉圈等待,知道能加上鎖為止
你突然肚子疼,廁所隻有一個坑位,恰好過去你就能上
你女朋友正好過來也想上廁所,發現是你在裡面就直接能進去
這就是可重入:辨別是同一個線程加鎖之後可以再次擷取鎖而不必等待,也不會出現死鎖
這節畫面感太強了 🤢
單機版鎖
在Java中,為我們提供了兩種加鎖方式:
- synchronized
- Lock
兩者的差別在于:
-
屬于指令關鍵字,标注在方法上,也可以包裹方法塊,而synchronized
屬于接口,隻适用于方法調用,使用起來比較靈活Lock
-
在退出的時候會自動釋放鎖,而synchronized
需要手動釋放Lock
-
屬于可重入,不可中斷,非公平鎖;而synchronized
屬于可重入,允許不公平【可以設定公平】Lock
-
基于CAS樂觀鎖,可以判斷鎖的狀态,基于不同場景可以設定不同的鎖以提高性能,而Lock
基于底層指令來實作鎖,且不能判斷鎖的狀态,在鎖競争非常激烈的時候性能會下降很多synchronized
案例這裡就不給了,後續實際操作出案例
分布式鎖?
你突然肚子疼,廁所隻有一個坑位。
你去上廁所,發現廁所門口站了三堆人,都在等這一個坑位
這就屬于分布式場景,那在哪裡展現出
鎖
呢?
廁所外面多了一個大門,等廁所裡的人出來之後,打開大門,三堆人發現之後開始搶着進去,第一個進去的人就将大門鎖住,然後去上廁所
那麼,我們要考慮一個問題:已經存在分布式鎖,那麼是否需要加單機鎖?
從理論上來講應該是需要加上單機鎖的。雖然是分布式場景,但是先從單機下将部分線程進行攔截,很大程度上能夠降低開銷。
既然是分布式場景下,那麼必然是需要借助第三方元件,這樣就涉及到了網絡傳輸過程,高并發情況下必然會影響系統的整體性能和高可用性
實作方式
基于MySQL實作原理
MySQL
加鎖使用量很少,如果并發量不是很大的情況下想要實作分布式鎖,而且還不想添加新的元件。可以考慮使用
我們知道MySQL自身是支援鎖操作的,如果采用InnoDB引擎的話,支援表級鎖和行級鎖。
當然我們在這裡使用行級鎖中排他鎖
将自動送出關閉,通過對行
select for update
增加排他鎖,執行成功之後就算是加鎖成功,而其他線程再沒辦法對同一條資料加鎖,到此我們就認為得到排他鎖的線程加鎖成功
想要解鎖的話,也很簡單,隻要
commit()
送出事務即可實作
基于
排他鎖
的實作方式雖然簡單,但是存在如下問題:
- 排他鎖會占用連接配接,産生連接配接爆滿的問題
- 存在并發問題【當然,也說過在并發量不大的情況下】
基于Redis實作原理
Redis
實作分布式鎖的原理也非常簡單:
- Redis是KV形式存儲,隻要我們能将一個KEY存儲在
中,那麼這個鎖也就添加成功,再有其他線程過來我們隻需要判斷這個key是否存在就可以Redis
# 鎖是否存在
EXISTS lock:1
# 加鎖
SET lock:1 1
- 其中一個線程加鎖成功,在執行業務的過程中出現異常而不能導緻鎖釋放,其他線程無法正常搶鎖。基于這種現象我們可以通過設定過期時間來處理這個問題
SET lock:1 1 EX 60
- 雖然這樣就可以實作,但是我們并不能保證
和exists
在執行的過程中是原子操作,我們将其合在一起set
# NX表示隻有KEY不存在的時候才會設定
# 如果傳回0,說明lock存在,如果傳回1,說明不存在
SET lock:1 1 NX EX 60
- 業務執行有長有短,如果執行時間超過了設定的有效期,那麼鎖自動釋放其他線程搶鎖就會造成問題。這裡有引出了看門狗機制:線程擷取到鎖之後,另外開啟一個線程循環判斷過期時間,在設定的過期時間内業務沒有執行完成那麼就給目前KEY續期
expire lock:1 60
- 釋放鎖隻需要将KEY删除就可以
# 釋放鎖
DELETE lock:1
# 為了能夠更加嚴謹一些,最好是能夠先判斷一下,防止誤釋放 ,是以我們來通過lua腳本來操作
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
以上就是Redis實作分布式鎖的原理,當然我們并不需要自己實作,已經有現成的技術架構我們可以直接使用
當然,為了加深印象,我們也會自己實作一次這個加鎖過程
基于Zookeeper實作原理
Zookeeper
作為分布式協調架構是非常NB的存在,在
Zookeeper
中已樹形結構存儲,每一層下不能存在重複節點,而節點可以分為:
- 持久節點,持久有序節點
- 臨時節點,臨時有序節點
有序就說明會根據建立順序從小到大排序
臨時節點
屬于當線程和Zookeeper的連接配接斷開,目前節點就會自動删除,能夠避免出現死鎖現象。
這連看門狗都省了
基于
Zookeeper
的實作方式就可以是這樣的:
- 建立
節點,用來統一管理鎖相關節點/lock
- 用戶端争先在
節點下建立臨時有序節點,/lock
根據其連接配接對節點進行排序Zookeeper
- 擷取
節點下的節點清單,根據/lock
傳回的資訊驗證自己是否是目前清單中最小的節點,如果是證明自己現在可以得到鎖;否則就通過Zookeeper
監聽自己前一個節點,一旦監聽到删除事件那麼自己就說明自己得到鎖Watch機制
- 執行業務邏輯之後,删除節點或者直接斷開連接配接,臨時節點自動删除就會釋放鎖
說起來可能比較簡單,但是做起來會相對有寫難度。不過别急,會專門來實作一下這個代碼!!!
基于Etcd實作原理
Etcd 是一個高度一緻的分布式鍵值存儲,主要用來做
K8s
的後端資料庫,是基于GO語言開發的一款開源項目。
在容器化盛行的時代,誕生了兩款開源項目:CoreOS和Etcd
我們主要是來分析一下Etcd如何實作分布式鎖:
- 以
為字首建立全局唯一key,那麼根據其Prefix機制,争搶用戶端想要連接配接寫操作,而實際上寫入的key會進行排序:/lock
。而在Etcd中是支援設定租約,當租約到期,key會自動失效/lock/UUID1, /lock/UUID2
- 當一個用戶端持有鎖期間,其他用戶端隻能等待。而為了避免等待期間租約失效,用戶端需建立一個定時任務作為
進行續約。如果再此期間執行業務出現異常,key會因租約到期而被删除,進而進行鎖釋放防止死鎖心跳
- 用戶端執行PUT操作,将資料寫入到Etcd中,根據其Revision機制,會給對應的用戶端傳回唯一的revision,用戶端根據得到revision判斷是否是目前
清單中最小的,如果是則認為獲得鎖;否則就監聽自己的前一個key,一旦監聽到删除時間或者租約失效而删除的事件,那麼自己就獲得鎖/lock
- 執行完對應的業務邏輯之後,将key删除就會自動釋放鎖