如何正確使用redis分散式鎖
前言
筆者在公司擔任技術面試官,在筆者面試過程中,如果面試候選人提到了reids分散式鎖,筆者都會問一下redis分散式鎖的知識點,但是令筆者遺憾的是,該知識點十個人中有九個人都答得不清楚,或者回答錯誤,這讓筆者有了寫這篇文章的想法,來幫助童鞋們正確認識reids分散式鎖.
什麼是分散式鎖?為什麼需要分散式鎖?
在java中,在單程序多執行緒的情況下,為了防止多個執行緒共同競爭同一個資源,因此需要鎖,java中有顯示鎖和隱式鎖來保證,而在多程序的情況下,普通的鎖就無法滿足要求了,因此我們需要分散式鎖,常用的分散式鎖解決方案有三種,分別是基於資料庫/redis/zookeeper,本文我們主要討論redis分散式鎖.
redis分散式鎖實現
筆者在面試過程中,問redis分散式鎖知識點時的第一個問題就是如何實現一個redis分散式鎖,許多候選人直接說,啊,這很簡單啊,使用setNx()方法,設定一個過期時間就可以了,我接著問,那如何釋放鎖鎖呢?候選人回答說,那很簡單啊,直接呼叫delete方法就可以了,我接著問,釋放鎖直接呼叫delete方法就可以了嗎?候選人回答,對啊,delete方法也是執行緒安全的,我.....那麼如何實現一個redis分散式鎖,我們用程式碼來演示一下,首先來看一下加鎖的程式碼片段:
首先我們的分散式鎖實現了Lock介面,然後主要看我們的lock方法,阻塞式自旋鎖,加鎖方法直接使用tryLock(),接下來我們再來看看tryLock()方法:
其實很簡單,主要是呼叫redis的set方法(其中UUID筆者為了方便演示,直接使用UUID,在分散式的生產環境下應該使用諸如雪花演算法等來保證分散式系統下UUDI的唯一性),如果返回OK則說明加鎖成功,否則失敗,再來看看釋放鎖的方法,面試過程中很多候選人童鞋說直接呼叫delete方法,我們寫一段程式碼,然後分析一下直接呼叫delete方法的問題:
如上一段程式碼,假設一種極端場景下有兩個執行緒A和B,A執行緒先獲取鎖,設定過期時間為10秒,然後A執行緒執行釋放鎖操作,執行到if判斷語句並且成功進入時,此時耗時剛好10秒,鎖過期了,並且CPU分配給A執行緒的時間片剛好用完,此時B執行緒開始執行並且成功獲取到該分散式鎖,然後執行一段時間後B執行緒的時間片用完,此時A繼續執行刪除操作,此時A刪除的就是B執行緒的鎖,會造成誤刪除操作,因此為了避免這種情況,我們需要一種機制來保證判斷和刪除操作的原子性,redis官方推薦我們使用lua指令碼,因此正確的解鎖方式如下:
其中的UNLOCK_LUA_SCRIPT如下:
redis使用lua指令碼能保證該操作的原子性,因此這樣才能正確釋放分散式鎖.這也回答了為什麼之前說釋放鎖的時候直接呼叫delete方法是錯誤的.
有什麼問題?
在上文中,我們使用redis構建了一個分散式鎖,但是請注意,該程式碼在單機環境下沒有任何問題,但是我們在生產中往往都是redis叢集部署,由於redis主從節點的資料同步是非同步的,如果Redis的master節點在鎖未同步到Slave節點的時候宕機了怎麼辦?舉例來說:
1.程序A在master節點獲得了鎖。
2.在鎖同步到slave之前,master宕機,資料還沒有同步到slave
3.slave變成了新的master節點
4.程序B也得到了和A相同的鎖.
因此,如果你的業務允許在master宕機期間,多個客戶端允許同時都持有鎖,那如上的分散式鎖是可以接受的,否則就不能使用上述的分散式鎖,在這種情況下,redis官方為我們提供了另一種解決方案----RedLock演算法.
RedLock演算法
假設我們有N個Master節點(N一般為奇數),這些節點互相之間相互獨立,不需要進行資料同步,我們用在單節點獲取和釋放鎖的方式來操縱這些節點,具體過程為:
1.獲取當前時間(單位是毫秒)。
2.輪流用相同的key和隨機值在N個節點上請求鎖,在這一步裡,客戶端在每個master上請求鎖時,會有一個和總的鎖釋放時間相比小的多的超時時間。比如如果鎖自動釋放時間是10秒鐘,那每個節點鎖請求的超時時間可能是5-50毫秒的範圍,這個可以防止一個客戶端在某個宕掉的master節點上阻塞過長時間,如果一個master節點不可用了,我們應該儘快嘗試下一個master節點。
3.客戶端計算第二步中獲取鎖所花的時間,只有當客戶端在大多數master節點上成功獲取了鎖(N/2+1在這裡是3個),而且總共消耗的時間不超過鎖釋放時間,這個鎖就認為是獲取成功了。
4.如果鎖獲取成功了,那現在鎖自動釋放時間就是最初的鎖釋放時間減去之前獲取鎖所消耗的時間。
5.如果鎖獲取失敗了,不管是因為獲取成功的鎖不超過一半(N/2+1)還是因為總消耗時間超過了鎖釋放時間,客戶端都會到每個master節點上釋放鎖,即便是那些他認為沒有獲取成功的鎖。
有關RedLock的演算法,可以詳見官網文件
RedLock演算法是否真的足夠安全?
要回答這個問題,可以看看國外大神Martin Kleppmann在他的一篇文章How to do distributed locking中詳細描述了為什麼他認為RedLock仍然是不安全的,簡單來說,RedLock最大的弊端有兩個:
1.程序由於各種原因pause,類似於上文說的多執行緒間的時間片切換,比如由於GC停頓等導致鎖過期,但是程序並未感知到,同時另一個程序已經獲取了該分散式鎖,就會導致奇怪的結果發生.
2.演算法對時鐘依賴性太強,假設N個節點為5,按照超過一半的原則,假設程序X成功獲取了A/B/C三個節點的鎖,此時認為X獲取鎖成功,此時X在TTL時間段內沒有執行完成,鎖到期自動釋放,此時由於C節點的時間比A/B節點快,導致C節點先釋放鎖,此時Y節點獲取了C/D/E三個節點的鎖,又導致兩個程序獲取了同一個鎖.
無論如何,在多master節點的情況下,沒有任何方案能完美保證RedLock的絕對安全,因此,我們在使用redis分散式鎖的時候一定要弄清楚我們的目的是什麼?一般來說,有兩種情況:
1.為了提高效能.比如持有該鎖使我們的程式不會進行重複的計算,在這種情況下,如果鎖失敗了我們付出的代價僅僅是進行了重複的計算,不會影響我們的業務結果.
2.為了保證業務的正確性.比如我們是一個銀行系統,為了保證轉賬操作扣款唯一性,擁有該鎖可以確保我們的扣款操作的唯一性,如果鎖失效,會導致多次扣款,這是無法接受的.
如果我們是為了提升效能,那沒有必要使用RedLock演算法,它成本高(假設需要5個master節點,這些節點還要保證高可用,則需要更多的節點)且又複雜,不如使用在單機情況下的分散式鎖,前提是你的業務能容忍我們上述說的宕機期間相同鎖的問題.
如果是為了保證業務的正確性,我們說了RedLock也不能完美保證絕對安全,因此也不能放心的使用RedLock.
總結
總而言之,使用Redis分散式鎖實在不是一個好的選擇,Redis設計的初衷也並不是滿足分散式鎖的需求.對於需求效能的分散式鎖應用它太重了且成本高;對於需求正確性的應用來說它不夠安全.如果你的應用只需要高效能的分散式鎖並且不要求多高的正確性,那麼單節點的Redis分散式鎖足夠了;如果你的應用想要保證正確性,那麼不建議 RedLock,建議使用一個合適的一致性協調系統,比如基於Zookeeper的分散式鎖!
本文由部落格一文多發平臺 OpenWrite 釋出!