分布式鎖的場景
首先在讀文章之前,我們要考慮一個問題,為什么要用分布式鎖,也就是什么場景下要用分布式鎖?
假如我們有一個搶購業(yè)務,之前是單機的時候我們可以用程序鎖,擴展到了多個服務節(jié)點的時候,那我無法再繼續(xù)使用lock sync等程序的鎖來控制并發(fā)中可能會造成的超賣。
這時候我們就該引入一個分布式鎖來解決這個問題,當然上面的例子有更好的解決辦法,這里僅僅提供一個分布式鎖的場景引入。
設計一個分布式鎖的要素
OK,我們知道什么情況下用分布式鎖了之后,我們要考慮下,如果讓我們設計一個分布式鎖,要考慮哪些問題?
第一點,既然是鎖,那么我要確保這個鎖在整個集群中唯一性
第二點,我要確保我某個獲取到鎖的節(jié)點掛掉之后不會因為無法釋放而產(chǎn)生死鎖的問題
第三點,我要確保我的鎖不會被其他的節(jié)點誤操作而錯誤的解鎖
第四點,我們要考慮我們的鎖其他的競爭線程,如何在持有鎖的節(jié)點釋放之后快速的受到通知重新競爭鎖
第五點,就是鎖的性能效率
第六點,就是鎖的可重入性(這里提一下,對于大多數(shù)程序和業(yè)務來講是沒必要實現(xiàn)這個功能的)
ok,上面就是我們做一個分布式鎖應該注意的地方,當然,這里說的情況并不是很全面,但是基本上已經(jīng)足夠大多數(shù)的業(yè)務使用了,那么我們帶著上面的這些注意的點,一起去看一下怎么實現(xiàn)一個分布式鎖。
構建分布式鎖
鎖的唯一性問題
大家應該都知道redis里有個過期時間的概念,也就是expire這個api可以設置一個key的過期時間,那么利用這個功能我們可以設置一個最大值,避免死鎖的問題。
那么說道這里,大家可能跟我之前一樣,會考慮到一個問題?
這個過期時間設置為多少好呢,如果設置太小了,會造成業(yè)務沒操作完,鎖就提前被其他線程獲取了,如果設置太大了,又可能比死鎖沒好多少。
redisson是怎么解決這個問題的?
redisson默認是設置一個key的過期時間為30秒,那么大家可能想,這也沒區(qū)別??!
如果看過redisson源碼的應該注意到他用了netty,那么他用netty干嘛了?他用netty做了這樣的一個事,他給每個上鎖的操作都加了一個事件。
什么樣的事件?
如果我一個上鎖操作,上鎖失敗了,就訂閱鎖,直到收到通知,否則就暫時等待,這里他是利用java用的信號量來實現(xiàn)的,如果有興趣的可以看一下他的具體代碼。
那么如果上鎖成功了呢?他會開啟一個異步線程,等待通知,這個通知可以是這樣的:如果我收到的通知是,我工作完了,要釋放鎖了。那么這時候他就把這個異步線程從工作者隊列中干掉。
那么,如果我沒有收到通知呢?這一步其實就是redisson的關鍵實現(xiàn)
如果我沒收到通知,我每隔離10s會調(diào)用一次這個事件,判斷一下過期時間,然后給這個持有鎖的線程的key,也就是當前鎖,重新設置上為30秒的過期時間,也就說,即使我這臺機器掛掉了,那我這個機器持有的鎖最多會保留30s的“死鎖”時間。
如果我有一堆遠程調(diào)用,30s根本不夠用呢?
沒關系,每隔10s你的過期時間都會更新為30s。也就是一直到你釋放鎖。當然,如果你害怕你的業(yè)務會發(fā)送阻塞而造成了鎖的一直持有的"假死鎖"情況,那怎么辦?
redisson提供了lockInterruptibly(long leaseTime, TimeUnit unit)的時間限制哈。也就是你在多少秒之內(nèi)如果沒完成任務也會自動釋放這個鎖。
鎖的標志
我怎么要確保我的鎖不會被其他的節(jié)點誤操作而錯誤的解鎖呢?
這個其實很好解決,一般對于一個鎖來講,都是需要一個onwer的標示,對于大多數(shù)的做法:都是使用uuid+ThreadId,然后操作線程保留這個onwer標示,在set的時候吧這個owner的標示放到value中,解鎖的時候判斷這個owner標示。
note:一般來講這個owner標示還起著做重入的時候的作用.
解鎖后的快速通知
這里其實是有兩種做法:
- 第一種,類似本地鎖的不斷重試(自旋)。
- 第二種方式,也就是redlock的實現(xiàn)RedisSon的做法,pub/sub的方式
自旋如何實現(xiàn)
我原來做這一塊的時候利用locksupport的park來做了短暫時間的暫停,再暫停之后不斷的重試獲取鎖。
但是這樣就會有這樣的幾個問題:
- 鎖的通知被釋放的時候我無法及時的收到通知,并且這個能獲取鎖的機會有可能就看運氣了,也就是說誰暫停完之后重試的時間正好是我釋放的時間,也就是無法實現(xiàn)公平鎖(按申請鎖的順序來獲取鎖)
- redis畢竟是網(wǎng)絡的,無論是網(wǎng)絡抖動的影響還是自身這種不斷發(fā)請求來講,都是很大的開銷,性能上爛到爆
但是上面的鎖翩翩適用一種常見,業(yè)務操作比較簡單短暫,不會出太多問題,耗時比較短,需要簡單的鎖模型
pub/sub如何實現(xiàn)
相信了解過redis的都知道它有個發(fā)布訂閱的功能
RedisSon是這么實現(xiàn)快速通知的:
獲取鎖的線程會去redis中發(fā)布一個key,然后所有沒有取到鎖的就去訂閱相應的channel,來接收鎖釋放的通知,獲取鎖的釋放了之后就會去這里發(fā)布釋放的通知。收到消息的就會繼續(xù)重試獲取鎖的過程
性能問題
首先,大家都知道,一個分布式鎖可以基于zk和redis來實現(xiàn)。
但是rediss做分布式鎖的效率要比zk高上很多很多倍,因為zk是基于文件系統(tǒng)的實現(xiàn),而redis是基于內(nèi)存的操作實現(xiàn)。
而且zk做分布式鎖的時候還會有可能因為網(wǎng)絡抖動的問題發(fā)生鎖被誤釋放的問題(這里我們暫時不討論)
可沖入特性
我這里簡單的說一下redisson是怎么實現(xiàn)的:
redisson是吧獲取鎖的行為變成了一次hashset的操作
這里就是核心的實現(xiàn):
上面lua代碼中第一個if就是先嘗試獲取鎖,如果獲取成功就返回,如果不成功就判斷要獲取鎖的線程,和持有鎖的線程是否是同一個線程,如果是,那就在value上加個1,代表重入了一次,最后那個return的pttl其實是一個else邏輯,也就是說我既沒有獲取鎖,也不是持有鎖的那個線程,也就意味我獲取鎖失敗,那我就返回一個過期時間的值
Redisson的流程簡述
redisson的整個過程簡介:利用lua在redis中的原子性,獲取鎖保證唯一性,在value中加上標示防止誤解鎖,不斷的疊加expire來保證持有鎖的時候不會被誤拿到,利用redis的pub/sub來即使的通知鎖的釋放,利用Semaphore來實現(xiàn)沒有獲取鎖的線程的等待。
要思考的問題
自旋鎖 然后 自旋鎖會導致饑餓 就開始使用阻塞,然后 阻塞會導致CPU等資源空置 就開始使用異步解耦(最常見的就是做完了,通知的方式),其實整個過程跟IO的幾種模式很像 從BIO到NIO到AIO的整個過程,然后大家看到發(fā)布訂閱這種通知的模式了。。
但如果發(fā)布訂閱模式突然掛了,你的線程可能永遠不會醒來了?這里我還沒有完全的關注到這個點,日后有機會吧這個點看一下然后補充上來。