程序員修神之路--redis做分布式鎖可能不那么簡單


菜菜哥,復聯四上映了,要不要一起去看看?

又想騙我電影票,對不對?

呵呵,想去看了叫我呀

看來你工作不飽和呀

哪有,這兩天我剛基於redis寫了一個分布式鎖,很簡單

不管你基於什么做分布式鎖,你覺得很簡單嗎?來來來


        在計算機世界里,對於鎖大家並不陌生,在現代所有的語言中幾乎都提供了語言級別鎖的實現,為什么我們的程序有時候會這么依賴鎖呢?這個問題還是要從計算機的發展說起,隨着計算機硬件的不斷升級,多核cpu,多線程,多通道等技術把計算機的計算速度大幅度提升,原來同一時間只能執行一條cpu指令的時代已經過去。隨着多條cpu指令可以並行執行的原因,原來不曾出現的資源競爭隨着出現,在程序中的體現就是隨處可見的多線程環境。比如要更新數據庫的一個信息,如果沒有並發控制,多個線程同時操作的話,就會出現互相覆蓋的現象發生。

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


0 1
為什么需要分布式鎖


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

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

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

        因此,為了解決這個問題,我們就必須引入「分布式鎖」。分布式鎖,是指在分布式的部署環境下,通過鎖機制來讓多客戶端互斥的對共享資源進行訪問。分布式鎖的特點如下:

1
互斥性
和我們本地鎖一樣互斥性是最基本,但是分布式鎖需要保證在不同節點的不同線程的互斥。
2
可重入性
同一個節點上的同一個線程如果獲取了鎖之后那么也可以再次獲取這個鎖。
3
鎖超時
和本地鎖一樣支持鎖超時,防止死鎖。
4
高效,高可用
加鎖和解鎖需要高效,同時也需要保證高可用防止分布式鎖失效,可以增加降級。
5
支持阻塞和非阻塞
和 ReentrantLock 一樣支持 lock 和 trylock 以及 tryLock(long timeOut)。


02
基於redis分布式鎖


        如果你通過網絡搜索分布式鎖,最多的就是基於redis的了。基於redis的分布式鎖得益於redis的單線程執行機制,單線程在執行上就保證了指令的順序化,所以很大程度上降低了開發人員的思考設計成本。但是,基於redis做分布式鎖難道真的這么容易嗎?

1
原子操作

基於redis的分布式鎖常用命令是

SETNX key value

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

redis> SETNX redislock "redislock"    # redislock 設置成功
(integer1
redis> SETNX redislock "redislock2"   # 嘗試覆蓋 redislock ,失敗
(integer0
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
2
超時問題

        在正常的業務當中,當一個線程獲取到鎖並且設置了鎖的過期時間之后,會出現由於業務代碼執行時間過長,鎖由於到達超時時間自動釋放的情況。自動釋放之后,其他的線程就會獲取到分布式鎖,導致業務代碼不會串行執行。如果業務上允許這樣的情況偶爾發生,那程序員就開干吧,最后頂多人工干預一下,update 一下數據庫。

        為了避免這類情況發生,在使用redis分布式鎖的時候,業務方應盡量避免長時間執行的代碼任務。

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

3
獲取鎖失敗

當鎖被一個調用方獲取之后,其他調用方在獲取鎖失敗之后,是繼續輪詢還是直接業務失敗呢?如果是繼續輪詢的話,同步情況下當前線程會一直處於阻塞狀態,所以這里輪詢的情況還是建議使用異步。

4
可重入性

        可重入性是指已經擁有鎖的客戶端再次請求加鎖,如果鎖支持同一個客戶端重復加鎖,那么這個鎖就是可重入的。如果基於redis的分布式鎖要想支持可重入性,需要客戶端封裝,可以使用threadlocal存儲持有鎖的信息。這個封裝過程會增加代碼的復雜度,所以菜菜不推薦這樣做。

5
redis掛了

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

6
時鍾跳躍問題

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


當把以上問題都有解決方案了之后,基於redis的分布式鎖才可以放心使用


基於redis設計簡單分布式鎖容易,但是設計完美分布式鎖不易, 還覺得基於redis的分布式鎖好做嗎?




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM