文章首發於公衆號:松花皮蛋的黑板報
做者就任於京東,在穩定性保障、敏捷開發、高級JAVA、微服務架構有深刻的理解java
通常狀況下咱們會經過下面的方法進行資源的一致性保護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
咱們可能會想到經過令牌或者叫版本號的方式,然而在使用Redis做爲鎖服務時並不能解決上述的問題。無論咱們怎麼修改Redlock生成token的算法,使用unique random隨機數是不安全的,使用引用計數也是不安全的,一個redis node服務可能會出宕機,多個redis node服務可能會出現同步異常(go out of sync)。Redlock鎖會失效的根本緣由是Redis使用getimeofday做爲key緩存失效時間而不是監視器(monitonic lock),服務器的時鐘出現異常回退沒法百分百避免,ntp分佈式時間服務也是個難點算法
分佈式鎖實現須要考慮鎖的排它性和不能釋放它人的鎖,做者不推薦使用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高級、微服務架構架構