基於緩存或zookeeper的分佈式鎖實現

緩存鎖

 咱們經常將緩存做爲分佈式鎖的解決方案,可是卻不能單純的判斷某個 key 是否存在 來做爲鎖的得到依據,由於不管是 exists 和 get 命名都不是線程安全的,都沒法保證只有一個線程能夠得到鎖,存在線程爭搶,可能會有多個線程同時拿到鎖的狀況(經典的 Redis 「讀後寫」的問題)。html

incr 緩存鎖

@Component
public class LockClient {

    private StringRedisTemplate stringRedisTemplate;

    private ValueOperations<String, String> valueOperations;

    @Autowired
    public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.valueOperations = stringRedisTemplate.opsForValue();
    }

    public void lockIncr() {
        Long lockIncr = valueOperations.increment("lockIncr", 1);
        // 說明拿到了鎖
        if (lockIncr == 1) {
            // 業務操做
        }
    }
}
  1. incr:遞增指定鍵對應的數值,若是不存在 key 對應的值,那麼會先將 key 的值設置爲 0,而後執行 incr 操做,返回遞增的值。
  2. 這種鎖的實現原理主要是利用 incr 命令的原子性,同一時間只會有一個線程操做這個命令。
  3. 這種鎖的實現方式,不在意結果數據。保證只有惟一線程可以執行到業務代碼。

setnx 緩存鎖

 上面的鎖實現方式,咱們對資源作了隔離,保證只有惟一線程能夠拿到資源並執行操做。可是若是資源並非惟一線程執行的呢?存在多個線程爭搶的狀況下呢?node

public void lockSetnx() {
        String lock = "lockSetnx";
        long millis = System.currentTimeMillis();
        long timeout = millis + 3000L + 1;
        try {
            while (true) {
                boolean setnx = valueOperations.setIfAbsent(lock, timeout + "");
                if (setnx == true) {
                    break;
                }
                String oldTimeout = valueOperations.get(lock);
                // 這一步是爲了解決客戶端異常宕機,鎖沒有被正常釋放的時候。
                // 當 p一、p2 同時執行到這裏,發現鎖的時間過時了。p一、p2 同時執行 getSet 命令。
                // 假設 p1 先執行成功了,那麼 p1 獲得的值就是原來鎖的過時時間(能夠符合下面的判斷式),表示爭搶鎖成功。
                // 假設 p2 後執行成功了,那麼 p2 獲得的值就是 p1 set 進去的值(不會符合下面的表達式),表示爭搶鎖失敗。
                String oldValue = valueOperations.getAndSet(lock, timeout + "");
                if (millis > Long.valueOf(oldTimeout) && millis > Long.valueOf(oldValue)) {
                    break;
                }
                // 休眠 100 毫秒,再去爭搶鎖
                Thread.sleep(100);
            }

            // 執行業務代碼
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (millis < timeout) {
                stringRedisTemplate.delete(lock);
            }
        }

    }
  1. setnx:只有第一個線程會執行成功,返回 true,其他線程執行失敗,返回 false。
  2. getSet:返回 key 中的舊值,並把新的值 set 進去。
  3. 細細看來,好像彷佛 setnx 命令就可以實現分佈式鎖了,爲何還要 getSet 命名呢?getSet 命令是爲了解決客戶端異常宕機,鎖沒有被正常釋放的狀況下,結合過時時間來保證線程安全。能夠看看官網的介紹,有詳細解釋這個問題。

zookeeper 鎖

zookeeper,天生的分佈式協調工具,生來就是爲了解決各類分佈式的難題,好比分佈式鎖、分佈式計數器、分佈式隊列等等。
zookeeper 分佈式鎖,若是本身實現的話,大抵的實現方式以下:redis

公平鎖:

  • 在 zookeeper 的指定節點(locks)下建立臨時順序節點 node_n ;
  • 獲取 locks 下面的全部子節點 children。
  • 對子節點按節點自增序號從小到大排序。
  • 判斷本節點是否是第一個子節點,若是是,則獲取到鎖。若是不是,則監聽比該節點小的那個節點的刪除事件。
  • 若監聽事件生效,則回到第二步從新判斷,直到獲取到鎖。

不公平鎖

  • 在 zookeeper 的某個節點(lock)上建立臨時節點 znode。
  • 建立成功,就表示獲取到了這個鎖;其餘客戶端來建立鎖會失敗,只能註冊對這個鎖的監聽。
  • 其餘客戶端監聽到這個鎖被釋放(znode節點被刪除),就會嘗試加鎖(建立節點),繼續執行第二步。

幸運的是,zookeeper recipes 客戶端爲咱們提供了多種分佈式鎖實現:緩存

  • InterProcessMutex(可重入排他鎖)
  • InterProcessSemaphoreMutex(不可重入排他鎖)
  • InterProcessReadWriteLock(分佈式讀寫鎖)
  • InterProcessSemaphore(共享信號量 —— 設置最大並行數量)

zookeeper recipes 鎖的簡單使用:安全

public InterProcessMutex interProcessMutex(String lockPath) {
        CuratorFramework client = CuratorFrameworkFactory.newClient(zookeeper, new ExponentialBackoffRetry(1000, 3));
        // 啓用命名空間,作微服務間隔離
        client.usingNamespace(namespace);
        client.start();
        return new InterProcessMutex(client, lockPath);
    }
public void lockUse() {
        InterProcessMutex interProcessMutex = interProcessMutex("/lockpath");
        try {
            // 獲取鎖
            if (interProcessMutex.acquire(100, TimeUnit.MILLISECONDS)) {
                // 執行業務代碼
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 釋放鎖
            try {
                interProcessMutex.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

比較

  • 緩存分佈式鎖,必須採用輪詢的方式去嘗試加鎖,對性能浪費很大;zookeeper 分佈式鎖,能夠經過監聽的方式等待通知或超時,當有鎖釋放,通知使用者便可。
  • 若是緩存獲取鎖的那個客戶端宕機了,鎖不會被釋放,只能經過其它方式解決(上面的 getSet 判斷);而 zookeeper 的話,由於建立的是臨時 znode,只要客戶端掛了,znode 就沒了,此時就自動釋放鎖。
相關文章
相關標籤/搜索