Redis分布式鎖思考

來源:互聯網
上載者:User

一般的鎖只能針對單機下同一進程的多個線程,或單機的多個進程。多機情況下,對同一個資源訪問,需要對每台機器的訪問進程或線程加鎖,這便是分布式鎖。分布式鎖可以利用多機的共用快取(例如redis)實現。redis的命令文檔[1],實現及分析參考文檔[2]。

利用redis的get、setnx、getset、del四個命令可以實現基於redis的分布式鎖: get key:表示從redis中讀取一個key的value,如果key沒有對應的value,返回nil,如果儲存的值不是字串類型,返回錯誤 setnx key value:表示往redis中儲存一個索引值對,但只有當key不存在時才成功,返回1;否則失敗,返回0,不改變key的value getset key:將給定 key 的值設為 value ,並返回 key 的舊值(old value)。當舊值不存在時返回nil,當舊值不為字串類型,返回錯誤 del key:表示刪除key,當key不存在時不做操作,返回刪除的key數量

關於加鎖思考,迴圈中:
0、setnx的value是當前機器時間+預估運算時間作為鎖的失效時間。這是為了防止獲得鎖的線程掛掉而無法釋放鎖而導致死結。
0.1、返回1,證明已經獲得鎖,返回啦
0.2、返回0,獲得鎖失敗,需要檢查鎖逾時時間
1、get 擷取到鎖,利用失效時間判斷鎖是否失效。
1.1、取鎖逾時時間的時刻可能鎖被刪除釋放,此時並沒有拿到鎖,應該重新迴圈加鎖邏輯。
2、取鎖逾時時間成功
2.1、鎖沒有逾時,休眠一下,重新迴圈加鎖
2.2、鎖逾時,但此時不能直接釋放鎖刪除。因為此時可能多個線程都讀到該鎖逾時,如果直接刪除鎖,所有線程都可能刪除上一個刪除鎖又新上的鎖,會有多個線程進入臨界區,產生競爭狀態。
3、此時採用樂觀鎖的思想,用getset再次擷取鎖的舊逾時時間。
3.1、如果此時獲得鎖舊逾時時間成功
3.1.1、等於上一次獲得的鎖逾時時間,證明兩次操作過程中沒有別人動過這個鎖,此時已經獲得鎖
3.1.2、不等於上一次獲得的鎖逾時時間,說明有人先動過鎖,擷取鎖失敗。雖然修改了別人的到期時間,但因為衝突的線程相差時間極短,所以修改後的到期時間並無大礙。此處依賴所有機器的時間一致。
3.2、如果此時獲得鎖舊逾時時間失敗,證明當前線程是第一個在鎖失效後又加上鎖的線程,所以也獲得鎖
4、其他情況都沒有獲得鎖,迴圈setnx吧

關於解鎖的思考:
在鎖的時候,如果鎖住了,回傳逾時時間,作為解鎖時候的憑證,解鎖時傳入鎖的索引值和憑證。我思考的解鎖時候有兩種寫法:
1、解鎖前get一下索引值的value,判斷是不是和自己的憑證一樣。但這樣存在一些問題: get時返回nil的可能,此時表示有別的線程拿到鎖並用完釋放 get返回非nil,但是不等於自身憑證。由於有getset那一步,當兩個競爭線程都在這個過程中時,存在持有鎖的線程憑證不等於value,而是value是稍慢那一步線程設定的value。

2、解鎖前用憑證判斷鎖是否已經逾時,如果沒有逾時,直接刪除;如果逾時,等著鎖自動到期就好,免得誤刪別人的鎖。但這種寫法同樣存在問題,由於線程調度的不確定性,判斷到刪除之間可能過去很久,並不是絕對意義上的正確解鎖。

一個範例代碼

public class RedisLock {    private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);    //顯然jedis還需要自己配置來初始化    private Jedis jedis = new Jedis();    //預設鎖住15秒,儘力規避鎖時間太短導致的錯誤釋放    private static final long DEFAULT_LOCK_TIME = 15 * 1000;    //嘗試鎖住一個lock,設定嘗試鎖住的次數和逾時時間(毫秒),預設最短15秒    //成功時返回這把鎖的key,解鎖時需要憑藉鎖的lock和key    //失敗時返回Null 字元串    public String lock(String lock, int retryCount, long timeout) {        Preconditions.checkArgument(retryCount > 0 && timeout > 0, "retry count <= 0 or timeout <= 0 !");        Preconditions.checkArgument(retryCount < Integer.MAX_VALUE && timeout < Long.MAX_VALUE - DEFAULT_LOCK_TIME,                "retry count is too big or timeout is too big!");        String $lock = Preconditions.checkNotNull(lock) + "_redis_lock";        long $timeout = timeout + DEFAULT_LOCK_TIME;        String ret = null;        //重試一定次數,還是拿不到,就放棄        try {            long i, status;            for (i = 0, status = 0; status == 0 && i < retryCount; ++i) {                //嘗試加鎖,並設定逾時時間為當前機器時間+逾時時間                if ((status = jedis.setnx($lock, ret = Long.toString(System.currentTimeMillis() + $timeout))) == 0) {                    //擷取鎖失敗,查看鎖是否逾時                    String time = jedis.get($lock);                    //在加鎖和檢查之間,鎖被刪除了,嘗試重新加鎖                    if (time == null) {                        continue;                    }                    //鎖的逾時時間戳記小於目前時間,證明鎖已經逾時                    if (Long.parseLong(time) < System.currentTimeMillis()) {                        String oldTime = jedis.getSet($lock, Long.toString(System.currentTimeMillis() + $timeout));                        if (oldTime == null || oldTime.equals(time)) {                            //拿到鎖了,跳出迴圈                            break;                        }                    }                    try {                        TimeUnit.MILLISECONDS.sleep(1L);                    } catch (InterruptedException e) {                        logger.error("lock key:{} sleep failed!", lock);                    }                }            }            if (i == retryCount && status == 0) {                logger.info("lock key:{} failed!", lock);                return "";            }            //給鎖加上到期時間            jedis.pexpire($lock, $timeout);            logger.info("lock key:{} succsee!", lock);            return ret;        } catch (Exception e) {            logger.error("redis lock key:{} failed! cached exception: ", lock, e);            return "";        }    }    //釋放lock的鎖,需要傳入lock和key    //儘力確保刪除屬於自己的鎖,但是不保證做得到    public void releaseLock(String lock, String key) {        String $lock = Preconditions.checkNotNull(lock) + "_redis_lock";        Preconditions.checkNotNull(key);        try {            long timeout = Long.parseLong(key);            //鎖還沒有逾時,鎖還屬於自己可以直接刪除            //但由於線程啟動並執行不確定性,其實不能完全保證刪除時鎖還屬於自己            //真正執行刪除操作時,距離上語句判斷可能過了很久            if (timeout <= System.currentTimeMillis()) {                jedis.del($lock);                logger.info("release lock:{} with key:{} success!", lock, key);            } else {                logger.info("lock:{} with key:{} timeout! wait to expire", lock, key);            }        } catch (Exception e) {            logger.error("redis release {}  with key:{} failed! cached exception: ", lock, key, e);        }    }}

2、解鎖前用憑證判斷鎖是否已經逾時,如果沒有逾時,直接刪除;如果逾時,等著鎖自動到期就好,免得誤刪別人的鎖。但這種寫法同樣存在問題,由於線程調度的不確定性,判斷到刪除之間可能過去很久,並不是絕對意義上的正確解鎖。對於新版的redis,在set方法中通過兩個參數達到一條命令執行。在舊版的redis中使用pipeline的方式也能達到這個效果。

關於解鎖我只想到這麼多,希望有協助,歡迎拍磚多交流。

參考連結:
[1].http://doc.redisfans.com/
[2].http://blog.csdn.net/ugg/article/details/41894947

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.