分佈式鎖解決併發三種方案

目錄

爲什麼使用分佈式鎖?

分佈式鎖應具備的條件

三種實現方式

1.數據庫鎖

1.1 樂觀鎖

2.基於redis的分佈式鎖

3.基於Zookeeper實現分佈式鎖

4.三種方案的比較

分佈式CAP理論


原文:

https://mp.weixin.qq.com/s/xcd8NWYMzpVJ3UKlGPIt9g

https://www.jianshu.com/p/8bddd381de06

Java中的鎖可以簡單的理解爲多線程情況下訪問臨界資源的一種線程同步機制。《Java多線程核心技術》

各樣的鎖的概念:公平鎖、非公平鎖、自旋鎖、可重入鎖、偏向鎖、輕量級鎖、重量級鎖、讀寫鎖、互斥鎖等。

爲什麼使用分佈式鎖?

我們在開發應用的時候,如果需要對某一個共享變量進行多線程同步訪問的時候,可以使用我們學到的Java多線程的18般武藝進行處理,並且可以完美的運行,毫無Bug!

注意這是單機應用,也就是所有的請求都會分配到當前服務器的JVM內部,然後映射爲操作系統的線程進行處理!而這個共享變量只是在這個JVM內部的一塊內存空間!

後來業務發展,需要做集羣,一個應用需要部署到幾臺機器上然後做負載均衡,大致如下圖:

上圖可以看到,變量A存在JVM1、JVM2、JVM3三個JVM內存中(這個變量A主要體現是在一個類中的一個成員變量,是一個有狀態的對象,例如:UserController控制器中的一個整形類型的成員變量),如果不加任何控制的話,變量A同時都會在JVM分配一塊內存,三個請求發過來同時對這個變量操作,顯然結果是不對的!即使不是同時發過來,三個請求分別操作三個不同JVM內存區域的數據,變量A之間不存在共享,也不具有可見性,處理的結果也是不對的!

如果我們業務中確實存在這個場景的話,我們就需要一種方法解決這個問題!

爲了保證一個方法或屬性在高併發情況下的同一時間只能被同一個線程執行,在傳統單體應用單機部署的情況下,可以使用Java併發處理相關的API(如ReentrantLock或Synchronized)進行互斥控制。在單機環境中,Java中提供了很多併發處理相關的API。但是,隨着業務發展的需要,原單體單機部署的系統被演化成分佈式集羣系統後,由於分佈式系統多線程、多進程並且分佈在不同機器上,這將使原單機部署情況下的併發控制鎖策略失效,單純的Java API並不能提供分佈式鎖的能力。爲了解決這個問題就需要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分佈式鎖要解決的問題!

分佈式鎖應具備的條件

1、互斥性 在分佈式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行; 
2、高可用的獲取鎖與釋放鎖; 
3、高性能的獲取鎖與釋放鎖; 
4、這把鎖要是一把可重入鎖(避免死鎖); 
5、具備鎖失效機制,防止死鎖; 
6、具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。

  • 不會發生死鎖:有一個客戶端在持有鎖的過程中崩潰而沒有解鎖,也能保證其他客戶端能夠加鎖

  • 這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)

  • 有高可用的獲取鎖和釋放鎖功能

  • 獲取鎖和釋放鎖的性能要好

三種實現方式

1.數據庫鎖

基於數據庫表

  • 要實現分佈式鎖,最簡單的方式可能就是直接創建一張鎖表,然後通過操作該表中的數據來實現了。

  • 當我們要鎖住某個方法或資源時,我們就在該表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。

當我們想要鎖住某個方法時,執行以下SQL:

因爲我們對method_name做了唯一性約束,這裏如果有多個請求同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,那麼我們就可以認爲

操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。

當方法執行完畢之後,想要釋放鎖的話,需要執行以下Sql:

上面這種簡單的實現有以下幾個問題:

  • 1、這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會導致業務系統不可用。

            方案:搞兩個數據庫,數據之前雙向同步。一旦掛掉快速切換到備庫上。

  • 2、這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖。

            方案:只要做一個定時任務,每隔一定時間把數據庫中的超時數據清理一遍。

  • 3、這把鎖只能是非阻塞的,因爲數據的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程並不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作。

            方案:搞一個while循環,直到insert成功再返回成功。

  • 4、這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因爲數據中數據已經存在了。

            方案:在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那麼下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了。

使用基於數據庫的這種實現方式很簡單,但是對於分佈式鎖應該具備的條件來說,它有一些問題需要解決及優化:

1、因爲是基於數據庫實現的,數據庫的可用性和性能將直接影響分佈式鎖的可用性及性能,所以,數據庫需要雙機部署、數據同步、主備切換;

2、不具備可重入的特性,因爲同一個線程在釋放鎖之前,行數據一直存在,無法再次成功插入數據,所以,需要在表中新增一列,用於記錄當前獲取到鎖的機器和線程信息,在再次獲取鎖的時候,先查詢表中機器和線程信息是否和當前機器和線程相同,若相同則直接獲取鎖;

3、沒有鎖失效機制,因爲有可能出現成功插入數據後,服務器宕機了,對應的數據沒有被刪除,當服務恢復後一直獲取不到鎖,所以,需要在表中新增一列,用於記錄失效時間,並且需要有定時任務清除這些失效的數據;

4、不具備阻塞鎖特性,獲取不到鎖直接返回失敗,所以需要優化獲取邏輯,循環多次去獲取。

5、在實施的過程中會遇到各種不同的問題,爲了解決這些問題,實現方式將會越來越複雜;依賴數據庫需要一定的資源開銷,性能問題需要考慮。

基於數據庫的排它鎖

除了可以通過增刪操作數據表中的記錄以外,其實還可以藉助數據庫中自帶的鎖來實現分佈式的鎖。

我們還用剛剛創建的那張數據庫表。可以通過數據庫的排他鎖來實現分佈式鎖。

在查詢語句後面增加for update,數據庫會在查詢過程中給數據庫表增加排他鎖。當某條記錄被加上排他鎖之後,其他線程無法再在該行記錄上增加排他鎖。

我們可以認爲獲得排它鎖的線程即可獲得分佈式鎖,當獲取到鎖之後,可以執行方法的業務邏輯,執行完方法之後,再通過以下方法解鎖:

我們可以認爲獲得排它鎖的線程即可獲得分佈式鎖,當獲取到鎖之後,可以執行方法的業務邏輯,執行完方法之後,再通過以下方法解鎖:

public void unlock(){
      connection.commit();
}

通過connection.commit()操作來釋放鎖。

這種方法可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題。

  • 阻塞鎖? for update語句會在執行成功後立即返回,在執行失敗時一直處於阻塞狀態,直到成功。
  • 鎖定之後服務宕機,無法釋放?使用這種方式,服務宕機之後數據庫會自己把鎖釋放掉。

但是還是無法直接解決數據庫單點和可重入問題。

總結:

  • 總結一下使用數據庫來實現分佈式鎖的方式,這兩種方式都是依賴數據庫的一張表,一種是通過表中的記錄的存在情況確定當前是否有鎖存在,另外一種是通過數據庫的排他鎖來實現分佈式鎖。

  • 數據庫實現分佈式鎖的優點: 直接藉助數據庫,容易理解。

  • 數據庫實現分佈式鎖的缺點: 會有各種各樣的問題,在解決問題的過程中會使整個方案變得越來越複雜。

  • 操作數據庫需要一定的開銷,性能問題需要考慮。

1.1 樂觀鎖

樂觀鎖假設認爲數據一般情況下不會造成衝突,只有在進行數據的提交更新時,纔會檢測數據的衝突情況,如果發現衝突了,則返回錯誤信息

實現方式:

  • 時間戳(timestamp)記錄機制實現:給數據庫表增加一個時間戳字段類型的字段,當讀取數據時,將timestamp字段的值一同讀出,數據每更新一次,timestamp也同步更新。當對數據做提交更新操作時,檢查當前數據庫中數據的時間戳和自己更新前取到的時間戳進行對比,若相等,則更新,否則認爲是失效數據。

  • 若出現更新衝突,則需要上層邏輯修改,啓動重試機制

  • 同樣也可以使用version的方式

性能對比

  • 1、悲觀鎖實現方式是獨佔數據,其它線程需要等待,不會出現修改的衝突,能夠保證數據的一致性,但是依賴數據庫的實現,且在線程較多時出現等待造成效率降低的問題。一般情況下,對於數據很敏感且讀取頻率較低的場景,可以採用悲觀鎖的方式
  • 2、 樂觀鎖可以多線程同時讀取數據,若出現衝突,也可以依賴上層邏輯修改,能夠保證高併發下的讀取,適用於讀取頻率很高而修改頻率較少的場景
  • 3、 由於庫存回寫數據屬於敏感數據且讀取頻率適中,所以建議使用悲觀鎖優化

2.基於redis的分佈式鎖

  • 相比較於基於數據庫實現分佈式鎖的方案來說,基於緩存來實現在性能方面會表現的更好一點。而且很多緩存是可以集羣部署的,可以解決單點問題。

爲了確保分佈式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:

  • 互斥性。在任意時刻,只有一個客戶端能持有鎖。

  • 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。!!!

  • 具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。

  • 解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。

加鎖就一行代碼:==jedis.set(String key, String value, String nxxx, String expx, int time)==,這個set()方法一共有五個形參:

  • 第一個爲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的過期時間。

總的來說,執行上面的set()方法就只會導致兩種結果:1. 當前沒有鎖(key不存在),那麼就進行加鎖操作,並對鎖設置個有效期,同時value表示加鎖的客戶端。2. 已有鎖存在,不做任何操作。

加鎖代碼滿足我們可靠性裏描述的三個條件。首先,set()加入了NX參數,可以保證如果已有key存在,則函數不會調用成功,也就是隻有一個客戶端能持有鎖,滿足互斥性。其次,由於我們對鎖設置了過期時間,即使鎖的持有者後續發生崩潰而沒有解鎖,鎖也會因爲到了過期時間而自動解鎖(即key被刪除),不會發生死鎖。最後,因爲我們將value賦值爲requestId,代表加鎖的客戶端請求標識,那麼在客戶端在解鎖的時候就可以進行校驗是否是同一個客戶端。由於我們只考慮Redis單機部署的場景,所以容錯性我們暫不考慮。

錯誤實例

  • 使用jedis.setnx()和jedis.expire()組合實現加鎖

setnx()方法作用就是SET IF NOT EXIST,expire()方法就是給鎖加一個過期時間。乍一看好像和前面的set()方法結果一樣,然而由於這是兩條Redis命令,不具有原子性,如果程序在執行完setnx()之後突然崩潰,導致鎖沒有設置過期時間。那麼將會發生死鎖。網上之所以有人這樣實現,是因爲低版本的jedis並不支持多參數的set()方法。

解鎖:

  • 首先獲取鎖對應的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)
  • 使用緩存實現分佈式鎖的優點
    • 通過超時時間來控制鎖的失效時間並不是十分的靠譜。

    • 使用緩存實現分佈式鎖的缺點

    • 性能好,實現起來較爲方便。

總結:

可以使用緩存來代替數據庫來實現分佈式鎖,這個可以提供更好的性能,同時,很多緩存服務都是集羣部署的,可以避免單點問題。並且很多緩存服務都提供了可以用來實現分佈式鎖的方法,比如redis的setnx方法等。並且,這些緩存服務也都提供了對數據的過期自動刪除的支持,可以直接設置超時時間來控制鎖的釋放。

1、選用Redis實現分佈式鎖原因:

(1)Redis有很高的性能; 
(2)Redis命令對此支持較好,實現起來比較方便

2、使用命令介紹:

(1)SETNX:SETNX key val:當且僅當key不存在時,set一個key爲val的字符串,返回1;若key存在,則什麼都不做,返回0。

(2)expire:expire key timeout:爲key設置一個超時時間,單位爲second,超過這個時間鎖會自動釋放,避免死鎖。

(3)delete:delete key:刪除key

在使用Redis實現分佈式鎖的時候,主要就會使用到這三個命令。

3、實現思想:

(1)獲取鎖的時候,使用setnx加鎖,並使用expire命令爲鎖添加一個超時時間,超過該時間則自動釋放鎖,鎖的value值爲一個隨機生成的UUID,通過此在釋放鎖的時候進行判斷。

(2)獲取鎖的時候還設置一個獲取的超時時間,若超過這個時間則放棄獲取鎖。

(3)釋放鎖的時候,通過UUID判斷是不是該鎖,若是該鎖,則執行delete進行鎖釋放。

4、 分佈式鎖的簡單實現代碼:

<dependency>
     <groupId>com.icloudbus</groupId>
     <artifactId>common-redis</artifactId>
     <version>1.0-SNAPSHOT</version>
</dependency>
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

import java.util.List;
import java.util.UUID;

/**
 * 分佈式鎖的簡單代碼實現
 */
public class DistributeLock {

    private final JedisPool jedisPool;

    public DistributeLock(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    /**
     * 加鎖
     *
     * @param lockName       鎖的key
     * @param acquireTimeout 獲取超時時間
     * @param timeout        鎖的超時時間
     * @return 鎖標識
     */
    public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
        Jedis conn = null;
        String retIdentifier = null;
        try {
            //獲取連接
            conn = jedisPool.getResource();
            //隨機生成一個value
            String indentifier = UUID.randomUUID().toString();
            //key值
            String lockKey = "lock:" + lockName;
            //設置超時時間,上鎖後超過此時間自動釋放鎖
            int lockExpire = (int) (timeout / 1000);
            //獲取鎖的超時時間,超過這個時間則放棄獲取鎖
            long end = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < end) {
                if (conn.setnx(lockKey, indentifier) == 1) {
                    conn.expire(lockKey, lockExpire);
                    //返回value值,用於釋放鎖時間確認
                    retIdentifier = indentifier;
                    return retIdentifier;
                }
                //返回-1代表key沒有設置超時時間,爲key設置一個超時時間
                //當 key 不存在時,返回 -2 。 當 key 存在但沒有設置剩餘生存時間時,返回 -1 。
                //否則,以毫秒爲單位,返回 key 的剩餘生存時間。
                if (conn.ttl(lockKey) == -1) {
                    conn.expire(lockKey, lockExpire);
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    //當線程阻塞時,調用interrupt方法後,該線程會得到一個interrupt異常,
                    //可以通過對該異常的處理而退出線程,對於正在運行的線程,沒有任何作用!
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retIdentifier;
    }

    /**
     * 釋放鎖
     *
     * @param lockName   鎖的key
     * @param identifier 釋放鎖的標識
     * @return
     */
    public boolean releaseLock(String lockName, String identifier) {
        Jedis conn = null;
        String lockKey = "lock:" + lockName;
        Boolean retFlag = false;
        try {
            conn = jedisPool.getResource();
            while (true) {
                //監視lock,準備開始事務
                conn.watch(lockKey);
                //通過前面返回的value值判斷是不是該鎖,若是該鎖,則刪除,釋放鎖
                if (identifier.equals(conn.get(lockKey))) {
                    //事務開始
                    Transaction transaction = conn.multi();
                    transaction.del(lockKey);
                    //事務提交
                    List<Object> results = transaction.exec();
                    if (null == results) {
                        continue;
                    }
                    retFlag = true;
                }
                conn.unwatch();
                break;
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retFlag;
    }

}

 

5、測試剛纔實現的分佈式鎖

例子中使用50個線程模擬秒殺一個商品,使用–運算符來實現商品減少,從結果有序性就可以看出是否爲加鎖狀態。

模擬秒殺服務,在其中配置了jedis線程池,在初始化的時候傳給分佈式鎖,供其使用。

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class Service {
    private static JedisPool pool = null;

    private DistributeLock distributeLock = new DistributeLock(pool);

    int n = 500;

    static {
        JedisPoolConfig config = new JedisPoolConfig();
        //最大連接數
        config.setMaxTotal(200);
        //最大空間數
        config.setMaxIdle(8);
        //最大等待時間
        config.setMaxWaitMillis(1000 * 100);
        //在borrow一個jedis實例時,是否需要驗證,若爲true,則所有的jedis實例均是可用的
        config.setTestOnBorrow(true);
        pool = new JedisPool(config, "127.0.0.1", 6379, 123456);
    }

    public void seckill() {
        //返回鎖的value值,供釋放鎖的時候進行判斷
        String identifier = distributeLock.lockWithTimeout("resource", 5000, 1000);
        System.out.print(Thread.currentThread().getName() + "獲得了鎖");
        System.out.println(--n);
        distributeLock.releaseLock("resource", identifier);
    }

}

模擬線程進行秒殺服務:

public class ThreadA extends Thread{
    private Service service;

    public ThreadA(Service service){
        this.service = service;
    }


    @Override
    public void run() {
        service.seckill();
    }
}

 

public class Test {
    public static void main(String[] args) {
        Service service = new Service();
        for (int i = 0; i < 50; i++) {
            ThreadA threadA = new ThreadA(service);
            threadA.start();
        }
    }
}

結果如下,有序的:

Thread-32獲得了鎖    499
Thread-19獲得了鎖    498
Thread-43獲得了鎖    497
Thread-44獲得了鎖    496
Thread-13獲得了鎖    495
Thread-40獲得了鎖    494
Thread-27獲得了鎖    493
.................

若註釋掉使用鎖的部分:

   public void seckill() {
        //返回鎖的value值,供釋放鎖的時候進行判斷
//        String identifier = distributeLock.lockWithTimeout("resource", 5000, 1000);
        System.out.println(Thread.currentThread().getName() + "獲得了鎖    " + --n);
//        distributeLock.releaseLock("resource", identifier);
    }

從結果可以看出,有一些是異步進行的:

Thread-3獲得了鎖    498
Thread-4獲得了鎖    499
Thread-5獲得了鎖    497
Thread-6獲得了鎖    496
Thread-16獲得了鎖    495
Thread-10獲得了鎖    494
Thread-7獲得了鎖    493
Thread-15獲得了鎖    492
Thread-11獲得了鎖    491
Thread-12獲得了鎖    490
Thread-19獲得了鎖    489
.................

 

3.基於Zookeeper實現分佈式鎖

大致思想即爲:每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。完成業務流程後,刪除對應的子節點釋放鎖。

ZooKeeper是一個爲分佈式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個唯一文件名。基於ZooKeeper實現分佈式鎖的步驟如下:

(1)創建一個目錄mylock; 
(2)線程A想獲取鎖就在mylock目錄下創建臨時順序節點; 
(3)獲取mylock目錄下所有的子節點,然後獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖; 
(4)線程B獲取所有節點,判斷自己不是最小節點,設置監聽比自己次小的節點; 
(5)線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。

這裏推薦一個Apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分佈式鎖的實現,acquire方法用於獲取鎖,release方法用於釋放鎖。

優點:具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。

缺點:因爲需要頻繁的創建和刪除節點,性能上不如Redis方式。

  • 鎖無法釋放?使用Zookeeper可以有效的解決鎖無法釋放的問題,因爲在創建鎖的時候,客戶端會在ZK中創建一個臨時節點,一旦客戶端獲取到鎖之後突然掛掉(Session連接斷開),那麼這個臨時節點就會自動刪除掉。其他客戶端就可以再次獲得鎖。
  • 非阻塞鎖?使用Zookeeper可以實現阻塞的鎖,客戶端可以通過在ZK中創建順序節點,並且在節點上綁定監聽器,一旦節點有變化,Zookeeper會通知客戶端,客戶端可以檢查自己創建的節點是不是當前所有節點中序號最小的,如果是,那麼自己就獲取到鎖,便可以執行業務邏輯了。
  • 不可重入?使用Zookeeper也可以有效的解決不可重入的問題,客戶端在創建節點的時候,把當前客戶端的主機信息和線程信息直接寫入到節點中,下次想要獲取鎖的時候和當前最小的節點中的數據比對一下就可以了。如果和自己的信息一樣,那麼自己直接獲取到鎖,如果不一樣就再創建一個臨時的順序節點,參與排隊。
  • 單點問題?使用Zookeeper可以有效的解決單點問題,ZK是集羣部署的,只要集羣中有半數以上的機器存活,就可以對外提供服務。
  • 可以直接使用zookeeper第三方庫Curator客戶端,這個客戶端中封裝了一個可重入的鎖服務。

  • Zookeeper實現的分佈式鎖其實存在一個缺點,那就是性能上可能並沒有緩存服務那麼高。

  • 因爲每次在創建鎖和釋放鎖的過程中,都要動態創建、銷燬瞬時節點來實現鎖功能。ZK中創建和刪除節點只能通過Leader服務器來執行,然後將數據同不到所有的Follower機器上。
  • 使用Zookeeper實現分佈式鎖的優點: 有效的解決單點問題,不可重入問題,非阻塞問題以及鎖無法釋放的問題。實現起來較爲簡單。

  • 使用Zookeeper實現分佈式鎖的缺點 : 性能上不如使用緩存實現分佈式鎖。 需要對ZK的原理有所瞭解。

4.三種方案的比較

  • 從理解的難易程度角度(從低到高): 數據庫 > 緩存 > Zookeeper

  • 從實現的複雜性角度(從低到高): Zookeeper >= 緩存 > 數據庫

  • 從性能角度(從高到低): 緩存 > Zookeeper >= 數據庫

  • 從可靠性角度(從高到低): Zookeeper > 緩存 > 數據庫

在實踐中,當然是從以可靠性爲主。所以首推Zookeeper。

分佈式CAP理論

分佈式系統架構理論,定義了三種指標,理論說我們最多隻能滿足兩個。

## 分佈式系統

首先我們這個理論所說的分佈式系統,是指系統內會共享數據,互相有連接有交互,才能完成系統功能的的分佈式系統。而這個理論的關注點是**數據**的讀寫。

## 三種指標

- Consistency 一致性:

這裏的一致性是針對於分佈式讀寫的。對於一個分佈式系統,當一條數據寫成功,那麼無論我怎麼使用這個系統,我都應當能馬上讀取到這條最新的數據。

不一致性的例子:我更新了一條微博,而我的關注者還不能看到。

- Avalilability 可用性:

是指系統應當隨時可用,在reasonable的時間內返回reasonable的結果。

一個反例:我更新了一條微博,我的關注者在刷我微博的時候顯示對方正在更新微博,請稍後再試,或者顯示一直在讀取中。

- Partition Toleranc 分區容忍性:

分佈式環境中數據必然會被劃分成多個區分到不同的機器上,不同的機器之間會有數據交換。

而機器一多某臺機器發生發生故障的概率就會比較高,而且機器間數據的交換依賴於網絡,網絡也很有可能會有延時、丟包之類的問題。

分區容忍性就要求在分佈式系統要考慮到分佈式環境的複雜性的前提下能正常提供服務。

(原版cap中的p其實指的是網絡分區現象[參考Wiki network partition] ,只由於網絡設備的影響,分佈式集羣被劃分成多個子網,但這樣理解我始終想不通,可能這樣解釋更合理)

## 三種指標的意義

- CAP-P:

首先我們來看分區容忍性,由於我們討論的就是分佈式環境,我們的系統肯定不能被這網絡環境機器環境所影響,分區容忍性就是個公認的前提,要麼你就是很多個單機提供服務,但那不是分佈式。爲了實現分區容忍性,就需要我們設計多個數據副本,多個副本還不能在同一臺機器上,甚至要在多個機房、多個地區存放副本。如果有必要機器之間的網絡也需要多個通道,防止網絡通路出現問題。

- CAP-CA:

 

前提環境保證了,我們就來討論下讀寫功能。系統的功能無外乎輸出輸出,也就是讀寫操作咯。對於讀寫操作我們需要在一致性和可用性之間有所取捨,當然並不是完全捨去另一方,而是我們不能完美的同時實現C和A。

爲啥呢,前提P已經說了我們需要多副本分佈在多機器上,這副本之間同步數據是會有延時,其次如何保證在寫的同時(副本未同步完成)我的讀操作可能會發生在各個副本上,那我應該如何返回正確的數據。所以C和A只能完美保證一個。

## 例子

- 舍C保A(AP)的例子:

比如剛剛的微博這個例子,我們更新了一條微博,不是所有的人都能馬上刷出來的,對於哪些還只能刷出舊的微博數據的人來說數據就和我真實的操作不一致了。然而這種業務也不需要要求我們強一致性,沒有刷出我的最新微博,也不是什麼大事,大不了認爲我沒有更新而已,對業務影響很小。但是呢也不能一直都不一致是吧,所以C還是不能丟的,可以遲到。

- 舍A保C(CP)的例子:

比如銀行賬戶的例子,大家生活中也許也已經注意到了,銀行轉賬需要幾個小時甚至幾天,都會顯示正在轉賬中。這時就是視作一種丟失可用性的狀態。當然這是業務決定的。

- 舍P保C又保A的場景:

不是分佈式的場景的話,我們可以選擇CA,比如我是個小銀行,我的轉賬功能可以設計爲多地賬戶不互通,只能本地轉賬,只在一臺服務器上操作,保證可用性和一致性。但整體來看可用性和一致性都丟失了。

# 思考 acid

通用的關係型數據庫設計理論,需要滿足四種指標:

- Atomicity 原子性:

- Consistency 一致性:

- Isolation 獨立性:

- Durability 持久性:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章