1.爲何用分佈式鎖redis
咱們在開發應用的時候,若是須要對某一個共享變量進行多線程同步訪問的時候,可使用咱們學到的Java多線程的18般武藝進行處理,而且能夠完美的運行,毫無Bug!spring
注意這是單機應用,也就是全部的請求都會分配到當前服務器的JVM內部,而後映射爲操做系統的線程進行處理!而這個共享變量只是在這個JVM內部的一塊內存空間!數據庫
後來業務發展,須要作集羣,一個應用須要部署到幾臺機器上而後作負載均衡,大體以下圖:服務器
上圖能夠看到,變量A存在JVM一、JVM二、JVM3三個JVM內存中(這個變量A主要體現是在一個類中的一個成員變量,是一個有狀態的對象,例如:UserController控制器中的一個整形類型的成員變量),若是不加任何控制的話,變量A同時都會在JVM分配一塊內存,三個請求發過來同時對這個變量操做,顯然結果是不對的!即便不是同時發過來,三個請求分別操做三個不一樣JVM內存區域的數據,變量A之間不存在共享,也不具備可見性,處理的結果也是不對的!多線程
若是咱們業務中確實存在這個場景的話,咱們就須要一種方法解決這個問題!併發
爲了保證一個方法或屬性在高併發狀況下的同一時間只能被同一個線程執行,在傳統單體應用單機部署的狀況下,可使用Java併發處理相關的API(如ReentrantLock或Synchronized)進行互斥控制。在單機環境中,Java中提供了不少併發處理相關的API。可是,隨着業務發展的須要,原單體單機部署的系統被演化成分佈式集羣系統後,因爲分佈式系統多線程、多進程而且分佈在不一樣機器上,這將使原單機部署狀況下的併發控制鎖策略失效,單純的Java API並不能提供分佈式鎖的能力。爲了解決這個問題就須要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分佈式鎖要解決的問題!負載均衡
2.分佈式鎖的實現方式分佈式
2.1 基於數據庫的實現高併發
1.解決方式:設計一個獨立的表結構來進行分佈式鎖信息的存儲,在相對應獲取鎖的時候進行添加鎖信息並持有鎖,直到對應的業務操做結束以後進行鎖的釋放工做工具
2.設計實現:
方案一:
列名 | 類型 | 描述 |
---|---|---|
id | bigint | 主鍵 |
method_name | varchar(64) | 方法名,創建惟一性索引 |
desc | varchar(1024) | 備註信息 |
update_time | datetime | 更新時間 |
out_date_time | datetime | 失效時間 |
實現原理:
獲取鎖:獲取鎖的時候,對象對錶結構進行添加數據,用來標誌鎖信息。
釋放鎖:刪除對應的數據,標誌鎖的釋放
方案二:
列名 | 類型 | 描述 |
---|---|---|
id | bigint | 主鍵 |
method_name | varchar(64) | 方法名,創建惟一性索引 |
desc | varchar(1024) | 備註信息 |
update_time | datetime | 更新時間 |
version | int | 版本號 |
state | int | 狀態:0未分配,1已分配 |
out_date_time | datetime | 失效時間 |
(1) 獲取鎖
① 第一次獲取的時候,相對應表結構沒有數據,則加入一條數據,state爲1,version爲1,表明鎖
② 若是數據庫表中有對應鎖的數據,先判斷是否爲未分配的鎖信息,若是是,更新數據爲已分配狀態。
(2) 釋放鎖:更新state狀態爲未分配表明釋放鎖
3. 存在問題:
1. 數據庫服務宕機的問題:數據庫主從配置?
2. 併發低,鎖記錄包含不少冗餘信息
2.2 基於redis的實現
2.2.1 基於redis的實現
1.命令介紹:
將key的值設置爲value,當且key不存在的時候
若是key存在,則setNX沒有效果
GETSET key value:將給key的值設爲value,並返回key的舊值(old value)。
GET key:獲取key對應的值
EXPIRE key seconds:給定key設置生存時間,當生存時間到的時候, key將被自動刪除
實現方式:
加鎖:
複製代碼
採用redis中setNX的功能來進行鎖信息的保存,進行持有鎖的操做,其中鎖key值爲鎖的名字,value採用uuid或者requestId來進行保存。當setNX成功以後,設定相對應的過時時間,來實現redis鎖的自動釋放功能
解鎖
複製代碼
(1) 超時自動解鎖
(2) 手動解鎖:當業務代碼執行完畢以後,經過相對應的key進行原有的鎖的獲取,經過del進行刪除動做。
存在問題:
1. Redis鎖的setNX和Expire不是原子操做,可能會發生setNx執行成功以後應用宕機,致使死鎖。(可行的解決辦法,鎖的值設置爲過時時間,獲取鎖的時候,比對鎖是否過時了,過時的鎖用GETSET****從新設值)
2. Redis鎖爲了不單點問題,須要作主備或者集羣,當主設置完鎖而且數據沒有同步到備的時候,主掛掉了,備切換成爲主,這個時候新的主裏面是沒有這個鎖的,會有併發問題。(歸根結底這個問題是因爲Redis是ap模型致使的,因而引入了下面基於cp****模型的分佈式鎖)
2.2.2 藉助工具類Redisson進行分佈式鎖開發
1.原理:Redisson鎖的原理其實就是redis的實現方式,只是Redisson提供 了更方便的封裝,讓咱們進行使用
2.redisson鎖介紹
1.可重入鎖(Reentrant Lock)
2.公平鎖(Fair Lock)
3.聯鎖(MultiLock)
4.紅鎖(Red Lock)
5.讀寫鎖(ReadWriteLock)
3.redisson分佈式鎖實現
1.加鎖
1.經過redissonClient獲取對應Rlock鎖對象,而後根據Rlock提供的方法進行鎖操做,具體Rlock提供的鎖方法以下:
void lock(Long leaseTime,TimeUnit unit )
boolean tryLock(Long waitTime,Long leaseTime,TimeUnit unit)
2.解鎖
根據Rlock提供的unlock進行解鎖
存在問題:
一、 相較於簡單的Redis鎖,redisson鎖種類更多,可是相應的須要依賴不少其餘組件,比較重
二、 由於基於ap模型的redis,因此也存在數據不一致的問題
2.3 基於zookeeper的實現
1.zookeeper節點四種類型:
1.持久節點(PERSISTENT)
2.持久順序節點(PERSISTENT_SEQUENTIAL)
3.臨時節點(EPHEMERAL)
4.臨時順序節點(EPHEMERAL_SEQUENTIAL)
2.實現原理:
1. 獲取鎖
首先,在Zookeeper當中建立一個持久節點ParentLock。當第一個客戶端想要得到鎖時,須要在ParentLock這個節點下面建立一個臨時順序節點 Lock1。
以後,Client1查找ParentLock下面全部的臨時順序節點並排序,判斷本身所建立的節點Lock1是否是順序最靠前的一個。若是是第一個節點,則成功得到鎖。
這時候,若是再有一個客戶端 Client2 前來獲取鎖,則在ParentLock下載再建立一個臨時順序節點Lock2。
Client2查找ParentLock下面全部的臨時順序節點並排序,判斷本身所建立的節點Lock2是否是順序最靠前的一個,結果發現節點Lock2並非最小的。
因而,Client2向排序僅比它靠前的節點Lock1註冊Watcher,用於監聽Lock1節點是否存在。這意味着Client2搶鎖失敗,進入了等待狀態。
Client3查找ParentLock下面全部的臨時順序節點並排序,判斷本身所建立的節點Lock3是否是順序最靠前的一個,結果一樣發現節點Lock3並非最小的。
因而,Client3向排序僅比它靠前的節點Lock2註冊Watcher,用於監聽Lock2節點是否存在。這意味着Client3一樣搶鎖失敗,進入了等待狀態。
這樣一來,Client1獲得了鎖,Client2監聽了Lock1,Client3監聽了Lock2。這偏偏造成了一個等待隊列,很像是Java當中ReentrantLock所依賴的
2. 釋放鎖
釋放鎖分爲兩種狀況:
1.任務完成,客戶端顯示釋放
當任務完成時,Client1會顯示調用刪除節點Lock1的指令。
2.任務執行過程當中,客戶端崩潰
得到鎖的Client1在任務執行過程當中,若是Duang的一聲崩潰,則會斷開與Zookeeper服務端的連接。根據臨時節點的特性,相關聯的節點Lock1會隨之自動刪除。
因爲Client2一直監聽着Lock1的存在狀態,當Lock1節點被刪除,Client2會馬上收到通知。這時候Client2會再次查詢ParentLock下面的全部節點,確認本身建立的節點Lock2是否是目前最小的節點。若是是最小,則Client2瓜熟蒂落得到了鎖。
同理,若是Client2也由於任務完成或者節點崩潰而刪除了節點Lock2,那麼Client3就會接到通知。
最終,Client3成功獲得了鎖。
存在問題:
1. 寫併發不高,由於zk寫入數據的時候,是要過半節點寫入成功纔算寫成功
2. 讀併發相較於redis鎖也有很大差距
2.4 基於etcd的實現
2.5 基於consul的實現