Java高级工程师必看系列,已拿到offer
1.為什么要使用分布式鎖
使用分布式鎖的目的,無外乎就是保證同一時間只有一個客戶端可以對共享資源進行操作。
1.1舉一個很長的例子
系統 A 是一個電商系統,目前是一臺機器部署,系統中有一個用戶下訂單的接口,但是用戶下訂單之前一定要去檢查一下庫存,確保庫存足夠了才會給用戶下單。由于系統有一定的并發,所以會預先將商品的庫存保存在 Redis 中,用戶下單的時候會更新 Redis 的庫存。此時系統架構如下:
但是這樣一來會產生一個問題:假如某個時刻,Redis 里面的某個商品庫存為 1。
此時兩個請求同時到來,其中一個請求執行到上圖的第 3 步,更新數據庫的庫存為 0,但是第 4 步還沒有執行。
而另外一個請求執行到了第 2 步,發現庫存還是 1,就繼續執行第 3 步。這樣的結果,是導致賣出了 2 個商品,然而其實庫存只有 1 個。
很明顯不對啊!這就是典型的庫存超賣問題。此時,我們很容易想到解決方案:用鎖把 2、3、4 步鎖住,讓他們執行完之后,另一個線程才能進來執行第 2 步。
按照上面的圖,在執行第 2 步時,使用 Java 提供的 Synchronized 或者 ReentrantLock 來鎖住,然后在第 4 步執行完之后才釋放鎖。
這樣一來,2、3、4 這 3 個步驟就被“鎖”住了,多個線程之間只能串行化執行。
當整個系統的并發飆升,一臺機器扛不住了。現在要增加一臺機器,如下圖:
增加機器之后,系統變成上圖所示,假設此時兩個用戶的請求同時到來,但是落在了不同的機器上,那么這兩個請求是可以同時執行了,還是會出現庫存超賣的問題。
因為上圖中的兩個 A 系統,運行在兩個不同的 JVM 里面,他們加的鎖只對屬于自己 JVM 里面的線程有效,對于其他 JVM 的線程是無效的。
因此,這里的問題是:Java 提供的原生鎖機制在多機部署場景下失效了,這是因為兩臺機器加的鎖不是同一個鎖(兩個鎖在不同的 JVM 里面)。
那么,我們只要保證兩臺機器加的鎖是同一個鎖,問題不就解決了嗎?此時,就該分布式鎖隆重登場了。
分布式鎖的思路是:在整個系統提供一個全局、唯一的獲取鎖的“東西”,然后每個系統在需要加鎖時,都去問這個“東西”拿到一把鎖,這樣不同的系統拿到的就可以認為是同一把鎖。
至于這個“東西”,可以是 Redis、Zookeeper,也可以是數據庫。此時的架構如圖:
通過上面的分析,我們知道了庫存超賣場景在分布式部署系統的情況下使用 Java 原生的鎖機制無法保證線程安全,所以我們需要用到分布式鎖的方案。
2.高效的分布式鎖
在設計分布式鎖的時候,應該考慮分布式鎖至少要滿足的一些條件,同時考慮如何高效的設計分布式鎖,以下幾點是必須要考慮的:
(1) 互斥
在分布式高并發的條件下,最需要保證在同一時刻只能有一個線程獲得鎖,這是最基本的一點。
(2) 防止死鎖
在分布式高并發的條件下,比如有個線程獲得鎖的同時,還沒有來得及去釋放鎖,就因為系統故障或者其它原因使它無法執行釋放鎖的命令,導致其它線程都無法獲得鎖,造成死鎖。所以分布式非常有必要設置鎖的有效時間,確保系統出現故障后,在一定時間內能夠主動去釋放鎖,避免造成死鎖的情況。
(3) 性能
對于訪問量大的共享資源,需要考慮減少鎖等待的時間,避免導致大量線程阻塞。
所以在鎖的設計時,需要考慮兩點。
1、 鎖的顆粒度要盡量小。比如你要通過鎖來減庫存,那這個鎖的名稱你可以設置成是商品的ID,而不是任取名稱。這樣這個鎖只對當前商品有效,鎖的顆粒度小。
2、 鎖的范圍盡量要小。比如只要鎖2行代碼就可以解決問題的,那就不要去鎖10行代碼了。
(4) 重入
我們知道ReentrantLock是可重入鎖,那它的特點就是:同一個線程可以重復拿到同一個資源的鎖。重入鎖非常有利于資源的高效利用。關于這點之后會做演示。
3.基于Redis實現分布式鎖
3.1 使用Redis命令實現分布式鎖
3.1.1加鎖
加鎖實際上就是在redis中,給Key鍵設置一個值,為避免死鎖,并給定一個過期時間。
使用的命令**:SET lock_key random_value NX PX 5000**
值得注意的是:
random_value 是客戶端生成的唯一的字符串。
NX 代表只在鍵不存在時,才對鍵進行設置操作。
PX 5000 設置鍵的過期時間為5000毫秒。
也可以使用另外一條命令:SETNX key value
只不過過期時間無法設置。
這樣,如果上面的命令執行成功,則證明客戶端獲取到了鎖。
3.1.2解鎖
解鎖的過程就是將Key鍵刪除,但要保證安全性,舉個例子:客戶端1的請求不能將客戶端2的鎖給刪除掉。
釋放鎖涉及到兩條指令,這兩條指令不是原子性的,需要用到redis的lua腳本支持特性,redis執行lua腳本是原子性的。腳本如下:
if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) elsereturn 0 end這種方式比較簡單,但是也有一個最重要的問題:鎖不具有可重入性。
3.2使用Redisson實現分布式鎖
3.2.1Redisson介紹
Redisson是架設在Redis基礎上的一個Java駐內存數據網格(In-Memory Data Grid)。充分的利用了Redis鍵值數據庫提供的一系列優勢,基于Java實用工具包中常用接口,為使用者提供了一系列具有分布式特性的常用工具類。使得原本作為協調單機多線程并發程序的工具包獲得了協調分布式多機多線程并發系統的能力,大大降低了設計和研發大規模分布式系統的難度。同時結合各富特色的分布式服務,更進一步簡化了分布式環境中程序相互之間的協作。
3.2.2Redisson簡單使用
Config config = new Config(); config.useClusterServers() .addNodeAddress("redis://192.168.31.101:7001") .addNodeAddress("redis://192.168.31.101:7002") .addNodeAddress("redis://192.168.31.101:7003") .addNodeAddress("redis://192.168.31.102:7001") .addNodeAddress("redis://192.168.31.102:7002") .addNodeAddress("redis://192.168.31.102:7003"); RedissonClient redisson = Redisson.create(config); RLock lock = redisson.getLock("anyLock"); lock.lock(); lock.unlock();只需要通過它的 API 中的 Lock 和 Unlock 即可完成分布式鎖,而且考慮了很多細節:
l Redisson 所有指令都通過 Lua 腳本執行,Redis 支持 Lua 腳本原子性執行。
l Redisson 設置一個 Key 的默認過期時間為 30s,但是如果獲取鎖之后,會有一個WatchDog每隔10s將key的超時時間設置為30s。
另外,Redisson 還提供了對 Redlock 算法的支持,它的用法也很簡單:
RedissonClient redisson = Redisson.create(config); RLock lock1 = redisson.getFairLock("lock1"); RLock lock2 = redisson.getFairLock("lock2"); RLock lock3 = redisson.getFairLock("lock3"); RedissonRedLock multiLock = new RedissonRedLock(lock1, lock2, lock3); multiLock.lock(); multiLock.unlock();3.2.3Redisson原理分析
(1) 加鎖機制
線程去獲取鎖,獲取成功: 執行lua腳本,保存數據到redis數據庫。
線程去獲取鎖,獲取失敗: 一直通過while循環嘗試獲取鎖,獲取成功后,執行lua腳本,保存數據到redis數據庫。
(2) WatchDog自動延期機制
在一個分布式環境下,假如一個線程獲得鎖后,突然服務器宕機了,那么這個時候在一定時間后這個鎖會自動釋放,也可以設置鎖的有效時間(不設置默認30秒),這樣的目的主要是防止死鎖的發生。但是在實際情況中會有一種情況,業務處理的時間可能會大于鎖過期的時間,這樣就可能**導致解鎖和加鎖不是同一個線程。**所以WatchDog作用就是Redisson實例關閉前,不斷延長鎖的有效期。
如果程序調用加鎖方法顯式地給了有效期,是不會開啟后臺線程(也就是watch dog)進行延期的,如果沒有給有效期或者給的是-1,redisson會默認設置30s有效期并且會開啟后臺線程(watch dog)進行延期
多久進行一次延期:(默認有效期/3),默認有效期可以設置修改的,即默認情況下每隔10s設置有效期為30s
(3) 可重入加鎖機制
Redisson可以實現可重入加鎖機制的原因:
l Redis存儲鎖的數據類型是Hash類型
l Hash數據類型的key值包含了當前線程的信息
下面是redis存儲的數據
這里表面數據類型是Hash類型,Hash類型相當于我們java的 <key,<key1,value>> 類型,這里key是指 ‘redisson’
它的有效期還有9秒,我們再來看里們的key1值為078e44a3-5f95-4e24-b6aa-80684655a15a:45它的組成是:
guid + 當前線程的ID。后面的value是就和可重入加鎖有關。value代表同一客戶端調用lock方法的次數,即可重入計數統計。
舉圖說明
上面這圖的意思就是可重入鎖的機制,它最大的優點就是相同線程不需要在等待鎖,而是可以直接進行相應操作。
3.2.4 獲取鎖的流程
其中的指定字段也就是hash結構中的field值(構成是uuid+線程id),即判斷鎖是否是當前線程
3.2.5 加鎖的流程
3.2.6 釋放鎖的流程
4. 使用Redis做分布式鎖的缺點
Redis有三種部署方式
l 單機模式
l Master-Slave+Sentienl選舉模式
l Redis Cluster模式
如果采用單機部署模式,會存在單點問題,只要 Redis 故障了。加鎖就不行了
采用 Master-Slave 模式,加鎖的時候只對一個節點加鎖,即便通過 Sentinel 做了高可用,但是如果 Master 節點故障了,發生主從切換,此時就會有可能出現鎖丟失的問題。
基于以上的考慮,Redis 的作者也考慮到這個問題,他提出了一個 RedLock 的算法。
這個算法的意思大概是這樣的:假設 Redis 的部署模式是 Redis Cluster,總共有 5 個 Master 節點。
通過以下步驟獲取一把鎖:
- 獲取當前時間戳,單位是毫秒。
- 輪流嘗試在每個 Master 節點上創建鎖,過期時間設置較短,一般就幾十毫秒。
- 嘗試在大多數節點上建立一個鎖,比如 5 個節點就要求是 3 個節點(n / 2 +1)。
- 客戶端計算建立好鎖的時間,如果建立鎖的時間小于超時時間,就算建立成功了。
- 要是鎖建立失敗了,那么就依次刪除這個鎖。
- 只要別人建立了一把分布式鎖,你就得不斷輪詢去嘗試獲取鎖。
但是這樣的這種算法,可能會出現節點崩潰重啟,多個客戶端持有鎖等其他問題,無法保證加鎖的過程一定正確。例如:
假設一共有5個Redis節點:A, B, C, D, E。設想發生了如下的事件序列:
(1)客戶端1成功鎖住了A, B, C,獲取鎖成功(但D和E沒有鎖住)。
(2)節點C崩潰重啟了,但客戶端1在C上加的鎖沒有持久化下來,丟失了。
(3)節點C重啟后,客戶端2鎖住了C, D, E,獲取鎖成功。
這樣,客戶端1和客戶端2同時獲得了鎖(針對同一資源)。
最后
即使是面試跳槽,那也是一個學習的過程。只有全面的復習,才能讓我們更好的充實自己,武裝自己,為自己的面試之路不再坎坷!今天就給大家分享一個Github上全面的Java面試題大全,就是這份面試大全助我拿下大廠Offer,月薪提至30K!
資料領取方式:藍色傳送門
我也是第一時間分享出來給大家,希望可以幫助大家都能去往自己心儀的大廠!為金三銀四做準備!
一共有20個知識點專題,分別是:
Dubbo面試專題
JVM面試專題
Java并發面試專題
Kafka面試專題
MongDB面試專題
MyBatis面試專題
MySQL面試專題
Netty面試專題
RabbitMQ面試專題
Redis面試專題
Spring Cloud面試專題
SpringBoot面試專題
zookeeper面試專題
常見面試算法題匯總專題
計算機網絡基礎專題
設計模式專題
0ullQ-1625205381307)]
SpringBoot面試專題
[外鏈圖片轉存中…(img-jq2MTUHc-1625205381308)]
zookeeper面試專題
[外鏈圖片轉存中…(img-B3WM9Ag9-1625205381309)]
常見面試算法題匯總專題
[外鏈圖片轉存中…(img-LKlPJkPM-1625205381310)]
計算機網絡基礎專題
[外鏈圖片轉存中…(img-vTsY1vVi-1625205381310)]
設計模式專題
[外鏈圖片轉存中…(img-xzO9715S-1625205381311)]
總結
以上是生活随笔為你收集整理的Java高级工程师必看系列,已拿到offer的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 成都欢乐谷自费项目
- 下一篇: Java高级工程师每日面试题精选,面试经