1. 程式人生 > 其它 >分散式鎖的實現(redis)

分散式鎖的實現(redis)

1、單機鎖

考慮在併發場景並且存在競態的狀況下,我們就要實現同步機制了,最簡單的同步機制就是加鎖。

加鎖可以幫我們鎖住資源,如記憶體中的變數,或者鎖住臨界區(執行緒中的一段程式碼),使得同一個時刻只有一個執行緒能訪問某一個區域。

如果是單例項(單程序部署),那麼單機鎖就可以滿足我們的要求了,如synchronized,ReentrantLock。

因為在一個程序中的不同執行緒可以共享這個鎖。

2、分散式鎖

但是如果場景來到了分散式系統呢?

分散式系統部署在不同的機器上,或者只是簡單的多程序部署。這樣各個程序之間無法共享同一個鎖。

這時候我們要加分散式鎖。

分散式鎖大概就是這麼一個東西:通過共享的儲存快取一個狀態值,用狀態值的變化標識鎖的佔用和釋放。

可以通過mysql,redis,zk等實現分散式鎖,這裡我們實現一個redis的。如果你用java其實使用zk會很簡單。

3、為什麼redis能用來實現分散式鎖?

1)Redis是單程序單執行緒模式

redis實現為單程序單執行緒模式,這樣多個客戶端並不存在競態關係。

2)原子性原語

redis提供了可以實現原子操作的原語如setnx、getset等。

setnx

1)SETNX key value

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

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

SETNX 是『SET if Not eXists』(如果不存在,則 SET)的簡寫。

可用版本:
>= 1.0.0
時間複雜度:
O(1)
返回值:
設定成功,返回 1 。
設定失敗,返回 0 。

getset

GETSET key value

將給定 key 的值設為 value ,並返回 key 的舊值(old value)。

當 key 存在但不是字串型別時,返回一個錯誤。

可用版本:
>= 1.0.0
時間複雜度:
O(1)
返回值:
返回給定 key 的舊值。
當 key 沒有舊值時,也即是, key 不存在時,返回 nil 。

4、實現

package com.xiaoju.dqa.fusor.utils;

import com.xiaoju.dqa.fusor.client.RedisClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


@Component
public class DistributeLockUtil {

    // 鎖超時時間, 防止死鎖
    private static final long LOCK_TIMEOUT = 60;

    @Autowired
    private RedisClient redisClient;

    private boolean locked = false;

    public boolean lock(String key) {
        String expireTime = String.valueOf(System.currentTimeMillis() + LOCK_TIMEOUT * 1000);
        /*
        *   setnx 返回1
        *   說明: 1)key不存在, 2)成功寫入鎖, 並更新鎖的生存時間
        *   也就是get鎖
        * */
        if (redisClient.setnx(key, expireTime) == 1) {
            locked = true;
            return true;
        }
        /*
        *  沒有get鎖, 下面進入判斷鎖超時邏輯
        * */
        String currentExpireTime = redisClient.get(key);
        /*
        *   鎖生存時間已經過了, 說明鎖已經超時
        * */
        if (Long.parseLong(currentExpireTime) < System.currentTimeMillis()) {
            String oldValueStr = redisClient.getSet(key, expireTime);
            /*
            *   判斷鎖生存時間和你改的寫那個時間是否相等
            *   相當於你競爭了一個更新鎖
            * */
            if (oldValueStr.equals(currentExpireTime)) {
                locked = true;
                return true;
            }
        }
        return false;
    }

    public void release(String key) {
        if (locked) {
            redisClient.del(key);
            locked = false;
        }
    }

}

5、死鎖

為了解決死鎖,這裡設定了鎖的超時時間。

    private static final long LOCK_TIMEOUT = 60;

並通過setnx時更新鎖生存時間來維護鎖超時的判定。

String expireTime = String.valueOf(System.currentTimeMillis() + LOCK_TIMEOUT * 1000);
...
if (redisClient.setnx(key, expireTime) == 1) {
...
}
...
String oldValueStr = redisClient.getSet(key, expireTime);
...

為什麼要使用這種方式,而不是expire呢?

因為setnx和expire不能作為一個原子性的操作存在,設想如果setnx之後,在執行expire之前出現了異常,那麼鎖將沒有超時時間。也就是死鎖。

6、解決鎖超時引入的競態

設想三個客戶端,C0,C1,C2

如果C0持有鎖並且崩潰,鎖沒有釋放。

C1和C2同時發現了鎖超時。

然後都通過getset去拿到了舊值,在對比了舊值和之前值之後,如果相等,那麼說明“我”成功修改了舊值,那麼我就拿到了鎖。

7、 時鐘同步

我們看到foo.lock的value值為時間戳,所以要在多客戶端情況下,保證鎖有效,一定要同步各伺服器的時間,如果各伺服器間,時間有差異。時間不一致的客戶端,在判斷鎖超時,就會出現偏差,從而產生競爭條件。 鎖的超時與否,嚴格依賴時間戳,時間戳本身也是有精度限制,假如我們的時間精度為秒,從加鎖到執行操作再到解鎖,一般操作肯定都能在一秒內完成。這樣的話,我們上面的CASE,就很容易出現。所以,最好把時間精度提升到毫秒級。這樣的話,可以保證毫秒級別的鎖是安全的。

8、一些處理不了的情況

設想三個客戶端,C0,C1,C2

如果C0持有鎖很長,鎖已經超時。這時候有C1,C2判斷鎖超時了,然後通過超時競爭,C1拿到了鎖。

這時C0醒了過來,刪除了C1的鎖。

這時,C1認為自己獨佔了鎖,其他的程序也進入了競爭鎖的情況

對於這種情況,這裡是沒有提供解決辦法的。

思路是:你降級你的鎖,比如給你的鎖加上uuid,對不同的業務或者不同的session加上對應粒度的鎖。

可以看看這篇部落格。

http://www.cnblogs.com/kangoroo/p/6953187.html