Redis作分佈式鎖可能不那麼簡單

image

在計算機世界裏,對於鎖你們並不陌生,在現代全部的語言中幾乎都提供了語言級別鎖的實現,爲何咱們的程序有時候會這麼依賴鎖呢?這個問題仍是要從計算機的發展提及,隨着計算機硬件的不斷升級,多核cpu,多線程,多通道等技術把計算機的計算速度大幅度提高,原來同一時間只能執行一條cpu指令的時代已通過去。隨着多條cpu指令能夠並行執行的緣由,原來未曾出現的資源競爭隨着出現,在程序中的體現就是隨處可見的多線程環境。好比要更新數據庫的一個信息,若是沒有併發控制,多個線程同時操做的話,就會出現互相覆蓋的現象發生。程序員

鎖要解決的就是資源競爭的問題,也就是要把執行的指令順序化redis

爲何須要分佈式鎖

隨着互聯網的興起,現代軟件發生了翻天覆地的變化,之前單機的程序,已經支撐不了現代的業務。不管是在抗壓,仍是在高可用等方面都須要多臺計算機協同工做來解決問題。現代的互聯網系統都是分佈式部署的,分佈式部署確實能帶來性能和效率上的提高,但爲此,咱們就須要多解決一個分佈式環境下,數據一致性的問題。算法

當某個資源在多系統之間共享的時候,爲了保證你們訪問這個資源數據是一致的,那麼就必需要求在同一時刻只能被一個客戶端處理,不能併發的執行,否者就會出現同一時刻有人寫有人讀,你們訪問到的數據就不一致了。數據庫

在分佈式系統的時代,傳統線程之間的鎖機制,就沒做用了,系統會有多份而且部署在不一樣的機器上,這些資源已經不是在線程之間共享了,而是屬於進程(服務器)之間共享的資源。設計模式

所以,爲了解決這個問題,咱們就必須引入「分佈式鎖」。分佈式鎖,是指在分佈式的部署環境下,經過鎖機制來讓多客戶端互斥的對共享資源進行訪問。分佈式鎖的特色以下:服務器

  • 互斥性:和咱們本地鎖同樣互斥性是最基本,可是分佈式鎖須要保證在不一樣節點的不一樣線程的互斥。
  • 可重入性:同一個節點上的同一個線程若是獲取了鎖以後那麼也能夠再次獲取這個鎖。
  • 鎖超時:和本地鎖同樣支持鎖超時,防止死鎖。
  • 高效,高可用:加鎖和解鎖須要高效,同時也須要保證高可用防止分佈式鎖失效,能夠增長降級。
  • 支持阻塞和非阻塞:和 ReentrantLock 同樣支持 lock 和 trylock 以及 tryLock(long timeOut)。

基於redis分佈式鎖

若是你經過網絡搜索分佈式鎖,最多的就是基於redis的了。基於redis的分佈式鎖得益於redis的單線程執行機制,單線程在執行上就保證了指令的順序化,因此很大程度上下降了開發人員的思考設計成本。可是,基於redis作分佈式鎖難道真的這麼容易嗎?網絡

原子操做

基於redis的分佈式鎖經常使用命令是數據結構

SETNX key value

只在鍵 key 不存在的狀況下,將鍵 key的值設置爲value 。若鍵key 已經存在, 則SETNX 命令不作任何動做。SETNX 是『SET if Not eXists』(若是不存在,則 SET)的簡寫。代碼示例:多線程

redis> SETNX redislock "redislock"    # redislock 設置成功
(integer) 1

redis> SETNX redislock "redislock2"   # 嘗試覆蓋 redislock ,失敗
(integer) 0

redis> GET redislock                   # 沒有被覆蓋
"redislock"

成功獲取到鎖以後,而後設置一個過時時間(這裏避免了客戶端down掉,鎖得不到釋放的問題)架構

redis> expire redislock 5

成功拿到鎖的客戶端順利進行本身的業務,業務代碼執行完,而後再刪除該key

redis> DEL redislock

若是一切都想一想象的那麼順利,程序員TMD就不用996了。假如客戶端拿到鎖以後,執行設置超時指令以前down掉了(現實老是那麼悲劇),那這個鎖就永遠都釋放不了.也許你會想到用 Redis 事務來解決。可是這裏不行,由於 expire 是依賴於 setnx 的執行結果的,若是 setnx 沒搶到鎖,expire 是不該該執行的。事務裏沒有 if-else 分支邏輯,事務的特色是一口氣執行,要麼所有執行要麼一個都不執行。公司幾個億的業務又被你耽誤了...

以上狀況的出現是由於兩個命令並不是一個原子性操做,因此在redis 2.8 版本以後出現了新的命令

SETEX key seconds value

因此如今能夠利用一條原子性操做的命令來獲取鎖了

redis> SETEX redislock 60 redislock
OK

redis> GET redislock  # 值
"redislock"

redis> TTL redislock  # 剩餘生存時間
(integer) 49
超時問題

在正常的業務當中,當一個線程獲取到鎖而且設置了鎖的過時時間以後,會出現因爲業務代碼執行時間過長,鎖因爲到達超時時間自動釋放的狀況。自動釋放以後,其餘的線程就會獲取到分佈式鎖,致使業務代碼不會串行執行。若是業務上容許這樣的狀況偶爾發生,那程序員就開幹吧,最後頂多人工干預一下,update 一下數據庫。

爲了不這類狀況發生,在使用redis分佈式鎖的時候,業務方應儘可能避免長時間執行的代碼任務。

若是設置鎖的超時時間比較長,在必定程度上能夠緩解業務代碼執行時間長鎖自動到期的問題,可是一旦業務代碼down掉,其餘等待鎖的線程等待的時間會比較長,這種狀況下,確保獲取到鎖的程序不會down 成爲了主要問題。

獲取鎖失敗

當鎖被一個調用方獲取以後,其餘調用方在獲取鎖失敗以後,是繼續輪詢仍是直接業務失敗呢?若是是繼續輪詢的話,同步狀況下當前線程會一直處於阻塞狀態,因此這裏輪詢的狀況仍是建議使用異步。

可重入性

可重入性是指已經擁有鎖的客戶端再次請求加鎖,若是鎖支持同一個客戶端重複加鎖,那麼這個鎖就是可重入的。若是基於redis的分佈式鎖要想支持可重入性,須要客戶端封裝,可使用threadlocal存儲持有鎖的信息。這個封裝過程會增長代碼的複雜度,因此菜菜不推薦這樣作。

redis掛了

若是在多個客戶端獲取鎖的過程當中,redis 掛了怎麼辦呢?假如一個客戶端已經獲取到了鎖,這個時候redis掛了(假如是redis集羣),其餘的redis服務器會接着提供服務,這個時候其餘客戶端能夠在新的服務器上獲取到鎖了,這也致使了鎖意義的丟失。有興趣的同窗能夠去看看RedLock,這種方案以犧牲性能的代價解決了這個問題。

時鐘跳躍問題

在某些時候,redis的服務器時間發生的跳躍,因爲鎖的過時時間依賴於服務器時間,因此也會出現兩個客戶端同時獲取到鎖的狀況發生。

當把以上問題都有解決方案了以後,基於redis的分佈式鎖才能夠放心使用

基於redis設計簡單分佈式鎖容易,可是設計完美分佈式鎖不易, 還以爲基於redis的分佈式鎖好作嗎?

更多精彩文章

image

相關文章
相關標籤/搜索