1. 程式人生 > >鎖和分散式鎖

鎖和分散式鎖

鎖的由來 :

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

兩個知識點:

1.高階快取Cache

鎖和分散式鎖

圖片描述(最多50字)

CPU為了提高處理速度,不和記憶體直接進行互動,而是使用Cache。

可能引發的問題:

鎖和分散式鎖

圖片描述(最多50字)

如果多個處理器同時對共享變數進行讀改寫操作 (i++就是經典的讀改寫操作),那麼共享變數就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的了,操作完之後共享變數的值會和期望的不一致。

造成此結果的原因:

多個處理器同時從各自的快取中讀取變數i,分別進行加1操作,然後分別寫入 系統記憶體中。

處理器層面的解決方案:

處理器使用匯流排鎖就是來解決這個問題的。所謂匯流排鎖就是使用處理器提供的一個 LOCK#訊號,當一個處理器在總線上輸出此訊號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔共享記憶體。

2.CAS(Compare And Swap)+volatile

CAS 操作包含三個運算元 —— 記憶體位置(V)、預期原值(A)和新值(B)。執行CAS操作的時候,將記憶體位置的值與預期原值比較,如果相匹配,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。

java的Atomic以及一些它自帶的類中的cas操作都是通過藉助cmpxchg指令完成的。他保證同一時刻只能有一個執行緒cas成功。

舉個例子

以AtomicIneger的原始碼為例來看看CAS操作:

鎖和分散式鎖

圖片描述(最多50字)

for(;;)表示迴圈,只有當if判斷為true才退出。而if判斷的內容就是是否CAS成功。

鎖和分散式鎖

圖片描述(最多50字)

鎖和分散式鎖

圖片描述(最多50字)

volatile的作用:

1)將當前處理器快取行的資料寫回到系統記憶體。

2)這個寫回記憶體的操作會使在其他CPU裡快取了該記憶體地址的資料無效。

迴圈CAS+volatile是實現鎖的關鍵。

Lock鎖的部分細節

鎖和分散式鎖

圖片描述(最多50字)

鎖和分散式鎖

圖片描述(最多50字)

不同場景鎖的表現不同:獨佔?共享?讀寫?

鎖和分散式鎖

圖片描述(最多50字)

分散式鎖(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的非原子性

鎖和分散式鎖

圖片描述(最多50字)

setnx剛執行成功,還未來得及執行expire指令,節點1 Duang的一聲掛掉了。

鎖和分散式鎖

圖片描述(最多50字)

這樣一來,這個鎖就長生不死了。

解決方案:

Redis 2.6.12以上版本為set指令增加了可選引數,虛擬碼如下:

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

鎖和分散式鎖

圖片描述(最多50字)

鎖和分散式鎖

圖片描述(最多50字)

鎖和分散式鎖

圖片描述(最多50字)

可以在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;
    }