通俗易懂地介紹分佈式鎖實現

文章首發於公衆號:松花皮蛋的黑板報
做者就任於京東,在穩定性保障、敏捷開發、高級JAVA、微服務架構有深刻的理解java

clipboard.png

通常狀況下咱們會經過下面的方法進行資源的一致性保護node

// THIS CODE IS BROKEN
function writeData(filename, data) {
    var lock = lockService.acquireLock(filename);
    if (!lock) {
        throw 'Failed to acquire lock';
    }

    try {
        var file = storage.readFile(filename);
        var updated = updateContents(file, data);
        storage.writeFile(filename, updated);
    } finally {
        lock.release();
    }
}

可是很遺憾的是,上面這段代碼是不安全的,好比客戶端client-1獲取鎖後因爲執行垃圾回收GC致使一段時間的停頓(stop-the-word GC pause)或者其餘長時間阻塞操做,此時鎖過時了,其餘客戶如client-2會得到鎖,當client-1恢復後就會出現client-1client-2同時處理得到鎖的狀態redis

clipboard.png

咱們可能會想到經過令牌或者叫版本號的方式,然而在使用Redis做爲鎖服務時並不能解決上述的問題。無論咱們怎麼修改Redlock生成token的算法,使用unique random隨機數是不安全的,使用引用計數也是不安全的,一個redis node服務可能會出宕機,多個redis node服務可能會出現同步異常(go out of sync)。Redlock鎖會失效的根本緣由是Redis使用getimeofday做爲key緩存失效時間而不是監視器(monitonic lock),服務器的時鐘出現異常回退沒法百分百避免,ntp分佈式時間服務也是個難點算法

clipboard.png

分佈式鎖實現須要考慮鎖的排它性和不能釋放它人的鎖,做者不推薦使用Redlock算法,推薦使用zookeeper或者數據庫事務(我的不推薦:for update性能太差了)數據庫

補充:使用zookeeper實現分佈式鎖緩存

能夠經過客戶端嘗試建立節點路徑,成功就得到鎖,可是性能較差。更好的方式是利用zookeeper有序臨時節點,最小序列得到鎖,其餘節點lock時須要阻塞等待前一個節點(比自身序列小的最大那個)釋放鎖(countDownLatch.wait()),當觸發watch事件時將計數器減一(countDownLatch.countDown()),而後此時最小序列節點將會得到鎖。能夠利用Curator簡化操做,示例以下安全

public static void main(String[] args) throws Exception {
            //重試策略
            RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

            //建立工廠鏈接
            final CuratorFramework curatorFramework = CuratorFrameworkFactory.builder().connectString(connetString)
                    .sessionTimeoutMs(sessionTimeOut).retryPolicy(retryPolicy).build();

            curatorFramework.start();

            //建立分佈式可重入排他鎖,監聽客戶端爲curatorFramework,鎖的根節點爲/locks
            final InterProcessMutex mutex = new InterProcessMutex(curatorFramework, "/lock");
            final CountDownLatch countDownLatch = new CountDownLatch(1);

            for (int i = 0; i < 100; i++) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            countDownLatch.await();
                            //加鎖
                            mutex.acquire();
                            process();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }finally {
                            try {
                                //釋放鎖
                                mutex.release();
                                System.out.println(Thread.currentThread().getName() + ": release lock");
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                },"Thread" + i).start();
            }

            Thread.sleep(100);
            countDownLatch.countDown();

        }
    }

補充:redis實現分佈式鎖服務器

public enum  FreeLockUtil {
    instance;

    public static FreeLockUtil getInstance()
    {
        return instance;
    }

    @Autowired
    @Qualifier("jimClient")
    private Cluster jimClient;

    @Autowired
    private TdeUtil tdeUtil;

    private String scriptHash;

    @PostConstruct
    public void init() {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        scriptHash = jimClient.scriptLoad(script);
    }

    /** 
    * @Description: 沒有得到鎖時會返回空 
    * @Param: [key] 
    * @return: java.lang.String 
    * @Author: Pidan
    */
    public String lock(String lockKey)
    {
        String token = tdeUtil.random();
        //不要將set和expire分開
        Boolean lockRes = jimClient.set(lockKey, token, 1L,TimeUnit.MINUTES, false);
        return lockRes?token:null;
    }
    /** 
    * @Description: 相似CAS版本號
    * @Param: [key, value] 
    * @return: void 
    * @Author: Pidan
    */
    public void unlock(String lockKey,String token)
    {
        //不要在客戶端使用get-if-equals-del
      jimClient.evalsha(scriptHash, Collections.singletonList(lockKey),Collections.singletonList(token),true);
    }
}

無論是基於Redis或者是Zookeeper實現分佈式鎖都有各點的優缺點,Redis的高併發是Zookeeper沒法比擬的,可是Redis緩存的內存大小若是不足的話極有可能會致使信息丟失,反觀使用Zookeeper實現分佈式鎖,會致使性能開銷比較高,由於須要動態建立刪除臨時節點,頻繁操做磁盤讀寫,不過它的可靠性更高session

文章來源:www.liangsonghua.me
做者介紹:京東資深工程師-梁鬆華,長期關注穩定性保障、敏捷開發、JAVA高級、微服務架構架構

clipboard.png

相關文章
相關標籤/搜索