天天看點

分布式前修課:分布式鎖技術

分布式鎖技術

什麼是鎖?

在介紹分布式鎖之前,我們先來聊一聊鎖:

和生活中的鎖是一樣的,都是為了鎖住門或者其他東西而保護某些東西。

在程式中也是一樣的,當出現多個線程對同一個資源進行操作的時候,如果沒有進行合理的控制,那麼就會出現問題,也就是我們常說的并發問題

舉個小例子:

分布式前修課:分布式鎖技術
 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

    屬于可重入,允許不公平【可以設定公平】
  • Lock

    基于CAS樂觀鎖,可以判斷鎖的狀态,基于不同場景可以設定不同的鎖以提高性能,而

    synchronized

    基于底層指令來實作鎖,且不能判斷鎖的狀态,在鎖競争非常激烈的時候性能會下降很多
案例這裡就不給了,後續實際操作出案例

分布式鎖?

你突然肚子疼,廁所隻有一個坑位。
你去上廁所,發現廁所門口站了三堆人,都在等這一個坑位

這就屬于分布式場景,那在哪裡展現出

呢?

廁所外面多了一個大門,等廁所裡的人出來之後,打開大門,三堆人發現之後開始搶着進去,第一個進去的人就将大門鎖住,然後去上廁所
分布式前修課:分布式鎖技術

那麼,我們要考慮一個問題:已經存在分布式鎖,那麼是否需要加單機鎖?

從理論上來講應該是需要加上單機鎖的。雖然是分布式場景,但是先從單機下将部分線程進行攔截,很大程度上能夠降低開銷。

既然是分布式場景下,那麼必然是需要借助第三方元件,這樣就涉及到了網絡傳輸過程,高并發情況下必然會影響系統的整體性能和高可用性
分布式前修課:分布式鎖技術

實作方式

基于MySQL實作原理

MySQL

加鎖使用量很少,如果并發量不是很大的情況下想要實作分布式鎖,而且還不想添加新的元件。可以考慮使用

我們知道MySQL自身是支援鎖操作的,如果采用InnoDB引擎的話,支援表級鎖和行級鎖。

當然我們在這裡使用行級鎖中排他鎖

将自動送出關閉,通過對行

select for update

增加排他鎖,執行成功之後就算是加鎖成功,而其他線程再沒辦法對同一條資料加鎖,到此我們就認為得到排他鎖的線程加鎖成功

想要解鎖的話,也很簡單,隻要

commit()

送出事務即可實作

基于

排他鎖

的實作方式雖然簡單,但是存在如下問題:

  • 排他鎖會占用連接配接,産生連接配接爆滿的問題
  • 存在并發問題【當然,也說過在并發量不大的情況下】
基于Redis實作原理

Redis

實作分布式鎖的原理也非常簡單:

  • Redis是KV形式存儲,隻要我們能将一個KEY存儲在

    Redis

    中,那麼這個鎖也就添加成功,再有其他線程過來我們隻需要判斷這個key是否存在就可以
 # 鎖是否存在
 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如何實作分布式鎖:

  • /lock

    為字首建立全局唯一key,那麼根據其Prefix機制,争搶用戶端想要連接配接寫操作,而實際上寫入的key會進行排序:

    /lock/UUID1, /lock/UUID2

    。而在Etcd中是支援設定租約,當租約到期,key會自動失效
  • 當一個用戶端持有鎖期間,其他用戶端隻能等待。而為了避免等待期間租約失效,用戶端需建立一個定時任務作為

    心跳

    進行續約。如果再此期間執行業務出現異常,key會因租約到期而被删除,進而進行鎖釋放防止死鎖
  • 用戶端執行PUT操作,将資料寫入到Etcd中,根據其Revision機制,會給對應的用戶端傳回唯一的revision,用戶端根據得到revision判斷是否是目前

    /lock

    清單中最小的,如果是則認為獲得鎖;否則就監聽自己的前一個key,一旦監聽到删除時間或者租約失效而删除的事件,那麼自己就獲得鎖
  • 執行完對應的業務邏輯之後,将key删除就會自動釋放鎖

最後

繼續閱讀