特別說明: 本人平時混跡于 B 站,不咋回復這里的評論,有問題可以到 B 站視頻評論區留言找我
視頻地址: https://space.bilibili.com/31137138/favlist?fid=326428938
課件說明: 本次提供的課件是 Spring Cloud Netflix 版微服務架構指南,如果有興趣想要學習 Spring Cloud Alibaba 版,可以前往 http://www.qfdmy.com 查看相關課程資源
案例代碼: https://github.com/topsale/hello-spring-cloud-netflix
什么是分布式協調
分布式協調技術主要用來解決分布式環境當中多個進程之間的同步控制,讓他們有序的去訪問某種臨界資源,防止造成"臟數據"的后果。
在這圖中有三臺機器,每臺機器各跑一個應用程序。然后我們將這三臺機器通過網絡將其連接起來,構成一個系統來為用戶提供服務,對用戶來說這個系統的架構是透明的,他感覺不到我這個系統是一個什么樣的架構。那么我們就可以把這種系統稱作一個分布式系統。
在這個分布式系統中如何對進程進行調度,我假設在第一臺機器上掛載了一個資源,然后這三個物理分布的進程都要競爭這個資源,但我們又不希望他們同時進行訪問,這時候我們就需要一個協調器,來讓他們有序的來訪問這個資源。這個協調器就是我們經常提到的那個鎖,比如說"進程-1"在使用該資源的時候,會先去獲得鎖,"進程1"獲得鎖以后會對該資源保持獨占,這樣其他進程就無法訪問該資源,"進程1"用完該資源以后就將鎖釋放掉,讓其他進程來獲得鎖,那么通過這個鎖機制,我們就能保證了分布式系統中多個進程能夠有序的訪問該臨界資源。那么我們把這個分布式環境下的這個鎖叫作分布式鎖。這個分布式鎖也就是我們分布式協調技術實現的核心內容。
什么是分布式鎖
為了防止分布式系統中的多個進程之間相互干擾,我們需要一種分布式協調技術來對這些進程進行調度。而這個分布式協調技術的核心就是來實現這個分布式鎖。
為什么需要分布式鎖
- 成員變量 A 存在 JVM1、JVM2、JVM3 三個 JVM 內存中
- 成員變量 A 同時都會在 JVM 分配一塊內存,三個請求發過來同時對這個變量操作,顯然結果是不對的
- 不是同時發過來,三個請求分別操作三個不同 JVM 內存區域的數據,變量 A 之間不存在共享,也不具有可見性,處理的結果也是不對的
注:該成員變量 A 是一個有狀態的對象
如果我們業務中確實存在這個場景的話,我們就需要一種方法解決這個問題,這就是分布式鎖要解決的問題
分布式鎖應該具備哪些條件
- 在分布式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行
- 高可用的獲取鎖與釋放鎖
- 高性能的獲取鎖與釋放鎖
- 具備可重入特性(可理解為重新進入,由多于一個任務并發使用,而不必擔心數據錯誤)
- 具備鎖失效機制,防止死鎖
- 具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗
分布式鎖的實現有哪些
- Memcached:利用 Memcached 的
add
命令。此命令是原子性操作,只有在key
不存在的情況下,才能add
成功,也就意味著線程得到了鎖。 - Redis:和 Memcached 的方式類似,利用 Redis 的
setnx
命令。此命令同樣是原子性操作,只有在key
不存在的情況下,才能set
成功。 - Zookeeper:利用 Zookeeper 的順序臨時節點,來實現分布式鎖和等待隊列。Zookeeper 設計的初衷,就是為了實現分布式鎖服務的。
- Chubby:Google 公司實現的粗粒度分布式鎖服務,底層利用了 Paxos 一致性算法。
Redis 實現分布式鎖
分布式鎖實現的三個核心要素:
加鎖
最簡單的方法是使用 setnx
命令。key
是鎖的唯一標識,按業務來決定命名。比如想要給一種商品的秒殺活動加鎖,可以給 key
命名為 “lock_sale_商品ID” 。而 value
設置成什么呢?我們可以姑且設置成 1
。加鎖的偽代碼如下:
setnx(lock_sale_商品ID,1)
當一個線程執行 setnx
返回 1
,說明 key
原本不存在,該線程成功得到了鎖;當一個線程執行 setnx
返回 0
,說明 key
已經存在,該線程搶鎖失敗。
解鎖
有加鎖就得有解鎖。當得到鎖的線程執行完任務,需要釋放鎖,以便其他線程可以進入。釋放鎖的最簡單方式是執行 del
指令,偽代碼如下:
del(lock_sale_商品ID)
釋放鎖之后,其他線程就可以繼續執行 setnx
命令來獲得鎖。
鎖超時
鎖超時是什么意思呢?如果一個得到鎖的線程在執行任務的過程中掛掉,來不及顯式地釋放鎖,這塊資源將會永遠被鎖住(死鎖),別的線程再也別想進來。所以,setnx
的 key
必須設置一個超時時間,以保證即使沒有被顯式釋放,這把鎖也要在一定時間后自動釋放。setnx
不支持超時參數,所以需要額外的指令,偽代碼如下:
expire(lock_sale_商品ID, 30)
綜合偽代碼如下:
if(setnx(lock_sale_商品ID,1) == 1){
expire(lock_sale_商品ID,30)
try {
do something ......
} finally {
del(lock_sale_商品ID)
}
}
存在什么問題
以上偽代碼中存在三個致命問題
-
setnx
和expire
的非原子性
設想一個極端場景,當某線程執行 setnx
,成功得到了鎖:
setnx
剛執行成功,還未來得及執行 expire
指令,節點 1 掛掉了。
這樣一來,這把鎖就沒有設置過期時間,變成死鎖,別的線程再也無法獲得鎖了。
怎么解決呢?setnx
指令本身是不支持傳入超時時間的,set
指令增加了可選參數,偽代碼如下:
set(lock_sale_商品ID,1,30,NX)
這樣就可以取代 setnx
指令。
-
del
導致誤刪
又是一個極端場景,假如某線程成功得到了鎖,并且設置的超時時間是 30 秒。
如果某些原因導致線程 A 執行的很慢很慢,過了 30 秒都沒執行完,這時候鎖過期自動釋放,線程 B 得到了鎖。
隨后,線程 A 執行完了任務,線程 A 接著執行 del
指令來釋放鎖。但這時候線程 B 還沒執行完,線程A實際上 刪除的是線程 B 加的鎖
。
怎么避免這種情況呢?可以在 del
釋放鎖之前做一個判斷,驗證當前的鎖是不是自己加的鎖。至于具體的實現,可以在加鎖的時候把當前的線程 ID 當做 value
,并在刪除之前驗證 key
對應的 value
是不是自己線程的 ID。
加鎖:
String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)
解鎖:
if(threadId .equals(redisClient.get(key))){
del(key)
}
但是,這樣做又隱含了一個新的問題,判斷和釋放鎖是兩個獨立操作,不是原子性。
- 出現并發的可能性
還是剛才第二點所描述的場景,雖然我們避免了線程 A 誤刪掉 key
的情況,但是同一時間有 A,B 兩個線程在訪問代碼塊,仍然是不完美的。怎么辦呢?我們可以讓獲得鎖的線程開啟一個守護線程,用來給快要過期的鎖“續航”。
當過去了 29 秒,線程 A 還沒執行完,這時候守護線程會執行 expire
指令,為這把鎖“續命”20 秒。守護線程從第 29 秒開始執行,每 20 秒執行一次。
當線程 A 執行完任務,會顯式關掉守護線程。
另一種情況,如果節點 1 忽然斷電,由于線程 A 和守護線程在同一個進程,守護線程也會停下。這把鎖到了超時的時候,沒人給它續命,也就自動釋放了。
什么是 Zookeeper
ZooKeeper 是一種分布式協調服務,用于管理大型主機。在分布式環境中協調和管理服務是一個復雜的過程。ZooKeeper 通過其簡單的架構和 API 解決了這個問題。ZooKeeper 允許開發人員專注于核心應用程序邏輯,而不必擔心應用程序的分布式特性。
Zookeeper 的數據模型
Zookeeper 的數據模型是什么樣子呢?它很像數據結構當中的樹,也很像文件系統的目錄。
樹是由節點所組成,Zookeeper 的數據存儲也同樣是基于節點,這種節點叫做 Znode
但是,不同于樹的節點,Znode 的引用方式是路徑引用,類似于文件路徑:
/動物/貓
/汽車/寶馬
這樣的層級結構,讓每一個 Znode 節點擁有唯一的路徑,就像命名空間一樣對不同信息作出清晰的隔離。
Znode 包含哪些元素
- data:Znode 存儲的數據信息。
- ACL:記錄 Znode 的訪問權限,即哪些人或哪些 IP 可以訪問本節點。
- stat:包含 Znode 的各種元數據,比如事務 ID、版本號、時間戳、大小等等。
- child:當前節點的子節點引用
這里需要注意一點,Zookeeper 是為讀多寫少的場景所設計。Znode 并不是用來存儲大規模業務數據,而是用于存儲少量的狀態和配置信息,每個節點的數據最大不能超過 1MB
。
Zookeeper 的基本操作
- 創建節點
create
- 刪除節點
delete
- 判斷節點是否存在
exists
- 獲得一個節點的數據
getData
- 設置一個節點的數據
setData
- 獲取節點下的所有子節點
getChildren
這其中,exists
,getData
,getChildren
屬于讀操作。Zookeeper 客戶端在請求讀操作的時候,可以選擇是否設置 Watch
Zookeeper 的事件通知
我們可以把 Watch 理解成是注冊在特定 Znode 上的觸發器。當這個 Znode 發生改變,也就是調用了 create
,delete
,setData
方法的時候,將會觸發 Znode 上注冊的對應事件,請求 Watch 的客戶端會接收到異步通知。
具體交互過程如下:
- 客戶端調用
getData
方法,watch
參數是true
。服務端接到請求,返回節點數據,并且在對應的哈希表里插入被 Watch 的 Znode 路徑,以及 Watcher 列表。
- 當被 Watch 的 Znode 已刪除,服務端會查找哈希表,找到該 Znode 對應的所有 Watcher,異步通知客戶端,并且刪除哈希表中對應的 Key-Value。
Zookeeper 的一致性
Zookeeper 身為分布式系統協調服務,如果自身掛了如何處理呢?為了防止單機掛掉的情況,Zookeeper 維護了一個集群。如下圖:
Zookeeper Service 集群是一主多從結構。
在更新數據時,首先更新到主節點(這里的節點是指服務器,不是 Znode),再同步到從節點。
在讀取數據時,直接讀取任意從節點。
為了保證主從節點的數據一致性,Zookeeper 采用了 ZAB 協議,這種協議非常類似于一致性算法 Paxos 和 Raft。
什么是 ZAB
Zookeeper Atomic Broadcast,有效解決了 Zookeeper 集群崩潰恢復,以及主從同步數據的問題。
ZAB 協議定義的三種節點狀態
Looking :選舉狀態。
Following :Follower 節點(從節點)所處的狀態。
Leading :Leader 節點(主節點)所處狀態。
最大 ZXID
最大 ZXID 也就是節點本地的最新事務編號,包含 epoch 和計數兩部分。epoch 是紀元的意思,相當于 Raft 算法選主時候的 term。
- ZAB 的崩潰恢復
假如 Zookeeper 當前的主節點掛掉了,集群會進行崩潰恢復。ZAB 的崩潰恢復分成三個階段:
Leader election
選舉階段,此時集群中的節點處于 Looking 狀態。它們會各自向其他節點發起投票,投票當中包含自己的服務器 ID 和最新事務 ID(ZXID)。
接下來,節點會用自身的 ZXID 和從其他節點接收到的 ZXID 做比較,如果發現別人家的 ZXID 比自己大,也就是數據比自己新,那么就重新發起投票,投票給目前已知最大的 ZXID 所屬節點。
每次投票后,服務器都會統計投票數量,判斷是否有某個節點得到半數以上的投票。如果存在這樣的節點,該節點將會成為準 Leader,狀態變為 Leading。其他節點的狀態變為 Following。
Discovery
發現階段,用于在從節點中發現最新的 ZXID 和事務日志。或許有人會問:既然 Leader 被選為主節點,已經是集群里數據最新的了,為什么還要從節點中尋找最新事務呢?
這是為了防止某些意外情況,比如因網絡原因在上一階段產生多個 Leader 的情況。
所以這一階段,Leader 集思廣益,接收所有 Follower 發來各自的最新 epoch 值。Leader 從中選出最大的 epoch,基于此值加 1,生成新的 epoch 分發給各個 Follower。
各個 Follower 收到全新的 epoch 后,返回 ACK 給 Leader,帶上各自最大的 ZXID 和歷史事務日志。Leader 選出最大的 ZXID,并更新自身歷史日志。
Synchronization
同步階段,把 Leader 剛才收集得到的最新歷史事務日志,同步給集群中所有的 Follower。只有當半數 Follower 同步成功,這個準 Leader 才能成為正式的 Leader。
自此,故障恢復正式完成。
- ZAB 的數據寫入
Broadcast
ZAB 的數據寫入涉及到 Broadcast 階段,簡單來說,就是 Zookeeper 常規情況下更新數據的時候,由 Leader 廣播到所有的 Follower。其過程如下:
- 客戶端發出寫入數據請求給任意 Follower。
- Follower 把寫入數據請求轉發給 Leader。
- Leader 采用二階段提交方式,先發送 Propose 廣播給 Follower。
- Follower 接到 Propose 消息,寫入日志成功后,返回 ACK 消息給 Leader。
- Leader 接到半數以上ACK消息,返回成功給客戶端,并且廣播 Commit 請求給 Follower
ZAB 協議既不是強一致性,也不是弱一致性,而是處于兩者之間的單調一致性(順序一致性)。它依靠事務 ID 和版本號,保證了數據的更新和讀取是有序的。
Zookeeper 的應用場景
分布式鎖
這是雅虎研究員設計 Zookeeper 的初衷。利用 Zookeeper 的臨時順序節點,可以輕松實現分布式鎖。
服務注冊和發現
利用 Znode 和 Watcher,可以實現分布式服務的注冊和發現。最著名的應用就是阿里的分布式 RPC 框架 Dubbo。
共享配置和狀態信息
Redis 的分布式解決方案 Codis,就利用了 Zookeeper 來存放數據路由表和 codis-proxy 節點的元信息。同時 codis-config 發起的命令都會通過 ZooKeeper 同步到各個存活的 codis-proxy。
此外,Kafka、HBase、Hadoop,也都依靠 Zookeeper 同步節點信息,實現高可用。
Zookeeper 實現分布式鎖
Znode 的四種類型
Zookeeper 的數據存儲結構就像一棵樹,這棵樹由節點組成,這種節點叫做 Znode。
- 持久節點(PERSISTENT)
默認的節點類型。創建節點的客戶端與 Zookeeper 斷開連接后,該節點依舊存在。
- 持久節點順序節點(PERSISTENT_SEQUENTIAL)
所謂順序節點,就是在創建節點時,Zookeeper 根據創建的時間順序給該節點名稱進行編號:
- 臨時節點(EPHEMERAL)
和持久節點相反,當創建節點的客戶端與 Zookeeper 斷開連接后,臨時節點會被刪除:
- 臨時順序節點(EPHEMERAL_SEQUENTIAL)
顧名思義,臨時順序節點結合和臨時節點和順序節點的特點:在創建節點時,Zookeeper 根據創建的時間順序給該節點名稱進行編號;當創建節點的客戶端與 Zookeeper 斷開連接后,臨時節點會被刪除。
Zookeeper 分布式鎖的原理
Zookeeper 分布式鎖恰恰應用了臨時順序節點。具體如何實現呢?讓我們來看一看詳細步驟:
- 獲取鎖
首先,在 Zookeeper 當中創建一個持久節點 ParentLock。當第一個客戶端想要獲得鎖時,需要在 ParentLock 這個節點下面創建一個臨時順序節點 Lock1。
之后,Client1 查找 ParentLock 下面所有的臨時順序節點并排序,判斷自己所創建的節點 Lock1 是不是順序最靠前的一個。如果是第一個節點,則成功獲得鎖。
這時候,如果再有一個客戶端 Client2 前來獲取鎖,則在 ParentLock 下載再創建一個臨時順序節點 Lock2。
Client2 查找 ParentLock 下面所有的臨時順序節點并排序,判斷自己所創建的節點 Lock2 是不是順序最靠前的一個,結果發現節點 Lock2 并不是最小的。
于是,Client2 向排序僅比它靠前的節點 Lock1 注冊 Watcher,用于監聽 Lock1 節點是否存在。這意味著 Client2 搶鎖失敗,進入了等待狀態。
這時候,如果又有一個客戶端 Client3 前來獲取鎖,則在 ParentLock 下載再創建一個臨時順序節點 Lock3。
Client3 查找 ParentLock 下面所有的臨時順序節點并排序,判斷自己所創建的節點 Lock3 是不是順序最靠前的一個,結果同樣發現節點 Lock3 并不是最小的。
于是,Client3 向排序僅比它靠前的節點 Lock2 注冊 Watcher,用于監聽 Lock2 節點是否存在。這意味著 Client3 同樣搶鎖失敗,進入了等待狀態。
這樣一來,Client1 得到了鎖,Client2 監聽了 Lock1,Client3 監聽了 Lock2。這恰恰形成了一個等待隊列,
- 釋放鎖
釋放鎖分為兩種情況:
- 任務完成,客戶端顯示釋放
當任務完成時,Client1 會顯示調用刪除節點 Lock1 的指令。
- 任務執行過程中,客戶端崩潰
獲得鎖的 Client1 在任務執行過程中,如果崩潰,則會斷開與 Zookeeper 服務端的鏈接。根據臨時節點的特性,相關聯的節點 Lock1 會隨之自動刪除。
由于 Client2 一直監聽著 Lock1 的存在狀態,當 Lock1 節點被刪除,Client2 會立刻收到通知。這時候 Client2 會再次查詢 ParentLock 下面的所有節點,確認自己創建的節點 Lock2 是不是目前最小的節點。如果是最小,則 Client2 順理成章獲得了鎖。
同理,如果 Client2 也因為任務完成或者節點崩潰而刪除了節點 Lock2,那么 Client3 就會接到通知。
最終,Client3 成功得到了鎖。
實戰 Redisson 實現分布式鎖
Redisson 目前是官方唯一推薦的 Java 版的分布式鎖并支持 Redlock
什么是 Redisson
Redisson 是一個在 Redis 的基礎上實現的 Java 駐內存數據網格(In-Memory Data Grid)。它不僅提供了一系列的分布式的 Java 常用對象,還提供了許多分布式服務。其中包括 (BitSet
, Set
, Multimap
, SortedSet
, Map
, List
, Queue
, BlockingQueue
, Deque
, BlockingDeque
, Semaphore
, Lock
, AtomicLong
, CountDownLatch
, Publish / Subscribe
, Bloom filter
, Remote service
, Spring cache
, Executor service
, Live Object service
, Scheduler service
) Redisson 提供了使用 Redis 的最簡單和最便捷的方法。Redisson 的宗旨是促進使用者對 Redis 的關注分離(Separation of Concern),從而讓使用者能夠將精力更集中地放在處理業務邏輯上
Redisson 應用場景
- Distributed Java applications 分布式 Java 應用,Redisson 為 Java 上的分布式應用程序提供了基于 Redis 的對象,集合,鎖,同步器和服務的分布式實現
- Caching 緩存,Redisson 為 Java 提供了不同的基于 Redis 的緩存實現,如 JCache API,Hibernate 二級緩存,Spring Cache 和應用程序級別緩存
- Data Source Caching 數據源緩存,Redisson 支持適用 Read-Through,Write-Behind 策略對數據庫。Web 服務器或者其他任何數據源進行基于 Redis 的讀寫緩存
- Distributed Java tasks Scheduling and execution 分布式 Java 任務調度和執行,在某些點上對 Java 的任務處理可以被拆分并且并行處理。Redisson 提供了 ExecutorService 和 ScheduledExecutorService 的分布式實現
- MapReduce Redisson 提供基于 Java 的 MapReduce 編程模型去處理存儲在 Redis 中的大型數據
- Easy Java Redis client 簡單的 Java Redis 客戶端,Redission 可以被用來作為 Java Redis 客戶端,沒有學習難度,有了它 就可以不需要去知道所有 Redis 命令 就開始使用 Redis
- Web session clustering web 會話集群,負載均衡用戶所有會話。Redisson 提供了基于 Tomcat 會話管理和 Spring Session 實現
- Distributed locks with Redis 實現 可重入鎖、讀寫鎖、公平鎖、信號量、CountDownLatch 等很多種復雜的鎖的語義,滿足我們對分布式鎖的不同層次的需求,這一點來說 ZK 分布式鎖就顯得匱乏一些了
Redisson 結構
Redisson 作為獨立節點可以用于獨立執行其他節點發布到分布式執行服務和分布式調度任務服務里的遠程任務。
Redisson 操作對象
-
RedissonObject
,操作通用對象
package com.funtl.hello.spring.cloud.commons.redisson.operation;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.redisson.spring.starter.RedissonProperties;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
public class RedissonObject {
/**
* 數據緩存時間,默認 30 分鐘
*/
private static final Long DATA_VALID_TIME = 1000 * 60 * 30L;
@Resource
private RedissonClient redissonClient;
@Resource
private RedissonProperties redissonProperties;
/**
* 獲取對象值
*
* @param name
* @param <T>
* @return
*/
public <T> T getValue(String name) {
RBucket<T> bucket = redissonClient.getBucket(name);
return bucket.get();
}
/**
* 獲取對象空間
*
* @param name
* @param <T>
* @return
*/
public <T> RBucket<T> getBucket(String name) {
return redissonClient.getBucket(name);
}
/**
* 設置對象的值
*
* @param name 鍵
* @param value 值
* @return
*/
public <T> void setValue(String name, T value) {
setValue(name, value, DATA_VALID_TIME);
}
/**
* 設置對象的值
*
* @param name 鍵
* @param value 值
* @param time 緩存時間 單位毫秒 -1 永久緩存
* @return
*/
public <T> void setValue(String name, T value, Long time) {
RBucket<Object> bucket = redissonClient.getBucket(name);
if (time == -1) {
bucket.set(value);
} else {
bucket.set(value, time, TimeUnit.MILLISECONDS);
}
}
/**
* 如果值已經存在則則不設置
*
* @param name 鍵
* @param value 值
* @param time 緩存時間 單位毫秒
* @return true 設置成功,false 值存在,不設置
*/
public <T> Boolean trySetValue(String name, T value, Long time) {
RBucket<Object> bucket = redissonClient.getBucket(name);
boolean b;
if (time == -1) {
b = bucket.trySet(value);
} else {
b = bucket.trySet(value, time, TimeUnit.MILLISECONDS);
}
return b;
}
/**
* 如果值已經存在則則不設置
*
* @param name 鍵
* @param value 值
* @return true 設置成功,false 值存在,不設置
*/
public <T> Boolean trySetValue(String name, T value) {
return trySetValue(name, value, DATA_VALID_TIME);
}
/**
* 刪除對象
*
* @param name 鍵
* @return true 刪除成功,false 不成功
*/
public Boolean delete(String name) {
return redissonClient.getBucket(name).delete();
}
}
-
RedissonBinary
,操作對象二進制
package com.funtl.hello.spring.cloud.commons.redisson.operation;
import org.redisson.api.RBinaryStream;
import org.redisson.api.RListMultimap;
import org.redisson.api.RedissonClient;
import javax.annotation.Resource;
import java.io.InputStream;
import java.io.OutputStream;
public class RedissonBinary {
@Resource
private RedissonClient redissonClient;
/**
* 獲取輸出流
*
* @param name
* @return
*/
public OutputStream getOutputStream(String name) {
RListMultimap<Object, Object> listMultimap = redissonClient.getListMultimap("");
RBinaryStream binaryStream = redissonClient.getBinaryStream(name);
return binaryStream.getOutputStream();
}
/**
* 獲取輸入流
*
* @param name
* @return
*/
public InputStream getInputStream(String name) {
RBinaryStream binaryStream = redissonClient.getBinaryStream(name);
return binaryStream.getInputStream();
}
/**
* 獲取輸入流
*
* @param name
* @return
*/
public InputStream getValue(String name, OutputStream stream) {
try {
RBinaryStream binaryStream = redissonClient.getBinaryStream(name);
InputStream inputStream = binaryStream.getInputStream();
byte[] buff = new byte[1024];
int len;
while ((len = inputStream.read(buff)) != -1) {
stream.write(buff, 0, len);
}
return binaryStream.getInputStream();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 獲取對象空間
*
* @param name
* @return
*/
public RBinaryStream getBucket(String name) {
return redissonClient.getBinaryStream(name);
}
/**
* 設置對象的值
*
* @param name 鍵
* @param value 值
* @return
*/
public void setValue(String name, InputStream value) {
try {
RBinaryStream binaryStream = redissonClient.getBinaryStream(name);
binaryStream.delete();
OutputStream outputStream = binaryStream.getOutputStream();
byte[] buff = new byte[1024];
int len;
while ((len = value.read(buff)) != -1) {
outputStream.write(buff, 0, len);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 刪除對象
*
* @param name 鍵
* @return true 刪除成功,false 不成功
*/
public Boolean delete(String name) {
RBinaryStream binaryStream = redissonClient.getBinaryStream(name);
return binaryStream.delete();
}
}
-
RedissonCollection
,操作集合
package com.funtl.hello.spring.cloud.commons.redisson.operation;
import org.redisson.api.RList;
import org.redisson.api.RMap;
import org.redisson.api.RSet;
import org.redisson.api.RedissonClient;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class RedissonCollection {
/**
* 數據緩存時間,默認 30 分鐘
*/
private static final Long DATA_VALID_TIME = 1000 * 60 * 30L;
@Resource
private RedissonClient redissonClient;
/**
* 獲取map集合
*
* @param name
* @param <K>
* @param <V>
* @return
*/
public <K, V> RMap<K, V> getMap(String name) {
return redissonClient.getMap(name);
}
/**
* 設置map集合
*
* @param name
* @param data
* @param time 緩存時間,單位毫秒 -1永久緩存
* @return
*/
public void setMapValues(String name, Map data, Long time) {
RMap map = redissonClient.getMap(name);
Long dataValidTime = DATA_VALID_TIME;
if (time != -1) {
map.expire(dataValidTime, TimeUnit.MILLISECONDS);
}
map.putAll(data);
}
/**
* 設置map集合
*
* @param name
* @param data
* @return
*/
public void setMapValues(String name, Map data) {
setMapValues(name, data, DATA_VALID_TIME);
}
/**
* 獲取List集合
*
* @param name
* @return
*/
public <T> RList<T> getList(String name) {
return redissonClient.getList(name);
}
/**
* 設置List集合
*
* @param name
* @param data
* @param time 緩存時間,單位毫秒 -1永久緩存
* @return
*/
public void setListValues(String name, List data, Long time) {
RList list = redissonClient.getList(name);
Long dataValidTime = DATA_VALID_TIME;
if (time != -1) {
list.expire(dataValidTime, TimeUnit.MILLISECONDS);
}
list.addAll(data);
}
/**
* 設置List集合
*
* @param name
* @param data
* @return
*/
public void setListValues(String name, List data) {
setListValues(name, data, DATA_VALID_TIME);
}
/**
* 獲取set集合
*
* @param name
* @return
*/
public <T> RSet<T> getSet(String name) {
return redissonClient.getSet(name);
}
/**
* 設置set集合
*
* @param name
* @param data
* @param time 緩存時間,單位毫秒 -1永久緩存
* @return
*/
public void setSetValues(String name, Set data, Long time) {
RSet set = redissonClient.getSet(name);
Long dataValidTime = DATA_VALID_TIME;
if (time != -1) {
set.expire(dataValidTime, TimeUnit.MILLISECONDS);
}
set.addAll(data);
}
/**
* 設置set集合
*
* @param name
* @param data
* @return
*/
public void setSetValues(String name, Set data) {
setSetValues(name, data, DATA_VALID_TIME);
}
}
-
RedissonConfiguration
,使用 Java 配置的方式注入工具類
package com.funtl.hello.spring.cloud.commons.configuration;
import com.funtl.hello.spring.cloud.commons.redisson.operation.RedissonBinary;
import com.funtl.hello.spring.cloud.commons.redisson.operation.RedissonCollection;
import com.funtl.hello.spring.cloud.commons.redisson.operation.RedissonObject;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfiguration {
@Bean
@ConditionalOnMissingBean(RedissonObject.class)
public RedissonObject redissonObject() {
return new RedissonObject();
}
@Bean
@ConditionalOnMissingBean(RedissonBinary.class)
public RedissonBinary redissonBinary() {
return new RedissonBinary();
}
@Bean
@ConditionalOnMissingBean(RedissonCollection.class)
public RedissonCollection redissonCollection() {
return new RedissonCollection();
}
}
通過 RLock 對象操作分布式鎖
注意: 此處新建一個名為
provider-item-service
的服務提供者,復制之前創建的provider-admin-service
項目并修改相關配置即可
- 創建測試表和數據,其中
num
表示庫存的數量,這里演示多 JVM 環境下對臨界資源的控制(解決超賣問題)
create table tb_item (id int not null primary key,name varchar(100),num int not null);
insert into tb_item(id, name,num) values(1000000, 'Apple', 3);
- 測試代碼如下
package com.funtl.hello.spring.cloud.provider.controller;
import com.funtl.hello.spring.cloud.provider.domain.TbItem;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@Slf4j
@RestController
public class ProviderItemController {
@Resource
private RedissonClient redissonClient;
@PostMapping(value = "num/local")
public String testNumLock(TbItem tbItem) {
// 加鎖,此處根據商品名稱加鎖
RLock lock = redissonClient.getLock(tbItem.getName());
lock.lock();
log.info("Thread {} 拿到了 {} 的鎖", Thread.currentThread().getId(), tbItem.getName());
try {
// 阻塞模擬業務操作時間
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 解鎖
lock.unlock();
log.info("Thread {} 釋放了 {} 的鎖", Thread.currentThread().getId(), tbItem.getName());
return "ok";
}
}
- 使用 PostMan 發起多個測試請求,控制臺輸出如下
Thread 104 拿到了 Apple 的鎖
Thread 104 釋放了 Apple 的鎖
Thread 106 拿到了 Apple 的鎖
Thread 106 釋放了 Apple 的鎖
- 如果在業務處理過程中服務宕機,Redis 會自動釋放鎖
通過自定義注解操作分布式鎖
從上面的代碼可以看出這是一個典型的環繞切面,我們可以使用 AOP 思想將交叉業務剝離出來,采用注解的方式切面操作分布式鎖
- 增加 AOP 依賴
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
- 創建一個名為
RedissonLockModel
的枚舉類型,用于設定各種鎖的模式
package com.funtl.hello.spring.cloud.commons.redisson.enums;
public enum RedissonLockModel {
/**
* 可重入鎖:某個線程已經獲得某個鎖,可以再次獲取鎖而不會出現死鎖
*/
REENTRANT,
/**
* 公平鎖:加鎖前先查看是否有排隊等待的線程,有的話優先處理排在前面的線程
*/
FAIR,
/**
* 聯鎖:可以把一組鎖當作一個鎖來加鎖和釋放
* 基于 Redis 的分布式 RedissonMultiLock 對象將多個 RLock 對象分組,并將它們作為一個鎖處理。
* 每個 RLock 對象可能屬于不同的 Redisson 實例
*/
MULTIPLE,
/**
* 紅鎖:用于解決異步數據丟失和腦裂問題
* 假設有多個 Redis 節點,這些節點之間既沒有主從,也沒有集群關系。
* 客戶端用相同的 key 和隨機值在多個節點上請求鎖,請求鎖的超時時間應小于鎖自動釋放時間。
* 當超過半數 Redis 上請求到鎖的時候,才算是真正獲取到了鎖。
* 如果沒有獲取到鎖,則把部分已鎖的 Redis 釋放掉
*/
REDLOCK,
/**
* 讀鎖(共享鎖):共享用于不更改或不更新數據的操作(只讀操作),如 SELECT 語句
* 如果事務 T 對數據 A 加上共享鎖后,則其他事務只能對 A 再加共享鎖,不能加排他鎖。
* 獲準共享鎖的事務只能讀數據,不能修改數據
*/
READ,
/**
* 寫鎖(排他鎖):用于數據修改操作,例如 INSERT、UPDATE 或 DELETE。確保不會同時同一資源進行多重更新
* 如果事務 T 對數據 A 加上排他鎖后,則其他事務不能再對 A 加任何類型的鎖。
* 獲準排他鎖的事務既能讀數據,又能修改數據。
* 我們在操作數據庫的時候,可能會由于并發問題而引起的數據的不一致性(數據沖突)
*/
WRITE,
/**
* 自動模式,當參數只有一個使用 REENTRANT 參數多個 REDLOCK
*/
AUTO
}
- 創建一個名為
RedissonLockException
自定義分布式鎖異常類
package com.funtl.hello.spring.cloud.commons.redisson.excepiton;
public class RedissonLockException extends RuntimeException {
public RedissonLockException() {
}
public RedissonLockException(String message) {
super(message);
}
public RedissonLockException(String message, Throwable cause) {
super(message, cause);
}
public RedissonLockException(Throwable cause) {
super(cause);
}
public RedissonLockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
- 創建一個名為
RedissonLock
自定義注解
package com.funtl.hello.spring.cloud.commons.redisson.annotation;
import com.funtl.hello.spring.cloud.commons.redisson.enums.RedissonLockModel;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedissonLock {
/**
* 鎖的模式:如果不設置,自動模式,當參數只有一個使用 REENTRANT 參數多個 MULTIPLE
*
* @return
*/
RedissonLockModel lockModel() default RedissonLockModel.AUTO;
/**
* 如果 keys 有多個,如果不設置則使用聯鎖
*
* @return
*/
String[] keys() default {};
/**
* key 的靜態常量:當 key 的 spel 的值是 List,數組時使用 + 號連接將會被 spel 認為這個變量是個字符串,只能產生一把鎖,達不到我們的目的
* 而我們如果又需要一個常量的話這個參數將會在拼接在每個元素的后面
*
* @return
*/
String keyConstant() default "";
/**
* 鎖超時時間,默認 30000 毫秒
*
* @return
*/
long lockWatchdogTimeout() default 0;
/**
* 等待加鎖超時時間,默認 10000 毫秒 -1 則表示一直等待
*
* @return
*/
long attemptTimeout() default 0;
}
- 創建一個名為
RedissonLockAop
用于實現RedissonLock
自定義注解
package com.funtl.hello.spring.cloud.commons.redisson.aop;
import com.funtl.hello.spring.cloud.commons.redisson.annotation.RedissonLock;
import com.funtl.hello.spring.cloud.commons.redisson.enums.RedissonLockModel;
import com.funtl.hello.spring.cloud.commons.redisson.excepiton.RedissonLockException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.RedissonMultiLock;
import org.redisson.RedissonRedLock;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Slf4j
@Aspect
@Order(-10)
public class RedissonLockAop {
/**
* 等待加鎖超時時間 -1 一直等待
*/
private static final Long ATTEMPT_TIMEOUT = 10000L;
/**
* 看門狗
* 在一個分布式環境下,多個服務實例請求獲取鎖,其中服務實例 A 成功獲取到了鎖,在執行業務邏輯的過程中,服務實例突然掛掉了可以采用鎖超時機制解決
* 如果服務實例 A 沒有宕機但是業務執行還沒有結束,鎖釋放掉了就會導致線程問題(誤刪鎖)。此時就一定要實現自動延長鎖有效期的機制
* 看門狗的主要作用:只要這臺服務實例沒有掛掉,并且沒有主動釋放鎖,看門狗都會每隔十秒給你續約一下,保證鎖一直在你手中
*/
private static final Long LOCK_WATCH_DOG_TIMEOUT = 30000L;
private RedissonLockModel lockModel;
@Autowired
private RedissonClient redissonClient;
@Pointcut("@annotation(redissonLock)")
public void controllerAspect(RedissonLock redissonLock) {
}
/**
* 通過 Spring SpEL 獲取參數
*
* @param key 定義的 key值以 # 開頭 例如:#user
* @param parameterNames 形參
* @param values 形參值
* @param keyConstant key的常亮
* @return
*/
public List<String> getVauleBySpel(String key, String[] parameterNames, Object[] values, String keyConstant) {
List<String> keys = new ArrayList<>();
if (!key.contains("#")) {
String s = "redisson:lock:" + key + keyConstant;
log.info("沒有使用 SpEL 表達式 value -> {}", s);
keys.add(s);
return keys;
}
// SpEL 解析器
ExpressionParser parser = new SpelExpressionParser();
// SpEL 上下文
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], values[i]);
}
Expression expression = parser.parseExpression(key);
Object value = expression.getValue(context);
if (value != null) {
if (value instanceof List) {
List value1 = (List) value;
for (Object o : value1) {
keys.add("redisson:lock:" + o.toString() + keyConstant);
}
} else if (value.getClass().isArray()) {
Object[] obj = (Object[]) value;
for (Object o : obj) {
keys.add("redisson:lock:" + o.toString() + keyConstant);
}
} else {
keys.add("redisson:lock:" + value.toString() + keyConstant);
}
}
log.info("SpEL 表達式 key={}, value={}", key, keys);
return keys;
}
@Around("controllerAspect(redissonLock)")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint, RedissonLock redissonLock) throws Throwable {
String[] keys = redissonLock.keys();
if (keys.length == 0) {
throw new RuntimeException("keys 不能為空");
}
String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(((MethodSignature) proceedingJoinPoint.getSignature()).getMethod());
Object[] args = proceedingJoinPoint.getArgs();
long attemptTimeout = redissonLock.attemptTimeout();
if (attemptTimeout == 0) {
attemptTimeout = ATTEMPT_TIMEOUT;
}
long lockWatchdogTimeout = redissonLock.lockWatchdogTimeout();
if (lockWatchdogTimeout == 0) {
lockWatchdogTimeout = LOCK_WATCH_DOG_TIMEOUT;
}
RedissonLockModel lockModel = redissonLock.lockModel();
if (lockModel.equals(RedissonLockModel.AUTO)) {
RedissonLockModel lockModel1 = lockModel;
if (lockModel1 != null && !lockModel1.equals(RedissonLockModel.AUTO)) {
lockModel = lockModel1;
} else if (keys.length > 1) {
lockModel = RedissonLockModel.REDLOCK;
} else {
lockModel = RedissonLockModel.REENTRANT;
}
}
if (!lockModel.equals(RedissonLockModel.MULTIPLE) && !lockModel.equals(RedissonLockModel.REDLOCK) && keys.length > 1) {
throw new RuntimeException("參數有多個, 鎖模式為 -> " + lockModel.name() + ".無法鎖定");
}
log.info("鎖模式 -> {}, 等待鎖定時間 -> {} 秒.鎖定最長時間 -> {} 秒", lockModel.name(), attemptTimeout / 1000, lockWatchdogTimeout / 1000);
boolean res = false;
RLock rLock = null;
// 一直等待加鎖
switch (lockModel) {
case FAIR:
rLock = redissonClient.getFairLock(getVauleBySpel(keys[0], parameterNames, args, redissonLock.keyConstant()).get(0));
break;
case REDLOCK:
List<RLock> rLocks = new ArrayList<>();
for (String key : keys) {
List<String> vauleBySpel = getVauleBySpel(key, parameterNames, args, redissonLock.keyConstant());
for (String s : vauleBySpel) {
rLocks.add(redissonClient.getLock(s));
}
}
RLock[] locks = new RLock[rLocks.size()];
int index = 0;
for (RLock r : rLocks) {
locks[index++] = r;
}
rLock = new RedissonRedLock(locks);
break;
case MULTIPLE:
rLocks = new ArrayList<>();
for (String key : keys) {
List<String> vauleBySpel = getVauleBySpel(key, parameterNames, args, redissonLock.keyConstant());
for (String s : vauleBySpel) {
rLocks.add(redissonClient.getLock(s));
}
}
locks = new RLock[rLocks.size()];
index = 0;
for (RLock r : rLocks) {
locks[index++] = r;
}
rLock = new RedissonMultiLock(locks);
break;
case REENTRANT:
List<String> vauleBySpel = getVauleBySpel(keys[0], parameterNames, args, redissonLock.keyConstant());
//如果spel表達式是數組或者LIST 則使用紅鎖
if (vauleBySpel.size() == 1) {
rLock = redissonClient.getLock(vauleBySpel.get(0));
} else {
locks = new RLock[vauleBySpel.size()];
index = 0;
for (String s : vauleBySpel) {
locks[index++] = redissonClient.getLock(s);
}
rLock = new RedissonRedLock(locks);
}
break;
case READ:
RReadWriteLock rwlock = redissonClient.getReadWriteLock(getVauleBySpel(keys[0], parameterNames, args, redissonLock.keyConstant()).get(0));
rLock = rwlock.readLock();
break;
case WRITE:
RReadWriteLock rwlock1 = redissonClient.getReadWriteLock(getVauleBySpel(keys[0], parameterNames, args, redissonLock.keyConstant()).get(0));
rLock = rwlock1.writeLock();
break;
}
// 執行 AOP
if (rLock != null) {
try {
if (attemptTimeout == -1) {
res = true;
// 一直等待加鎖
rLock.lock(lockWatchdogTimeout, TimeUnit.MILLISECONDS);
} else {
res = rLock.tryLock(attemptTimeout, lockWatchdogTimeout, TimeUnit.MILLISECONDS);
}
if (res) {
Object obj = proceedingJoinPoint.proceed();
return obj;
} else {
throw new RedissonLockException("獲取鎖失敗");
}
} finally {
if (res) {
rLock.unlock();
}
}
}
throw new RedissonLockException("獲取鎖失敗");
}
}
- 在
RedissonConfiguration
中注入RedissonLockAop
@Bean
@ConditionalOnMissingBean(RedissonLockAop.class)
public RedissonLockAop redissonLockAop() {
return new RedissonLockAop();
}
- 在 Controller 層使用注解的方式加鎖
@RedissonLock(keys = "#tbItem.name", lockModel = RedissonLockModel.AUTO)
@PostMapping(value = "num/annotation")
public String testNumAnnotation(TbItem tbItem) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "ok";
}
- 使用 PostMan 測試,控制臺打印結果如下
鎖模式 -> REENTRANT, 等待鎖定時間 -> 10 秒.鎖定最長時間 -> 30 秒
SpEL 表達式 key=#tbItem.name, value=[redisson:lock:Apple]
注意: 記得同時通過 Redis 客戶端工具觀察數據變化
特別說明: 本人平時混跡于 B 站,不咋回復這里的評論,有問題可以到 B 站視頻評論區留言找我
視頻地址: https://space.bilibili.com/31137138/favlist?fid=326428938
課件說明: 本次提供的課件是 Spring Cloud Netflix 版微服務架構指南,如果有興趣想要學習 Spring Cloud Alibaba 版,可以前往 http://www.qfdmy.com 查看相關課程資源
案例代碼: https://github.com/topsale/hello-spring-cloud-netflix