分佈式鎖解決方案

什麼是鎖

在多線程的軟件世界裏,對共享資源的爭搶過程(Data Race)就是併發,而對共享資源數據進行訪問保護的最直接辦法就是引入鎖。html

POSIX threads(簡稱Pthreads)是在多核平臺上進行並行編程的一套經常使用的API。線程同步(Thread Synchronization)是並行編程中很是重要的通信手段,其中最典型的應用就是用Pthreads提供的鎖機制(lock)來對多個線程之間共 享的臨界區(Critical Section)進行保護(另外一種經常使用的同步機制是barrier)。java

無鎖編程也是一種辦法,但它不在本文的討論範圍,併發多線程轉爲單線程(Disruptor),函數式編程,鎖粒度控制(ConcurrentHashMap桶),信號量(Semaphore)等手段均可以實現無鎖或鎖優化。redis

技術上來講,鎖也能夠理解成將大量併發請求串行化,但請注意串行化不能簡單等同爲** 排隊 ,由於這裏和現實世界沒什麼不一樣,排隊意味着你們是公平Fair的領到資源,先到先得,然而不少狀況下爲了性能考量多線程之間仍是會不公平Unfair**的去搶。Java中ReentrantLock可重入鎖,提供了公平鎖和非公平鎖兩種實現。sql

再注意一點,串行也不是意味着只有一個排隊的隊伍,每次只能進一個。固然能夠好多個隊伍,每次進入多個。好比餐館一共10個餐桌,服務員可能一次放行最多10我的進去,有人出來再放行同數量的人進去。Java中Semaphore信號量,至關於同時管理一批鎖。數據庫

鎖的類型

自旋鎖(Spin Lock)

自旋鎖是一種非阻塞鎖,也就是說,若是某線程須要獲取自旋鎖,但該鎖已經被其餘線程佔用時,該線程不會被掛起,而是在不斷的消耗CPU的時間,不停的試圖獲取自旋鎖。apache

互斥鎖 (Mutex Lock)

互斥鎖是阻塞鎖,當某線程沒法獲取互斥鎖時,該線程會被直接掛起,再也不消耗CPU時間,當其餘線程釋放互斥鎖後,操做系統會喚醒那個被掛起的線程。編程

可重入鎖 (Reentrant Lock)

可重入鎖是一種特殊的互斥鎖,它能夠被同一個線程屢次獲取,而不會產生死鎖。緩存

鎖舉例

本地鎖

java環境下能夠經過synchronized和lock開實現本地鎖。多線程

//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();
      }
   }
複製代碼

鎖非靜態是鎖了對象的實例;鎖靜態是鎖了對象的類型。併發

一些特性

  • 可重入。以下能夠直接進入testWrite方法不用從新申請鎖。synchronized和lock都是可重入鎖。
synchronized void testRead(){
        this.testWrite();
    }
    synchronized void testWrite(){}
複製代碼
  • 可中斷鎖。例如A正在執行鎖中的代碼,另外一線程B正在等待獲取該鎖若是B能夠中斷則該鎖爲可中斷鎖。synchronized就不是可中斷鎖,而Lock是可中斷鎖。
  • 公平鎖和非公平鎖。以請求鎖的順序來獲取鎖是公平鎖。synchronized是非公平鎖,lock默認是非公平鎖,可是能夠設置爲公平鎖。

對比

名稱 優勢 缺點
synchronized 實現簡單,語義清晰,便於JVM堆棧跟蹤,加鎖解鎖過程由JVM自動控制,提供了多種優化方案,使用更普遍 悲觀的排他鎖,不能進行高級功能
lock 可定時的、可輪詢的與可中斷的鎖獲取操做,提供了讀寫鎖、公平鎖和非公平鎖 需手動釋放鎖unlock,不適合JVM進行堆棧跟蹤

分佈式鎖

使用分佈式鎖的目的有兩個,一個是避免屢次執行冪等操做提高效率;一個是避免多個節點同時執行非冪等操做致使數據不一致。 接下來咱們來看如何實現分佈式鎖,在java環境下有三種也即經過數據庫,經過redis及經過Zk來實現。

經過數據庫實現

經過主鍵及其餘約束使用拋異常來實現分佈式鎖不在本文討論範圍。一下爲基於數據庫排他鎖來實現分佈式鎖

/** * 超時獲取鎖 * @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();

    }
複製代碼

經過緩存系統實現

加鎖

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 請求標識 * @param expireTime 超期時間 * @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;
 
    }
 
}
複製代碼

第一個爲key,咱們使用key來當鎖,由於key是惟一的。
第二個爲value,咱們傳的是requestId,不少童鞋可能不明白,有key做爲鎖不就夠了嗎,爲何還要用到value?緣由就是咱們在上面講到可靠性時,分佈式鎖要知足第四個條件解鈴還須繫鈴人,經過給value賦值爲requestId,咱們就知道這把鎖是哪一個請求加的了,在解鎖的時候就能夠有依據。requestId可使用UUID.randomUUID().toString()方法生成。
第三個爲nxxx,這個參數咱們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,咱們進行set操做;若key已經存在,則不作任何操做;
第四個爲expx,這個參數咱們傳的是PX,意思是咱們要給這個key加一個過時的設置,具體時間由第五個參數決定。
第五個爲time,與第四個參數相呼應,表明key的過時時間。

解鎖

public class RedisTool {
 
    private static final Long RELEASE_SUCCESS = 1L;
 
    /** * 釋放分佈式鎖 * @param jedis Redis客戶端 * @param lockKey 鎖 * @param requestId 請求標識 * @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;
 
    }
 
}
複製代碼

第一行代碼,咱們寫了一個簡單的Lua腳本代碼
第二行代碼,咱們將Lua代碼傳到jedis.eval()方法裏,並使參數KEYS[1]賦值爲lockKey,ARGV[1]賦值爲requestId。eval()方法是將Lua代碼交給Redis服務端執行。

基於Redlock實現分佈式鎖的爭論見

Redlock

how-to-do-distributed-locking

經過ZK實現

使用curator來實現分佈式鎖。

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;
}
複製代碼

分佈式鎖對比

方式 優勢 缺點
基於DB 直接藉助數據庫,容易理解 會有各類各樣的問題,在解決問題的過程當中會使整個方案變得愈來愈複雜
操做數據庫須要必定的開銷,性能問題須要考慮
使用數據庫的行級鎖並不必定靠譜,尤爲是當咱們的鎖表並不大的時候
基於緩存 性能好,實現起來較爲方便 經過超時時間來控制鎖的失效時間並非十分的合理
基於ZK 有效的解決單點問題,不可重入問題,非阻塞問題以及鎖沒法釋放的問題。實現起來較爲簡單 性能上不如使用緩存實現分佈式鎖。 須要對ZK的原理有所瞭解

結論

zookeeper可靠性比redis強太多,只是效率低了點,若是併發量不是特別大,追求可靠性,首選zookeeper。爲了效率,則首選redis實現。

相關文章
相關標籤/搜索