欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

分布式面試分布式鎖實(shí)現(xiàn)及應(yīng)用場景

 更新時(shí)間:2022年03月09日 15:27:41   作者:Q.E.D  
這篇文章主要為大家介紹了關(guān)于分布式的面試問題,分布式鎖的實(shí)現(xiàn)及應(yīng)用不同場景下的使用,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步

引言

鎖是開發(fā)過程中十分常見的工具,你一定不陌生,悲觀鎖,樂觀鎖,排它鎖,公平鎖,非公平鎖等等,很多概念,如果你對java里的鎖還不了解,可以參考這一篇:不可不說的Java“鎖”事,這一篇寫的很全面了,但是對于初學(xué)者,知道這些鎖的概念,由于缺乏實(shí)際工作經(jīng)驗(yàn),可能并不了解鎖的實(shí)際使用場景,Java中可以通過Volatile、Synchronized、ReentrantLock 三個(gè)關(guān)鍵字來實(shí)現(xiàn)線程的安全,這部分知識在第一輪基礎(chǔ)面試?yán)镆欢〞枺ㄒ炀氄莆张叮?/p>

在分布式系統(tǒng)中Java這些鎖技術(shù)是無法同時(shí)鎖住兩臺機(jī)器上的代碼,所以要通過分布式鎖來實(shí)現(xiàn),熟練使用分布式鎖也是大廠開發(fā)必會的技能。

1、面試官:

你有遇到需要使用分布式鎖的場景嗎?

問題分析:這個(gè)問題主要作為引子,先要了解什么場景下需要使用分布式鎖,分布式鎖要解決什么問題,在此前提下有助于你更好的理解分布式鎖的實(shí)現(xiàn)原理。

使用分布式鎖的場景一般需要滿足以下場景:

  • 系統(tǒng)是一個(gè)分布式系統(tǒng),java的鎖已經(jīng)鎖不住了。
  • 操作共享資源,比如庫里唯一的用戶數(shù)據(jù)。
  • 同步訪問,即多個(gè)進(jìn)程同時(shí)操作共享資源。

答:說一個(gè)我在項(xiàng)目中使用分布式鎖場景的例子:

消費(fèi)積分在很多系統(tǒng)里都有,信用卡,電商網(wǎng)站,通過積分換禮品等,這里“消費(fèi)積分”這個(gè)操作是需要使用鎖的典型場景。

事件A:

 以積分兌換禮品為例來講,完整的積分消費(fèi)過程簡單分成3步:

A1:用戶選中商品,發(fā)起兌換提交訂單。

A2:系統(tǒng)讀取用戶剩余積分:判斷用戶當(dāng)前積分是否充足。

A3:扣掉用戶積分。

事件B: 

系統(tǒng)給用戶發(fā)放積分也簡單分成3步:

B1:計(jì)算用戶當(dāng)天應(yīng)得積分

B2:讀取用戶原有積分

B3:在原有積分上增加本次應(yīng)得積分

那么問題來了,如果用戶消費(fèi)積分和用戶累加積分同時(shí)發(fā)生(同時(shí)用戶積分進(jìn)行操作)會怎樣?

假設(shè):用戶在消費(fèi)積分的同時(shí)恰好離線任務(wù)在計(jì)算積分給用戶發(fā)放積分(如根據(jù)用戶當(dāng)天的消費(fèi)額),這兩件事同時(shí)進(jìn)行,下面的邏輯有點(diǎn)繞,耐心理解。

用戶U有1000積分(記錄用戶積分的數(shù)據(jù)可以理解為共享資源),本次兌換要消耗掉999積分。

不加鎖的情況:事件A程序在執(zhí)行到第2步讀積分時(shí),A:2操作讀到的結(jié)果是1000分,判斷剩余積分夠本次兌換,緊接著要執(zhí)行第3步A:3操作扣積分(1000 - 999 = 1),正常結(jié)果應(yīng)該是用戶還是1分。但是這個(gè)時(shí)候事件B也在執(zhí)行,本次要給用戶U發(fā)放100積分,兩個(gè)線程同時(shí)進(jìn)行(同步訪問),不加鎖的情況,就會有下面這種可能,A:2 -> B:2 -> A:3 -> B:3 ,在A:3尚未完成前(扣積分,1000 - 999),用戶U總積分被事件B的線程讀取了,最后用戶U的總積分變成了1100分,還白白兌換了一個(gè)999積分的禮物,這顯然不符合預(yù)期結(jié)果。

有人說怎么可能這么巧同時(shí)操作用戶積分,cpu那么快,只要用戶足夠多,并發(fā)量足夠大,墨菲定律遲早生效,出現(xiàn)上述bug只是時(shí)間問題,還有可能被黑產(chǎn)行業(yè)卡住這個(gè)bug瘋狂薅羊毛,這個(gè)時(shí)候作為開發(fā)人員要解決這個(gè)隱患就必須了解鎖的使用。

(寫代碼是一項(xiàng)嚴(yán)謹(jǐn)?shù)氖聝海。?/p>

Java本身提供了兩種內(nèi)置的鎖的實(shí)現(xiàn),一種是由JVM實(shí)現(xiàn)的synchronized 和 JDK 提供的 Lock,以及很多原子操作類都是線程安全的,當(dāng)你的應(yīng)用是單機(jī)或者說單進(jìn)程應(yīng)用時(shí),可以使用這兩種鎖來實(shí)現(xiàn)鎖。

但是當(dāng)下互聯(lián)網(wǎng)公司的系統(tǒng)幾乎都是分布式的,這個(gè)時(shí)候Java自帶的 synchronized 或 Lock 已經(jīng)無法滿足分布式環(huán)境下鎖的要求了,因?yàn)榇a會部署在多臺機(jī)器上,為了解決這個(gè)問題,分布式鎖應(yīng)運(yùn)而生,分布式鎖的特點(diǎn)是多進(jìn)程,多個(gè)物理機(jī)器上無法共享內(nèi)存,常見的解決辦法是基于內(nèi)存層的干涉,落地方案就是基于Redis的分布式鎖 or ZooKeeper分布式鎖。

(我分析的不能更詳細(xì)了,面試官再不滿意?)

2、面試官:

Redis分布式鎖實(shí)現(xiàn)方法

問題分析:目前分布式鎖的實(shí)現(xiàn)方式主要有兩種,1.基于Redis Cluster模式。2.基于Zookeeper 集群模式。

優(yōu)先掌握這兩種,應(yīng)付面試基本沒問題了。

答:

1、基于Redis的分布式鎖

方法一:使用setnx命令加鎖

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
		// 第一步:加鎖
    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 第二步:設(shè)置過期時(shí)間
        jedis.expire(lockKey, expireTime);
    }
}

代碼解釋:

setnx命令,意思就是 set if not exist,如果lockKey不存在,把key存入Redis,保存成功后如果result返回1,表示設(shè)置成功,如果非1,表示失敗,別的線程已經(jīng)設(shè)置過了。

expire(),設(shè)置過期時(shí)間,防止死鎖,假設(shè),如果一個(gè)鎖set后,一直不刪掉,那這個(gè)鎖相當(dāng)于一直存在,產(chǎn)生死鎖。

(講到這里,我還要和面試官強(qiáng)調(diào)一個(gè)“但是”)

思考,我上面的方法哪里與缺陷?繼續(xù)給面試官解釋…

加鎖總共分兩步,第一步j(luò)edis.setnx,第二步j(luò)edis.expire設(shè)置過期時(shí)間,setnx與expire不是一個(gè)原子操作,如果程序執(zhí)行完第一步后異常了,第二步j(luò)edis.expire(lockKey, expireTime)沒有得到執(zhí)行,相當(dāng)于這個(gè)鎖沒有過期時(shí)間,有產(chǎn)生死鎖的可能。正對這個(gè)問題如何改進(jìn)?

改進(jìn):

public class RedisLockDemo {
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    /**
     * 獲取分布式鎖
     * @param jedis Redis客戶端
     * @param lockKey 鎖
     * @param requestId 請求標(biāo)識
     * @param expireTime 超期時(shí)間
     * @return 是否獲取成功
     */
    public static boolean getLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
				// 兩步合二為一,一行代碼加鎖并設(shè)置 + 過期時(shí)間。
        if (1 == jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)) {
            return true;//加鎖成功
        }
        return false;//加鎖失敗
    }
}

代碼解釋:

將加鎖和設(shè)置過期時(shí)間合二為一,一行代碼搞定,原子操作。

(沒等面試官開口追問,面試官很滿意了)

3、面試官: 那解鎖操作呢?

答:釋放鎖就是刪除key

使用del命令解鎖

public static void unLock(Jedis jedis, String lockKey, String requestId) {
    // 第一步: 使用 requestId 判斷加鎖與解鎖是不是同一個(gè)客戶端
    if (requestId.equals(jedis.get(lockKey))) {
        // 第二步: 若在此時(shí),這把鎖突然不是這個(gè)客戶端的,則會誤解鎖
        jedis.del(lockKey);
    }
}

代碼解釋: 通過 requestId 判斷加鎖與解鎖是不是同一個(gè)客戶端和 jedis.del(lockKey) 兩步不是原子操作,理論上會出現(xiàn)在執(zhí)行完第一步if判斷操作后鎖其實(shí)已經(jīng)過期,并且被其它線程獲取,這是時(shí)候在執(zhí)行jedis.del(lockKey)操作,相當(dāng)于把別人的鎖釋放了,這是不合理的。當(dāng)然,這是非常極端的情況,如果unLock方法里第一步和第二步?jīng)]有其它業(yè)務(wù)操作,把上面的代碼扔到線上,可能也不會真的出現(xiàn)問題,原因第一是業(yè)務(wù)并發(fā)量不高,根本不會暴露這個(gè)缺陷,那么問題還不大。

但是寫代碼是嚴(yán)謹(jǐn)?shù)墓ぷ鳎芡昝绖t必須完美。針對上述代碼中的問題,提出改進(jìn)。

代碼改進(jìn):

public class RedisTool {
    private static final Long RELEASE_SUCCESS = 1L;
    /**
     * 釋放分布式鎖
     * @param jedis Redis客戶端
     * @param lockKey 鎖
     * @param requestId 請求標(biāo)識
     * @return 是否釋放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

代碼解釋:

通過 jedis 客戶端的 eval 方法和 script 腳本一行代碼搞定,解決方法一中的原子問題。

3、面試官:

基于 ZooKeeper 的分布式鎖實(shí)現(xiàn)原理

答:還是積分消費(fèi)與積分累加的例子:事件A和事件B同時(shí)需要進(jìn)行對積分的修改操作,兩臺機(jī)器同時(shí)進(jìn)行,正確的業(yè)務(wù)邏輯上讓一臺機(jī)器先執(zhí)行完后另外一個(gè)機(jī)器再執(zhí)行,要么事件A先執(zhí)行,要么事件B先執(zhí)行,這樣才能保證不會出現(xiàn)A:2 -> B:2 -> A:3 -> B:3這種積分越花越多的情況(想到這種bug一旦上線,老板要生氣了,我可能要哭了)。

怎么辦?使用 zookeeper 分布式鎖。

一個(gè)機(jī)器接收到了請求之后,先獲取 zookeeper 上的一把分布式鎖(zk會創(chuàng)建一個(gè) znode),執(zhí)行操作;然后另外一個(gè)機(jī)器也嘗試去創(chuàng)建那個(gè) znode,結(jié)果發(fā)現(xiàn)自己創(chuàng)建不了,因?yàn)楸粍e人創(chuàng)建了,那只能等待,等第一個(gè)機(jī)器執(zhí)行完了方可拿到鎖。

使用 ZooKeeper 的順序節(jié)點(diǎn)特性,假如我們在/lock/目錄下創(chuàng)建3個(gè)節(jié)點(diǎn),ZK集群會按照發(fā)起創(chuàng)建的順序來創(chuàng)建節(jié)點(diǎn),節(jié)點(diǎn)分為/lock/0000000001、/lock/0000000002、/lock/0000000003,最后一位數(shù)是依次遞增的,節(jié)點(diǎn)名由zk來完成。

ZK中還有一種名為臨時(shí)節(jié)點(diǎn)的節(jié)點(diǎn),臨時(shí)節(jié)點(diǎn)由某個(gè)客戶端創(chuàng)建,當(dāng)客戶端與ZK集群斷開連接,則該節(jié)點(diǎn)自動被刪除。EPHEMERAL_SEQUENTIAL為臨時(shí)順序節(jié)點(diǎn)。

根據(jù)ZK中節(jié)點(diǎn)是否存在,可以作為分布式鎖的鎖狀態(tài),以此來實(shí)現(xiàn)一個(gè)分布式鎖,下面是分布式鎖的基本邏輯:

  • 客戶端調(diào)用create()方法創(chuàng)建名為“/dlm-locks/lockname/lock-”的臨時(shí)順序節(jié)點(diǎn)。
  • 客戶端調(diào)用getChildren(“lockname”)方法來獲取所有已經(jīng)創(chuàng)建的子節(jié)點(diǎn)。
  • 客戶端獲取到所有子節(jié)點(diǎn)path之后,如果發(fā)現(xiàn)自己在步驟1中創(chuàng)建的節(jié)點(diǎn)是所有節(jié)點(diǎn)中序號最小的,就是看自己創(chuàng)建的序列號是否排第一,如果是第一,那么就認(rèn)為這個(gè)客戶端獲得了鎖,在它前面沒有別的客戶端拿到鎖。
  • 如果創(chuàng)建的節(jié)點(diǎn)不是所有節(jié)點(diǎn)中需要最小的,那么則監(jiān)視比自己創(chuàng)建節(jié)點(diǎn)的序列號小的最大的節(jié)點(diǎn),進(jìn)入等待。直到下次監(jiān)視的子節(jié)點(diǎn)變更的時(shí)候,再進(jìn)行子節(jié)點(diǎn)的獲取,判斷是否獲取鎖。

釋放鎖的過程相對比較簡單,就是刪除自己創(chuàng)建的那個(gè)子節(jié)點(diǎn)即可,不過也仍需要考慮刪除節(jié)點(diǎn)失敗等異常情況。

額外補(bǔ)充

分布式鎖還可以從數(shù)據(jù)庫下手解決問題

方法一:

利用 Mysql 的鎖表,創(chuàng)建一張表,設(shè)置一個(gè) UNIQUE KEY 這個(gè) KEY 就是要鎖的 KEY,所以同一個(gè) KEY 在mysql表里只能插入一次了,這樣對鎖的競爭就交給了數(shù)據(jù)庫,處理同一個(gè) KEY 數(shù)據(jù)庫保證了只有一個(gè)節(jié)點(diǎn)能插入成功,其他節(jié)點(diǎn)都會插入失敗。

這樣 lock 和 unlock 的思路就很簡單了,偽代碼:

def lock :
    exec sql: insert into locked—table (xxx) values (xxx)
    if result == true :
        return true
    else :
        return false
def unlock :
    exec sql: delete from lockedOrder where order_id='order_id'

方法二:

使用流水號+時(shí)間戳做冪等操作,可以看作是一個(gè)不會釋放的鎖。

總結(jié)

針對分布式鎖的兩種實(shí)現(xiàn)方法,使用哪種需要取決于業(yè)務(wù)場景,如果系統(tǒng)接口的讀寫操作完全是基于內(nèi)存操作的,那顯然使用Redis更合適,Mysql表鎖or行鎖明顯不合適。同樣是基于內(nèi)存的 Redis鎖 和 ZK鎖具體選用哪一種,要根據(jù)是否有具體環(huán)境和架構(gòu)師對哪種技術(shù)更為了解,原則就是選你最了解的,目的是能解決問題。

以上就是分布式面試分布式鎖實(shí)現(xiàn)及應(yīng)用場景的詳細(xì)內(nèi)容,更多關(guān)于分布式鎖實(shí)現(xiàn)及應(yīng)用場景的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java?輪詢鎖使用時(shí)遇到問題解決方案

    Java?輪詢鎖使用時(shí)遇到問題解決方案

    這篇文章主要介紹了Java?輪詢鎖使用時(shí)遇到問題解決方案,當(dāng)我們遇到死鎖之后,除了可以手動重啟程序解決之外,還可以考慮使用順序鎖和輪詢鎖,但是過程也會遇到一些問題,接下來我們一起進(jìn)入下面文章了解解決方案,需要的小伙伴可以參考一下
    2022-05-05
  • ResultSet如何動態(tài)獲取列名和值

    ResultSet如何動態(tài)獲取列名和值

    這篇文章主要介紹了ResultSet如何動態(tài)獲取列名和值問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-12-12
  • java實(shí)現(xiàn) 二叉搜索樹功能

    java實(shí)現(xiàn) 二叉搜索樹功能

    這篇文章主要介紹了java實(shí)現(xiàn) 二叉搜索樹功能,代碼簡單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2018-07-07
  • Java使用OpenCV3.2實(shí)現(xiàn)視頻讀取與播放

    Java使用OpenCV3.2實(shí)現(xiàn)視頻讀取與播放

    這篇文章主要為大家詳細(xì)介紹了Java使用OpenCV3.2實(shí)現(xiàn)視頻讀取與播放,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-07-07
  • springboot日期轉(zhuǎn)換器實(shí)現(xiàn)實(shí)例解析

    springboot日期轉(zhuǎn)換器實(shí)現(xiàn)實(shí)例解析

    這篇文章主要介紹了springboot日期轉(zhuǎn)換器實(shí)現(xiàn)實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-12-12
  • java隨機(jī)抽取指定范圍不重復(fù)的數(shù)字

    java隨機(jī)抽取指定范圍不重復(fù)的數(shù)字

    這篇文章主要介紹了java隨機(jī)抽取指定范圍不重復(fù)的數(shù)字的相關(guān)資料,需要的朋友可以參考下
    2016-06-06
  • java開源項(xiàng)目jeecgboot的超詳細(xì)解析

    java開源項(xiàng)目jeecgboot的超詳細(xì)解析

    JeecgBoot是一款基于BPM的低代碼平臺,下面這篇文章主要給大家介紹了關(guān)于java開源項(xiàng)目jeecgboot的相關(guān)資料,文中通過圖文以及實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-10-10
  • springboot單文件下載和多文件壓縮zip下載的實(shí)現(xiàn)

    springboot單文件下載和多文件壓縮zip下載的實(shí)現(xiàn)

    這篇文章主要介紹了springboot單文件下載和多文件壓縮zip下載的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-11-11
  • nacos服務(wù)注冊服務(wù)發(fā)現(xiàn)依賴配置詳解

    nacos服務(wù)注冊服務(wù)發(fā)現(xiàn)依賴配置詳解

    這篇文章主要為大家介紹了nacos服務(wù)注冊服務(wù)發(fā)現(xiàn)依賴配置詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-09-09
  • Spring Boot 單元測試JUnit的實(shí)踐

    Spring Boot 單元測試JUnit的實(shí)踐

    JUnit是一款優(yōu)秀的開源Java單元測試框架,也是目前使用率最高最流行的測試框架,這篇文章主要介紹了Spring Boot 單元測試JUnit的實(shí)踐,感興趣的小伙伴們可以參考一下
    2018-11-11

最新評論