## 一、Golang中除了加Mutex鎖以外還有哪些方式安全讀寫共享變量?
Golang中Goroutine 可以通過 Channel 進行安全讀寫共享變量。
## 二、#### 無緩沖 Chan 的發送和接收是否同步?
~~~go
ch := make(chan int) 無緩沖的channel由于沒有緩沖發送和接收需要同步.
ch := make(chan int, 2) 有緩沖channel不要求發送和接收操作同步.
~~~
* channel無緩沖時,發送阻塞直到數據被接收,接收阻塞直到讀到數據。
* channel有緩沖時,當緩沖滿時發送阻塞,當緩沖空時接收阻塞。
## 三、 JSON 標準庫對 nil slice 和 空 slice 的處理是一致的嗎?
首先JSON 標準庫對 nil slice 和 空 slice 的處理是不一致.
通常錯誤的用法,會報數組越界的錯誤,因為只是聲明了slice,卻沒有給實例化的對象。
~~~go
var slice []int
slice[1] = 0
~~~
此時slice的值是nil,這種情況可以用于需要返回slice的函數,當函數出現異常的時候,保證函數依然會有nil的返回值。
empty slice 是指slice不為nil,但是slice沒有值,slice的底層的空間是空的,此時的定義如下:
~~~go
slice := make([]int,0)
slice := []int{}
~~~
當我們查詢或者處理一個空的列表的時候,這非常有用,它會告訴我們返回的是一個列表,但是列表內沒有任何值。
總之,nil slice 和 empty slice是不同的東西,需要我們加以區分的.
## 四、互斥鎖,讀寫鎖,死鎖問題是怎么解決
* 互斥鎖
互斥鎖就是互斥變量mutex,用來鎖住臨界區的.
條件鎖就是條件變量,當進程的某些資源要求不滿足時就進入休眠,也就是鎖住了。當資源被分配到了,條件鎖打開,進程繼續運行;讀寫鎖,也類似,用于緩沖區等臨界資源能互斥訪問的。
* 讀寫鎖
通常有些公共數據修改的機會很少,但其讀的機會很多。并且在讀的過程中會伴隨著查找,給這種代碼加鎖會降低我們的程序效率。讀寫鎖可以解決這個問題。
[](https://github.com/KeKe-Li/golang-interview-questions/blob/master/src/images/61.jpg)
注意:寫獨占,讀共享,寫鎖優先級高
* 死鎖
一般情況下,如果同一個線程先后兩次調用lock,在第二次調用時,由于鎖已經被占用,該線程會掛起等待別的線程釋放鎖,然而鎖正是被自己占用著的,該線程又被掛起而沒有機會釋放鎖,因此就永遠處于掛起等待狀態了,這叫做死鎖(Deadlock)。 另外一種情況是:若線程A獲得了鎖1,線程B獲得了鎖2,這時線程A調用lock試圖獲得鎖2,結果是需要掛起等待線程B釋放鎖2,而這時線程B也調用lock試圖獲得鎖1,結果是需要掛起等待線程A釋放鎖1,于是線程A和B都永遠處于掛起狀態了。
死鎖產生的四個必要條件:
1. 互斥條件:一個資源每次只能被一個進程使用
2. 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
3. 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
4. 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。 這四個條件是死鎖的必要條件,只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。
a. 預防死鎖
可以把資源一次性分配:(破壞請求和保持條件)
然后剝奪資源:即當某進程新的資源未滿足時,釋放已占有的資源(破壞不可剝奪條件)
資源有序分配法:系統給每類資源賦予一個編號,每一個進程按編號遞增的順序請求資源,釋放則相反(破壞環路等待條件)
b. 避免死鎖
預防死鎖的幾種策略,會嚴重地損害系統性能。因此在避免死鎖時,要施加較弱的限制,從而獲得 較滿意的系統性能。由于在避免死鎖的策略中,允許進程動態地申請資源。因而,系統在進行資源分配之前預先計算資源分配的安全性。若此次分配不會導致系統進入不安全狀態,則將資源分配給進程;否則,進程等待。其中最具有代表性的避免死鎖算法是銀行家算法。
c. 檢測死鎖
首先為每個進程和每個資源指定一個唯一的號碼,然后建立資源分配表和進程等待表.
d. 解除死鎖
當發現有進程死鎖后,便應立即把它從死鎖狀態中解脫出來,常采用的方法有.
e. 剝奪資源
從其它進程剝奪足夠數量的資源給死鎖進程,以解除死鎖狀態.
f. 撤消進程
可以直接撤消死鎖進程或撤消代價最小的進程,直至有足夠的資源可用,死鎖狀態.消除為止.所謂代價是指優先級、運行代價、進程的重要性和價值等。
## 五、Data Race問題怎么解決?能不能不加鎖解決這個問題?
同步訪問共享數據是處理數據競爭的一種有效的方法.golang在1.1之后引入了競爭檢測機制,可以使用 go run -race 或者 go build -race來進行靜態檢測。 其在內部的實現是,開啟多個協程執行同一個命令, 并且記錄下每個變量的狀態.
競爭檢測器基于C/C++的ThreadSanitizer 運行時庫,該庫在Google內部代碼基地和Chromium找到許多錯誤。這個技術在2012年九月集成到Go中,從那時開始,它已經在標準庫中檢測到42個競爭條件。現在,它已經是我們持續構建過程的一部分,當競爭條件出現時,它會繼續捕捉到這些錯誤。
競爭檢測器已經完全集成到Go工具鏈中,僅僅添加-race標志到命令行就使用了檢測器。
~~~go
$ go test -race mypkg // 測試包
$ go run -race mysrc.go // 編譯和運行程序 $ go build -race mycmd // 構建程序 $ go install -race mypkg // 安裝程序
~~~
要想解決數據競爭的問題可以使用互斥鎖sync.Mutex,解決數據競爭(Data race),也可以使用管道解決,使用管道的效率要比互斥鎖高.
## 六、什么是channel,為什么它可以做到線程安全?
Channel是Go中的一個核心類型,可以把它看成一個管道,通過它并發核心單元就可以發送或者接收數據進行通訊(communication),Channel也可以理解是一個先進先出的隊列,通過管道進行通信。
Golang的Channel,發送一個數據到Channel 和 從Channel接收一個數據 都是 原子性的。而且Go的設計思想就是:不要通過共享內存來通信,而是通過通信來共享內存,前者就是傳統的加鎖,后者就是Channel。也就是說,設計Channel的主要目的就是在多任務間傳遞數據的,這當然是安全的。
## 七、Golang GC 時會發生什么?
首先我們先來了解下垃圾回收.什么是垃圾回收?
內存管理是程序員開發應用的一大難題。傳統的系統級編程語言(主要指C/C++)中,程序開發者必須對內存小心的進行管理操作,控制內存的申請及釋放。因為稍有不慎,就可能產生內存泄露問題,這種問題不易發現并且難以定位,一直成為困擾程序開發者的噩夢。如何解決這個頭疼的問題呢?
過去一般采用兩種辦法:
* 內存泄露檢測工具。這種工具的原理一般是靜態代碼掃描,通過掃描程序檢測可能出現內存泄露的代碼段。然而檢測工具難免有疏漏和不足,只能起到輔助作用。
* 智能指針。這是 c++ 中引入的自動內存管理方法,通過擁有自動內存管理功能的指針對象來引用對象,是程序員不用太關注內存的釋放,而達到內存自動釋放的目的。這種方法是采用最廣泛的做法,但是對程序開發者有一定的學習成本(并非語言層面的原生支持),而且一旦有忘記使用的場景依然無法避免內存泄露。
為了解決這個問題,后來開發出來的幾乎所有新語言(java,python,php等等)都引入了語言層面的自動內存管理 – 也就是語言的使用者只用關注內存的申請而不必關心內存的釋放,內存釋放由虛擬機(virtual machine)或運行時(runtime)來自動進行管理。而這種對不再使用的內存資源進行自動回收的行為就被稱為垃圾回收。
常用的垃圾回收的方法:
* 引用計數(reference counting)
這是最簡單的一種垃圾回收算法,和之前提到的智能指針異曲同工。對每個對象維護一個引用計數,當引用該對象的對象被銷毀或更新時被引用對象的引用計數自動減一,當被引用對象被創建或被賦值給其他對象時引用計數自動加一。當引用計數為0時則立即回收對象。
這種方法的優點是實現簡單,并且內存的回收很及時。這種算法在內存比較緊張和實時性比較高的系統中使用的比較廣泛,如ios cocoa[框架](https://so.csdn.net/so/search?q=%E6%A1%86%E6%9E%B6&spm=1001.2101.3001.7020),php,python等。
但是簡單引用計數算法也有明顯的缺點:
1. 頻繁更新引用計數降低了性能。
一種簡單的解決方法就是編譯器將相鄰的引用計數更新操作合并到一次更新;還有一種方法是針對頻繁發生的臨時變量引用不進行計數,而是在引用達到0時通過掃描堆棧確認是否還有臨時對象引用而決定是否釋放。等等還有很多其他方法,具體可以參考這里。
2. 循環引用。
當對象間發生循環引用時引用鏈中的對象都無法得到釋放。最明顯的解決辦法是避免產生循環引用,如cocoa引入了strong指針和weak指針兩種指針類型。或者系統檢測循環引用并主動打破循環鏈。當然這也增加了垃圾回收的復雜度。
* 標記-清除(mark and sweep)
標記-清除(mark and sweep)分為兩步,標記從根變量開始迭代得遍歷所有被引用的對象,對能夠通過應用遍歷訪問到的對象都進行標記為“被引用”;標記完成后進行清除操作,對沒有標記過的內存進行回收(回收同時可能伴有碎片整理操作)。這種方法解決了引用計數的不足,但是也有比較明顯的問題:每次啟動垃圾回收都會暫停當前所有的正常代碼執行,回收是系統響應能力大大降低!當然后續也出現了很多mark&sweep算法的變種(如三色標記法)優化了這個問題。
* 分代搜集(generation)
java的jvm 就使用的分代回收的思路。在面向對象編程語言中,絕大多數對象的生命周期都非常短。分代收集的基本思想是,將堆劃分為兩個或多個稱為代(generation)的空間。新創建的對象存放在稱為新生代(young generation)中(一般來說,新生代的大小會比 老年代小很多),隨著垃圾回收的重復執行,生命周期較長的對象會被提升(promotion)到老年代中(這里用到了一個分類的思路,這個是也是科學思考的一個基本思路)。
因此,新生代垃圾回收和老年代垃圾回收兩種不同的垃圾回收方式應運而生,分別用于對各自空間中的對象執行垃圾回收。新生代垃圾回收的速度非常快,比老年代快幾個數量級,即使新生代垃圾回收的頻率更高,執行效率也仍然比老年代垃圾回收強,這是因為大多數對象的生命周期都很短,根本無需提升到老年代。
Golang GC 時會發生什么?
Golang 1.5后,采取的是“非分代的、非移動的、并發的、三色的”標記清除垃圾回收算法。
golang 中的 gc 基本上是標記清除的過程:
[](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/images/2.jpg)
gc的過程一共分為四個階段:
1. 棧掃描(開始時STW)
2. 第一次標記(并發)
3. 第二次標記(STW)
4. 清除(并發)
整個進程空間里申請每個對象占據的內存可以視為一個圖,初始狀態下每個內存對象都是白色標記。
1. 先STW,做一些準備工作,比如 enable write barrier。然后取消STW,將掃描任務作為多個并發的goroutine立即入隊給調度器,進而被CPU處理
2. 第一輪先掃描root對象,包括全局指針和 goroutine 棧上的指針,標記為灰色放入隊列
3. 第二輪將第一步隊列中的對象引用的對象置為灰色加入隊列,一個對象引用的所有對象都置灰并加入隊列后,這個對象才能置為黑色并從隊列之中取出。循環往復,最后隊列為空時,整個圖剩下的白色內存空間即不可到達的對象,即沒有被引用的對象;
4. 第三輪再次STW,將第二輪過程中新增對象申請的內存進行標記(灰色),這里使用了write barrier(寫屏障)去記錄
Golang gc 優化的核心就是盡量使得 STW(Stop The World) 的時間越來越短。
詳細的Golang的GC介紹可以參看[Golang垃圾回收](https://github.com/KeKe-Li/For-learning-Go-Tutorial/blob/master/src/spec/02.0.md).
## 七、Golang 中 Goroutine 如何調度?
goroutine是Golang語言中最經典的設計,也是其魅力所在,goroutine的本質是協程,是實現并行計算的核心。 goroutine使用方式非常的簡單,只需使用go關鍵字即可啟動一個協程,并且它是處于異步方式運行,你不需要等它運行完成以后在執行以后的代碼。
~~~go
go func()//通過go關鍵字啟動一個協程來運行函數
~~~
協程:
協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧。 因此,協程能保留上一次調用時的狀態(即所有局部狀態的一個特定組合),每次過程重入時,就相當于進入上一次調用的狀態,換種說法:進入上一次離開時所處邏輯流的位置。 線程和進程的操作是由程序觸發系統接口,最后的執行者是系統;協程的操作執行者則是用戶自身程序,goroutine也是協程。
groutine能擁有強大的并發實現是通過GPM調度模型實現.
[](https://github.com/KeKe-Li/golang-interview-questions/blob/master/src/images/59.jpg)
Go的調度器內部有四個重要的結構:M,P,S,Sched,如上圖所示(Sched未給出).
* M:M代表內核級線程,一個M就是一個線程,goroutine就是跑在M之上的;M是一個很大的結構,里面維護小對象內存cache(mcache)、當前執行的goroutine、隨機數發生器等等非常多的信息
* G:代表一個goroutine,它有自己的棧,instruction pointer和其他信息(正在等待的channel等等),用于調度。
* P:P全稱是Processor,處理器,它的主要用途就是用來執行goroutine的,所以它也維護了一個goroutine隊列,里面存儲了所有需要它來執行的goroutine
* Sched:代表調度器,它維護有存儲M和G的隊列以及調度器的一些狀態信息等。
調度實現:
[](https://github.com/KeKe-Li/golang-interview-questions/blob/master/src/images/65.jpg)
從上圖中可以看到,有2個物理線程M,每一個M都擁有一個處理器P,每一個也都有一個正在運行的goroutine。P的數量可以通過GOMAXPROCS()來設置,它其實也就代表了真正的并發度,即有多少個goroutine可以同時運行。
圖中灰色的那些goroutine并沒有運行,而是出于ready的就緒態,正在等待被調度。P維護著這個隊列(稱之為runqueue),Go語言里,啟動一個goroutine很容易:go function 就行,所以每有一個go語句被執行,runqueue隊列就在其末尾加入一個goroutine,在下一個調度點,就從runqueue中取出(如何決定取哪個goroutine?)一個goroutine執行。
當一個OS線程M0陷入阻塞時,P轉而在運行M1,圖中的M1可能是正被創建,或者從線程緩存中取出。
[](https://github.com/KeKe-Li/golang-interview-questions/blob/master/src/images/60.jpg)
當MO返回時,它必須嘗試取得一個P來運行goroutine,一般情況下,它會從其他的OS線程那里拿一個P過來, 如果沒有拿到的話,它就把goroutine放在一個global runqueue里,然后自己睡眠(放入線程緩存里)。所有的P也會周期性的檢查global runqueue并運行其中的goroutine,否則global runqueue上的goroutine永遠無法執行。
另一種情況是P所分配的任務G很快就執行完了(分配不均),這就導致了這個處理器P很忙,但是其他的P還有任務,此時如果global runqueue沒有任務G了,那么P不得不從其他的P里拿一些G來執行。
[](https://github.com/KeKe-Li/golang-interview-questions/blob/master/src/images/64.jpg)
通常來說,如果P從其他的P那里要拿任務的話,一般就拿run queue的一半,這就確保了每個OS線程都能充分的使用。
## 八、并發編程概念是什么?
并行是指兩個或者多個事件在同一時刻發生;并發是指兩個或多個事件在同一時間間隔發生。
并行是在不同實體上的多個事件,并發是在同一實體上的多個事件。在一臺處理器上“同時”處理多個任務,在多臺處理器上同時處理多個任務。如hadoop分布式集群
并發偏重于多個任務交替執行,而多個任務之間有可能還是串行的。而并行是真正意義上的“同時執行”。
并發編程是指在一臺處理器上“同時”處理多個任務。并發是在同一實體上的多個事件。多個事件在同一時間間隔發生。并發編程的目標是充分的利用處理器的每一個核,以達到最高的處理性能。
## 九、微服務架構是什么樣子的?
通常傳統的項目體積龐大,需求、設計、開發、測試、部署流程固定。新功能需要在原項目上做修改。
但是微服務可以看做是對大項目的拆分,是在快速迭代更新上線的需求下產生的。新的功能模塊會發布成新的服務組件,與其他已發布的服務組件一同協作。 服務內部有多個生產者和消費者,通常以http rest的方式調用,服務總體以一個(或幾個)服務的形式呈現給客戶使用。
微服務架構是一種思想對微服務架構我們沒有一個明確的定義,但簡單來說微服務架構是:
采用一組服務的方式來構建一個應用,服務獨立部署在不同的進程中,不同服務通過一些輕量級交互機制來通信,例如 RPC、HTTP 等,服務可獨立擴展伸縮,每個服務定義了明確的邊界,不同的服務甚至可以采用不同的編程語言來實現,由獨立的團隊來維護。
Golang的微服務框架[kit](https://gokit.io/)中有詳細的微服務的例子,可以參考學習.
微服務架構設計包括:
1. 服務熔斷降級限流機制 熔斷降級的概念(Rate Limiter 限流器,Circuit breaker 斷路器).
2. 框架調用方式解耦方式 Kit 或 Istio 或 Micro 服務發現(consul zookeeper kubeneters etcd ) RPC調用框架.
3. 鏈路監控,zipkin和prometheus.
4. 多級緩存.
5. 網關 (kong gateway).
6. Docker部署管理 Kubenetters.
7. 自動集成部署 CI/CD 實踐.
8. 自動擴容機制規則.
9. 壓測 優化.
10. Trasport 數據傳輸(序列化和反序列化).
11. Logging 日志.
12. Metrics 指針對每個請求信息的儀表盤化.
微服務架構介紹詳細的可以參考:
* [Microservice Architectures](http://www.pst.ifi.lmu.de/Lehre/wise-14-15/mse/microservice-architectures.pdf)
## 十、分布式鎖實現原理,用過嗎?
在分析分布式鎖的三種實現方式之前,先了解一下分布式鎖應該具備哪些條件:
1. 在分布式系統環境下,一個方法在同一時間只能被一個機器的一個線程執行;
2. 高可用的獲取鎖與釋放鎖;
3. 高性能的獲取鎖與釋放鎖;
4. 具備可重入特性;
5. 具備鎖失效機制,防止死鎖;
6. 具備非阻塞鎖特性,即沒有獲取到鎖將直接返回獲取鎖失敗。
分布式的CAP理論告訴我們“任何一個分布式系統都無法同時滿足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance),最多只能同時滿足兩項。”所以,很多系統在設計之初就要對這三者做出取舍。在互聯網領域的絕大多數的場景中,都需要犧牲強一致性來換取系統的高可用性,系統往往只需要保證“最終一致性”,只要這個最終時間是在用戶可以接受的范圍內即可。
通常分布式鎖以單獨的服務方式實現,目前比較常用的分布式鎖實現有三種:
* 基于數據庫實現分布式鎖。
* 基于緩存(redis,memcached,tair)實現分布式鎖。
* 基于Zookeeper實現分布式鎖。
盡管有這三種方案,但是不同的業務也要根據自己的情況進行選型,他們之間沒有最好只有更適合!
* 基于數據庫的實現方式
基于數據庫的實現方式的核心思想是:在數據庫中創建一個表,表中包含方法名等字段,并在方法名字段上創建唯一索引,想要執行某個方法,就使用這個方法名向表中插入數據,成功插入則獲取鎖,執行完成后刪除對應的行數據釋放鎖。
創建一個表:
~~~sql
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='鎖定中的方法';
~~~
想要執行某個方法,就使用這個方法名向表中插入數據:
~~~sql
INSERT INTO method_lock (method_name, desc) VALUES ('methodName', '測試的methodName');
~~~
因為我們對method\_name做了唯一性約束,這里如果有多個請求同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,那么我們就可以認為操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。
成功插入則獲取鎖,執行完成后刪除對應的行數據釋放鎖:
~~~sql
delete from method_lock where method_name ='methodName';
~~~
注意:這里只是使用基于數據庫的一種方法,使用數據庫實現分布式鎖還有很多其他的用法可以實現!
使用基于數據庫的這種實現方式很簡單,但是對于分布式鎖應該具備的條件來說,它有一些問題需要解決及優化:
1、因為是基于數據庫實現的,數據庫的可用性和性能將直接影響分布式鎖的可用性及性能,所以,數據庫需要雙機部署、數據同步、主備切換;
2、不具備可重入的特性,因為同一個線程在釋放鎖之前,行數據一直存在,無法再次成功插入數據,所以,需要在表中新增一列,用于記錄當前獲取到鎖的機器和線程信息,在再次獲取鎖的時候,先查詢表中機器和線程信息是否和當前機器和線程相同,若相同則直接獲取鎖;
3、沒有鎖失效機制,因為有可能出現成功插入數據后,服務器宕機了,對應的數據沒有被刪除,當服務恢復后一直獲取不到鎖,所以,需要在表中新增一列,用于記錄失效時間,并且需要有定時任務清除這些失效的數據;
4、不具備阻塞鎖特性,獲取不到鎖直接返回失敗,所以需要優化獲取邏輯,循環多次去獲取。
5、在實施的過程中會遇到各種不同的問題,為了解決這些問題,實現方式將會越來越復雜;依賴數據庫需要一定的資源開銷,性能問題需要考慮。
* 基于Redis的實現方式
選用Redis實現分布式鎖原因:
1. Redis有很高的性能;
2. Redis命令對此支持較好,實現起來比較方便
主要實現方式:
1. SET lock currentTime+expireTime EX 600 NX,使用set設置lock值,并設置過期時間為600秒,如果成功,則獲取鎖;
2. 獲取鎖后,如果該節點掉線,則到過期時間ock值自動失效;
3. 釋放鎖時,使用del刪除lock鍵值;
使用redis單機來做分布式鎖服務,可能會出現單點問題,導致服務可用性差,因此在服務穩定性要求高的場合,官方建議使用redis集群(例如5臺,成功請求鎖超過3臺就認為獲取鎖),來實現redis分布式鎖。詳見RedLock。
優點:性能高,redis可持久化,也能保證數據不易丟失,redis集群方式提高穩定性。
缺點:使用redis主從切換時可能丟失部分數據。
* 基于ZooKeeper的實現方式
ZooKeeper是一個為分布式應用提供一致性服務的開源組件,它內部是一個分層的文件系統目錄樹結構,規定同一個目錄下只能有一個唯一文件名。基于ZooKeeper實現分布式鎖的步驟如下:
1. 創建一個目錄mylock;
2. 線程A想獲取鎖就在mylock目錄下創建臨時順序節點;
3. 獲取mylock目錄下所有的子節點,然后獲取比自己小的兄弟節點,如果不存在,則說明當前線程順序號最小,獲得鎖;
4. 線程B獲取所有節點,判斷自己不是最小節點,設置監聽比自己次小的節點;
5. 線程A處理完,刪除自己的節點,線程B監聽到變更事件,判斷自己是不是最小的節點,如果是則獲得鎖。
這里推薦一個Apache的開源庫Curator,它是一個ZooKeeper客戶端,Curator提供的InterProcessMutex是分布式鎖的實現,acquire方法用于獲取鎖,release方法用于釋放鎖。
優點:具備高可用、可重入、阻塞鎖特性,可解決失效死鎖問題。
缺點:因為需要頻繁的創建和刪除節點,性能上不如Redis方式。
上面的三種實現方式,沒有在所有場合都是完美的,所以,應根據不同的應用場景選擇最適合的實現方式。
在分布式環境中,對資源進行上鎖有時候是很重要的,比如搶購某一資源,這時候使用分布式鎖就可以很好地控制資源。
## 十一、#### Etcd怎么實現分布式鎖?
首先思考下Etcd是什么?可能很多人第一反應可能是一個鍵值存儲倉庫,卻沒有重視官方定義的后半句,用于配置共享和服務發現。
~~~gfm
A highly-available key value store for shared configuration and service discovery.
~~~
實際上,etcd 作為一個受到 ZooKeeper 與 doozer 啟發而催生的項目,除了擁有與之類似的功能外,更專注于以下四點。
* 簡單:基于 HTTP+JSON 的 API 讓你用 curl 就可以輕松使用。
* 安全:可選 SSL 客戶認證機制。
* 快速:每個實例每秒支持一千次寫操作。
* 可信:使用 Raft 算法充分實現了分布式。
但是這里我們主要講述Etcd如何實現分布式鎖?
因為 Etcd 使用 Raft 算法保持了數據的強一致性,某次操作存儲到集群中的值必然是全局一致的,所以很容易實現分布式鎖。鎖服務有兩種使用方式,一是保持獨占,二是控制時序。
* 保持獨占即所有獲取鎖的用戶最終只有一個可以得到。etcd 為此提供了一套實現分布式鎖原子操作 CAS(CompareAndSwap)的 API。通過設置prevExist值,可以保證在多個節點同時去創建某個目錄時,只有一個成功。而創建成功的用戶就可以認為是獲得了鎖。
* 控制時序,即所有想要獲得鎖的用戶都會被安排執行,但是獲得鎖的順序也是全局唯一的,同時決定了執行順序。etcd 為此也提供了一套 API(自動創建有序鍵),對一個目錄建值時指定為POST動作,這樣 etcd 會自動在目錄下生成一個當前最大的值為鍵,存儲這個新的值(客戶端編號)。同時還可以使用 API 按順序列出所有當前目錄下的鍵值。此時這些鍵的值就是客戶端的時序,而這些鍵中存儲的值可以是代表客戶端的編號。
在這里Ectd實現分布式鎖基本實現原理為:
1. 在ectd系統里創建一個key
2. 如果創建失敗,key存在,則監聽該key的變化事件,直到該key被刪除,回到1
3. 如果創建成功,則認為我獲得了鎖
- 一、經典(一)
- 二、經典(二)
- 三、經典(三)
- 四、經典(四)
- 五、經典(五)
- 六、經典(六)
- 七、經典(七)
- 八、經典(八)
- 九、經典(九)
- 十、經典(十)
- 十一、經典(十一)
- 十二、經典(十二)
- 其他
- 1、知識點一
- 2、面試集
- 3、負載均衡原理
- 4、LVS相關了解
- 5、微服務架構
- 6、分布式鎖實現原理
- 7、Etcd怎么實現分布式鎖
- 8、Redis的數據結構有哪些,以及實現場景
- 9、Mysql高可用方案有哪些
- 10、Go語言的棧空間管理是怎么樣的
- 11、Goroutine和Channel的作用分別是什么
- 12、Go中的鎖有哪些?三種鎖,讀寫鎖,互斥鎖,還有map的安全的鎖?
- 13、怎么限制Goroutine的數量
- 14、Goroutine和線程的區別?
- 15、中間件原理