redis分佈式鎖,面試官請隨便問,我都會

前言

如今的業務場景愈來愈複雜,使用的架構也就愈來愈複雜,分佈式、高併發已是業務要求的常態。像騰訊系的很多服務,還有CDN優化、異地多備份等處理。 說到分佈式,就必然涉及到分佈式鎖的概念,如何保證不一樣機器不一樣線程的分佈式鎖同步呢?程序員

實現要點

  1. 互斥性,同一時刻,智能有一個客戶端持有鎖。
  2. 防止死鎖發生,若是持有鎖的客戶端崩潰沒有主動釋放鎖,也要保證鎖能夠正常釋放及其餘客戶端能夠正常加鎖。
  3. 加鎖和釋放鎖必須是同一個客戶端。
  4. 容錯性,只有redis還有節點存活,就能夠進行正常的加鎖解鎖操做。

正確的redis分佈式鎖實現

錯誤加鎖方式

錯誤方式一

保證互斥和防止死鎖,首先想到的使用redis的setnx命令保證互斥,爲了防止死鎖,鎖須要設置一個超時時間。redis

public static void wrongLock(Jedis jedis, String key, String uniqueId, int expireTime) {
        Long result = jedis.setnx(key, uniqueId);
        if (1 == result) {
            //若是該redis實例崩潰,那就沒法設置過時時間了
            jedis.expire(key, expireTime);
        }
    }
複製代碼

在多線程併發環境下,任何非原子性的操做,均可能致使問題。這段代碼中,若是設置過時時間時,redis實例崩潰,就沒法設置過時時間。若是客戶端沒有正確的釋放鎖,那麼該鎖(永遠不會過時),就永遠不會被釋放。bash

錯誤方式二

比較容易想到的就是設置值和超時時間爲原子原子操做就能夠解決問題。那使用setnx命令,將value設置爲過時時間不就ok了嗎?服務器

public static boolean wrongLock(Jedis jedis, String key, int expireTime) {
        long expireTs = System.currentTimeMillis() + expireTime;
        // 鎖不存在,當前線程加鎖成果
        if (jedis.setnx(key, String.valueOf(expireTs)) == 1) {
            return true;
        }

        String value = jedis.get(key);
        //若是當前鎖存在,且鎖已過時
        if (value != null && NumberUtils.toLong(value) < System.currentTimeMillis()) {
            //鎖過時,設置新的過時時間
            String oldValue = jedis.getSet(key, String.valueOf(expireTs));
            if (oldValue != null && oldValue.equals(value)) {
                // 多線程併發下,只有一個線程會設置成功
                // 設置成功的這個線程,key的舊值必定和設置以前的key的值一致
                return true;
            }
        }
        // 其餘狀況,加鎖失敗
        return true;
    }
複製代碼

乍看之下,沒有什麼問題。但仔細分析,有以下問題:多線程

  1. value設置爲過時時間,就要求各個客戶端嚴格的時鐘同步,這就須要使用到同步時鐘。即便有同步時鐘,分佈式的服務器通常來講時間確定是存在少量偏差的。
  2. 鎖過時時,使用 jedis.getSet雖然能夠保證只有一個線程設置成功,可是不能保證加鎖和解鎖爲同一個客戶端,由於沒有標誌鎖是哪一個客戶端設置的嘛。

錯誤解鎖方式

解鎖錯誤方式一

直接刪除key架構

public static void wrongReleaseLock(Jedis jedis, String key) {
        //不是本身加鎖的key,也會被釋放
        jedis.del(key);
    }
複製代碼

簡單粗暴,直接解鎖,可是不是本身加鎖的,也會被刪除,這好像有點太隨意了吧!併發

解鎖錯誤方式二

判斷本身是否是鎖的持有者,若是是,則只有持有者才能夠釋放鎖。dom

public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) {
        if (uniqueId.equals(jedis.get(key))) {
            // 若是這時鎖過時自動釋放,又被其餘線程加鎖,該線程就會釋放不屬於本身的鎖
            jedis.del(key);
        }
    }
複製代碼

看起來很完美啊,可是若是你判斷的時候鎖是本身持有的,這時鎖超時自動釋放了。而後又被其餘客戶端從新上鎖,而後當前線程執行到jedis.del(key),這樣這個線程不就刪除了其餘線程上的鎖嘛,好像有點亂套了哦!分佈式

正確加鎖釋放鎖方式

基本上避免了以上幾種錯誤方式以外,就是正確的方式了。要知足如下幾個條件:高併發

  1. 命令必須保證互斥
  2. 設置的key必需要有過時時間,防止崩潰時鎖沒法釋放
  3. value使用惟一id標誌每一個客戶端,保證只有鎖的持有者才能夠釋放鎖

加鎖直接使用set命令同時設置惟一id和過時時間;其中解鎖稍微複雜些,加鎖以後能夠返回惟一id,標誌此鎖是該客戶端鎖擁有;釋放鎖時要先判斷擁有者是不是本身,而後刪除,這個須要redis的lua腳本保證兩個命令的原子性執行。 下面是具體的加鎖和釋放鎖的代碼:

@Slf4j
public class RedisDistributedLock {
    private static final String LOCK_SUCCESS = "OK";
    private static final Long RELEASE_SUCCESS = 1L;
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    // 鎖的超時時間
    private static int EXPIRE_TIME = 5 * 1000;
    // 鎖等待時間
    private static int WAIT_TIME = 1 * 1000;

    private Jedis jedis;
    private String key;

    public RedisDistributedLock(Jedis jedis, String key) {
        this.jedis = jedis;
        this.key = key;
    }

    // 不斷嘗試加鎖
    public String lock() {
        try {
            // 超過等待時間,加鎖失敗
            long waitEnd = System.currentTimeMillis() + WAIT_TIME;
            String value = UUID.randomUUID().toString();
            while (System.currentTimeMillis() < waitEnd) {
                String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);
                if (LOCK_SUCCESS.equals(result)) {
                    return value;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (Exception ex) {
            log.error("lock error", ex);
        }
        return null;
    }

    public boolean release(String value) {
        if (value == null) {
            return false;
        }
        // 判斷key存在而且刪除key必須是一個原子操做
        // 且誰擁有鎖,誰釋放
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = new Object();
        try {
            result = jedis.eval(script, Collections.singletonList(key),
                    Collections.singletonList(value));
            if (RELEASE_SUCCESS.equals(result)) {
                log.info("release lock success, value:{}", value);
                return true;
            }
        } catch (Exception e) {
            log.error("release lock error", e);
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        log.info("release lock failed, value:{}, result:{}", value, result);
        return false;
    }
}
複製代碼

單是一個redis的分佈式鎖就有這麼多道道,不知道你是否看明白了?留言討論下吧!

程序員的小夥伴們,以爲本身孤單麼,那就加入公衆號[程序員之道],一塊兒交流溝通,走出咱們的程序員之道!

掃碼加入吧!
相關文章
相關標籤/搜索