淺談分布式鎖的實現

為什么要使用鎖

業(yè)務在同一時刻只能有一個實例在運行,比如活動開獎、耗時數據庫操作等場景玖喘,通常都以保證業(yè)務執(zhí)行正常、節(jié)約服務器資源或者提高程序健壯性為目的曹鸠。

引子 —— 文件鎖實現鎖

借助文件系統(tǒng)自帶的鎖機制 —— 排它鎖 來實現煌茬,即一個進程在啟動時獲得一個文件的排它鎖,并在自己的整個運行期間都保留句柄資源而不釋放鎖彻桃,使得另一個進程實例在啟動時想要獲得同一文件時失敗坛善,從而保證在 單臺機器上同一時刻至多只有一個實例 在運行
PHP代碼實現如下:

<?php
$filename = __FILE__ . '.lock';
$handle = fopen($filename, 'w');
if (!flock($handle, LOCK_EX | LOCK_NB)) {
    // 獲取排它鎖不成功,說明已經有其他進程獲取到文件鎖邻眷,視作已有實例在執(zhí)行眠屎,當前進程退出
    echo "當前已經有進程在執(zhí)行,本進程不執(zhí)行", PHP_EOL;
    exit();
}
/* 業(yè)務邏輯 */

在這里講一個本人在實踐中遇到的一個小坑
上述示例代碼中沒有采用面向對象的程序設計肆饶,習慣面向對象編碼的同學喜歡將上述程序過程整合到一個類里頭改衩,注意這個時候需要保證 $handle 在你的方法結束之后還沒有被釋放,如下面這樣的寫法就是有問題的:

<?php
/* 這段示例代碼是有問題的驯镊,是個錯誤示范葫督,可不要在自己的項目中使用噢 */
class Locker {
    public static function doLock() {
        $filename = __FILE__ . '.lock';
        $handle = fopen($filename, 'w');
        if (!flock($handle, LOCK_EX | LOCK_NB)) {
            // 獲取排它鎖不成功,說明已經有其他進程獲取到文件鎖板惑,視作已有實例在執(zhí)行橄镜,當前進程退出
            echo "當前已經有進程在執(zhí)行,本進程不執(zhí)行", PHP_EOL;
            exit();
        }
    }
}

Locker::doLocker();
/* 業(yè)務邏輯 */

因為 doLocker() 方法執(zhí)行完畢之后冯乘,句柄 $handle 作為局部變量會被立即會收掉洽胶,所以排它鎖也會被釋放掉,進程鎖就直接失效了裆馒。

  • 優(yōu)點
    穩(wěn)定妖异!本人在不計其數的業(yè)務中使用過文件鎖,兩年多程序員職業(yè)生涯還沒有在此方面翻過車领追。只要磁盤不出問題他膳,文件鎖還是很給力的;

  • 缺點
    從上述示例可見绒窑,文件鎖依賴本次文件系統(tǒng)棕孙,只能在單個操作系統(tǒng)中產生作用。
    由于業(yè)務的橫向擴展些膨,通常情況下一套業(yè)務需要部署在多臺服務器上蟀俊,此時文件鎖便不能滿足需求了。
    如果要實現跨越操作系統(tǒng)的鎖限制订雾,則必須引入 外部存儲方案;

幾個分布式鎖實現方案

從理論上來說肢预,任何第三方的存儲都能夠實現本地文件系統(tǒng)功能。只不過各種操作需要走網絡IO洼哎,在穩(wěn)定性烫映、速率上同本地文件系統(tǒng)會存在一定的差距沼本。下面來研究一下幾個比較常用的外部存儲方案來實現分布式鎖。

MySQL

論及最常用的外部存儲方案锭沟,MySQL作為從大學時期就接觸的數據庫選型抽兆,必然首當其沖。使用數據庫實現分布式鎖也有兩種方式:僅將數據庫作為存儲數據庫排它鎖族淮;

僅將數據庫作為存儲

  • 基本思路

    1. 定義鎖的標識符辫红,這個標識符作為數據庫表中的表征字段(主鍵),以此字段查詢表中是否存在對應記錄祝辣,若存在贴妻,則說明存在鎖,則稍后再來檢查蝙斜;否則執(zhí)行下一步揍瑟;
    2. 執(zhí)行 INSERT 語句插入鎖信息,此時可能有好幾個進程同時執(zhí)行插入語句乍炉,但是由于插入字段中會含有主鍵,所以只有會一條 INSERT語句執(zhí)行成功滤馍,執(zhí)行成功的實例獲得排它鎖岛琼,則開始執(zhí)行自己的業(yè)務邏輯;其他執(zhí)行失敗的實例則稍后再來檢查巢株,從第一步重新開始槐瑞;
    3. 業(yè)務邏輯執(zhí)行完畢之后,執(zhí)行 DELETE 語句刪除掉數據庫中的鎖記錄從而釋放鎖阁苞;
    4. 另外起一個維護鎖的業(yè)務來定期刪除掉數據庫中過期的鎖記錄困檩,防止因為程序意外退出而沒有刪除掉鎖記錄造成死鎖;
  • 存在問題
    上述業(yè)務雖然能夠滿足簡單的一些需求那槽,但是還是存在問題:

    1. 需要保證MySQL服務的高可用悼沿;
    2. 必須使用輪詢的方式去檢查和獲得鎖,輪詢間隔時間長了骚灸,業(yè)務執(zhí)行中斷到重新啟動業(yè)務之間存在空檔期變長了糟趾,不能忍受中斷時間過長的業(yè)務不適用;輪詢間隔短了甚牲,MySQL操作將會變得頻繁义郑,一旦業(yè)務增多,將會出現性能問題丈钙;
    3. 最后一步中的死鎖清理存在誤判風險非驮,存在業(yè)務還未執(zhí)行完畢鎖就被清除的情況,從而導致同一時刻運行多個實例雏赦;

利用數據庫的排它鎖

采用數據庫排它鎖必須滿足兩個條件:

  1. 數據庫表格使用 InnoDB 引擎劫笙;
  2. 必須使用到表格主鍵芙扎,否則會將整個表格都鎖住邀摆;
  • 表格設計:
字段 類型 注釋
name VARCHAR(100) Primary Key 名稱
info VARCHAR(100) - 名稱
created_at INT(10) UNSIGNED - 創(chuàng)建時間

構造語句如下:

CREATE TABLE `business_lock` (
    `name` VARCHAR(100) NOT NULL COMMENT '名稱',
    `info` VARCHAR(100) NOT NULL COMMENT '信息',
    `created_at` INT(10) UNSIGNED NOT NULL COMMENT '創(chuàng)建時間',
    PRIMARY KEY (`name`)
)
COMMENT='業(yè)務鎖'
ENGINE=InnoDB
;
  • 基本思路

    1. 假定鎖名稱為 lock 纵顾,則先到數據庫中查詢是否存在 name = 'lock' 的記錄,不存在則 INSERT 一條栋盹,再向下執(zhí)行施逾;否則直接向下執(zhí)行;
    2. 執(zhí)行數據庫語句:
      START TRANSACTION; SELECT * FROM `business_lock` WHERE `name` = 'lock' FOR UPDATE;
      
      執(zhí)行之后例获,由于數據行鎖的作用汉额,只有有一個實例會直接返回記錄信息,其他的實例都會進入阻塞狀態(tài)榨汤;
    3. 執(zhí)行業(yè)務邏輯蠕搜,執(zhí)行完畢之后,只用 COMMIT 語句釋放行鎖收壕;
  • 優(yōu)點

    1. 使用了MySQL行鎖帶來的阻塞特性妓灌,使得實例A掛掉,實例B馬上可以接替A的工作蜜宪,期間空檔期會縮短虫埂;
    2. 不用輪詢MySQL;
  • 缺點

    1. 業(yè)務增多并且MySQL的排它鎖長期不釋放圃验,會導致MySQL的連接變多掉伏,占據大量的MySQL連接池資源;

Redis

除了MySQL這樣的關系型數據庫之外澳窑,我們用得最多的就是 Redis 斧散,Redis 作為非關系型的內存數據庫,在執(zhí)行速度上比MySQL要快上不少摊聋。Redis實現分布式鎖本質上和上面介紹的第一種MySQL方案是一樣的鸡捐。

單點 Redis

  • 基本思路

    1. 定義鎖的標識符,并生成 token麻裁;
    2. 以標識符作為 key 執(zhí)行 setnx 設置值為 tokenexpire語句 (用LUA封裝闯参,保證原子性),若數據庫中無記錄悲立,則會執(zhí)行成功鹿寨,表示實例獲得鎖;否則執(zhí)行失敗表示鎖已經被其他實例已經獲得鎖薪夕,不繼續(xù)向下執(zhí)行脚草;
    3. 執(zhí)行業(yè)務邏輯,實例結束之前執(zhí)行獲取 key 對應的內容原献,如果內容和 token 相同馏慨,則執(zhí)行刪除(get 和 del 操作用LUA進行封裝來保證原子性)埂淮;
  • 優(yōu)點

  1. 執(zhí)行效率高,而且自帶過期操作写隶,開發(fā)友好倔撞;
  • 缺點
  1. 強依賴Redis,單點Redis風險高慕趴,掛掉之后造成實例都不會進行痪蝇;
  2. 存在 key 過期之后實例還沒執(zhí)行完畢的情況,有概率在同一時刻會執(zhí)行多個實例冕房;

RedLock

Redis Distlock

Zookeeper

ZooKeeper是一個高可用的分布式數據管理與系統(tǒng)協(xié)調框架躏啰,在Paxos算法的加持之下,該框架在分布式的環(huán)境中可以保持非常強的數據一致性耙册,從而可以幫助解決很多分布式問題给僵。
我們可以簡單地將它看成是一個遠程的小文件服務,而每個小文件又支持狀態(tài)變化的監(jiān)聽和通知详拙,它的數據模型如下:

/-
 |--- locks/
      |--- mylock0000001
      |--- mylock0000002
 |--- service/
 |--- users/

上面根路徑下的各種路徑和節(jié)點都是由我們自己手動創(chuàng)建的帝际。
在上面的每個節(jié)點上,我們都可以新增監(jiān)聽器饶辙,當zookeeper發(fā)現節(jié)點發(fā)生變化時(增蹲诀、改、刪)畸悬,都會通知到監(jiān)聽它的客戶端。

如何使用Zookeeper實現分布式鎖

利用Zookeeper實現分布式鎖也有兩種基本思路:

  1. 利用節(jié)點名稱唯一性實現共享所珊佣,和文件鎖有些類似蹋宦;
    由于和文件鎖比較類似,原理不再贅述咒锻。不過要提的是冷冗,當節(jié)點(鎖)被釋放時,zookeeper會通知到所有監(jiān)聽這個節(jié)點的客戶端惑艇,從而各個客戶端開始競爭蒿辙,最終只有一個客戶端獲得鎖。雖然實現簡單滨巴,但鎖釋放時喚醒了所有的客戶端思灌,產生了「驚群效應」,故在性能上不是很客觀恭取。
  2. 利用臨時順序節(jié)點實現共享鎖泰偿;
    Zookeeper 還支持一個很厲害的特性:臨時節(jié)點和順序節(jié)點。
    臨時節(jié)點顧名思義蜈垮,就是臨時創(chuàng)建的節(jié)點耗跛,客戶端創(chuàng)建的該類節(jié)點裕照,在客戶端和服務連接斷開時就會刪除;
    而順序節(jié)點就是在節(jié)點名稱最后自動加上后綴调塌,這個后綴在節(jié)點所在路徑中時自增的晋南。
    依靠這兩種特性,聰明而偉大的開發(fā)者就想到了一種比較好的監(jiān)聽流程
 ↑:表示監(jiān)聽


Instance1 -> /locks/0000001
 ↑
Instance2 -> /locks/0000002
 ↑
Instance3 -> /locks/0000003
 ↑
Instance4 -> /locks/0000005
 ↑
......

上面這段示例中InstanceX表示運行實例羔砾,/locks/000000X為獲得的節(jié)點路徑负间,節(jié)點路徑后綴最小的節(jié)點獲得鎖,其他的每一個實例都去監(jiān)聽所有節(jié)點中比自己次醒亚选(比自己小的節(jié)點中最大的節(jié)點)的節(jié)點的變化唉擂。
當1號實例釋放了鎖,那么2號實例就會得到通知檀葛,再掃描一下所有節(jié)點玩祟,判斷到自己是最小的節(jié)點了,于是便獲得了鎖屿聋,后續(xù)的節(jié)點按照這個邏輯類推空扎;
如果在1號實例釋放前3號實例突然意外掛了,4號節(jié)點得到通知润讥,掃描一下所有節(jié)點發(fā)現自己并不是最小的转锈,于是開始監(jiān)聽2號節(jié)點的變化,所以整個監(jiān)聽鏈路是穩(wěn)健的楚殿。
該方法每次鎖釋放時只會通知到一個客戶端撮慨,所以不會有「驚群效應」。

總結

總之脆粥,設計分布式鎖無非就是處理如下這三個方面的問題:

  1. 獲得鎖砌溺;
    • 數據庫用行鎖或者用唯一約束字段實現;
    • Redis用 setnx 實現变隔;
    • Zookeeper用 最小節(jié)點 實現规伐;
  2. 釋放鎖;
    • 正常情況下客戶端會主動釋放匣缘,如刪掉數據庫的某條數據猖闪,釋放行鎖等;
    • 異常情況下肌厨,需要借助過期鎖清理機制釋放鎖培慌,而zookeeper和客戶端之間就存在心跳,如果客戶端意外退出柑爸,心跳檢測可以立即發(fā)現检柬,從而服務端主動清鎖;
  3. 釋放鎖通知:分為客戶端主動獲取 和 服務端主動告知,前者需要客戶端做輪詢操作何址,在時效性上不如后者里逆;

從這幾點看,zookeeper在實現分布式鎖最為簡單用爪。
后續(xù)我會再研究一下zookeeper的性能原押。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市偎血,隨后出現的幾起案子诸衔,更是在濱河造成了極大的恐慌,老刑警劉巖颇玷,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笨农,死亡現場離奇詭異,居然都是意外死亡帖渠,警方通過查閱死者的電腦和手機谒亦,發(fā)現死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來空郊,“玉大人份招,你說我怎么就攤上這事∧酰” “怎么了锁摔?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長哼审。 經常有香客問我谐腰,道長,這世上最難降的妖魔是什么涩盾? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任十气,我火速辦了婚禮,結果婚禮上旁赊,老公的妹妹穿的比我還像新娘桦踊。我一直安慰自己椅野,他們只是感情好终畅,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著竟闪,像睡著了一般离福。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上炼蛤,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天妖爷,我揣著相機與錄音,去河邊找鬼。 笑死絮识,一個胖子當著我的面吹牛绿聘,可吹牛的內容都是我干的。 我是一名探鬼主播次舌,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼熄攘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了彼念?” 一聲冷哼從身側響起挪圾,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎逐沙,沒想到半個月后哲思,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡吩案,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年棚赔,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片务热。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡忆嗜,死狀恐怖,靈堂內的尸體忽然破棺而出崎岂,到底是詐尸還是另有隱情捆毫,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布冲甘,位于F島的核電站绩卤,受9級特大地震影響,放射性物質發(fā)生泄漏江醇。R本人自食惡果不足惜濒憋,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望陶夜。 院中可真熱鬧凛驮,春花似錦、人聲如沸条辟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽羽嫡。三九已至本姥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間杭棵,已是汗流浹背婚惫。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人先舷。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓艰管,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蒋川。 傳聞我的和親對象是個殘疾皇子蛙婴,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348