Java面试:java简历包装医疗项目
1.為什么要使用分布式鎖
使用分布式鎖的目的,無外乎就是保證同一時間只有一個客戶端可以對共享資源進行操作。
1.1舉一個很長的例子
系統(tǒng) A 是一個電商系統(tǒng),目前是一臺機器部署,系統(tǒng)中有一個用戶下訂單的接口,但是用戶下訂單之前一定要去檢查一下庫存,確保庫存足夠了才會給用戶下單。由于系統(tǒng)有一定的并發(fā),所以會預(yù)先將商品的庫存保存在 Redis 中,用戶下單的時候會更新 Redis 的庫存。此時系統(tǒng)架構(gòu)如下:
但是這樣一來會產(chǎn)生一個問題:假如某個時刻,Redis 里面的某個商品庫存為 1。
此時兩個請求同時到來,其中一個請求執(zhí)行到上圖的第 3 步,更新數(shù)據(jù)庫的庫存為 0,但是第 4 步還沒有執(zhí)行。
而另外一個請求執(zhí)行到了第 2 步,發(fā)現(xiàn)庫存還是 1,就繼續(xù)執(zhí)行第 3 步。這樣的結(jié)果,是導(dǎo)致賣出了 2 個商品,然而其實庫存只有 1 個。
很明顯不對啊!這就是典型的庫存超賣問題。此時,我們很容易想到解決方案:用鎖把 2、3、4 步鎖住,讓他們執(zhí)行完之后,另一個線程才能進來執(zhí)行第 2 步。
按照上面的圖,在執(zhí)行第 2 步時,使用 Java 提供的 Synchronized 或者 ReentrantLock 來鎖住,然后在第 4 步執(zhí)行完之后才釋放鎖。
這樣一來,2、3、4 這 3 個步驟就被“鎖”住了,多個線程之間只能串行化執(zhí)行。
當(dāng)整個系統(tǒng)的并發(fā)飆升,一臺機器扛不住了。現(xiàn)在要增加一臺機器,如下圖:
增加機器之后,系統(tǒng)變成上圖所示,假設(shè)此時兩個用戶的請求同時到來,但是落在了不同的機器上,那么這兩個請求是可以同時執(zhí)行了,還是會出現(xiàn)庫存超賣的問題。
因為上圖中的兩個 A 系統(tǒng),運行在兩個不同的 JVM 里面,他們加的鎖只對屬于自己 JVM 里面的線程有效,對于其他 JVM 的線程是無效的。
因此,這里的問題是:Java 提供的原生鎖機制在多機部署場景下失效了,這是因為兩臺機器加的鎖不是同一個鎖(兩個鎖在不同的 JVM 里面)。
那么,我們只要保證兩臺機器加的鎖是同一個鎖,問題不就解決了嗎?此時,就該分布式鎖隆重登場了。
分布式鎖的思路是:在整個系統(tǒng)提供一個全局、唯一的獲取鎖的“東西”,然后每個系統(tǒng)在需要加鎖時,都去問這個“東西”拿到一把鎖,這樣不同的系統(tǒng)拿到的就可以認為是同一把鎖。
至于這個“東西”,可以是 Redis、Zookeeper,也可以是數(shù)據(jù)庫。此時的架構(gòu)如圖:
通過上面的分析,我們知道了庫存超賣場景在分布式部署系統(tǒng)的情況下使用 Java 原生的鎖機制無法保證線程安全,所以我們需要用到分布式鎖的方案。
2.高效的分布式鎖
在設(shè)計分布式鎖的時候,應(yīng)該考慮分布式鎖至少要滿足的一些條件,同時考慮如何高效的設(shè)計分布式鎖,以下幾點是必須要考慮的:
(1) 互斥
在分布式高并發(fā)的條件下,最需要保證在同一時刻只能有一個線程獲得鎖,這是最基本的一點。
(2) 防止死鎖
在分布式高并發(fā)的條件下,比如有個線程獲得鎖的同時,還沒有來得及去釋放鎖,就因為系統(tǒng)故障或者其它原因使它無法執(zhí)行釋放鎖的命令,導(dǎo)致其它線程都無法獲得鎖,造成死鎖。所以分布式非常有必要設(shè)置鎖的有效時間,確保系統(tǒng)出現(xiàn)故障后,在一定時間內(nèi)能夠主動去釋放鎖,避免造成死鎖的情況。
(3) 性能
對于訪問量大的共享資源,需要考慮減少鎖等待的時間,避免導(dǎo)致大量線程阻塞。
所以在鎖的設(shè)計時,需要考慮兩點。
1、 鎖的顆粒度要盡量小。比如你要通過鎖來減庫存,那這個鎖的名稱你可以設(shè)置成是商品的ID,而不是任取名稱。這樣這個鎖只對當(dāng)前商品有效,鎖的顆粒度小。
2、 鎖的范圍盡量要小。比如只要鎖2行代碼就可以解決問題的,那就不要去鎖10行代碼了。
(4) 重入
我們知道ReentrantLock是可重入鎖,那它的特點就是:同一個線程可以重復(fù)拿到同一個資源的鎖。重入鎖非常有利于資源的高效利用。關(guān)于這點之后會做演示。
3.基于Redis實現(xiàn)分布式鎖
3.1 使用Redis命令實現(xiàn)分布式鎖
3.1.1加鎖
加鎖實際上就是在redis中,給Key鍵設(shè)置一個值,為避免死鎖,并給定一個過期時間。
使用的命令**:SET lock_key random_value NX PX 5000**
值得注意的是:
random_value 是客戶端生成的唯一的字符串。
NX 代表只在鍵不存在時,才對鍵進行設(shè)置操作。
PX 5000 設(shè)置鍵的過期時間為5000毫秒。
也可以使用另外一條命令:SETNX key value
只不過過期時間無法設(shè)置。
這樣,如果上面的命令執(zhí)行成功,則證明客戶端獲取到了鎖。
3.1.2解鎖
解鎖的過程就是將Key鍵刪除,但要保證安全性,舉個例子:客戶端1的請求不能將客戶端2的鎖給刪除掉。
釋放鎖涉及到兩條指令,這兩條指令不是原子性的,需要用到redis的lua腳本支持特性,redis執(zhí)行l(wèi)ua腳本是原子性的。腳本如下:
if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) elsereturn 0 end這種方式比較簡單,但是也有一個最重要的問題:鎖不具有可重入性。
3.2使用Redisson實現(xiàn)分布式鎖
3.2.1Redisson介紹
Redisson是架設(shè)在Redis基礎(chǔ)上的一個Java駐內(nèi)存數(shù)據(jù)網(wǎng)格(In-Memory Data Grid)。充分的利用了Redis鍵值數(shù)據(jù)庫提供的一系列優(yōu)勢,基于Java實用工具包中常用接口,為使用者提供了一系列具有分布式特性的常用工具類。使得原本作為協(xié)調(diào)單機多線程并發(fā)程序的工具包獲得了協(xié)調(diào)分布式多機多線程并發(fā)系統(tǒng)的能力,大大降低了設(shè)計和研發(fā)大規(guī)模分布式系統(tǒng)的難度。同時結(jié)合各富特色的分布式服務(wù),更進一步簡化了分布式環(huán)境中程序相互之間的協(xié)作。
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 即可完成分布式鎖,而且考慮了很多細節(jié):
l Redisson 所有指令都通過 Lua 腳本執(zhí)行,Redis 支持 Lua 腳本原子性執(zhí)行。
l Redisson 設(shè)置一個 Key 的默認過期時間為 30s,但是如果獲取鎖之后,會有一個WatchDog每隔10s將key的超時時間設(shè)置為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) 加鎖機制
線程去獲取鎖,獲取成功: 執(zhí)行l(wèi)ua腳本,保存數(shù)據(jù)到redis數(shù)據(jù)庫。
線程去獲取鎖,獲取失敗: 一直通過while循環(huán)嘗試獲取鎖,獲取成功后,執(zhí)行l(wèi)ua腳本,保存數(shù)據(jù)到redis數(shù)據(jù)庫。
(2) WatchDog自動延期機制
在一個分布式環(huán)境下,假如一個線程獲得鎖后,突然服務(wù)器宕機了,那么這個時候在一定時間后這個鎖會自動釋放,也可以設(shè)置鎖的有效時間(不設(shè)置默認30秒),這樣的目的主要是防止死鎖的發(fā)生。但是在實際情況中會有一種情況,業(yè)務(wù)處理的時間可能會大于鎖過期的時間,這樣就可能**導(dǎo)致解鎖和加鎖不是同一個線程。**所以WatchDog作用就是Redisson實例關(guān)閉前,不斷延長鎖的有效期。
如果程序調(diào)用加鎖方法顯式地給了有效期,是不會開啟后臺線程(也就是watch dog)進行延期的,如果沒有給有效期或者給的是-1,redisson會默認設(shè)置30s有效期并且會開啟后臺線程(watch dog)進行延期
多久進行一次延期:(默認有效期/3),默認有效期可以設(shè)置修改的,即默認情況下每隔10s設(shè)置有效期為30s
(3) 可重入加鎖機制
Redisson可以實現(xiàn)可重入加鎖機制的原因:
l Redis存儲鎖的數(shù)據(jù)類型是Hash類型
l Hash數(shù)據(jù)類型的key值包含了當(dāng)前線程的信息
下面是redis存儲的數(shù)據(jù)
這里表面數(shù)據(jù)類型是Hash類型,Hash類型相當(dāng)于我們java的 <key,<key1,value>> 類型,這里key是指 ‘redisson’
它的有效期還有9秒,我們再來看里們的key1值為078e44a3-5f95-4e24-b6aa-80684655a15a:45它的組成是:
guid + 當(dāng)前線程的ID。后面的value是就和可重入加鎖有關(guān)。value代表同一客戶端調(diào)用lock方法的次數(shù),即可重入計數(shù)統(tǒng)計。
舉圖說明
上面這圖的意思就是可重入鎖的機制,它最大的優(yōu)點就是相同線程不需要在等待鎖,而是可以直接進行相應(yīng)操作。
3.2.4 獲取鎖的流程
其中的指定字段也就是hash結(jié)構(gòu)中的field值(構(gòu)成是uuid+線程id),即判斷鎖是否是當(dāng)前線程
3.2.5 加鎖的流程
3.2.6 釋放鎖的流程
4. 使用Redis做分布式鎖的缺點
Redis有三種部署方式
l 單機模式
l Master-Slave+Sentienl選舉模式
l Redis Cluster模式
如果采用單機部署模式,會存在單點問題,只要 Redis 故障了。加鎖就不行了
采用 Master-Slave 模式,加鎖的時候只對一個節(jié)點加鎖,即便通過 Sentinel 做了高可用,但是如果 Master 節(jié)點故障了,發(fā)生主從切換,此時就會有可能出現(xiàn)鎖丟失的問題。
基于以上的考慮,Redis 的作者也考慮到這個問題,他提出了一個 RedLock 的算法。
這個算法的意思大概是這樣的:假設(shè) Redis 的部署模式是 Redis Cluster,總共有 5 個 Master 節(jié)點。
通過以下步驟獲取一把鎖:
- 獲取當(dāng)前時間戳,單位是毫秒。
- 輪流嘗試在每個 Master 節(jié)點上創(chuàng)建鎖,過期時間設(shè)置較短,一般就幾十毫秒。
- 嘗試在大多數(shù)節(jié)點上建立一個鎖,比如 5 個節(jié)點就要求是 3 個節(jié)點(n / 2 +1)。
- 客戶端計算建立好鎖的時間,如果建立鎖的時間小于超時時間,就算建立成功了。
- 要是鎖建立失敗了,那么就依次刪除這個鎖。
- 只要別人建立了一把分布式鎖,你就得不斷輪詢?nèi)L試獲取鎖。
但是這樣的這種算法,可能會出現(xiàn)節(jié)點崩潰重啟,多個客戶端持有鎖等其他問題,無法保證加鎖的過程一定正確。例如:
假設(shè)一共有5個Redis節(jié)點:A, B, C, D, E。設(shè)想發(fā)生了如下的事件序列:
(1)客戶端1成功鎖住了A, B, C,獲取鎖成功(但D和E沒有鎖住)。
(2)節(jié)點C崩潰重啟了,但客戶端1在C上加的鎖沒有持久化下來,丟失了。
(3)節(jié)點C重啟后,客戶端2鎖住了C, D, E,獲取鎖成功。
這樣,客戶端1和客戶端2同時獲得了鎖(針對同一資源)。
最后
由于細節(jié)內(nèi)容實在太多了,為了不影響文章的觀賞性,只截出了一部分知識點大致的介紹一下,每個小節(jié)點里面都有更細化的內(nèi)容!
需要這份文檔的朋友可以幫忙點個贊,點擊下方神秘超鏈接,就可以免費獲取到了,還有小編準(zhǔn)備的一份Java進階學(xué)習(xí)路線圖(Xmind)以及來年金三銀四必備的一份《Java面試必備指南》
資料領(lǐng)取鏈接:Java進階學(xué)習(xí)路線圖(Xmind)+《Java面試必備指南》
了不影響文章的觀賞性,只截出了一部分知識點大致的介紹一下,每個小節(jié)點里面都有更細化的內(nèi)容!
[外鏈圖片轉(zhuǎn)存中…(img-RvSGKBYn-1627019170405)]
需要這份文檔的朋友可以幫忙點個贊,點擊下方神秘超鏈接,就可以免費獲取到了,還有小編準(zhǔn)備的一份Java進階學(xué)習(xí)路線圖(Xmind)以及來年金三銀四必備的一份《Java面試必備指南》
資料領(lǐng)取鏈接:Java進階學(xué)習(xí)路線圖(Xmind)+《Java面試必備指南》
總結(jié)
以上是生活随笔為你收集整理的Java面试:java简历包装医疗项目的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从gitlab上拉代码_从gitlab上
- 下一篇: Java面试中如何介绍自己的项目经验?