分布式鎖的實現(xiàn)

前言

我們在開發(fā)應用時,如果需要對一個共享變量進行多線程同步訪問的時候奸焙,我們可以使用Java多線程的各個技能點來處理嫌术,保證完美運行無BUG。
但是這里的都只是單機應用曙求,即在同一個JVM中碍庵;然后隨著業(yè)務發(fā)展、微服務化悟狱,一個應用需要部署到多臺服務器上然后做負載均衡静浴,大概的架構圖如下:


集群模式下的負載均衡

在上圖可以看到,變量A在JVM1挤渐、JVM2苹享、JVM3三個JVM內存中(這個變量A主要體現(xiàn)是在一個類中的一個成員變量,是一個有狀態(tài)的對象)浴麻,如果我們不加任何控制的話得问,變量A同進都會在JVM分配一塊內存,三個請求發(fā)過來同時對這個變量進行操作软免,顯然結果不是我們想要的宫纬。

如果我們業(yè)務中存在這樣的場景的話,就需要找到一種方法來解決膏萧。

為了保證一個方法或屬性在高并發(fā)的情況下同一時間只能被同一個線程執(zhí)行漓骚,在傳統(tǒng)單機部署的情況下蝌衔,可以使用Java并發(fā)處理相關的API(如ReentrantLockSynchronized)進行互斥控制。但是认境,隨之業(yè)務發(fā)展的需要胚委,原單機部署的系統(tǒng)演化成分布式集群系統(tǒng)后,由于分布式系統(tǒng)多線程叉信、多進程并且分布在不同的機器上亩冬,這將原來的單機部署情況下的并發(fā)控制鎖策略失效,單純的Java API并不能提供分布式鎖的能力硼身。
為了解決這個問題硅急,就需要一種跨JVM的互斥機制來控制共享資源的訪問,這就是分布式鎖要解決的問題佳遂!

分布式鎖應該具備哪些條件

  • 在分布式系統(tǒng)環(huán)境下营袜,一個方法在同一時間只能被一個機器的一個線程執(zhí)行;
  • 高可用丑罪、高性能的獲取鎖與釋放鎖荚板;
  • 具備可重入特性;
  • 具備鎖失效機制吩屹、防止死鎖跪另;
  • 具備非阻塞鎖特性,即沒有獲取到鎖直接返回獲取鎖失斆核选免绿;

分布式鎖的實現(xiàn)方式

目前幾乎所有大型網站及應用都是分布式部署,分布式場景中的數(shù)據(jù)一致性問題一直是一個比較重要的話題擦盾,分布式的CAP理論告訴我們任何一個分布式系統(tǒng)都無法同時滿足一致性(Consistency)嘲驾、可用性(Availability)和分區(qū)容錯性(Partition tolerance),最多只能同時滿足兩項
一般情況下迹卢,都需要犧牲強一致性來換取系統(tǒng)的高可用性辽故,系統(tǒng)往往只需要保證最終一致性,只要這個最終時間是在用戶可以接受的范圍內即可婶希。
在很多時候榕暇,為了保證數(shù)據(jù)的最終一致性,需要很多的技術方案來支持喻杈,比如分布式事務彤枢、分布式鎖等。有的時候筒饰,我們需要保證一信方法在同一時間內只能被同一個線程執(zhí)行缴啡。
而分布式鎖的具體實現(xiàn)方案有如下三種:

基于數(shù)據(jù)庫實現(xiàn);
基于緩存(Redis等)實現(xiàn)瓷们;
基于Zookeeper實現(xiàn)业栅;

以上盡管有三種方案秒咐,但是我們需要根據(jù)不同的業(yè)務進行選型。

基于數(shù)據(jù)庫的實現(xiàn)方式

基于數(shù)據(jù)庫的實現(xiàn)方式的思想核心為:

在數(shù)據(jù)庫中創(chuàng)建一個表碘裕,表中包含方法名等字段携取,并在方法名字段上創(chuàng)建唯一索引,想要執(zhí)行某個方法帮孔,就使用這個方法名向表中插入數(shù)據(jù)雷滋,成功插入則獲取鎖,執(zhí)行完成后刪除對應的行數(shù)據(jù)釋放鎖文兢。

一晤斩、創(chuàng)建一個表

DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
  `id`          INT(11) UNSIGNED NOT NULL AUTO_INCREMENT
  COMMENT '主鍵',
  `method_name` VARCHAR(64)      NOT NULL
  COMMENT '鎖定的方法名',
  `desc`        VARCHAR(255)     NOT NULL
  COMMENT '備注信息',
  `update_time` TIMESTAMP        NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
)
  ENGINE = InnoDB
  AUTO_INCREMENT = 3
  DEFAULT CHARSET = utf8
  COMMENT = '鎖定中的方法';

二、想要執(zhí)行某個方法姆坚,就使用這個方法名向表中插入數(shù)據(jù)

INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '測試的methodName');

由于我們對method_name做了唯一性約束澳泵,如果有多個請求同時提交插入操作時,數(shù)據(jù)庫能確保只有一個操作可以成功兼呵,那么我們就可以認為操作成功的那個線程獲得了該方法的鎖兔辅,可以執(zhí)行方法體中的內容。

三击喂、執(zhí)行完成后幢妄,刪除對應的行數(shù)據(jù)釋放鎖

delete from method_lock where method_name ='methodName';

這里只是基于數(shù)據(jù)庫實現(xiàn)的一種方法(比較粗的一種)。
但是對于分布式鎖應該具備的條件來說茫负,還有一些問題需要解決及優(yōu)化

  • 因為是基于數(shù)據(jù)庫實現(xiàn)的,數(shù)據(jù)庫的可用性和性能將直接影響分布式鎖的可用性及性能乎赴。所以忍法,數(shù)據(jù)庫需要雙機部署、數(shù)據(jù)同步榕吼、主備切換饿序;
  • 它不具備可重入的特性,因為同一個線程在釋放鎖之前羹蚣,行數(shù)據(jù)一直存在原探,無法再次成功插入數(shù)據(jù)。所以顽素,需要在表中新增一列咽弦,用于記錄當前獲取到鎖的機器和線程信息,在再次獲取鎖的時候胁出,先查詢表中機器和線程信息是否和當前機器線程相同型型,若相同則直接獲取鎖。
  • 沒有鎖失效機制全蝶,因為有可能出現(xiàn)成功插入數(shù)據(jù)后闹蒜,服務器宕機了寺枉,對應的數(shù)據(jù)沒有被刪除,當服務恢復后一直獲取不到鎖绷落,所以姥闪,需要在表中新增一列,用于記錄失效時間砌烁,并且需要有定時任務清除這些失效的數(shù)據(jù)筐喳;
  • 不具備阻塞鎖特性,獲取不到鎖直接返回失敗往弓,所以需要優(yōu)化獲取邏輯疏唾,循環(huán)多次去獲取函似;
  • 依賴數(shù)據(jù)庫需要一定的資源開銷槐脏,性能問題需要考慮;

基于緩存(Redis)的實現(xiàn)方式

使用Redis實現(xiàn)分布式鎖的理由:

  1. Redis具有很高的性能撇寞;
  2. Redis的命令對此支持較好顿天,實現(xiàn)起來很方便;

Redis命令介紹:

SETNX

// 當且僅當key不存在時蔑担,set一個key為val的字符串牌废,返回1;
// 若key存在啤握,則什么都不做鸟缕,返回0。
SETNX key val;

expire

// 為key設置一個超時時間排抬,單位為second懂从,超過這個時間鎖會自動釋放,避免死鎖蹲蒲。
expire key timeout;

delete

// 刪除key
delete key;

我們通過Redis實現(xiàn)分布式鎖時番甩,主要通過上面的這三個命令。

通過Redis實現(xiàn)分布式的核心思想為:

  1. 獲取鎖的時候届搁,使用setnx加鎖缘薛,并使用expire命令為鎖添加一個超時時間,超過該時間自動釋放鎖卡睦,鎖的value值為一個隨機生成的UUID宴胧,通過這個value值,在釋放鎖的時候進行判斷表锻。
  2. 獲取鎖的時候還設置一個獲取的超時時間牺汤,若超過這個時間則放棄獲取鎖。
    3.釋放鎖的時候浩嫌,通過UUID判斷是不是當前持有的鎖檐迟,若時該鎖补胚,則執(zhí)行delete進行鎖釋放。

具體實現(xiàn)代碼如下:

public class DistributedLock {

    private final JedisPool jedisPool;
    private final static String KEY_PREF = "lock:"; // 鎖的前綴

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

    /**
     * 加鎖
     *
     * @param lockName       String 鎖的名稱(key)
     * @param acquireTimeout long 獲取超時時間
     * @param timeout        long 鎖的超時時間
     * @return 鎖標識
     */
    public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
        Jedis conn = null;

        try {
            // 獲取連接
            conn = jedisPool.getResource();
            // 隨機生成一個value
            String identifier = UUID.randomUUID().toString();
            // 鎖名,即 key值
            String lockKey = KEY_PREF + lockName;
            // 超時時間, 上鎖后超過此時間則自動釋放鎖
            int lockExpire = (int) (timeout / 1000);

            // 獲取鎖的超時時間,超過這個時間則放棄獲取鎖
            long end = System.currentTimeMillis() + acquireTimeout;
            while (System.currentTimeMillis() < end) {
                if (conn.setnx(lockKey, identifier) == 1) {
                    conn.expire(lockKey, lockExpire);
                    // 返回value值,用于釋放鎖時間確認
                    return identifier;
                }

                // 返回-1代表key沒有設置超時時間,為key設置一個超時時間
                if (conn.ttl(lockKey) == -1) {
                    conn.expire(lockKey, lockExpire);
                }

                try {
                    Thread.sleep(10);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (JedisException e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return null;
    }

    /**
     * 釋放鎖
     *
     * @param lockName   String 鎖key
     * @param identifier String 釋放鎖的標識
     * @return boolean
     */
    public boolean releaseLock(String lockName, String identifier) {
        Jedis conn = null;
        String lockKey = KEY_PREF + lockName;
        boolean retFlag = false;
        try {
            conn = jedisPool.getResource();
            while (true) {
                // 監(jiān)視lock, 準備開始事務
                conn.watch(lockKey);
                // 通過前面返回的value值判斷是不是該鎖,若時該鎖,則刪除釋放鎖
                if (identifier.equals(conn.get(lockKey))) {
                    Transaction transaction = conn.multi();
                    transaction.del(lockKey);
                    List<Object> results = transaction.exec();
                    if (results == null) continue;

                    retFlag = true;
                }

                conn.unwatch();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
        return retFlag;
    }
}

基于Zookeeper的實現(xiàn)方式

基于Zookeeper臨時有序節(jié)點同樣可以實現(xiàn)分布式鎖追迟。
大致思想為:

每個客戶端對某個方法加鎖時溶其,在zookeeper上的該方法對應的指定節(jié)點目錄下,生成一個唯一的瞬時有序節(jié)點敦间。
判斷是否獲取鎖的方式很簡單瓶逃,只需要判斷有序節(jié)點中序號最小的一個。如果獲取到比自己小的兄弟節(jié)點不存在廓块,則說明當前線程順序號最小厢绝,獲得鎖。
如果判斷自己不是那最小的一個節(jié)點带猴,則設置監(jiān)聽比自己次小的節(jié)點昔汉;
如果已處理完成,則刪除自己的節(jié)點拴清。

優(yōu)點
具備高可用靶病、可重入、阻塞鎖特性口予、可解決失效死鎖問題娄周。
缺點
因為需要頻繁的創(chuàng)建和刪除節(jié)點,性能上不如Redis方式沪停。
PS
在這里有一個很好用的Zookeeper客戶端開源庫Apache Curator

Apache Curator

三種方案的比較

從理解的難易程度(從低到高)

數(shù)據(jù)庫 > 緩存 > Zookeeper

從實現(xiàn)的復雜性角度(從低到高)

Zookeeper >= 緩存 > 數(shù)據(jù)庫

從性能角度(從高到低)

緩存 > Zookeeper >= 數(shù)據(jù)庫

從可靠性角度(從高到低)

Zookeeper > 緩存 > 數(shù)據(jù)庫

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末煤辨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子木张,更是在濱河造成了極大的恐慌掷酗,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窟哺,死亡現(xiàn)場離奇詭異,居然都是意外死亡技肩,警方通過查閱死者的電腦和手機且轨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來虚婿,“玉大人旋奢,你說我怎么就攤上這事∪蝗” “怎么了至朗?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長剧浸。 經常有香客問我锹引,道長矗钟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任嫌变,我火速辦了婚禮吨艇,結果婚禮上,老公的妹妹穿的比我還像新娘腾啥。我一直安慰自己东涡,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布倘待。 她就那樣靜靜地躺著疮跑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪凸舵。 梳的紋絲不亂的頭發(fā)上祖娘,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音贞间,去河邊找鬼贿条。 笑死,一個胖子當著我的面吹牛增热,可吹牛的內容都是我干的整以。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼峻仇,長吁一口氣:“原來是場噩夢啊……” “哼公黑!你這毒婦竟也來了?” 一聲冷哼從身側響起摄咆,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤凡蚜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后吭从,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體朝蜘,經...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年涩金,在試婚紗的時候發(fā)現(xiàn)自己被綠了谱醇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡步做,死狀恐怖副渴,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情全度,我是刑警寧澤煮剧,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響勉盅,放射性物質發(fā)生泄漏佑颇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一菇篡、第九天 我趴在偏房一處隱蔽的房頂上張望漩符。 院中可真熱鬧,春花似錦驱还、人聲如沸嗜暴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闷沥。三九已至,卻和暖如春咐容,著一層夾襖步出監(jiān)牢的瞬間舆逃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工戳粒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留路狮,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓蔚约,卻偏偏與公主長得像奄妨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子苹祟,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353