分佈式鎖的一點理解

如下內容爲目前本身理解的總結,若有錯誤請你們指正。java

什麼是鎖

  • 在單進程的系統中,當存在多個線程能夠同時改變某個變量(可變共享變量)時,就須要對變量或代碼塊作同步,使其在修改這種變量時可以線性執行消除併發修改變量。node

  • 而同步的本質是經過鎖來實現的。爲了實現多個線程在一個時刻同一個代碼塊只能有一個線程可執行,那麼須要在某個地方作個標記,這個標記必須每一個線程都能看到,當標記不存在時能夠設置該標記,其他後續線程發現已經有標記了則等待擁有標記的線程結束同步代碼塊取消標記後再去嘗試設置標記。這個標記能夠理解爲鎖。linux

  • 不一樣地方實現鎖的方式也不同,只要能知足全部線程都能看獲得標記便可。如java中synchronize是在對象頭設置標記,Lock接口的實現類基本上都只是某一個volitile修飾的int型變量其保證滅個線程都能擁有對該int的可見性和原子修改,linux內核中也是利用互斥量或信號量等內存數據作標記。
  • 除了利用內存數據作鎖其實任何互斥的都能作鎖(只考慮互斥狀況),如流水錶中流水號與時間結合作冪等校驗能夠看做是一個不會釋放的鎖,或者使用某個文件是否存在做爲鎖等。只須要知足在對標記進行修改能保證原子性和內存可見性便可。redis


分佈式

分佈式狀況

此處主要指集羣模式下,多個相同服務同時開啓.數據庫

  • 分佈式與單機狀況下最大的不一樣在於其不是多線程而是多進程。
  • 多線程因爲能夠共享堆內存,所以能夠簡單的採起內存做爲標記存儲位置。而進程之間甚至可能都不在同一臺物理機上,所以須要將標記存儲在一個全部進程都能看到的地方。

分佈式鎖

  • 當在分佈式模型下,數據只有一份(或有限制),此時須要利用鎖的技術控制某一時刻修改數據的進程數。
  • 與單機模式下的鎖不只須要保證進程可見,還須要考慮進程與鎖之間的網絡問題。(我以爲分佈式狀況下之因此問題變得複雜,主要就是須要考慮到網絡的延時和不可靠。。。一個大坑)安全

  • 分佈式鎖仍是能夠將標記存在內存,只是該內存不是某個進程分配的內存而是公共內存如Redis、Memcache。至於利用數據庫、文件等作鎖與單機的實現是同樣的,只要保證標記能互斥就行。服務器


單機Redis鎖

基本鎖

  • 原理:利用Redis的setnx若是不存在某個key則設置值,設置成功則表示取得鎖成功。
  • 缺點:若是獲取鎖後的進程,在還沒執行完的時候掛調了,則鎖永遠不會釋放。

改進型

  • 改進:在基本型是鎖上的setnx後設置expire,保證即便獲取鎖的進程不主動釋放鎖,過一段時間後也能自動釋放。
  • 缺點:
    1. setnx與expire不是一個原子操做,可能執行完setnx該進程就掛了。
    2. 當鎖過時後,該進程還沒執行完,可能形成同時多個進程取得鎖。(貌似這個問題目前尚未很優雅的解決方案)

再改進

  • 改進:利用Lua腳本,將setnx與expire變成一個原子操做,可解決一部分問題。
  • 缺點:仍是鎖過時的問題。

步驟

1. 直接調用Lua腳本原子setnx同時expire,設置一個隨機值。
2. 獲取到鎖則執行同步代碼塊,沒獲取則根據業務場景能夠選擇自旋、休眠、或作一個等待隊列等擁有鎖進程來喚醒(相似Synchronize的同步隊列)。
3. 當同步代碼塊執行完成,先判斷鎖的key是不是本身設置的,若是是則刪除key(可利用Lua作成原子操做),不是則代表本身的鎖已通過期,不須要刪除。(這時候就出現了多進程同時有鎖的問題了)

總結

通常狀況下直接用setnx加expire就夠了,但從安全性的角度看仍是存在一下幾個問題:網絡

  1. 單點問題。單機Redis只在單機上,若是單機down了,那麼全部須要用分佈式鎖的地方均獲取不到鎖,所有阻塞。須要作好降級的處理。
  2. 可能出現多進程同時擁有鎖。

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. 取鎖成功則執行代碼,最後刪除自己節點,釋放了鎖。

分佈式鎖總結

分佈式鎖存在的問題

  1. 都可能存在多進程擁有鎖的狀況。redis鎖主要是expire時間與代碼執行時間的問題,zk鎖的問題在於zk是經過心跳監控進程存活狀態,若是進程進行GC pause或者由於網絡緣由致使很長時間沒與zk聯繫,則將致使zk認爲進程已掛,然後鎖自動釋放,而此時進程並未掛任然在執行。
  2. Redlock鎖的時間問題。因爲redis的expire的實現是經過pexpireat,若是某個節點發生時鐘跳躍,則該節點可能過早釋放鎖致使一系列問題。

解決方案

  1. 獲取鎖時提供一個fencing token(兩種說法,一種說須要有序,一種說隨機值就能夠,我以爲隨機值就能夠),在進程獲取鎖後對數據進行操做時,數據所在的資源服務器須要去鎖中查看當前token,若是token對的才執行,不對則放棄執行。
  2. 我以爲對於放棄執行的應該在咱們的代碼塊中增長相似事物的rollback的操做。所以若是資源服務器拒絕了咱們的操做則代表此時起碼已經存在了另一個進程擁有鎖了,爲了保證數據安全性不能繼續執行,所以須要回滾到執行代碼塊以前而繼續去競爭鎖。
  3. 至於Redis鎖的時間問題,Antirez說在運維層面是能夠控制時鐘跳躍的區間的,只要能控制跳躍區間與expire的比例就沒問題,詳細可看《基於Redis的分佈式鎖真的安全嗎?》

總結

  1. 大多數時候採用zk鎖就行了,不必再考慮安全性的問題。其實也能夠經過zk鎖+冪等校驗來達到雙層保障。
  2. fencing 機制須要對數據服務進行修改適配,我的以爲沒這個必要吧。。。

目前就這些了。。。。後面想到再補充吧。

引用:基於Redis的分佈式鎖真的安全嗎?
基於Redis的分佈式鎖真的安全嗎?上
基於Redis的分佈式鎖真的安全嗎?下

相關文章
相關標籤/搜索