<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          分布式鎖的常見實現(xiàn)思路

          共 5413字,需瀏覽 11分鐘

           ·

          2020-09-28 06:16

          點擊上方藍色字體,選擇“標(biāo)星公眾號”

          優(yōu)質(zhì)文章,第一時間送達

          ? 作者?|??Ellen艾倫?

          來源 |? urlify.cn/F36JFz

          66套java從入門到精通實戰(zhàn)課程分享

          一. 概述

          1.1 引言

          當(dāng)前參與的項目中會遇到一些線程安全問題,由于業(yè)務(wù)是多節(jié)點部署的,Java的單機的并發(fā)同步手段synchronized和java.util.concurrent包已經(jīng)不太夠用了,這個時候我們需要分布式鎖來保證線程安全問題,所以這里學(xué)習(xí)總結(jié)了幾種分布式鎖的實現(xiàn)思路。

          分布式的CAP理論告訴我們?nèi)魏我粋€分布式系統(tǒng)都無法同時滿足一致性(Consistency)、可用性(Availability)和分區(qū)容錯性(Partition tolerance),最多只能同時滿足兩項。一般情況下,都需要犧牲強一致性來換取系統(tǒng)的高可用性,系統(tǒng)往往只需要保證最終一致性,只要這個最終時間是在用戶可以接受的范圍內(nèi)即可。在很多時候,為了保證數(shù)據(jù)的最終一致性,需要很多的技術(shù)方案來支持,比如分布式事務(wù)、分布式鎖等。這里我們主要介紹對象分布式鎖,分布式鎖的的具體實現(xiàn)方案主要如下三種:

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

          • 基于緩存(redis)的實現(xiàn)

          • 基于zookeeper的實現(xiàn)

          1.2 分布式鎖的要求

          一個可靠的、高可用的分布式鎖需要滿足以下幾點

          • 互斥性:任意時刻只能有一個客戶端擁有鎖,不能被多個客戶端獲取

          • 安全性:鎖只能被持有該鎖的客戶端刪除,不能被其它客戶端刪除

          • 死鎖避免:獲取鎖的客戶端因為某些原因而宕機,而未能釋放鎖,其它客戶端也就無法獲取該鎖,需要有機制來避免該類問題的發(fā)生

          • 高可用:當(dāng)部分節(jié)點宕機,客戶端仍能獲取鎖或者釋放鎖

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

          2.1 基于數(shù)據(jù)庫實現(xiàn)的樂觀鎖

          樂觀鎖的通常是基于數(shù)據(jù)版本號來實現(xiàn)的。比如,有個商品表t_goods,有一個字段left_count用來記錄商品的庫存?zhèn)€數(shù)。在并發(fā)的情況下,為了保證不出現(xiàn)超賣現(xiàn)象,即left_count不為負(fù)數(shù)。樂觀鎖的實現(xiàn)方式為給商品表增加一個版本號字段version,默認(rèn)為0,每修改一次數(shù)據(jù),將版本號加1。

          無版本號并發(fā)超賣示例:

          --?線程1查詢,當(dāng)前l(fā)eft_count為1,則有記錄
          select?*?from?t_goods?where?id?=?10001?and?left_count?>?0
          --?線程2查詢,當(dāng)前l(fā)eft_count為1,也有記錄
          select?*?from?t_goods??where?id?=?10001?and?left_count?>?0
          --?線程1下單成功庫存減一,修改left_count為0,
          update?t_goods?set?left_count?=?left_count?-?1?where?id?=?10001
          --?線程2下單成功庫存減一,修改left_count為-1,產(chǎn)生臟數(shù)據(jù)
          update?t_goods?set?left_count?=?left_count?-?1?where?id?=?10001

          有版本號的樂觀鎖示例:

          --?線程1查詢,當(dāng)前l(fā)eft_count為1,則有記錄,當(dāng)前版本號為999
          select?left_count,?version?from?t_goods?where?id?=?10001?and?left_count?>?0;
          --?線程2查詢,當(dāng)前l(fā)eft_count為1,也有記錄,當(dāng)前版本號為999
          select?left_count,?version?from?t_goods?where?id?=?10001?and?left_count?>?0;
          --?線程1,更新完成后當(dāng)前的version為1000,update狀態(tài)為1,更新成功
          update?t_goods?set?version?=?1000,?left_count?=?left_count-1?where?id?=?10001?and?version?=?999;
          --?線程2,更新由于當(dāng)前的version為1000,udpate狀態(tài)為0,更新失敗,再針對相關(guān)業(yè)務(wù)做異常處理
          update?t_goods?set?version?=?1000,?left_count?=?left_count-1?where?id?=?10001?and?version?=?999;

          可以發(fā)現(xiàn),這種和CAS的樂觀鎖機制是類似的,所不同的是CAS的硬件來保證原子性,而這里是通過數(shù)據(jù)庫來保證單條SQL語句的原子性。順帶一提CAS的ABA問題一般也是通過版本號來解決。

          2.2 基于數(shù)據(jù)庫實現(xiàn)的排他鎖

          基于數(shù)據(jù)庫的排他鎖需要通過數(shù)據(jù)庫的唯一性約束UNIQUE KEY來保證數(shù)據(jù)的唯一性,從而為鎖的獨占性提供基礎(chǔ)。

          表結(jié)構(gòu)如下:

          CREATE?TABLE?`distribute_lock`?(
          ???`id`?int(11)?unsigned?NOT?NULL?AUTO_INCREMENT?COMMENT?'主鍵',
          ???`unique_mutex`?varchar(64)?NOT?NULL?COMMENT?'需要鎖住的資源或者方法',
          ???--?`state`?tinyint?NOT?NULL?DEFAULT?1?COMMENT?'1:未分配;2:已分配
          ???PRIMARY?KEY?(`id`),
          ???UNIQUE?KEY?`unique_mutex`
          );

          其中,unique_mutex就是我們需要加鎖的對象,需要用UNIQUE KEY來保證此對象唯一。

          加鎖時增加一條記錄:

          insert?into?distribute_lock(unique_mutex)?values('mutex_demo');?

          如果當(dāng)前SQL執(zhí)行成功代表加鎖成功,如果拋出唯一索引異常(DuplicatedKeyException)則代表加鎖失敗,當(dāng)前鎖已經(jīng)被其他競爭者獲取。

          解鎖鎖時刪除該記錄:

          delete?from?distribute_lock(unique_mutex)?values('muetx_demo');

          除了增刪記錄,也可以通過更新state字段來標(biāo)識是否獲取到鎖。

          --?獲取鎖
          update?distribute_lock?set?state?=?2?where?`unique_mutex`?=?'muetx_demo'?and?state=1;

          更新之前需要SELECT確認(rèn)鎖在數(shù)據(jù)庫中存在,沒有則創(chuàng)建之。如果創(chuàng)建或更新失敗,則說明這個資源已經(jīng)被別的線程占用了。

          2.3 小結(jié)

          數(shù)據(jù)庫排他鎖可能出現(xiàn)的問題及解決思路:

          1. 沒有失效時間, 一旦解鎖失敗,會導(dǎo)致鎖記錄一直在數(shù)據(jù)庫中,其他線程無法再獲得鎖。

            • 可通過定時任務(wù)清除超時數(shù)據(jù)來解決

          2. 是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。

            • 可通過增加字段記錄當(dāng)前主機信息和當(dāng)線程信息,

          3. 這個鎖只能是非阻塞的,因為數(shù)據(jù)的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的在線程并不會進入阻塞隊列,需要不停自旋直到獲得鎖,相對耗資源。

          總的來說,基于數(shù)據(jù)庫的分布式鎖,能夠滿足一些簡單的需求,好處是能夠少引入依賴,實現(xiàn)較為簡單,缺點是性能較低,且難以滿足復(fù)雜場景下的高并發(fā)需求。

          三. 基于redis的實現(xiàn)

          3.1 基本實現(xiàn)思路

          一個簡單的分布式鎖機制是使用setnx、expire 、del 三個命令的組合來實現(xiàn)的。setnx命令的含義為:當(dāng)且僅當(dāng)key不存在時,value設(shè)置成功,返回1;否則返回0。另外兩個命令,見名知意,就不多做解釋了。

          #?加鎖,設(shè)置鎖的唯一標(biāo)識key,返回1說明加鎖成功,返回0加鎖失敗
          setnx?key?value
          #?設(shè)置鎖超時時間為30s,防止死鎖
          expire?key?30
          #?解鎖,?刪除鎖
          del?key

          這種思路存在的問題:

          1. setnx和expire的非原子性:如果加鎖之后,服務(wù)器宕機,導(dǎo)致expire和del均執(zhí)行不了,會導(dǎo)致死鎖。

          2. del導(dǎo)致誤刪:A線程超時之后未執(zhí)行完, 鎖過期釋放;B線程獲得鎖,此時A線程執(zhí)行完,執(zhí)行del將B線程的鎖刪除。

          3. 鎖過期后引起的并發(fā):A線程超時之后未執(zhí)行完, 鎖過期釋放;B線程獲得鎖,此時A、B線程并發(fā)執(zhí)行會導(dǎo)致線程安全問題。

          對應(yīng)的解決思路:

          1. 將加鎖和設(shè)置鎖過期時間做成一個原子性操作

            • 在Redis 2.6.12版本之后,set命令增加了NX可選參數(shù),可替代setnx命令;增加了EX可選參數(shù),可以設(shè)置key的同時指定過期時間

            • 或者將兩個操作封裝在lua腳本中,發(fā)送給Redis執(zhí)行,從而實現(xiàn)操作的原子性。

          2. 將key的value設(shè)置為線程相關(guān)信息,del釋放鎖之前先判斷一下鎖是不是自己的。(釋放和判斷不是原子性的,需要封裝在lua腳本中)

          3. 啟動一個守護線程,在后臺自動給自己的鎖''續(xù)期“,執(zhí)行完成,顯式關(guān)掉守護進程

          3.2?redis分布式鎖的缺點

          在大型的應(yīng)用中,一般redis服務(wù)都是集群形式部署的,由于Slave同步Master是異步的,所以會出現(xiàn)客戶端A在Master上加鎖,此時Master宕機,Slave沒有完成鎖的同步,Slave變?yōu)镸aster,客戶端B此時可以完成加鎖操作。

          為了解決這一問題,官方給出了redlock算法,即使這樣在一些較復(fù)雜的場景下也不能100%保證沒有問題。較復(fù)雜,留待后續(xù)研究。

          四. 基于zookeeper的實現(xiàn)

          4.1 基本實現(xiàn)思路

          zookeeper 是一個開源的分布式協(xié)調(diào)服務(wù)框架,主要用來解決分布式集群中的一致性問題和數(shù)據(jù)管理問題。zookeeper本質(zhì)上是一個分布式文件系統(tǒng),由一群樹狀節(jié)點組成,每個節(jié)點可以存放少量數(shù)據(jù),且具有唯一性。

          zookeeper有四種類型的節(jié)點:

          • 持久節(jié)點(PERSISTENT)

            • 默認(rèn)節(jié)點類型,斷開連接仍然存在

          • 持久順序節(jié)點(PERSISTENT_SEQUENTIAL)

            • 在持久節(jié)點的基礎(chǔ)上,增加了順序性。指定創(chuàng)建同名節(jié)點,會根據(jù)創(chuàng)建順序在指定的節(jié)點名稱后面帶上順序編號,以保證節(jié)點具有唯一性和順序性

          • 臨時節(jié)點(EPHEMERAL)

            • 斷開連接后,節(jié)點會被刪除

          • 臨時順序節(jié)點(EPHEMERAL_SEQUENTIAL)

            • 在臨時節(jié)點的基礎(chǔ)上,增加了順序性。

          基于zookeeper實現(xiàn)的分布式鎖主要利用了zookeeper臨時順序節(jié)點的特性和事件監(jiān)聽機制。主要思路如下:

          1. 創(chuàng)建節(jié)點實現(xiàn)加鎖,通過節(jié)點的唯一性,來實現(xiàn)鎖的互斥

            • 如果使用臨時節(jié)點,節(jié)點創(chuàng)建成功表示獲取到鎖

            • 如果使用臨時順序節(jié)點,客戶端創(chuàng)建的節(jié)點為順序最小節(jié)點,表示獲取到鎖

          2. 刪除節(jié)點實現(xiàn)解鎖

          3. 通過臨時節(jié)點的斷開連接自動刪除的特性來避免持有鎖的服務(wù)器宕機而導(dǎo)致的死鎖

          4. 通過節(jié)點的順序性和事件監(jiān)聽機制,大節(jié)點監(jiān)聽小節(jié)點,形成節(jié)點監(jiān)聽鏈,來實現(xiàn)等待隊列(公平鎖)

          其他思路:

          • 不使用監(jiān)聽機制,未獲取到鎖的線程自旋重試或者失敗退出(根據(jù)業(yè)務(wù)決定),可實現(xiàn)非阻塞的樂觀鎖。

          • 不使用臨時順序節(jié)點,而使用臨時節(jié)點,所有客戶端都去監(jiān)聽該臨時節(jié)點,可實現(xiàn)非公平鎖。但是會產(chǎn)生"羊群效應(yīng)",單個事件,引發(fā)多個服務(wù)器響應(yīng),占用服務(wù)器資源和網(wǎng)絡(luò)帶寬,需要根據(jù)業(yè)務(wù)場景選用。

          4.2?zookeeper分布式鎖的缺點

          zookeeper分布式鎖有著較好的可靠性,但是也有如下缺點:

          • zookeeper分布式鎖是性能可能沒有redis分布式鎖高,因為每次在創(chuàng)建鎖和釋放鎖的過程中,都要動態(tài)創(chuàng)建、銷毀臨時節(jié)點來實現(xiàn)鎖功能。

          • 使用zookeeper也有可能帶來并發(fā)問題,只是并不常見而已。比如,由于網(wǎng)絡(luò)抖動,客戶端與zk集群的session連接斷了,那么zk以為客戶端掛了,就會刪除臨時節(jié)點,這時候其他客戶端就可以獲取到分布式鎖了。就可能產(chǎn)生并發(fā)問題。這個問題不常見是因為zk有重試機制,一旦zk集群檢測不到客戶端的心跳,就會重試,curator客戶端支持多種重試策略。多次重試之后還不行的話才會刪除臨時節(jié)點。

          五 總結(jié)

          上面幾種方式,哪種方式都無法做到完美。就像CAP一樣,在復(fù)雜性、可靠性、性能等方面無法同時滿足,所以,根據(jù)不同的應(yīng)用場景選擇最適合自己的才是王道。

          1. 從實現(xiàn)的復(fù)雜性角度(從高到低)zookeeper?>=?redis> 數(shù)據(jù)庫

            • 數(shù)據(jù)庫實現(xiàn)的分布式鎖易于理解和實現(xiàn),且不會給項目引入其他依賴。zookeeper和redis需要考慮的情況更多,實現(xiàn)相對較為復(fù)雜,但是都有現(xiàn)成的分布式鎖框架curator和redision,用起來代碼反而可能會更簡潔。

          2. 從性能角度(從高到低)redis>zookeeper?> 數(shù)據(jù)庫

            • redis數(shù)據(jù)存在內(nèi)存,速度很快;zookeeper雖然數(shù)據(jù)也存在內(nèi)存中,但是本身維護節(jié)點的一致性。需要耗費一些性能;數(shù)據(jù)庫則只有索引在內(nèi)存中,數(shù)據(jù)存于磁盤,性能較差。

          3. 從可靠性角度(從高到低)zookeeper?>?redis?> 數(shù)據(jù)庫

            • zookeeper天生設(shè)計定位就是分布式協(xié)調(diào),強一致性,可靠性較高;redis分布式鎖需要較多額外手段去保證可靠性;數(shù)據(jù)庫則較難滿足復(fù)雜場景的需求。






          粉絲福利:Java從入門到入土學(xué)習(xí)路線圖

          ???

          ?長按上方微信二維碼?2 秒


          感謝點贊支持下哈?

          瀏覽 38
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  精品一区二区三区东京热 | 欧美黄色大片网站 | 麻豆中文字幕 | 香蕉网久久| 成人精品一区二区婷婷 |