分布式鎖

什么是鎖

在多線程的軟件世界里,對共享資源的爭搶過程(Data Race)就是并發(fā)朵逝,而對共享資源數(shù)據(jù)進行訪問保護的最直接辦法就是引入鎖拴曲。

POSIX threads(簡稱Pthreads)是在多核平臺上進行并行編程的一套常用的API占哟。線程同步(Thread Synchronization)是并行編程中非常重要的通訊手段坑鱼,其中最典型的應(yīng)用就是用Pthreads提供的鎖機制(lock)來對多個線程之間共 享的臨界區(qū)(Critical Section)進行保護(另一種常用的同步機制是barrier)。

無鎖編程也是一種辦法动漾,但它不在本文的討論范圍丁屎,并發(fā)多線程轉(zhuǎn)為單線程(Disruptor),函數(shù)式編程旱眯,鎖粒度控制(ConcurrentHashMap桶)晨川,信號量(Semaphore)等手段都可以實現(xiàn)無鎖或鎖優(yōu)化。

技術(shù)上來說删豺,鎖也可以理解成將大量并發(fā)請求串行化共虑,但請注意串行化不能簡單等同為** 排隊 ,因為這里和現(xiàn)實世界沒什么不同呀页,排隊意味著大家是公平Fair的領(lǐng)到資源妈拌,先到先得,然而很多情況下為了性能考量多線程之間還是會不公平Unfair**的去搶蓬蝶。Java中ReentrantLock可重入鎖尘分,提供了公平鎖和非公平鎖兩種實現(xiàn)。

再注意一點丸氛,串行也不是意味著只有一個排隊的隊伍培愁,每次只能進一個。當然可以好多個隊伍缓窜,每次進入多個竭钝。比如餐館一共10個餐桌,服務(wù)員可能一次放行最多10個人進去雹洗,有人出來再放行同數(shù)量的人進去香罐。Java中Semaphore信號量,相當于同時管理一批鎖时肿。

鎖的類型

自旋鎖(Spin Lock)

自旋鎖是一種非阻塞鎖庇茫,也就是說,如果某線程需要獲取自旋鎖螃成,但該鎖已經(jīng)被其他線程占用時旦签,該線程不會被掛起,而是在不斷的消耗CPU的時間寸宏,不停的試圖獲取自旋鎖宁炫。

互斥鎖 (Mutex Lock)

互斥鎖是阻塞鎖,當某線程無法獲取互斥鎖時氮凝,該線程會被直接掛起羔巢,不再消耗CPU時間,當其他線程釋放互斥鎖后,操作系統(tǒng)會喚醒那個被掛起的線程竿秆。

可重入鎖 (Reentrant Lock)

可重入鎖是一種特殊的互斥鎖启摄,它可以被同一個線程多次獲取,而不會產(chǎn)生死鎖幽钢。

鎖舉例

本地鎖

java環(huán)境下可以通過synchronized和lock開實現(xiàn)本地鎖歉备。


//synchronized

    public synchronized void demoMethod(){}
    
    public void demoMethod(){
        synchronized (this)
        {
            //other thread safe code
        }
    }

    private final Object lock = new Object();
    public void demoMethod(){
        synchronized (lock)
        {
            //other thread safe code
        }
    }

    public synchronized static void demoMethod(){}

//lock

   private final Lock queueLock = new ReentrantLock();
 
   public void printJob(Object document)
   {
      queueLock.lock();
      try
      {
         Long duration = (long) (Math.random() * 10000);
         System.out.println(Thread.currentThread().getName() + ": PrintQueue: Printing a Job during " + (duration / 1000) + " seconds :: Time - " + new Date());
         Thread.sleep(duration);
      } catch (InterruptedException e)
      {
         e.printStackTrace();
      } finally
      {
         System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
         queueLock.unlock();
      }
   }

鎖非靜態(tài)是鎖了對象的實例;鎖靜態(tài)是鎖了對象的類型匪燕。

一些特性

  • 可重入蕾羊。如下可以直接進入testWrite方法不用重新申請鎖。synchronized和lock都是可重入鎖帽驯。
    synchronized void testRead(){
        this.testWrite();
    }
    synchronized void testWrite(){}
  • 可中斷鎖肚豺。例如A正在執(zhí)行鎖中的代碼,另一線程B正在等待獲取該鎖如果B可以中斷則該鎖為可中斷鎖界拦。synchronized就不是可中斷鎖,而Lock是可中斷鎖梗劫。
  • 公平鎖和非公平鎖享甸。以請求鎖的順序來獲取鎖是公平鎖。synchronized是非公平鎖梳侨,lock默認是非公平鎖蛉威,但是可以設(shè)置為公平鎖。

對比

名稱 優(yōu)點 缺點
synchronized 實現(xiàn)簡單走哺,語義清晰蚯嫌,便于JVM堆棧跟蹤,加鎖解鎖過程由JVM自動控制丙躏,提供了多種優(yōu)化方案择示,使用更廣泛 悲觀的排他鎖,不能進行高級功能
lock 可定時的晒旅、可輪詢的與可中斷的鎖獲取操作栅盲,提供了讀寫鎖、公平鎖和非公平鎖 需手動釋放鎖unlock废恋,不適合JVM進行堆棧跟蹤

分布式鎖

使用分布式鎖的目的有兩個谈秫,一個是避免多次執(zhí)行冪等操作提升效率;一個是避免多個節(jié)點同時執(zhí)行非冪等操作導致數(shù)據(jù)不一致鱼鼓。
接下來我們來看如何實現(xiàn)分布式鎖拟烫,在java環(huán)境下有三種也即通過數(shù)據(jù)庫,通過redis及通過Zk來實現(xiàn)迄本。

通過數(shù)據(jù)庫實現(xiàn)

通過主鍵及其他約束使用拋異常來實現(xiàn)分布式鎖不在本文討論范圍硕淑。一下為基于數(shù)據(jù)庫排他鎖來實現(xiàn)分布式鎖

/**
     * 超時獲取鎖
     * @param lockID
     * @param timeOuts
     * @return
     * @throws InterruptedException
     */
    public boolean acquireByUpdate(String lockID, long timeOuts) throws InterruptedException, SQLException {

        String sql = "SELECT id from test_lock where id = ? for UPDATE ";
        long futureTime = System.currentTimeMillis() + timeOuts;
        long ranmain = timeOuts;
        long timerange = 500;
        connection.setAutoCommit(false);
        while (true) {
            CountDownLatch latch = new CountDownLatch(1);
            try {
                PreparedStatement statement = connection.prepareStatement(sql);
                statement.setString(1, lockID);
                statement.setInt(2, 1);
                statement.setLong(1, System.currentTimeMillis());
                boolean ifsucess = statement.execute();//如果成功,那么就是獲取到了鎖
                if (ifsucess)
                    return true;
            } catch (SQLException e) {
                e.printStackTrace();
            }
            latch.await(timerange, TimeUnit.MILLISECONDS);
            ranmain = futureTime - System.currentTimeMillis();
            if (ranmain <= 0)
                break;
            if (ranmain < timerange) {
                timerange = ranmain;
            }
            continue;
        }
        return false;

    }


    /**
     * 釋放鎖
     * @param lockID
     * @return
     * @throws SQLException
     */
    public void unlockforUpdtate(String lockID) throws SQLException {
        connection.commit();

    }

通過緩存系統(tǒng)實現(xiàn)

加鎖

public class RedisTool {
 
    private static final String LOCK_SUCCESS = "OK";
    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 請求標識
     * @param expireTime 超期時間
     * @return 是否獲取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
 
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
 
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
 
    }
 
}

第一個為key,我們使用key來當鎖喜颁,因為key是唯一的稠氮。

第二個為value,我們傳的是requestId半开,很多童鞋可能不明白隔披,有key作為鎖不就夠了嗎,為什么還要用到value寂拆?原因就是我們在上面講到可靠性時奢米,分布式鎖要滿足第四個條件解鈴還須系鈴人,通過給value賦值為requestId纠永,我們就知道這把鎖是哪個請求加的了鬓长,在解鎖的時候就可以有依據(jù)。requestId可以使用UUID.randomUUID().toString()方法生成尝江。

第三個為nxxx涉波,這個參數(shù)我們填的是NX,意思是SET IF NOT EXIST炭序,即當key不存在時啤覆,我們進行set操作;若key已經(jīng)存在惭聂,則不做任何操作窗声;

第四個為expx,這個參數(shù)我們傳的是PX辜纲,意思是我們要給這個key加一個過期的設(shè)置笨觅,具體時間由第五個參數(shù)決定。

第五個為time耕腾,與第四個參數(shù)相呼應(yīng)见剩,代表key的過期時間。

解鎖

public class RedisTool {
 
    private static final Long RELEASE_SUCCESS = 1L;
 
    /**
     * 釋放分布式鎖
     * @param jedis Redis客戶端
     * @param lockKey 鎖
     * @param requestId 請求標識
     * @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;
 
    }
 
}

第一行代碼扫俺,我們寫了一個簡單的Lua腳本代碼
第二行代碼炮温,我們將Lua代碼傳到j(luò)edis.eval()方法里,并使參數(shù)KEYS[1]賦值為lockKey牵舵,ARGV[1]賦值為requestId柒啤。eval()方法是將Lua代碼交給Redis服務(wù)端執(zhí)行。

基于Redlock實現(xiàn)分布式鎖的爭論見

Redlock

how-to-do-distributed-locking

通過ZK實現(xiàn)

使用[curator]{https://curator.apache.org/}來實現(xiàn)分布式鎖畸颅。

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    try {
        return interProcessMutex.acquire(timeout, unit);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return true;
}
public boolean unlock() {
    try {
        interProcessMutex.release();
    } catch (Throwable e) {
        log.error(e.getMessage(), e);
    } finally {
        executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
    }
    return true;
}

分布式鎖對比

方式 優(yōu)點 缺點
基于DB 直接借助數(shù)據(jù)庫担巩,容易理解 會有各種各樣的問題,在解決問題的過程中會使整個方案變得越來越復雜
操作數(shù)據(jù)庫需要一定的開銷没炒,性能問題需要考慮
使用數(shù)據(jù)庫的行級鎖并不一定靠譜涛癌,尤其是當我們的鎖表并不大的時候
基于緩存 性能好,實現(xiàn)起來較為方便 通過超時時間來控制鎖的失效時間并不是十分的合理
基于ZK 有效的解決單點問題,不可重入問題拳话,非阻塞問題以及鎖無法釋放的問題先匪。實現(xiàn)起來較為簡單 性能上不如使用緩存實現(xiàn)分布式鎖。 需要對ZK的原理有所了解

結(jié)論

zookeeper可靠性比redis強太多弃衍,只是效率低了點呀非,如果并發(fā)量不是特別大,追求可靠性镜盯,首選zookeeper岸裙。為了效率,則首選redis實現(xiàn)速缆。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末降允,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子艺糜,更是在濱河造成了極大的恐慌剧董,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件破停,死亡現(xiàn)場離奇詭異翅楼,居然都是意外死亡,警方通過查閱死者的電腦和手機辱挥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來边涕,“玉大人晤碘,你說我怎么就攤上這事」︱眩” “怎么了园爷?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長式撼。 經(jīng)常有香客問我童社,道長,這世上最難降的妖魔是什么著隆? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任扰楼,我火速辦了婚禮,結(jié)果婚禮上美浦,老公的妹妹穿的比我還像新娘弦赖。我一直安慰自己,他們只是感情好浦辨,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布蹬竖。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪币厕。 梳的紋絲不亂的頭發(fā)上列另,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天,我揣著相機與錄音旦装,去河邊找鬼页衙。 笑死,一個胖子當著我的面吹牛同辣,可吹牛的內(nèi)容都是我干的拷姿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼旱函,長吁一口氣:“原來是場噩夢啊……” “哼响巢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起棒妨,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤踪古,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后券腔,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伏穆,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年纷纫,在試婚紗的時候發(fā)現(xiàn)自己被綠了枕扫。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡辱魁,死狀恐怖烟瞧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情染簇,我是刑警寧澤参滴,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站锻弓,受9級特大地震影響砾赔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜青灼,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一暴心、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧杂拨,春花似錦酷勺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽甚亭。三九已至,卻和暖如春击胜,著一層夾襖步出監(jiān)牢的瞬間亏狰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工偶摔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留暇唾,地道東北人。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓辰斋,卻偏偏與公主長得像策州,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子宫仗,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容