細說分佈式鎖實現

I. 分佈式鎖

分佈式鎖的主要目的是:在分佈式系統多進程下,保證只有一個進程能夠執行。

參考單機的鎖特點來說,我們實現的分佈式鎖需要擁有以下特性:

  • 互斥性
  • 不發生死鎖
  • 正確釋放鎖
  • 高性能
  • 高可用

最好還能支持:

  • 可重入
  • 阻塞獲取/超時獲取/中斷獲取
  • 公平/非公平

II. Redis分佈式鎖

Redis獲取分佈式鎖主要是靠 setnx 命令來實現的,該命令表示當 key 存在時,set 失敗,返回爲0,key 不存在 set 成功返回 1。

單機Redis分佈式鎖實現

分佈式鎖的主要關注點在於獲取鎖、釋放鎖以及避免死鎖,在實現一個鎖工具支持的功能時可以考慮的文章開始的8個點。Redis分佈式鎖的實現,其中獲取鎖主要依靠 setnx 命令,釋放鎖刪除對應的 key 即可,避免死鎖則依靠 key 的自動失效來保證。

目前看到其他的文章主要的實現方式以下幾種:

分佈式鎖看這篇就夠了
 
獲取鎖:當一個進程前來請求分佈式鎖,首先通過 setnx 來嘗試獲取鎖,如果獲取鎖成功,OK,設置好過期時間,可以返回去幹自己事情了。如果 setnx 沒獲取成功,事情麻煩了,說明肯定有人拿到鎖了。這個時候就要看看拿鎖的進程是不是該讓讓位了——比較當前時間和鎖裏設置的過期時間(通過 get 查看),如果沒到過期時間,好吧,自己就只好放棄或者繼續等待了。如果發現當前時間已經超過過期時間了,不好意思,自己獲取鎖咯,利用 getset 設置一個新的過期時間,拿出舊的過期時間。看看舊的過期時間是不是和上一次看到的一樣,臥槽,竟然不一樣說明被哪個進程先下手給改了新的過期時間,那自己又沒獲取鎖成功。
釋放鎖:直接刪除key

這種實現思路乍一看沒問題,其實問題很多,首先沒有正確的釋放鎖。比如:當A進程獲取到鎖,由於某些原因執行時間較久,到了鎖過期時間自動釋放鎖。這時,B進程過來獲取鎖,也執行一段代碼,正在執行着,A進程釋放鎖(直接刪除key),結果把B的鎖反而給釋放了。這出大事情了!!C進程可以獲取到鎖了…

這種方式的改進在於釋放鎖時,不要直接釋放,而是判斷只有 key 未過期才進行釋放。這樣A就不會刪除B設置的 key 了。也就是說,只有未超時就結束業務的進程才能釋放鎖,鎖的互斥性簡介保證了正確的釋放鎖——解鈴還須繫鈴人。但還是沒有完全解決問題,因爲可能剛判斷完key未過期,然後當前線程停頓了一會,在這過程中發生了鎖到了失效時間被其他進程獲取,然後原來線程釋放鎖,GG,還是出現之前的莫名其妙釋放鎖問題。

其次,該方案利用時間戳,決定了該方法強依賴分佈式系統的時間同步。如果不同步,必然會給算法帶來影響。

同時,getset 是一個原子操作,但是每一個嘗試獲取鎖的進程都會進行 set,也就是說這一步其實是會發生覆蓋操作的。比如AB兩進程同時獲取鎖,A getset 成功獲取鎖,B也進行getset 獲取,B會把新的過期時間設置進去,A的過期時間返回出來。但B獲取鎖是失敗的,卻莫名其妙更新的A的過期時間,這是不合理的。

另一種極爲常見的實現方案:

setnxexpire 兩步操作非原子、釋放鎖檢查和釋放動作兩步操作也非原子
 
首先我們分析 setnxexpire 兩步操作非原子帶來的影響,如果先 setnx 成功,如果此時因爲某種原因,當前進程掛了,則無法 expire,最終導致死鎖。這其實問題還是不可忽略的。
其次釋放鎖時講究解鈴還須繫鈴人,那麼就需要進行鎖的檢查,是否是自己的。當鎖檢查完畢時自己加上的,那麼便可以釋放鎖。這兩步動作如果不是原子,出現了胡亂釋放鎖的問題前文已經討論過。

那麼考慮到以上細節,這裏實現一個單機版的Redis鎖實現。

@Slf4j
public class RedisLock {

    private RedisTemplate<String, Serializable> redisTemplate;
    private Integer sleepInterval;

    public RedisLock(RedisTemplate<String, Serializable> redisTemplate) {
        this(redisTemplate, 5);
    }

    public RedisLock(RedisTemplate<String, Serializable> redisTemplate, Integer sleepInterval) {
        this.redisTemplate = redisTemplate;
        this.sleepInterval = sleepInterval;
    }

    /**
     * 阻塞獲取鎖
     */
    public void lock(String key, String lockId, long expireTime, TimeUnit timeUnit) {
        for (;;) {
            if (tryLock(key, lockId, expireTime, timeUnit)) {
                return;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(sleepInterval);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 快速嘗試獲取鎖
     * set nx ex 整體原子性
     * @param key 鎖key
     * @param lockId 鎖value(區分鎖的id號)
     * @param expireTime 鎖的過期時間
     * @param timeUnit 時間單位
     * @return
     */
    public boolean tryLock(String key, String lockId, long expireTime, TimeUnit timeUnit) {
        Boolean res = redisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(key.getBytes(), lockId.getBytes(), Expiration.from(expireTime, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));
        if (Boolean.TRUE.equals(res)) {
            log.info("Thread {} get lock success, key:{} lockId:{} expire:{} timeUnit:{}.", Thread.currentThread(), key, lockId, expireTime, timeUnit);
            return true;
        }
        return false;
    }

    /**
     * 超時獲取鎖
     * @param key 鎖key
     * @param lockId 鎖value(區分鎖的id號)
     * @param expireTime 鎖的過期時間
     * @param time 獲取鎖超時時間
     * @param unit 時間單位
     * @return
     * @throws InterruptedException
     */
    public boolean tryLock(String key, String lockId, long expireTime, long time, TimeUnit unit) {
        long timeout = System.currentTimeMillis() + unit.toMillis(time);
        while (System.currentTimeMillis() <= timeout) {
            if (tryLock(key, lockId, expireTime, unit)) {
                return true;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(sleepInterval);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    /**
     * 釋放鎖
     * 如果鎖還未釋放,則釋放自己的鎖(原子操作,且保證瞭解鈴還須繫鈴人)
     * @param key 鎖的key
     * @param lockId 鎖id
     * @return
     */
    public boolean unlock(String key, String lockId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Long res = redisTemplate.execute((RedisCallback<Long>) connection -> connection.eval(script.getBytes(), ReturnType.INTEGER, 1, key.getBytes(), lockId.getBytes()));
        if (Long.valueOf(1).equals(res)) {
            log.info("Thread {} release lock success, key:{} lockId:{}.", Thread.currentThread(), key, lockId);
            return true;
        }
        return false;
    }

    /**
     * 生成鎖的id
     * @return
     */
    public static String generateLockId() {
        return UUID.randomUUID().toString() + "-" + Thread.currentThread().getId();
    }
}

測試實現的Redis分佈式鎖組件:

@Controller
public class HelloController {
    @Autowired
    private RedisTemplate<String, Serializable> redisTemplate;

    private int i = 0;

    @RequestMapping("/hello")
    @ResponseBody
    public String hello() {
        RedisLock lock = new RedisLock(redisTemplate);
        String key = "hello";
        String lockId = RedisLock.generateLockId();
        try {
            lock.lock(key, lockId, 2, TimeUnit.SECONDS);
            i++;
            System.out.println("do something......" + i);
        } finally {
            lock.unlock(key, lockId);
        }
        return "hello world!";
    }

    @RequestMapping("/hello2")
    @ResponseBody
    public String hello2() {
        i++;
        System.out.println("do something......" + i);
        return "hello world!";
    }
}

其中 /hello 爲加鎖同步方法,/hello2 不加鎖。加鎖同步的 i 是有序的遞增的,而未加鎖是亂序的,讀者可以自行嘗試。

RedLock與Redisson

上面實現的單機Redis分佈式鎖,基本滿足了互斥性、不發生死鎖、正確釋放鎖。高性能還算勉強,至於高可用,如果單機Redis掛了,那麼分佈式鎖服務就直接沒用了。

現有保證Redis分佈式鎖的高可用方案,有RedLock算法,其Java實現是Redisson裏的分佈式鎖。

關於RedLock的正確與否,Redis作者和一位分佈式系統專家有過相關的討論:

裏面比較關注的一個細節可能你早就注意到了,爲了防止死鎖的發生,我們給鎖設置了自動釋放時間。那麼就會擔心,如果A進程在拿到鎖之後執行了很長一段時間的業務代碼,超過了租期,鎖自動釋放了,那麼其他進程就可以拿到鎖繼續執行,這就會引來併發問題。沒錯,這個問題是無法解決的。

瞭解RedLock,推薦官方文檔 Distributed locks with Redis

關於Redisson的分佈式鎖,參考Distributed-locks-and-synchronizers

III. ZooKeeper分佈式鎖

ZooKeeper實現分佈式鎖主要依靠ZK上創建臨時唯一節點實現,多個進程競爭在ZK上創建一個臨時節點,ZK保證只有一個節點創建成功,創建成功的進程相當於成功獲取鎖,其他未獲取鎖的進程則監聽該臨時節點的刪除事件(Watcher),重新獲取鎖。

目前關於ZK實現分佈式鎖原理的文章大多相似,目前主要實現思路如下:

七張圖徹底講清楚ZooKeeper分佈式鎖的實現原理
 
文中的方案即是避免“驚羣效應”的ZK分佈式鎖方案,但最終實現的效果是一個公平鎖,大家排隊獲取鎖,取鎖順序和臨時節點號順序有關。
其他一些文章自己動手實現的分佈式鎖,要麼質量無法保證,沒有生產環境的驗證,或者只是個demo;又或者功能不夠完善,沒有達到本文開頭所說的那幾個要求。

本文着重主動分析已經實現好基於ZK的分佈式鎖Curator框架源碼,來仔細看看其算法實現。

public class InterProcessMutex implements InterProcessLock, Revocable<InterProcessMutex> {
    private final LockInternals internals;
    private final String basePath;
    private final ConcurrentMap<Thread, InterProcessMutex.LockData> threadData;
    private static final String LOCK_NAME = "lock-";

    public InterProcessMutex(CuratorFramework client, String path) {
        this(client, path, new StandardLockInternalsDriver());
    }

    public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver) {
        this(client, path, "lock-", 1, driver);
    }

	InterProcessMutex(CuratorFramework client, String path, String lockName, int maxLeases, LockInternalsDriver driver) {
        this.threadData = Maps.newConcurrentMap();
        this.basePath = PathUtils.validatePath(path);
        this.internals = new LockInternals(client, driver, path, lockName, maxLeases);
    }

	/**
	 * 阻塞獲取鎖,獲取成功/失去連接 纔會退出方法
	 */ 
    public void acquire() throws Exception {
        if (!this.internalLock(-1L, (TimeUnit)null)) {
            throw new IOException("Lost connection while trying to acquire lock: " + this.basePath);
        }
    }

	/**
	 * 超時獲取鎖,獲取成功/超時 纔會退出方法
	 */
    public boolean acquire(long time, TimeUnit unit) throws Exception {
        return this.internalLock(time, unit);
    }

    public boolean isAcquiredInThisProcess() {
        return this.threadData.size() > 0;
    }

	/**
	 * 釋放鎖
	 */	
    public void release() throws Exception {
        Thread currentThread = Thread.currentThread();
        InterProcessMutex.LockData lockData = (InterProcessMutex.LockData)this.threadData.get(currentThread);
        if (lockData == null) {
            throw new IllegalMonitorStateException("You do not own the lock: " + this.basePath);
        } else {
            int newLockCount = lockData.lockCount.decrementAndGet();
            if (newLockCount <= 0) {
                if (newLockCount < 0) {
                    throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + this.basePath);
                } else {
                	// 真正釋放鎖
                    try {
                        this.internals.releaseLock(lockData.lockPath);
                    } finally {
                        this.threadData.remove(currentThread);
                    }

                }
            }
        }
    }

    public Collection<String> getParticipantNodes() throws Exception {
        return LockInternals.getParticipantNodes(this.internals.getClient(), this.basePath, this.internals.getLockName(), this.internals.getDriver());
    }

    protected byte[] getLockNodeBytes() {
        return null;
    }

    protected String getLockPath() {
        InterProcessMutex.LockData lockData = (InterProcessMutex.LockData)this.threadData.get(Thread.currentThread());
        return lockData != null ? lockData.lockPath : null;
    }

	/**
	 * 指定時間內獲取鎖
	 */
    private boolean internalLock(long time, TimeUnit unit) throws Exception {
    	// 獲取當前線程,用於支持可重入
        Thread currentThread = Thread.currentThread();
        InterProcessMutex.LockData lockData = (InterProcessMutex.LockData)this.threadData.get(currentThread);
        if (lockData != null) {
        	// 鎖計數器+1 (對於當前線程來說應線程安全,是否可以不要原子類,普通i++也可以)
            lockData.lockCount.incrementAndGet();
            return true;
        } else {
        	// 阻塞調用attemptLock方法獲取鎖,獲取成功則返回zk節點名稱
            String lockPath = this.internals.attemptLock(time, unit, this.getLockNodeBytes());
            if (lockPath != null) {
                InterProcessMutex.LockData newLockData = new InterProcessMutex.LockData(currentThread, lockPath, null);
                this.threadData.put(currentThread, newLockData);
                return true;
            } else {
                return false;
            }
        }
    }

    private static class LockData {
        final Thread owningThread;
        final String lockPath;
        // 鎖計數器
        final AtomicInteger lockCount;

        private LockData(Thread owningThread, String lockPath) {
            this.lockCount = new AtomicInteger(1);
            this.owningThread = owningThread;
            this.lockPath = lockPath;
        }
    }
}

進一步進入 LockInternals 查看:

public class LockInternals {
    private final WatcherRemoveCuratorFramework client;
 	// path=/basepath/lockname前綴+順序號
    private final String path;
    private final String basePath;
    private final LockInternalsDriver driver;
    private final String lockName;
    private final AtomicReference<RevocationSpec> revocable = new AtomicReference((Object)null);
    private final CuratorWatcher revocableWatcher = new CuratorWatcher() {
        public void process(WatchedEvent event) throws Exception {
            if (event.getType() == EventType.NodeDataChanged) {
                LockInternals.this.checkRevocableWatcher(event.getPath());
            }

        }
    };
    private final Watcher watcher = new Watcher() {
        public void process(WatchedEvent event) {
        	// 喚醒所有的線程
            LockInternals.this.notifyFromWatcher();
        }
    };
    private volatile int maxLeases;
    static final byte[] REVOKE_MESSAGE = "__REVOKE__".getBytes();

    public void clean() throws Exception {
        try {
            this.client.delete().forPath(this.basePath);
        } catch (BadVersionException var2) {
            ;
        } catch (NotEmptyException var3) {
            ;
        }

    }

    LockInternals(CuratorFramework client, LockInternalsDriver driver, String path, String lockName, int maxLeases) {
        this.driver = driver;
        this.lockName = lockName;
        this.maxLeases = maxLeases;
        this.client = client.newWatcherRemoveCuratorFramework();
        this.basePath = PathUtils.validatePath(path);
        this.path = ZKPaths.makePath(path, lockName);
    }

    synchronized void setMaxLeases(int maxLeases) {
        this.maxLeases = maxLeases;
        this.notifyAll();
    }

    void makeRevocable(RevocationSpec entry) {
        this.revocable.set(entry);
    }

	/**
	 * 釋放鎖調用方法
	 */
    final void releaseLock(String lockPath) throws Exception {
        this.client.removeWatchers();
        this.revocable.set((Object)null);
        this.deleteOurPath(lockPath);
    }

    CuratorFramework getClient() {
        return this.client;
    }

    public static Collection<String> getParticipantNodes(CuratorFramework client, final String basePath, String lockName, LockInternalsSorter sorter) throws Exception {
        List<String> names = getSortedChildren(client, basePath, lockName, sorter);
        Iterable<String> transformed = Iterables.transform(names, new Function<String, String>() {
            public String apply(String name) {
                return ZKPaths.makePath(basePath, name);
            }
        });
        return ImmutableList.copyOf(transformed);
    }


    LockInternalsDriver getDriver() {
        return this.driver;
    }

	/**
	 * 嘗試獲取鎖
	 */
    String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception {
        long startMillis = System.currentTimeMillis();
        Long millisToWait = unit != null ? unit.toMillis(time) : null;
        byte[] localLockNodeBytes = this.revocable.get() != null ? new byte[0] : lockNodeBytes;
        int retryCount = 0;
        String ourPath = null;
        boolean hasTheLock = false;  // 標記當前線程是否成功獲取鎖
        boolean isDone = false;

        while(!isDone) {
            isDone = true;

            try {
            	// 創建臨時有序節點,返回屬於當前線程的節點名稱
                ourPath = this.driver.createsTheLock(this.client, this.path, localLockNodeBytes);
                // 循環獲取鎖,返回獲取鎖是否成功
                hasTheLock = this.internalLockLoop(startMillis, millisToWait, ourPath);
            } catch (NoNodeException var14) {
                if (!this.client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper())) {
                    throw var14;
                }

                isDone = false;
            }
        }

        return hasTheLock ? ourPath : null;
    }

    private void checkRevocableWatcher(String path) throws Exception {
        RevocationSpec entry = (RevocationSpec)this.revocable.get();
        if (entry != null) {
            try {
                byte[] bytes = (byte[])((BackgroundPathable)this.client.getData().usingWatcher(this.revocableWatcher)).forPath(path);
                if (Arrays.equals(bytes, REVOKE_MESSAGE)) {
                    entry.getExecutor().execute(entry.getRunnable());
                }
            } catch (NoNodeException var4) {
                ;
            }
        }

    }

    private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception {
        boolean haveTheLock = false;  // 獲取成功標記
        boolean doDelete = false;  // 

        try {
            if (this.revocable.get() != null) {
                ((BackgroundPathable)this.client.getData().usingWatcher(this.revocableWatcher)).forPath(ourPath);
            }

			// 沒有獲取到鎖會一直循環,發生異常會跳出循環
            while(this.client.getState() == CuratorFrameworkState.STARTED && !haveTheLock) {
            	// 獲取當前從小到大排序的節點
                List<String> children = this.getSortedChildren();
                // 獲取當前線程所屬的節點序號
                String sequenceNodeName = ourPath.substring(this.basePath.length() + 1);
                // 判斷是否獲取鎖
                PredicateResults predicateResults = this.driver.getsTheLock(this.client, children, sequenceNodeName, this.maxLeases);
                if (predicateResults.getsTheLock()) {
                    haveTheLock = true;
                } else {
                	// 獲取前一個節點
                    String previousSequencePath = this.basePath + "/" + predicateResults.getPathToWatch();
                    // 用this進行來實現線程等待喚醒
                    synchronized(this) {
                        try {
                            // 使用getData()接口而不是checkExists()是因爲,如果前一個子節點已經被刪除了那麼會拋出異常而且不會設置事件監聽器,而checkExists雖然也可以獲取到節點是否存在的信息但是同時設置了監聽器,這個監聽器其實永遠不會觸發,對於zookeeper來說屬於資源泄露
                            ((BackgroundPathable)this.client.getData().usingWatcher(this.watcher)).forPath(previousSequencePath);
                            if (millisToWait == null) {
                            	// 未設置超時時間,一直等待直到被喚醒再次循環
                                this.wait();
                            } else {
                            	// 更新等待時間
                                millisToWait = millisToWait.longValue() - (System.currentTimeMillis() - startMillis);
                              	// 更新開始時間
                                startMillis = System.currentTimeMillis();
                                if (millisToWait.longValue() > 0L) {
                                	// 未超時則最多等待一段時間,等待被喚醒再次循環
                                    this.wait(millisToWait.longValue());
                                } else {
                                	// 超時退出循環
                                    doDelete = true;
                                    break;
                                }
                            }
                        } catch (NoNodeException var19) {
                            ;
                        }
                    }
                }
            }
        } catch (Exception var21) {
            ThreadUtils.checkInterrupted(var21);
            doDelete = true;
            throw var21;
        } finally {
        	// 如果超時了,則刪除節點
            if (doDelete) {
                this.deleteOurPath(ourPath);
            }

        }

        return haveTheLock;
    }

    private void deleteOurPath(String ourPath) throws Exception {
        try {
            ((ChildrenDeletable)this.client.delete().guaranteed()).forPath(ourPath);
        } catch (NoNodeException var3) {
            ;
        }
    }

    private synchronized void notifyFromWatcher() {
        this.notifyAll();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章