鎖和分佈式鎖

鎖的由來

多線程環境中,常常遇到多個線程訪問同一個 共享資源 ,這時候做爲開發者必須考慮如何維護數據一致性,這就須要某種機制來保證只有知足某個條件(獲取鎖成功)的線程才能訪問資源,而不知足條件(獲取鎖失敗)的線程只能等待,在下一輪競爭中來獲取鎖才能訪問資源。java

兩個知識點:

1.高級緩存Cache
6609c93d70cf3bc79dd144c5d300baa1cc112ac1.jpgredis

CPU爲了提升處理速度,不和內存直接進行交互,而是使用Cache。
可能引起的問題:
clipboard.png
若是多個處理器同時對共享變量進行讀改寫操做 (i++就是經典的讀改寫操做),那麼共享變量就會被多個處理器同時進行操做,這樣讀改寫操做就不是原子的了,操做完以後共享變量的值會和指望的不一致。緩存

形成此結果的緣由:
多個處理器同時從各自的緩存中讀取變量i,分別進行加1操做,而後分別寫入 系統內存中。
處理器層面的解決方案:
處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個 LOCK#信號,當一個處理器在總線上輸出此信號時,其餘處理器的請求將被阻塞住,那麼該處理器能夠獨佔共享內存。多線程

2.CAS(Compare And Swap)+volatile
CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。執行CAS操做的時候,將內存位置的值與預期原值比較,若是相匹配,那麼處理器會自動將該位置值更新爲新值。不然,處理器不作任何操做。
java的Atomic以及一些它自帶的類中的cas操做都是經過藉助cmpxchg指令完成的。他保證同一時刻只能有一個線程cas成功。
舉個例子
以AtomicIneger的源碼爲例來看看CAS操做:併發

clipboard.png
for(;;)表示循環,只有當if判斷爲true才退出。而if判斷的內容就是是否CAS成功。jvm

clipboard.png

clipboard.png

volatile的做用:
1)將當前處理器緩存行的數據寫回到系統內存。
2)這個寫回內存的操做會使在其餘CPU裏緩存了該內存地址的數據無效。分佈式

循環CAS+volatile是實現鎖的關鍵。lua

Lock鎖的部分細節

clipboard.png

clipboard.png

不一樣場景鎖的表現不一樣:獨佔?共享?讀寫?spa

clipboard.png

分佈式鎖(redis的簡單實現)

分佈式鎖實現的三個核心要素:線程

  • 1.加鎖

最簡單的方法是使用setnx命令。key是鎖的惟一標識,按業務來決定命名。好比想要給一種商品的秒殺活動加鎖,能夠給key命名爲 「lock_sale_商品ID」 。而value設置成什麼呢?咱們能夠姑且設置成1。加鎖的僞代碼以下:
setnx(key,1)
SETNX key value

將 key 的值設爲 value ,當且僅當 key 不存在。

若給定的 key 已經存在,則 SETNX 不作任何動做。

SETNX 是『SET if Not eXists』(若是不存在,則 SET)的簡寫。
時間複雜度:
O(1)
返回值:
設置成功,返回 1 。
設置失敗,返回 0 。

當一個線程執行setnx返回1,說明key本來不存在,該線程成功獲得了鎖;當一個線程執行setnx返回0,說明key已經存在,該線程搶鎖失敗。

  • 2.解鎖

有加鎖就得有解鎖。當獲得鎖的線程執行完任務,須要釋放鎖,以便其餘線程能夠進入。釋放鎖的最簡單方式是執行del指令,僞代碼以下:

del(key)

釋放鎖以後,其餘線程就能夠繼續執行setnx命令來得到鎖。

  • 3.設置超時時間

若是一個獲得鎖的線程在執行任務的過程當中掛掉,來不及顯式地釋放鎖,這塊資源將會永遠被鎖住,別的線程再也別想進來。

因此,setnx的key必須設置一個超時時間,以保證即便沒有被顯式釋放,這把鎖也要在必定時間後自動釋放。setnx不支持超時參數,因此須要額外的指令,僞代碼以下:

expire(key, 30)

綜合起來,咱們分佈式鎖實現的初版僞代碼以下:

if(setnx(key,1) == 1){
    expire(key,30)
    do something ......
    del(key)
    }

上述代碼的問題:

  • 1 setnx和expire的非原子性

20180603120204236
setnx剛執行成功,還將來得及執行expire指令,節點1 Duang的一聲掛掉了。
20180603120239262
這樣一來,這個鎖就長生不死了。
解決方案:
Redis 2.6.12以上版本爲set指令增長了可選參數,僞代碼以下:

set(key,1,30,NX)
  • 2 del 致使誤刪

2018060312051738
20180603120634638
20180603120745184
能夠在del釋放鎖以前作一個判斷,驗證當前的鎖是否是本身加的鎖
至於具體的實現,能夠在加鎖的時候把當前的線程ID當作value,並在刪除以前驗證key對應的value是否是本身線程的ID。
加鎖:

String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)

解鎖:

if(threadId .equals(redisClient.get(key))){
    del(key)
}

這樣作又隱含了一個新的問題,判斷和釋放鎖是兩個獨立操做,不是原子性。
這一塊要用Lua腳原本實現:

String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));

redis官方說:eval命令在執行lua腳本時會看成一個命令去執行,而且直到命令執行完成redis纔會去執行其餘命令,因此就變成了一個原子操做。

  • 3出現併發的可能性

進程1在超時時間內未執行完代碼,此時進程2是能夠獲取鎖的,會出現兩個進程同時訪問一個資源的狀況。
解決方案:能夠在進程1所在的jvm環境中開一個線程專門用來「續命」,當須要解鎖的時候,通知這個續命線程結束執行。

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 線程Id
     * @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;

    }
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;

    }
相關文章
相關標籤/搜索