前言
分佈式概念提出之前,項目結構基本都是通過單機部署,針對多線程併發問題,java爲我們已經提供了各種鎖來解決問題。隨着用戶量的提升單體服務已經不能滿足高併發場景的需求,於是興起了分佈式系統以及微服務的理念。由此引出分佈式鎖的概念,在多臺機器與客戶端之間引入一個分佈式鎖層,在高併發場景當多個線程訪問服務器資源時,可以通過不同的機器對共享資源進行操作,jdk鎖保證了單臺機器內的線程安全(即單機裏的多線程場景是共享堆內存的),但是多線程多個機器的訪問的場景,jdk自帶的鎖已經不解決資源訪問一致性問題。通過分佈式鎖層對多個線程進行控制,獲取到鎖的線程才能對集羣中的資源進行訪問操作,因此保證了線程安全。本文主要講解redis分佈式鎖原理。
分佈式鎖
1、分佈式鎖背景
目前幾乎很多大型網站及應用都是分佈式部署的,分佈式場景中的數據一致性問題一直是一個比較重要的話題。分佈式的CAP理論告訴我們“任何一個分佈式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多隻能同時滿足兩項。”所以,很多系統在設計之初就要對這三者做出取捨。在互聯網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證“最終一致性”,只要這個最終時間是在用戶可以接受的範圍內即可。 在很多場景中,我們爲了保證數據的最終一致性,需要很多的技術方案來支持,比如分佈式事務、分佈式鎖等。有的時候,我們需要保證一個方法在同一時間內只能被同一個線程執行。
2、分佈式鎖定義
在分佈式環境下(即多臺機器),某臺計算機上的堆內存中的變量對於其他計算機上的線程肯定是不可見的。那麼,根據鎖的本質和原理,我們就要找到另外的對於多機上的線程都可見的標誌,以它來作爲鎖,就可以了。這樣的鎖,就是分佈式鎖。
3、分佈式鎖常見的三種實現方式:
- 數據庫樂觀鎖;
- 基於Redis的分佈式鎖;
- 基於ZooKeeper的分佈式鎖。
你對Redis使用熟悉嗎?Redis中是如何實現分佈式鎖的。
Redis中分佈式鎖的實現機制
Redis要實現分佈式鎖,以下條件應該得到滿足
- 互斥性:在任意時刻,只有一個客戶端能持有鎖。
- 不能死鎖:客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
- 容錯性:只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。
普通實現
說道Redis分佈式鎖大部分人都會想到:setnx+lua
,或者知道set key value px milliseconds nx
。Set 這個命令,目前已經支持這麼多參數可選:
SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
EX seconds
: 將鍵的過期時間設置爲seconds
秒。 執行SET key value EX seconds
的效果等同於執行SETEX key seconds value
。PX milliseconds
: 將鍵的過期時間設置爲milliseconds
毫秒。 執行SET key value PX milliseconds
的效果等同於執行PSETEX key milliseconds value
。NX
: 只在鍵不存在時, 纔對鍵進行設置操作。 執行SET key value NX
的效果等同於執行SETNX key value
。XX
: 只在鍵已經存在時, 纔對鍵進行設置操作。
Note:因爲 SET
命令可以通過參數來實現 SETNX
、 SETEX
以及 PSETEX
命令的效果, 所以 Redis 將來的版本可能會移除並廢棄 SETNX
、 SETEX
和 PSETEX
這三個命令。
後一種方式的核心實現命令如下:
- 獲取鎖(unique_value可以是UUID等)
SET resource_name unique_value NX PX 30000
- 釋放鎖(lua腳本中,一定要比較value,防止誤解鎖)
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
這種實現方式有3大要點(也是面試概率非常高的地方):
- set命令要用
set key value px milliseconds nx:
替代 setnx + expire 需要分兩次執行命令的方式,保證了原子性。 - value要具有唯一性:可以使用UUID.randomUUID().toString()方法生成,用來標識這把鎖是屬於哪個請求加的,在解鎖的時候就可以有依據;
- 釋放鎖時要驗證value值,不能誤解鎖:同時利用了eval命令執行Lua腳本的原子性
存在的風險:事實上這類瑣最大的缺點就是它加鎖時只作用在一個Redis節點上,即使Redis通過sentinel保證高可用,如果這個master節點由於某些原因發生了主從切換,那麼就會出現鎖丟失的情況,導致出現多個客戶端持有鎖的情況,這樣就不能實現資源的獨享了:
- 客戶端A從master獲取到鎖
- 在master將鎖同步到slave之前,master宕掉了(Redis的主從同步通常是異步的)。
主從切換,slave節點被晉級爲master節點 - 客戶端B取得了同一個資源被客戶端A已經獲取到的另外一個鎖。導致存在同一時刻存不止一個線程獲取到鎖的情況。
官方推薦的集羣方案:以上風險針對此集羣方案,可查看前篇瞭解:https://blog.csdn.net/Mr_lisj/article/details/105890666
Redis 集羣沒有使用一致性hash, 而是引入了 哈希槽的概念.Redis 集羣有16384個哈希槽(slot),每個key通過CRC16校驗後對16384取模來決定放置哪個槽.集羣的每個節點負責一部分hash槽.
正因爲如此,Redis作者antirez基於分佈式環境下提出了一種更高級的分佈式鎖的實現方式:Redlock。筆者認爲,Redlock也是Redis所有分佈式鎖實現方式中唯一能讓面試官高潮的方式。
Redlock算法實現
antirez提出的redlock算法大概是這樣的:
在Redis的分佈式環境中,我們假設有N個Redis master。這些節點完全互相獨立,不存在主從複製或者其他集羣協調機制。我們確保將在N個實例上使用與在Redis單實例下相同方法獲取和釋放鎖。現在我們假設有5個Redis master節點,同時我們需要在5臺服務器上面運行這些Redis實例,這樣保證他們不會同時都宕掉。
- 如果你不熟悉 Redis 高可用部署,那麼沒關係。RedLock 算法雖然是需要多個實例,但是這些實例都是獨自部署的,沒有主從關係。
- RedLock 作者指出,之所以要用獨立的,是避免了 Redis 異步復製造成的鎖丟失,比如:主節點沒來的及把剛剛 Set 進來這條數據給從節點,就掛了。
- 有些人是不是覺得大佬們都是槓精啊,天天就想着極端情況。其實高可用嘛,拼的就是 99.999...% 中小數點後面的位數。
爲了取到鎖,客戶端應該執行以下操作:
- 獲取當前Unix時間,以毫秒爲單位。
- 依次嘗試從5個實例,使用相同的key和具有唯一性的value(例如UUID)獲取鎖。當向Redis請求獲取鎖時,客戶端應該設置一個網絡連接和響應超時時間,這個超時時間應該小於鎖的失效時間。例如你的鎖自動失效時間爲10秒,則超時時間應該在5-50毫秒之間。這樣可以避免服務器端Redis已經掛掉的情況下,客戶端還在死死地等待響應結果。如果服務器端沒有在規定時間內響應,客戶端應該儘快嘗試去另外一個Redis實例請求獲取鎖。
- 客戶端使用當前時間減去開始獲取鎖時間(步驟1記錄的時間)就得到獲取鎖使用的時間。當且僅當從大多數(N/2+1,這裏是3個節點)的Redis節點都取到鎖,並且使用的時間小於鎖失效時間時,鎖纔算獲取成功。
- 如果取到了鎖,key的真正有效時間等於有效時間減去獲取鎖所使用的時間(步驟3計算的結果)。
- 如果因爲某些原因,獲取鎖失敗(沒有在至少N/2+1個Redis實例取到鎖或者取鎖時間已經超過了有效時間),客戶端應該在所有的Redis實例上進行解鎖(即便某些Redis實例根本就沒有加鎖成功,防止某些節點獲取到鎖但是客戶端沒有得到響應而導致接下來的一段時間不能被重新獲取鎖)。
總結:在鎖失效時間、獲取鎖超時時間、鎖使用時間 正常情況下,redlock算法認爲,只要 N/2+1 個節點加鎖成功,那麼就認爲獲取了鎖, 解鎖時將所有實例解鎖。
Redis 官方給出了以上兩種基於 Redis 實現分佈式鎖的方法,詳細說明可以查看:
https://redis.io/topics/distlock 。
Redisson
Redisson 是 Java 的 Redis 客戶端之一,提供了一些 API 方便操作 Redis。Redisson提供了使用Redis的最簡單和最便捷的方法。Redisson的宗旨是促進使用者對Redis的關注分離(Separation of Concern),從而讓使用者能夠將精力更集中地放在處理業務邏輯上。
Redisson 幫我們搞了分佈式的版本。比如 AtomicLong,直接用 RedissonAtomicLong 就行了,連類名都不用去新記,很人性化了。鎖只是它的冰山一角,並且從它的 Wiki 頁面看到,對主從,哨兵,集羣等模式都支持,當然了,單節點模式肯定是支持的。
RedissonLock是可重入的,並且考慮了失敗重試,可以設置鎖的最大等待時間, 在實現上也做了一些優化,減少了無效的鎖申請,提升了資源的利用率。
NOTE:需要特別注意的是,RedissonLock 同樣沒有解決 節點掛掉的時候,存在丟失鎖的風險的問題。而現實情況是有一些場景無法容忍的,所以 Redisson 提供了實現了redlock算法的 RedissonRedLock,RedissonRedLock 真正解決了單點失敗的問題,代價是需要額外的爲 RedissonRedLock 搭建Redis環境。
所以,如果業務場景可以容忍這種小概率的錯誤,則推薦使用 RedissonLock, 如果無法容忍,則推薦使用 RedissonRedLock。
Redisson 和 Jedis區別
- Jedis是Redis的Java實現的客戶端,其API提供了比較全面的Redis命令的支持;
- Jedis客戶端實例不是線程安全的,所以需要通過連接池來使用Jedis。
- Redisson的API是線程安全的,所以可以操作單個Redisson連接來完成各種操作。
- Redisson實現了分佈式和可擴展的Java數據結構,和Jedis相比,功能較爲簡單,不支持字符串操作,不支持排序、事務、管道、分區等Redis特性。
- Redisson的宗旨是促進使用者對Redis的關注分離,從而讓使用者能夠將精力更集中地放在處理業務邏輯上。
文章參考:
https://www.toutiao.com/a6808355292833645070/
https://www.toutiao.com/a6758222821052137992/