不會使用分布式鎖?從零開始基于 etcd 實現(xiàn)分布式鎖

      網(wǎng)友投稿 1000 2022-05-30

      為什么需要分布式鎖?

      在單進(jìn)程的系統(tǒng)中,當(dāng)存在多個線程可以同時改變某個變量時,就需要對變量或代碼塊做同步,使其在修改這種變量時能夠線性執(zhí)行消除并發(fā)修改變量。而同步本質(zhì)上通過鎖來實現(xiàn)。為了實現(xiàn)多個線程在一個時刻同一個代碼塊只能有一個線程可執(zhí)行,那么需要在某個地方做個標(biāo)記,這個標(biāo)記必須每個線程都能看到,當(dāng)標(biāo)記不存在時可以設(shè)置該標(biāo)記,其余后續(xù)線程發(fā)現(xiàn)已經(jīng)有標(biāo)記了則等待擁有標(biāo)記的線程結(jié)束同步代碼塊取消標(biāo)記后再去嘗試設(shè)置標(biāo)記。

      而在分布式環(huán)境下,數(shù)據(jù)一致性問題一直是難點(diǎn)。相比于單進(jìn)程,分布式環(huán)境的情況更加復(fù)雜。分布式與單機(jī)環(huán)境最大的不同在于其不是多線程而是多進(jìn)程。多線程由于可以共享堆內(nèi)存,因此可以簡單的采取內(nèi)存作為標(biāo)記存儲位置。而進(jìn)程之間甚至可能都不在同一臺物理機(jī)上,因此需要將標(biāo)記存儲在一個所有進(jìn)程都能看到的地方。

      常見的是秒殺場景,訂單服務(wù)部署了多個服務(wù)實例。如秒殺商品有 4 個,第一個用戶購買 3 個,第二個用戶購買 2 個,理想狀態(tài)下第一個用戶能購買成功,第二個用戶提示購買失敗,反之亦可。而實際可能出現(xiàn)的情況是,兩個用戶都得到庫存為 4,第一個用戶買到了 3 個,更新庫存之前,第二個用戶下了 2 個商品的訂單,更新庫存為 2,導(dǎo)致業(yè)務(wù)邏輯出錯。

      在上面的場景中,商品的庫存是共享變量,面對高并發(fā)情形,需要保證對資源的訪問互斥。在單機(jī)環(huán)境中,比如 Java 語言中其實提供了很多并發(fā)處理相關(guān)的 API,但是這些 API 在分布式場景中就無能為力了,由于分布式系統(tǒng)具備多線程和多進(jìn)程的特點(diǎn),且分布在不同機(jī)器中,synchronized 和 lock 關(guān)鍵字將失去原有鎖的效果,。僅依賴這些語言自身提供的 API 并不能實現(xiàn)分布式鎖的功能,因此需要我們想想其它方法實現(xiàn)分布式鎖。

      常見的鎖方案如下:

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

      基于 Zookeeper 實現(xiàn)分布式鎖

      基于緩存實現(xiàn)分布式鎖,如 redis、etcd 等

      下面我們簡單介紹下這幾種鎖的實現(xiàn),并重點(diǎn)介紹 etcd 實現(xiàn)鎖的方法。

      還不會使用分布式鎖?從零開始基于 etcd 實現(xiàn)分布式鎖

      基于數(shù)據(jù)庫的鎖

      基于數(shù)據(jù)庫的鎖實現(xiàn)也有兩種方式,一是基于數(shù)據(jù)庫表,另一種是基于數(shù)據(jù)庫的排他鎖。

      基于數(shù)據(jù)庫表增刪是最簡單的方式,首先創(chuàng)建一張鎖的表主要包含下列字段:方法名,時間戳等字段。

      具體使用的方法為:當(dāng)需要鎖住某個方法時,往該表中插入一條相關(guān)的記錄。需要注意的是,方法名有唯一性約束。如果有多個請求同時提交到數(shù)據(jù)庫的話,數(shù)據(jù)庫會保證只有一個操作可以成功,那么我們就可以認(rèn)為操作成功的那個線程獲得了該方法的鎖,可以執(zhí)行方法體內(nèi)容。執(zhí)行完畢,需要刪除該記錄。

      對于上述方案可以進(jìn)行優(yōu)化,如應(yīng)用主從數(shù)據(jù)庫,數(shù)據(jù)之間雙向同步。一旦主庫掛掉,將應(yīng)用服務(wù)快速切換到從庫上。除此之外還可以記錄當(dāng)前獲得鎖的機(jī)器的主機(jī)信息和線程信息,那么下次再獲取鎖的時候先查詢數(shù)據(jù)庫,如果當(dāng)前機(jī)器的主機(jī)信息和線程信息在數(shù)據(jù)庫可以查到的話,直接把鎖分配給該線程,實現(xiàn)可重入鎖。

      我們還可以通過數(shù)據(jù)庫的排他鎖來實現(xiàn)分布式鎖?;?Mysql 的 InnoDB 引擎,可以使用以下方法來實現(xiàn)加鎖操作:

      public void lock(){ connection.setAutoCommit(false) int count = 0; while(count < 4){ try{ select * from lock where lock_name=xxx for update; if(結(jié)果不為空){ //代表獲取到鎖 return; } }catch(Exception e){ } //為空或者拋異常的話都表示沒有獲取到鎖 sleep(1000); count++; } throw new LockException(); }

      在查詢語句后面增加 for update,數(shù)據(jù)庫會在查詢過程中給數(shù)據(jù)庫表增加排他鎖。當(dāng)某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖。其他沒有獲取到鎖的就會阻塞在上述 select 語句上,可能的結(jié)果有 2 種,在超時之前獲取到了鎖,在超時之前仍未獲取到鎖。

      獲得排它鎖的線程即可獲得分布式鎖,當(dāng)獲取到鎖之后,可以執(zhí)行業(yè)務(wù)邏輯,執(zhí)行完業(yè)務(wù)之后釋放鎖。

      上面兩種方式都是依賴數(shù)據(jù)庫的一張表,一種是通過表中的記錄的存在情況確定當(dāng)前是否有鎖存在,另外一種是通過數(shù)據(jù)庫的排他鎖來實現(xiàn)分布式鎖。優(yōu)點(diǎn)是直接借助現(xiàn)有的關(guān)系型數(shù)據(jù)庫,簡單且容易理解;缺點(diǎn)是操作數(shù)據(jù)庫需要一定的開銷,性能問題以及 SQL 執(zhí)行超時的異常需要考慮。

      基于 Zookeeper

      基于 Zookeeper 的臨時節(jié)點(diǎn)和順序特性可以實現(xiàn)分布式鎖。

      申請對某個方法加鎖時,在 Zookeeper 上與該方法對應(yīng)的指定節(jié)點(diǎn)的目錄下,生成一個唯一的臨時有序節(jié)點(diǎn)。當(dāng)需要獲取鎖時,只需要判斷有序節(jié)點(diǎn)中該節(jié)點(diǎn)是否為序號最小的一個。業(yè)務(wù)邏輯執(zhí)行完成釋放鎖,只需將這個臨時節(jié)點(diǎn)刪除即可。這種方式也可以避免由于服務(wù)宕機(jī)導(dǎo)致的鎖無法釋放,而產(chǎn)生的死鎖問題。

      Netflix 開源了一套 Zookeeper 客戶端框架 curator,你可以自行去看一下具體使用方法。Curator 提供的 InterProcessMutex 是分布式鎖的一種實現(xiàn)。acquire 方法獲取鎖,release 方法釋放鎖。另外,鎖釋放、阻塞鎖、可重入鎖等問題都可以有效解決。

      關(guān)于阻塞鎖的實現(xiàn),客戶端可以通過在 Zookeeper 中創(chuàng)建順序節(jié)點(diǎn),并且在節(jié)點(diǎn)上綁定- Watch。一旦節(jié)點(diǎn)發(fā)生變化,Zookeeper 會通知客戶端,客戶端可以檢查自己創(chuàng)建的節(jié)點(diǎn)是不是當(dāng)前所有節(jié)點(diǎn)中序號最小的,如果是就獲取到鎖,便可以執(zhí)行業(yè)務(wù)邏輯。

      Zookeeper 實現(xiàn)的分布式鎖也存在一些缺陷。在性能上可能不如基于緩存實現(xiàn)的分布式鎖。因為每次在創(chuàng)建鎖和釋放鎖的過程中,都要動態(tài)創(chuàng)建、銷毀瞬時節(jié)點(diǎn)來實現(xiàn)鎖功能。

      此外,Zookeeper 中創(chuàng)建和刪除節(jié)點(diǎn)只能通過 Leader 節(jié)點(diǎn)來執(zhí)行,然后將數(shù)據(jù)同步到集群中的其他節(jié)點(diǎn)。分布式環(huán)境中難免存在網(wǎng)絡(luò)抖動,導(dǎo)致客戶端和 Zookeeper 集群之間的 session 連接中斷,此時 Zookeeper 服務(wù)端以為客戶端掛了,就會刪除臨時節(jié)點(diǎn)。其他客戶端就可以獲取到分布式鎖了,導(dǎo)致了同時獲取鎖的不一致問題。

      基于緩存實現(xiàn)分布式鎖

      相對于基于數(shù)據(jù)庫實現(xiàn)分布式鎖的方案來說,基于緩存來實現(xiàn)在性能方面會表現(xiàn)的更好一點(diǎn),存取速度快很多。而且很多緩存是可以集群部署的,可以解決單點(diǎn)問題?;诰彺娴逆i有好幾種,如memcached、redis、本文下面主要講解基于 etcd 實現(xiàn)分布式鎖。

      通過 etcd 實現(xiàn)分布式鎖,同樣需要滿足一致性、互斥性和可靠性等要求。etcd 中的事務(wù) txn、lease 租約以及 watch 監(jiān)聽特性,能夠使得基于 etcd 實現(xiàn)上述要求的分布式鎖。

      通過 etcd 的事務(wù)特性可以幫助我們實現(xiàn)一致性和互斥性。etcd 的事務(wù)特性,使用的 IF-Then-Else 語句,IF 語言判斷 etcd 服務(wù)端是否存在指定的 key,即該 key 創(chuàng)建版本號 create_revision 是否為 0 來檢查 key 是否已存在,因為該 key 已存在的話,它的 create_revision 版本號就不是 0。滿足 IF 條件的情況下則使用 then 執(zhí)行 put 操作,否則 else 語句返回?fù)屾i失敗的結(jié)果。當(dāng)然,除了使用 key 是否創(chuàng)建成功作為 IF 的判斷依據(jù),還可以創(chuàng)建前綴相同的 key,比較這些 key 的 revision 來判斷分布式鎖應(yīng)該屬于哪個請求。

      客戶端請求在獲取到分布式鎖之后,如果發(fā)生異常,需要及時將鎖給釋放掉。因此需要租約,當(dāng)我們申請分布式鎖的時候需要指定租約時間。超過 lease 租期時間將會自動釋放鎖,保證了業(yè)務(wù)的可用性。是不是這樣就夠了呢?在執(zhí)行業(yè)務(wù)邏輯時,如果客戶端發(fā)起的是一個耗時的操作,操作未完成的請情況下,租約時間過期,導(dǎo)致其他請求獲取到分布式鎖,造成不一致。這種情況下則需要續(xù)租,即刷新租約,使得客戶端能夠和 etcd 服務(wù)端保持心跳。

      我們基于如上分析的思路,繪制出實現(xiàn) etcd 分布式鎖的流程圖,如下所示:

      基于 Go 語言實現(xiàn)的 etcd 分布式鎖,測試代碼如下所示:

      func TestLock(t *testing.T) { // 客戶端配置 config = clientv3.Config{ Endpoints: []string{"localhost:2379"}, DialTimeout: 5 * time.Second, } // 建立連接 if client, err = clientv3.New(config); err != nil { fmt.Println(err) return } // 1. 上鎖并創(chuàng)建租約 lease = clientv3.NewLease(client) if leaseGrantResp, err = lease.Grant(context.TODO(), 5); err != nil { panic(err) } leaseId = leaseGrantResp.ID // 2 自動續(xù)約 // 創(chuàng)建一個可取消的租約,主要是為了退出的時候能夠釋放 ctx, cancelFunc = context.WithCancel(context.TODO()) // 3. 釋放租約 defer cancelFunc() defer lease.Revoke(context.TODO(), leaseId) if keepRespChan, err = lease.KeepAlive(ctx, leaseId); err != nil { panic(err) } // 續(xù)約應(yīng)答 go func() { for { select { case keepResp = <-keepRespChan: if keepRespChan == nil { fmt.Println("租約已經(jīng)失效了") goto END } else { // 每秒會續(xù)租一次, 所以就會受到一次應(yīng)答 fmt.Println("收到自動續(xù)租應(yīng)答:", keepResp.ID) } } } END: }() // 1.3 在租約時間內(nèi)去搶鎖(etcd 里面的鎖就是一個 key) kv = clientv3.NewKV(client) // 創(chuàng)建事務(wù) txn = kv.Txn(context.TODO()) //if 不存在 key,then 設(shè)置它,else 搶鎖失敗 txn.If(clientv3.Compare(clientv3.CreateRevision("lock"), "=", 0)). Then(clientv3.OpPut("lock", "g", clientv3.WithLease(leaseId))). Else(clientv3.OpGet("lock")) // 提交事務(wù) if txnResp, err = txn.Commit(); err != nil { panic(err) } if !txnResp.Succeeded { fmt.Println("鎖被占用:", string(txnResp.Responses[0].GetResponseRange().Kvs[0].Value)) return } // 搶到鎖后執(zhí)行業(yè)務(wù)邏輯,沒有搶到退出 fmt.Println("處理任務(wù)") time.Sleep(5 * time.Second) }

      預(yù)期的執(zhí)行結(jié)果如下所示:

      === RUN TestLock 處理任務(wù) 收到自動續(xù)租應(yīng)答: 7587848943239472601 收到自動續(xù)租應(yīng)答: 7587848943239472601 收到自動續(xù)租應(yīng)答: 7587848943239472601 --- PASS: TestLock (5.10s) PASS

      總得來說,如上關(guān)于 etcd 分布式鎖的實現(xiàn)過程分為四個步驟:

      客戶端初始化與建立連接;

      創(chuàng)建租約,自動續(xù)租;

      創(chuàng)建事務(wù),獲取鎖;

      執(zhí)行業(yè)務(wù)邏輯,最后釋放鎖。

      創(chuàng)建租約的時候,需要創(chuàng)建一個可取消的租約,主要是為了退出的時候能夠釋放。釋放鎖對應(yīng)的步驟,在上面的 defer 語句中。當(dāng) defer 租約關(guān)掉的時候,分布式鎖對應(yīng)的 key 就會被釋放掉了。

      小結(jié)

      本文主要介紹了基于 etcd 實現(xiàn)分布式鎖的案例。首先介紹了分布式鎖產(chǎn)生的背景以及必要性,分布式架構(gòu)不同于單體架構(gòu),涉及到多服務(wù)之間多個實例的調(diào)用,跨進(jìn)程的情況下使用編程語言自帶的并發(fā)原語沒有辦法實現(xiàn)數(shù)據(jù)的一致性,因此分布式鎖出現(xiàn),用來解決分布式環(huán)境中的資源互斥操作。接著介紹了基于數(shù)據(jù)庫實現(xiàn)分布式鎖的兩種方式:數(shù)據(jù)表增刪和數(shù)據(jù)庫的排它鎖?;?Zookeeper 的臨時節(jié)點(diǎn)和順序特性也可以實現(xiàn)分布式鎖,這兩種方式或多或少存在性能和穩(wěn)定性方面的缺陷。

      接著本文重點(diǎn)介紹了基于 etcd 實現(xiàn)分布式鎖的方案,根據(jù) etcd 的特點(diǎn),利用事務(wù) txn、lease 租約以及 watch 監(jiān)測實現(xiàn)分布式鎖。

      在我們上面的案例中,搶鎖失敗,客戶端就直接返回了。那么當(dāng)該鎖被釋放之后,或者持有鎖的客戶端出現(xiàn)了故障退出了,其他鎖如何快速獲取鎖呢?所以上述代碼可以基于 watch 監(jiān)測特性進(jìn)行改進(jìn),各位同學(xué)可以自行試試。

      etcd 與 Zookeeper、Consul 等其它 k-v 組件的對比

      徹底搞懂 etcd 系列文章(一):初識 etcd

      徹底搞懂 etcd 系列文章(二):etcd 的多種安裝姿勢

      徹底搞懂 etcd 系列文章(三):etcd 集群運(yùn)維部署

      徹底搞懂 etcd 系列文章(四):etcd 安全

      徹底搞懂 etcd 系列文章(五):etcdctl 的使用

      徹底搞懂 etcd 系列文章(六):etcd 核心 API v3

      徹底搞懂 etcd 系列文章(七):etcd gRPC 服務(wù) API

      徹底搞懂 etcd 系列文章(八):etcd 事務(wù) API

      徹底搞懂 etcd 系列文章(九):etcd compact 和 watch API

      etcd docs

      Go 分布式 架構(gòu)設(shè)計

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實后本網(wǎng)站將在24小時內(nèi)刪除侵權(quán)內(nèi)容。

      上一篇:UEditor前端配置項說明
      下一篇:ROS機(jī)器人操作系統(tǒng)資料與資訊(2018年5月)
      相關(guān)文章
      亚洲AV永久无码精品一百度影院 | 亚洲欧洲一区二区三区| 天堂亚洲国产中文在线| 亚洲免费电影网站| 亚洲国产成人精品电影| 亚洲精品中文字幕无码AV| 亚洲男人电影天堂| 亚洲国产成人无码av在线播放| 亚洲精品美女久久久久| 亚洲视频手机在线| 亚洲一线产区二线产区精华| 亚洲Av无码精品色午夜| 国产成人精品久久亚洲| 亚洲乱亚洲乱少妇无码| 亚洲欧洲日产国码一级毛片| 久久精品国产亚洲AV不卡| 国产性爱在线观看亚洲黄色一级片 | 亚洲男人天堂2017| 亚洲综合久久综合激情久久| 亚洲人成网www| 亚洲最新在线视频| 亚洲18在线天美| 91在线亚洲综合在线| 亚洲AV无码专区在线观看成人| 人人狠狠综合久久亚洲| 亚洲VA综合VA国产产VA中| 亚洲性日韩精品一区二区三区| 国产亚洲老熟女视频| 亚洲av综合av一区| 亚洲资源在线视频| 国产精品亚洲精品| 亚洲a∨无码一区二区| 亚洲无线一二三四区手机| 国产亚洲一区二区精品| 精品亚洲麻豆1区2区3区| 亚洲三级在线视频| 婷婷亚洲综合五月天小说在线| 亚洲日韩国产一区二区三区| 亚洲精品无码精品mV在线观看| 亚洲av无码精品网站| 亚洲欧洲中文日产|