摘要:在平常開發中,應用大多數是分佈式部署的,常常會面臨分佈式環境下應用對數據操做的一致性問題。這時就須要找出一個在分佈式環境下同一個應用多個實例之間可以訪問的臨界資源,並對該臨界資源作互斥訪問,從而保證數據一致性。本文結合筆者實際工做中的經驗,對分佈式環境下實現應用分佈式鎖的關鍵思路進行探討。redis
關鍵詞: 分佈式鎖、互斥資源、數據一致性 分佈式環境下,分佈式部署的應用不少時候須要對同一個資源數據進行操做以知足業務須要,這裏就會面臨數據一致性的問題。 例如商品售賣,同一時間可能會有多個線程請求庫存扣減,若是不對庫存扣減進行互斥訪問,將會致使商品超賣,這是不可容忍的。所以,利用分佈式鎖對庫存扣減進行互斥訪問,能夠解決庫存數據的一致性問題。 本文介紹利用數據庫,緩存,zookeepr三種資源實現分佈式鎖的關鍵思路。sql
利用一個分佈式環境下,多實例可以訪問的公共資源來實現分佈式鎖,具體這個公共資源能夠是數據庫,緩存(redis/memcache),zookeeper等等。 這裏須要明確一點,以JDK爲例,在實現分佈式鎖時可能會考慮使用ReentrantLock,但其實是有問題的,ReentrantLock是同一個JVM上的鎖,lock和unlock都須要在同一個線程上進行,而分佈式環境下,是多JVM的,並且lock和unlock極有多是兩次不一樣的甚至不相關的請求,所以確定不是同一個線程,從而沒法使用ReentrantLock來實現分佈式鎖。數據庫
目前主流的數據庫引擎,都支持select for update的語法,這是一種排他鎖,當兩個不一樣事務都執行到select xxx where id=? for update時,假設id上有惟一索引,那麼後面執行的事務將會阻塞在該sql語句上,直到前一個事務提交或者回滾。所以能夠利用這種特性來實現分佈式鎖。緩存
2.1流程圖架構
圖 1 數據庫分佈式鎖關鍵流程 (1) 設置數據庫鏈接爲事務不自動提交。 (2) 執行select * for lock where lock_name=xxx for update語句,判斷結果集,若是結果集有記錄,表示某一個事務已經得到到了鎖資源,則轉向步驟(3),不然結束。 (3) 進行業務操做,轉向步驟(4)。 (4) 釋放鎖:事務提交。併發
2.2 僞代碼 2.2.1 獲取鎖框架
上述爲獲取鎖的具體實現,關鍵是分佈式
select * for lock where lock_name=xxx for update性能
語句,同一時間只有一個事務可以順利執行該sql獲得返回值,其他事務將在該sql語句上阻塞。 當sql語句的返回的結果集不爲空時,表示獲取到鎖,不然結果集爲空或者拋異常都視爲沒有獲取到鎖。操作系統
2.2.2 釋放鎖!
釋放說的方式相對簡單,只須要簡單調用commit方法就能夠。
2.3 鎖表的設計 上述僞代碼lock表的定義根據場景的不用而有所不一樣。 這裏舉個例子,lock表能夠設計成以下(以MySQL爲例子):
注意到lock_name字段建了惟一索引,由於對於select for update語句,若是where條件的字段沒有惟一索引,那麼for update將會失效,這點須要特別注意。
分佈式緩存,例如redis,memcache等都會提供一些命令,這些命令都有一個共同的特徵:原子性。 例如:redis的setnx命令,memcache的add命令等等。這裏以redis爲例,實際場景中,通常是setnx,get,getset三個命令結合使用來實現分佈式鎖。 setnx命令的含義是set if not exists,參數有兩個:key,value。該命令是原子的,若是key不存在,則設置當前key爲value成功,返回1;若是key已經存在,則設置當前key爲value失敗,返回0。 get命令的含義是獲取指定key的值。 getset命令的含義是設置指定key一個新的值,而且返回舊的值,該命令也是原子的。
3.1 流程圖
圖 2 redis緩存分佈式鎖關鍵流程 (1)調用setnx:flag=setnx(lockkey,當前時間+過時超時時間),判斷flag的返回值,若是flag=1表明獲取鎖成功,則轉向步驟(4)。若是flag=0表明沒有獲取到鎖,則轉向步驟(2)
(2)獲取lockkey的值:oldExpireTime=get(lockkey)。若是當前系統時間<= oldExpireTime,表示未超時,仍然有另外的線程持有鎖,所以轉向步驟(1)繼續嘗試搶鎖;若是前系統時間> oldExpireTime,表示已經超時,轉向步驟(3)
(3)計算新的時間值:newExpireTime=當前時間+過時超時時間,調用getset命令,即currentExpireTime=getset(lockkey, newExpireTime),若是這時newExpireTime不等於currentExpireTime,表明已經有另外的線程在當前線程調用getset以前調用了setnx,而且返回值是1,所以當前線程搶鎖失敗,轉向步驟(1)繼續嘗試搶鎖。若是newExpireTime等於currentExpireTime,表示當前線程搶鎖成功,則轉向步驟(4)。
(4)具體業務操做。業務操做完成以後,比較業務處理時間和鎖的超時時間。若是業務處理時間>=鎖超時時間,表示鎖已經被redis的超時機制刪除了,則轉向步驟(6)。若是業務處理時間<鎖超時時間,則轉向步驟(5)。
(5)調用del命令刪除鎖:del(lockkey)。轉向步驟(6)。
(6)其餘業務操做。
3.2 僞代碼 3.2.1 獲取鎖
3.2.2 釋放鎖
Zookeeper是一個分佈式的應用程序協調服務,它包含一些簡單的原語集,分佈式應用能夠基於它實現諸如同步服務,配置管理,命名服務等。 Zookeeper的管理基於層級命名空間,相似操做系統的目錄結構,每一個目錄節點能夠關聯數據。其中有一種很是特殊的節點:臨時節點,同一時間只有一個會話能夠建立節點成功,當會話結束節點會被自動刪除。所以能夠利用該特性來實現分佈式鎖。
4.1 流程圖
圖 3 zookeeper緩存分佈式鎖關鍵流程 (1) 建立鏈接zookeeper的會話。 (2) 嘗試建立臨時節點:/exclusiveLock/lock,若是建立成功,則轉向步驟(3),不然繼續進行循環,嘗試下一次建立節點。 (3) 進行業務操做,轉向步驟(4)。 (4) 釋放鎖:會話結束或者刪除臨時節點。
4.2 僞代碼 4.2.1 獲取鎖 經過構建一個目錄,當葉子節點能建立成功,則認爲獲取到鎖,由於一旦一個節點被某個會話建立,其它會話再次建立創這個節點時,將會拋出異常。 好比目錄爲:
圖 4 zookeeper臨時節點結構
4.2.2 釋放鎖 刪除節點或者會話失效
本文闡述了分佈式環境下實現分佈式鎖的關鍵技術,經過基於數據庫的分佈式鎖,基於緩存的分佈式鎖,基於zookeeper的分佈式鎖三種經常使用的分佈式鎖實現技術進行原理說明,對關鍵代碼進行了流程說明和僞代碼說明。 這三種方案基本能夠解決平常工做中不少業務場景下的分佈式鎖問題,從而解決數據一致性問題。
固然,三種方案都有各自的優缺點: (1)基於數據庫的分佈式鎖:在併發量很高的狀況下,系統會有不少個分佈式鎖資源,對數據庫性能有必定影響,特別是在分佈式鎖表和業務表在同一個數據庫時性能降低尤其明顯,這時能夠對分佈式鎖表進行分庫分表來下降壓力,提供性能。總的來講,基於數據庫分佈式鎖的方案,只適用於併發量不大的場景下使用。 (2)基於緩存的分佈式鎖:會存在單點問題,若是master節點宕機了,那麼分佈式鎖就無效了,從而致使數據一致性問題。而假如redis是master-slave架構,那麼會有以下狀況出現:請求A在master節點上拿到了鎖,master節點把請求A建立的鎖信息寫入到slave節點以前就宕機了,slave節點變成master節點以後,這時請求B有可能會拿到跟請求A相同的鎖,由於slave節點尚未請求A的鎖信息。 (3)基於zookeeper的分佈式鎖:zookeeper的優勢是高可用,公平鎖,心跳保持鎖,順序節點,臨時節點,可以支撐大併發。同事zookeeper有成熟的客戶端框架Curator,該框架封裝了與zookeeper通訊的細節,實現起分佈式鎖更加簡單。總上所述:沒有一種一勞永逸的分佈式鎖解決方案,只有適用某種場景下的具體實現方案,在真實環境下須要具體問題具體分析。 另外,同時也須要考慮以下問題: (1)如何避免死鎖的出現,一旦出現死鎖,對應用的影響是致命的。(2)怎麼釋放鎖。(3)怎麼知道鎖釋放了。(4)鎖超時處理。