來自公眾號:架構師修行之路
菜菜哥,復聯四上映了,要不要一起去看看?
又想騙我電影票,對不對?
呵呵,想去看了叫我呀
看來你工作不飽和呀
哪有,這兩天我剛基於redis寫了一個分散式鎖,很簡單
不管你基於什麼做分散式鎖,你覺得很簡單嗎?來來來
在計算機世界裡,對於鎖大家並不陌生,在現代所有的語言中幾乎都提供了語言級別鎖的實現,為什麼我們的程式有時候會這麼依賴鎖呢?這個問題還是要從計算機的發展說起,隨著計算機硬體的不斷升級,多核cpu,多執行緒,多通道等技術把計算機的計算速度大幅度提升,原來同一時間只能執行一條cpu指令的時代已經過去。隨著多條cpu指令可以並行執行的原因,原來不曾出現的資源競爭隨著出現,在程式中的體現就是隨處可見的多執行緒環境。比如要更新資料庫的一個資訊,如果沒有併發控制,多個執行緒同時操作的話,就會出現互相改寫的現象發生。
鎖要解決的就是資源競爭的問題,也就是要把執行的指令順序化
隨著網際網路的興起,現代軟體發生了翻天覆地的變化,以前單機的程式,已經支撐不了現代的業務。無論是在抗壓,還是在高可用等方面都需要多臺計算機協同工作來解決問題。現代的網際網路系統都是分散式部署的,分散式部署確實能帶來效能和效率上的提升,但為此,我們就需要多解決一個分散式環境下,資料一致性的問題。
當某個資源在多系統之間共享的時候,為了保證大家訪問這個資源資料是一致的,那麼就必須要求在同一時刻只能被一個客戶端處理,不能併發的執行,否則就會出現同一時刻有人寫有人讀,大家訪問到的資料就不一致了。
在分散式系統的時代,傳統執行緒之間的鎖機制,就沒作用了,系統會有多份並且部署在不同的機器上,這些資源已經不是在執行緒之間共享了,而是屬於行程(伺服器)之間共享的資源。
因此,為瞭解決這個問題,我們就必須引入「分散式鎖」。分散式鎖,是指在分散式的部署環境下,透過鎖機制來讓多客戶端互斥的對共享資源進行訪問。分散式鎖的特點如下:
如果你透過網路搜尋分散式鎖,最多的就是基於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伺服器會接著提供服務,這個時候其他客戶端可以在新的伺服器上獲取到鎖了,這也導致了鎖意義的丟失。有興趣的同學可以去看看RedLock,這種方案以犧牲效能的代價解決了這個問題。
在某些時候,redis的伺服器時間發生的跳躍,由於鎖的過期時間依賴於伺服器時間,所以也會出現兩個客戶端同時獲取到鎖的情況發生。
當把以上問題都有解決方案了之後,基於redis的分散式鎖才可以放心使用
朋友會在“發現-看一看”看到你“在看”的內容