分布式鎖

什么是鎖

在多線程的軟件世界里,對(duì)共享資源的爭(zhēng)搶過(guò)程(Data Race)就是并發(fā),而對(duì)共享資源數(shù)據(jù)進(jìn)行訪問(wèn)保護(hù)的最直接辦法就是引入鎖。

POSIX threads(簡(jiǎn)稱Pthreads)是在多核平臺(tái)上進(jìn)行并行編程的一套常用的API。線程同步(Thread Synchronization)是并行編程中非常重要的通訊手段,其中最典型的應(yīng)用就是用Pthreads提供的鎖機(jī)制(lock)來(lái)對(duì)多個(gè)線程之間共 享的臨界區(qū)(Critical Section)進(jìn)行保護(hù)(另一種常用的同步機(jī)制是barrier)。

無(wú)鎖編程也是一種辦法,但它不在本文的討論范圍,并發(fā)多線程轉(zhuǎn)為單線程(Disruptor),函數(shù)式編程,鎖粒度控制(ConcurrentHashMap桶),信號(hào)量(Semaphore)等手段都可以實(shí)現(xiàn)無(wú)鎖或鎖優(yōu)化。

技術(shù)上來(lái)說(shuō),鎖也可以理解成將大量并發(fā)請(qǐng)求串行化,但請(qǐng)注意串行化不能簡(jiǎn)單等同為** 排隊(duì) ,因?yàn)檫@里和現(xiàn)實(shí)世界沒(méi)什么不同,排隊(duì)意味著大家是公平Fair的領(lǐng)到資源,先到先得,然而很多情況下為了性能考量多線程之間還是會(huì)不公平Unfair**的去搶。Java中ReentrantLock可重入鎖,提供了公平鎖和非公平鎖兩種實(shí)現(xiàn)。

再注意一點(diǎn),串行也不是意味著只有一個(gè)排隊(duì)的隊(duì)伍,每次只能進(jìn)一個(gè)。當(dāng)然可以好多個(gè)隊(duì)伍,每次進(jìn)入多個(gè)。比如餐館一共10個(gè)餐桌,服務(wù)員可能一次放行最多10個(gè)人進(jìn)去,有人出來(lái)再放行同數(shù)量的人進(jìn)去。Java中Semaphore信號(hào)量,相當(dāng)于同時(shí)管理一批鎖。

鎖的類型

自旋鎖(Spin Lock)

自旋鎖是一種非阻塞鎖,也就是說(shuō),如果某線程需要獲取自旋鎖,但該鎖已經(jīng)被其他線程占用時(shí),該線程不會(huì)被掛起,而是在不斷的消耗CPU的時(shí)間,不停的試圖獲取自旋鎖。

互斥鎖 (Mutex Lock)

互斥鎖是阻塞鎖,當(dāng)某線程無(wú)法獲取互斥鎖時(shí),該線程會(huì)被直接掛起,不再消耗CPU時(shí)間,當(dāng)其他線程釋放互斥鎖后,操作系統(tǒng)會(huì)喚醒那個(gè)被掛起的線程。

可重入鎖 (Reentrant Lock)

可重入鎖是一種特殊的互斥鎖,它可以被同一個(gè)線程多次獲取,而不會(huì)產(chǎn)生死鎖。

鎖舉例

本地鎖

java環(huán)境下可以通過(guò)synchronized和lock開(kāi)實(shí)現(xiàn)本地鎖。


//synchronized

    public synchronized void demoMethod(){}
    
    public void demoMethod(){
        synchronized (this)
        {
            //other thread safe code
        }
    }

    private final Object lock = new Object();
    public void demoMethod(){
        synchronized (lock)
        {
            //other thread safe code
        }
    }

    public synchronized static void demoMethod(){}

//lock

   private final Lock queueLock = new ReentrantLock();
 
   public void printJob(Object document)
   {
      queueLock.lock();
      try
      {
         Long duration = (long) (Math.random() * 10000);
         System.out.println(Thread.currentThread().getName() + ": PrintQueue: Printing a Job during " + (duration / 1000) + " seconds :: Time - " + new Date());
         Thread.sleep(duration);
      } catch (InterruptedException e)
      {
         e.printStackTrace();
      } finally
      {
         System.out.printf("%s: The document has been printed\n", Thread.currentThread().getName());
         queueLock.unlock();
      }
   }

鎖非靜態(tài)是鎖了對(duì)象的實(shí)例;鎖靜態(tài)是鎖了對(duì)象的類型。

一些特性

  • 可重入。如下可以直接進(jìn)入testWrite方法不用重新申請(qǐng)鎖。synchronized和lock都是可重入鎖。
    synchronized void testRead(){
        this.testWrite();
    }
    synchronized void testWrite(){}
  • 可中斷鎖。例如A正在執(zhí)行鎖中的代碼,另一線程B正在等待獲取該鎖如果B可以中斷則該鎖為可中斷鎖。synchronized就不是可中斷鎖,而Lock是可中斷鎖。
  • 公平鎖和非公平鎖。以請(qǐng)求鎖的順序來(lái)獲取鎖是公平鎖。synchronized是非公平鎖,lock默認(rèn)是非公平鎖,但是可以設(shè)置為公平鎖。

對(duì)比

名稱 優(yōu)點(diǎn) 缺點(diǎn)
synchronized 實(shí)現(xiàn)簡(jiǎn)單,語(yǔ)義清晰,便于JVM堆棧跟蹤,加鎖解鎖過(guò)程由JVM自動(dòng)控制,提供了多種優(yōu)化方案,使用更廣泛 悲觀的排他鎖,不能進(jìn)行高級(jí)功能
lock 可定時(shí)的、可輪詢的與可中斷的鎖獲取操作,提供了讀寫鎖、公平鎖和非公平鎖 需手動(dòng)釋放鎖unlock,不適合JVM進(jìn)行堆棧跟蹤

分布式鎖

使用分布式鎖的目的有兩個(gè),一個(gè)是避免多次執(zhí)行冪等操作提升效率;一個(gè)是避免多個(gè)節(jié)點(diǎn)同時(shí)執(zhí)行非冪等操作導(dǎo)致數(shù)據(jù)不一致。
接下來(lái)我們來(lái)看如何實(shí)現(xiàn)分布式鎖,在java環(huán)境下有三種也即通過(guò)數(shù)據(jù)庫(kù),通過(guò)redis及通過(guò)Zk來(lái)實(shí)現(xiàn)。

通過(guò)數(shù)據(jù)庫(kù)實(shí)現(xiàn)

通過(guò)主鍵及其他約束使用拋異常來(lái)實(shí)現(xiàn)分布式鎖不在本文討論范圍。一下為基于數(shù)據(jù)庫(kù)排他鎖來(lái)實(shí)現(xiàn)分布式鎖

/**
     * 超時(shí)獲取鎖
     * @param lockID
     * @param timeOuts
     * @return
     * @throws InterruptedException
     */
    public boolean acquireByUpdate(String lockID, long timeOuts) throws InterruptedException, SQLException {

        String sql = "SELECT id from test_lock where id = ? for UPDATE ";
        long futureTime = System.currentTimeMillis() + timeOuts;
        long ranmain = timeOuts;
        long timerange = 500;
        connection.setAutoCommit(false);
        while (true) {
            CountDownLatch latch = new CountDownLatch(1);
            try {
                PreparedStatement statement = connection.prepareStatement(sql);
                statement.setString(1, lockID);
                statement.setInt(2, 1);
                statement.setLong(1, System.currentTimeMillis());
                boolean ifsucess = statement.execute();//如果成功,那么就是獲取到了鎖
                if (ifsucess)
                    return true;
            } catch (SQLException e) {
                e.printStackTrace();
            }
            latch.await(timerange, TimeUnit.MILLISECONDS);
            ranmain = futureTime - System.currentTimeMillis();
            if (ranmain <= 0)
                break;
            if (ranmain < timerange) {
                timerange = ranmain;
            }
            continue;
        }
        return false;

    }


    /**
     * 釋放鎖
     * @param lockID
     * @return
     * @throws SQLException
     */
    public void unlockforUpdtate(String lockID) throws SQLException {
        connection.commit();

    }

通過(guò)緩存系統(tǒng)實(shí)現(xiàn)

加鎖

public class RedisTool {
 
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
 
    /**
     * 嘗試獲取分布式鎖
     * @param jedis Redis客戶端
     * @param lockKey 鎖
     * @param requestId 請(qǐng)求標(biāo)識(shí)
     * @param expireTime 超期時(shí)間
     * @return 是否獲取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
 
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
 
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
 
    }
 
}

第一個(gè)為key,我們使用key來(lái)當(dāng)鎖,因?yàn)閗ey是唯一的。

第二個(gè)為value,我們傳的是requestId,很多童鞋可能不明白,有key作為鎖不就夠了嗎,為什么還要用到value?原因就是我們?cè)谏厦嬷v到可靠性時(shí),分布式鎖要滿足第四個(gè)條件解鈴還須系鈴人,通過(guò)給value賦值為requestId,我們就知道這把鎖是哪個(gè)請(qǐng)求加的了,在解鎖的時(shí)候就可以有依據(jù)。requestId可以使用UUID.randomUUID().toString()方法生成。

第三個(gè)為nxxx,這個(gè)參數(shù)我們填的是NX,意思是SET IF NOT EXIST,即當(dāng)key不存在時(shí),我們進(jìn)行set操作;若key已經(jīng)存在,則不做任何操作;

第四個(gè)為expx,這個(gè)參數(shù)我們傳的是PX,意思是我們要給這個(gè)key加一個(gè)過(guò)期的設(shè)置,具體時(shí)間由第五個(gè)參數(shù)決定。

第五個(gè)為time,與第四個(gè)參數(shù)相呼應(yīng),代表key的過(guò)期時(shí)間。

解鎖

public class RedisTool {
 
    private static final Long RELEASE_SUCCESS = 1L;
 
    /**
     * 釋放分布式鎖
     * @param jedis Redis客戶端
     * @param lockKey 鎖
     * @param requestId 請(qǐng)求標(biāo)識(shí)
     * @return 是否釋放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
 
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
 
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
 
    }
 
}

第一行代碼,我們寫了一個(gè)簡(jiǎn)單的Lua腳本代碼
第二行代碼,我們將Lua代碼傳到j(luò)edis.eval()方法里,并使參數(shù)KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId。eval()方法是將Lua代碼交給Redis服務(wù)端執(zhí)行。

基于Redlock實(shí)現(xiàn)分布式鎖的爭(zhēng)論見(jiàn)

Redlock

how-to-do-distributed-locking

通過(guò)ZK實(shí)現(xiàn)

使用[curator]{https://curator.apache.org/}來(lái)實(shí)現(xiàn)分布式鎖。

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    try {
        return interProcessMutex.acquire(timeout, unit);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return true;
}
public boolean unlock() {
    try {
        interProcessMutex.release();
    } catch (Throwable e) {
        log.error(e.getMessage(), e);
    } finally {
        executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
    }
    return true;
}

分布式鎖對(duì)比

方式 優(yōu)點(diǎn) 缺點(diǎn)
基于DB 直接借助數(shù)據(jù)庫(kù),容易理解 會(huì)有各種各樣的問(wèn)題,在解決問(wèn)題的過(guò)程中會(huì)使整個(gè)方案變得越來(lái)越復(fù)雜
操作數(shù)據(jù)庫(kù)需要一定的開(kāi)銷,性能問(wèn)題需要考慮
使用數(shù)據(jù)庫(kù)的行級(jí)鎖并不一定靠譜,尤其是當(dāng)我們的鎖表并不大的時(shí)候
基于緩存 性能好,實(shí)現(xiàn)起來(lái)較為方便 通過(guò)超時(shí)時(shí)間來(lái)控制鎖的失效時(shí)間并不是十分的合理
基于ZK 有效的解決單點(diǎn)問(wèn)題,不可重入問(wèn)題,非阻塞問(wèn)題以及鎖無(wú)法釋放的問(wèn)題。實(shí)現(xiàn)起來(lái)較為簡(jiǎn)單 性能上不如使用緩存實(shí)現(xiàn)分布式鎖。 需要對(duì)ZK的原理有所了解

結(jié)論

zookeeper可靠性比redis強(qiáng)太多,只是效率低了點(diǎn),如果并發(fā)量不是特別大,追求可靠性,首選zookeeper。為了效率,則首選redis實(shí)現(xiàn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容