溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

古有程咬金“三板斧”威震隋唐,現(xiàn)有我“三板斧”大破分布式

發(fā)布時(shí)間:2020-07-06 19:08:12 來(lái)源:網(wǎng)絡(luò) 閱讀:278 作者:wx5d9ed7c8443c3 欄目:編程語(yǔ)言

在分布式系統(tǒng)的很多場(chǎng)景中,我們?yōu)榱吮WC數(shù)據(jù)的最終一致性,需要很多的技術(shù)方案來(lái)支持,比如分布式事務(wù)、分布式鎖等。

有的時(shí)候,我們需要保證一個(gè)方法在同一時(shí)間內(nèi)只能被同一個(gè)線程執(zhí)行。在單機(jī)環(huán)境中,Java中其實(shí)提供了很多并發(fā)處理相關(guān)的API,但是這些API在分布式場(chǎng)景中就無(wú)能為力了。也就是說(shuō)單純的Java Api并不能提供分布式鎖的能力。

目前針對(duì)分布式鎖的實(shí)現(xiàn)目前有多種方案:

  1. 基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖
  2. 基于緩存(redis,memcached)實(shí)現(xiàn)分布式鎖
  3. 基于Zookeeper實(shí)現(xiàn)分布式鎖

在分析這幾種實(shí)現(xiàn)方案之前我們先來(lái)想一下,我們需要的分布式鎖應(yīng)該是怎么樣的?(這里以方法鎖為例,資源鎖同理)

可以保證在分布式部署的應(yīng)用集群中,同一個(gè)方法在同一時(shí)間只能被一臺(tái)機(jī)器上的一個(gè)線程執(zhí)行。

  • 這把鎖要是一把可重入鎖(避免死鎖)
  • 這把鎖最好是一把阻塞鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
  • 有高可用的獲取鎖和釋放鎖功能
  • 獲取鎖和釋放鎖的性能要好

一. 基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖

1.1 基于數(shù)據(jù)庫(kù)表

要實(shí)現(xiàn)分布式鎖,最簡(jiǎn)單的方式可能就是直接創(chuàng)建一張鎖表,然后通過(guò)操作該表中的數(shù)據(jù)來(lái)實(shí)現(xiàn)了。

當(dāng)我們要鎖住某個(gè)方法或資源時(shí),我們就在該表中增加一條記錄,想要釋放鎖的時(shí)候就刪除這條記錄。

創(chuàng)建這樣一張數(shù)據(jù)庫(kù)表:

CREATE TABLE `methodLock` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
    `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法名',
    `desc` varchar(1024) NOT NULL DEFAULT '備注信息',
    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON 
    UPDATE CURRENT_TIMESTAMP COMMENT '保存數(shù)據(jù)時(shí)間,自動(dòng)生成',
    PRIMARY KEY (`id`),
    UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';

當(dāng)我們想要鎖住某個(gè)方法時(shí),執(zhí)行以下SQL:

insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

因?yàn)槲覀儗?duì)method_name做了唯一性約束,這里如果有多個(gè)請(qǐng)求同時(shí)提交到數(shù)據(jù)庫(kù)的話,數(shù)據(jù)庫(kù)會(huì)保證只有一個(gè)操作可以成功,那么我們就可以認(rèn)為操作成功的那個(gè)線程獲得了該方法的鎖,可以執(zhí)行方法體內(nèi)容。

當(dāng)方法執(zhí)行完畢之后,想要釋放鎖的話,需要執(zhí)行以下Sql:

delete from methodLock where method_name ='method_name'

上面這種簡(jiǎn)單的實(shí)現(xiàn)有以下幾個(gè)問(wèn)題:

  • 這把鎖強(qiáng)依賴數(shù)據(jù)庫(kù)的可用性,數(shù)據(jù)庫(kù)是一個(gè)單點(diǎn),一旦數(shù)據(jù)庫(kù)掛掉,會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)不可用。

  • 這把鎖沒(méi)有失效時(shí)間,一旦解鎖操作失敗,就會(huì)導(dǎo)致鎖記錄一直在數(shù)據(jù)庫(kù)中,其他線程無(wú)法再獲得到鎖。

  • 這把鎖只能是非阻塞的,因?yàn)閿?shù)據(jù)的insert操作,一旦插入失敗就會(huì)直接報(bào)錯(cuò)。沒(méi)有獲得鎖的線程并不會(huì)進(jìn)入排隊(duì)隊(duì)列,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作。

  • 這把鎖是非重入的,同一個(gè)線程在沒(méi)有釋放鎖之前無(wú)法再次獲得該鎖。因?yàn)閿?shù)據(jù)中數(shù)據(jù)已經(jīng)存在了。

當(dāng)然,我們也可以有其他方式解決上面的問(wèn)題。

針對(duì) 數(shù)據(jù)庫(kù)是單點(diǎn)問(wèn)題搞兩個(gè)數(shù)據(jù)庫(kù),數(shù)據(jù)之前雙向同步。一旦掛掉快速切換到備庫(kù)上。

針對(duì) 沒(méi)有失效時(shí)間?只要做一個(gè)定時(shí)任務(wù),每隔一定時(shí)間把數(shù)據(jù)庫(kù)中的超時(shí)數(shù)據(jù)清理一遍。

針對(duì) 非阻塞的?搞一個(gè)while循環(huán),直到insert成功再返回成功。

針對(duì) 非重入的?在數(shù)據(jù)庫(kù)表中加個(gè)字段,記錄當(dāng)前獲得鎖的機(jī)器的主機(jī)信息和線程信息,那么下次再獲取鎖的時(shí)候先查詢數(shù)據(jù)庫(kù),如果當(dāng)前機(jī)器的主機(jī)信息和線程信息在數(shù)據(jù)庫(kù)可以查到的話,直接把鎖分配給他就可以了。

1.2 基于數(shù)據(jù)庫(kù)排他鎖

除了可以通過(guò)增刪操作數(shù)據(jù)表中的記錄以外,其實(shí)還可以借助數(shù)據(jù)中自帶的鎖來(lái)實(shí)現(xiàn)分布式的鎖。

我們還用剛剛創(chuàng)建的那張數(shù)據(jù)庫(kù)表??梢酝ㄟ^(guò)數(shù)據(jù)庫(kù)的排他鎖來(lái)實(shí)現(xiàn)分布式鎖。 基于MySql的InnoDB引擎,可以使用以下方法來(lái)實(shí)現(xiàn)加鎖操作:

public boolean lock(){
    connection.setAutoCommit(false)
    while(true){
        try{
            result = select * from methodLock where method_name=xxx 
            for update;
            if(result==null){
                return true;
            }
        }catch(Exception e){

        }
        sleep(1000);
    }
    return false;
}

在查詢語(yǔ)句后面增加for update,數(shù)據(jù)庫(kù)會(huì)在查詢過(guò)程中給數(shù)據(jù)庫(kù)表增加排他鎖。當(dāng)某條記錄被加上排他鎖之后,其他線程無(wú)法再在該行記錄上增加排他鎖。

我們可以認(rèn)為獲得排它鎖的線程即可獲得分布式鎖,當(dāng)獲取到鎖之后,可以執(zhí)行方法的業(yè)務(wù)邏輯,執(zhí)行完方法之后,再通過(guò)以下方法解鎖:

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

通過(guò)connection.commit()操作來(lái)釋放鎖。

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

阻塞鎖? for update語(yǔ)句會(huì)在執(zhí)行成功后立即返回,在執(zhí)行失敗時(shí)一直處于阻塞狀態(tài),直到成功。

鎖定之后 服務(wù)宕機(jī),無(wú)法釋放?使用這種方式,服務(wù)宕機(jī)之后數(shù)據(jù)庫(kù)會(huì)自己把鎖釋放掉。
但是還是無(wú)法直接解決數(shù)據(jù)庫(kù)單點(diǎn)和可重入問(wèn)題。

1.3 總結(jié)

總結(jié)一下使用數(shù)據(jù)庫(kù)來(lái)實(shí)現(xiàn)分布式鎖的方式,這兩種方式都是依賴數(shù)據(jù)庫(kù)的一張表,一種是通過(guò)表中的記錄的存在情況確定當(dāng)前是否有鎖存在,另外一種是通過(guò)數(shù)據(jù)庫(kù)的排他鎖來(lái)實(shí)現(xiàn)分布式鎖。

數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖的 優(yōu)點(diǎn): 直接借助數(shù)據(jù)庫(kù),容易理解。

數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖的 缺點(diǎn): 會(huì)有各種各樣的問(wèn)題,在解決問(wèn)題的過(guò)程中會(huì)使整個(gè)方案變得越來(lái)越復(fù)雜。

操作數(shù)據(jù)庫(kù)需要一定的開(kāi)銷,性能問(wèn)題需要考慮。

二.基于緩存實(shí)現(xiàn)分布式鎖

相比較于基于數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖的方案來(lái)說(shuō),基于緩存來(lái)實(shí)現(xiàn)在性能方面會(huì)表現(xiàn)的更好一點(diǎn)。而且很多緩存是可以集群部署的,可以解決單點(diǎn)問(wèn)題。

目前有很多成熟的緩存產(chǎn)品,包括Redis,memcached等。

在實(shí)現(xiàn)的時(shí)候要注意的幾個(gè)關(guān)鍵點(diǎn):

  1. 鎖信息必須是會(huì)過(guò)期超時(shí)的,不能讓一個(gè)線程長(zhǎng)期占有一個(gè)鎖而導(dǎo)致死鎖;

  2. 同一時(shí)刻只能有一個(gè)線程獲取到鎖。

幾個(gè)要用到的redis命令:

setnx(key, value):“set if not exits”,若該key-value不存在,則成功加入緩存并且返回1,否則返回0。

get(key):獲得key對(duì)應(yīng)的value值,若不存在則返回nil。

getset(key, value):先獲取key對(duì)應(yīng)的value值,若不存在則返回nil,然后將舊的value更新為新的value。

expire(key, seconds):設(shè)置key-value的有效期為seconds秒。

看一下流程圖:

古有程咬金“三板斧”威震隋唐,現(xiàn)有我“三板斧”大破分布式

在這個(gè)流程下,不會(huì)導(dǎo)致死鎖。

我采用Jedis作為Redis客戶端的api,下面來(lái)看一下具體實(shí)現(xiàn)的代碼。

(1)首先要?jiǎng)?chuàng)建一個(gè)Redis連接池。
public class RedisPool {

    private static JedisPool pool;//jedis連接池

    private static int maxTotal = 20;//最大連接數(shù)

    private static int maxIdle = 10;//最大空閑連接數(shù)

    private static int minIdle = 5;//最小空閑連接數(shù)

    private static boolean testOnBorrow = true;//在取連接時(shí)測(cè)試連接的可用性

    private static boolean testOnReturn = false;//再還連接時(shí)不測(cè)試連接的可用性

    static {
        initPool();//初始化連接池
    }

    public static Jedis getJedis(){
        return pool.getResource();
    }

    public static void close(Jedis jedis){
        jedis.close();
    }

    private static void initPool(){
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(maxTotal);
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);
        config.setTestOnBorrow(testOnBorrow);
        config.setTestOnReturn(testOnReturn);
        config.setBlockWhenExhausted(true);
        pool = new JedisPool(config, "127.0.0.1", 6379, 5000, "liqiyao");
    }
}
(2)對(duì)Jedis的api進(jìn)行封裝,封裝一些實(shí)現(xiàn)分布式鎖需要用到的操作。
public class RedisPoolUtil {

    private RedisPoolUtil(){}

    private static RedisPool redisPool;

    public static String get(String key){
        Jedis jedis = null;
        String result = null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.get(key);
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
            return result;
        }
    }

    public static Long setnx(String key, String value){
        Jedis jedis = null;
        Long result = null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.setnx(key, value);
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
            return result;
        }
    }

    public static String getSet(String key, String value){
        Jedis jedis = null;
        String result = null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.getSet(key, value);
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
            return result;
        }
    }

    public static Long expire(String key, int seconds){
        Jedis jedis = null;
        Long result = null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.expire(key, seconds);
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
            return result;
        }
    }

    public static Long del(String key){
        Jedis jedis = null;
        Long result = null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.del(key);
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            if (jedis != null) {
                jedis.close();
            }
            return result;
        }
    }
}
(3)分布式鎖工具類
public class DistributedLockUtil {

    private DistributedLockUtil(){
    }

    public static boolean lock(String lockName){//lockName可以為共享變量
    名,也可以為方法名,主要是用于模擬鎖信息
        System.out.println(Thread.currentThread() + "開(kāi)始嘗試加鎖!");
        Long result = RedisPoolUtil.setnx
        (lockName, String.valueOf(System.currentTimeMillis() + 5000));
        if (result != null && result.intValue() == 1){
            System.out.println(Thread.currentThread() + "加鎖成功!");
            RedisPoolUtil.expire(lockName, 5);
            System.out.println(Thread.currentThread() + "執(zhí)行業(yè)務(wù)邏輯!");
            RedisPoolUtil.del(lockName);
            return true;
        } else {
            String lockValueA = RedisPoolUtil.get(lockName);
            if (lockValueA != null && Long.parseLong(lockValueA) >= 
            System.currentTimeMillis()){
                String lockValueB = RedisPoolUtil.getSet(lockName, 
                String.valueOf(System.currentTimeMillis() + 5000));
                if (lockValueB == null || lockValueB.equals(lockValueA)){
                    System.out.println(Thread.currentThread() + "加鎖成功!");
                    RedisPoolUtil.expire(lockName, 5);
                    System.out.println(Thread.currentThread() + "執(zhí)行業(yè)務(wù)邏輯!");
                    RedisPoolUtil.del(lockName);
                    return true;
                } else {
                    return false;
                }
            } else {
                return false;
            }
        }
    }
}

三. 基于Zookeeper實(shí)現(xiàn)分布式鎖

基于zookeeper臨時(shí)有序節(jié)點(diǎn)可以實(shí)現(xiàn)的分布式鎖。大致思想即為:每個(gè)客戶端對(duì)某個(gè)方法加鎖時(shí),在zookeeper上的與該方法對(duì)應(yīng)的指定節(jié)點(diǎn)的目錄下,生成一個(gè)唯一的

瞬時(shí)有序節(jié)點(diǎn)。 判斷是否獲取鎖的方式很簡(jiǎn)單,只需要判斷有序節(jié)點(diǎn)中序號(hào)最小的一個(gè)。 當(dāng)釋放鎖的時(shí)候,只需將這個(gè)瞬時(shí)節(jié)點(diǎn)刪除即可。同時(shí),其可以避免服務(wù)宕機(jī)導(dǎo)致的鎖無(wú)法釋放,而產(chǎn)生的死鎖問(wèn)題。

來(lái)看下Zookeeper能不能解決前面提到的問(wèn)題。

鎖無(wú)法釋放?
使用Zookeeper可以有效的解決鎖無(wú)法釋放的問(wèn)題,因?yàn)樵趧?chuàng)建鎖的時(shí)候,客戶端會(huì)在ZK中創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn),一旦客戶端獲取到鎖之后突然掛掉(Session連接斷開(kāi)),那么這個(gè)臨時(shí)節(jié)點(diǎn)就會(huì)自動(dòng)刪除掉。其他客戶端就可以再次獲得鎖。

非阻塞鎖?
使用Zookeeper可以實(shí)現(xiàn)阻塞的鎖,客戶端可以通過(guò)在ZK中創(chuàng)建順序節(jié)點(diǎn),并且在節(jié)點(diǎn)上綁定監(jiān)聽(tīng)器,一旦節(jié)點(diǎn)有變化,Zookeeper會(huì)通知客戶端,客戶端可以檢查自己創(chuàng)建的節(jié)點(diǎn)是不是當(dāng)前所有節(jié)點(diǎn)中序號(hào)最小的,如果是,那么自己就獲取到鎖,便可以執(zhí)行業(yè)務(wù)邏輯了。

不可重入
使用Zookeeper也可以有效的解決不可重入的問(wèn)題,客戶端在創(chuàng)建節(jié)點(diǎn)的時(shí)候,把當(dāng)前客戶端的主機(jī)信息和線程信息直接寫(xiě)入到節(jié)點(diǎn)中,下次想要獲取鎖的時(shí)候和當(dāng)前最小的節(jié)點(diǎn)中的數(shù)據(jù)比對(duì)一下就可以了。如果和自己的信息一樣,那么自己直接獲取到鎖,如果不一樣就再創(chuàng)建一個(gè)臨時(shí)的順序節(jié)點(diǎn),參與排隊(duì)。

單點(diǎn)問(wèn)題
使用Zookeeper可以有效的解決單點(diǎn)問(wèn)題,ZK是集群部署的,只要集群中有半數(shù)以上的機(jī)器存活,就可以對(duì)外提供服務(wù)。
可以直接使用zookeeper第三方庫(kù)Curator客戶端,這個(gè)客戶端中封裝了一個(gè)可重入的鎖服務(wù)。

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;
}

Curator提供的InterProcessMutex是分布式鎖的實(shí)現(xiàn)。acquire方法用戶獲取鎖,release方法用于釋放鎖。

使用ZK實(shí)現(xiàn)的分布式鎖好像完全符合了本文開(kāi)頭我們對(duì)一個(gè)分布式鎖的所有期望。但是,其實(shí)并不是,Zookeeper實(shí)現(xiàn)的分布式鎖其實(shí)存在一個(gè)缺點(diǎn),那就是性能上可能并沒(méi)有緩存服務(wù)
那么高。因?yàn)槊看卧趧?chuàng)建鎖和釋放鎖的過(guò)程中,都要?jiǎng)討B(tài)創(chuàng)建、銷毀瞬時(shí)節(jié)點(diǎn)來(lái)實(shí)現(xiàn)鎖功能。ZK中創(chuàng)建和刪除節(jié)點(diǎn)只能通過(guò)Leader服務(wù)器來(lái)執(zhí)行,然后將數(shù)據(jù)同不到所有的Follower機(jī)器上。

總結(jié)

使用Zookeeper實(shí)現(xiàn)分布式鎖的優(yōu)點(diǎn): 有效的解決單點(diǎn)問(wèn)題,不可重入問(wèn)題,非阻塞問(wèn)題以及鎖無(wú)法釋放的問(wèn)題。實(shí)現(xiàn)起來(lái)較為簡(jiǎn)單。

使用Zookeeper實(shí)現(xiàn)分布式鎖的缺點(diǎn) : 性能上不如使用緩存實(shí)現(xiàn)分布式鎖。 需要對(duì)ZK的原理有所了解。

四.三種方案的比較

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

從實(shí)現(xiàn)的復(fù)雜性角度(從低到高): Zookeeper >= 緩存 > 數(shù)據(jù)庫(kù)

從性能角度(從高到低): 緩存 > Zookeeper >= 數(shù)據(jù)庫(kù)

從可靠性角度(從高到低): Zookeeper > 緩存 > 數(shù)據(jù)庫(kù)

因此我個(gè)人更加傾向于使用緩存來(lái)實(shí)現(xiàn),后續(xù)的文章中會(huì)基于Redis封裝一個(gè)我們自己的分布式鎖實(shí)現(xiàn)。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI