分布式鎖常見方案

? ? ? 最近碰到幾個業務場景,會遇到并發的問題。在單實例情況下,我們會通過java.util.concurrent包下的API或者synchronized關鍵字來處理。但是集群部署情況,我們就要利用分布式鎖來解決了。

分布式與單機情況下最大的不同在于其不是多線程而是多進程。

多線程由于可以共享堆內存,因此可以簡單的采取內存作為標記存儲位置。而進程之間甚至可能都不在同一臺物理機上,因此需要將標記存儲在一個所有進程都能看到的地方。

與單機模式下的鎖不僅需要保證進程可見,還需要考慮進程與鎖之間的網絡問題。(我覺得分布式情況下之所以問題變得復雜,主要就是需要考慮到網絡的延時和不可靠)

分布式鎖的要求:

可以保證在分布式部署的應用集群中,同一個方法在同一時間只能被一臺機器上的一個線程執行。

這把鎖要是一把可重入鎖(避免死鎖)

這把鎖最好是一把阻塞鎖(根據業務需求考慮要不要這條)

有高可用的獲取鎖和釋放鎖功能

獲取鎖和釋放鎖的性能要好

這篇文章把我以前用到分布式鎖方案和現在調研到的方案做一個總結。

分布式鎖常用的方案

基于數據庫

基于分布式緩存(redis、memcached、日本人Mikio Hirabayashi 開發的ttserver、公司的tair等等)

基于zookeeper

本人目前只用過基于數據庫、zookeeper、redis的分布式鎖

再說一句,要基于你的業務場景選擇合適方案。

一、基于數據庫的分布式鎖

1、相關基礎知識了解

再談數據庫的分布式鎖之前,先了解一下相關基礎知識。

(1)事務

事務是不能解決分布式鎖要解決的問題的,不過在某些情況下事務也可以解決分布式鎖要解決的問題。

事務是一組原子性sql查詢語句,被當作一個工作單元。若MySQL對改事務單元內的所有sql語句都正常的執行完,則事務操作視為成功,所有的sql語句才對數據生效,若sql中任意不能執行或出錯則事務操作失敗,所有對數據的操作則無效(通過回滾恢復數據)。事務有四個屬性:

a、原子性:事務被認為不可分的一個工作單元,要么全部正常執行,要么全部不執行。

b、一致性:事務操作對數據庫總是從一種一致性的狀態轉換成另外一種一致性狀態。

c、隔離性:一個事務的操作結果在內部一致,可見,而對除自己以外的事務是不可見的。

d、永久性:事務在未提交前數據一般情況下可以回滾恢復數據,一旦提交(commit)數據的改變則變成永久(當然用update肯定還能修改)。

事務的4個隔離級別:

a、Read uncommitted

讀未提交,顧名思義,就是一個事務可以讀取另一個未提交事務的數據。

實際中不會使用

b、Read committed

讀提交,顧名思義,就是一個事務要等另一個事務提交后才能讀取數據。

針對當前讀,加行鎖。存在幻讀情況。?

c、Repeatable read

讀提交,顧名思義,就是一個事務要等另一個事務提交后才能讀取數據。

針對當前讀,加行鎖,加間隙鎖(GAP鎖)。所以不存在幻讀情況。

d、Serializable

Serializable 是最高的事務隔離級別,在該級別下,事務串行化順序執行,可以避免臟讀、不可重復讀與幻讀。但是這種事務隔離級別效率低下,比較耗數據庫性能,一般不使用。

從MVCC退回到基于鎖的并發控制。讀加共享鎖,寫加排它鎖,且讀寫沖突,還會鎖定這個范圍。并發性能急劇下降,實際中不會使用。

MYSAM 引擎的數據庫不支持事務,所以事務最好不要對混合引擎(如INNODB 、MYISAM)操作,若能正常運行且是你想要的最好,否則事務中對非支持事務表的操作是不能回滾恢復的。

(3)MVCC

MVCC是一種多版本并發控制機制。

解決的問題?

大多數的MYSQL事務型存儲引擎,如,InnoDB,Falcon以及PBXT都不使用一種簡單的行鎖機制.事實上,他們都和MVCC–多版本并發控制來一起使用。

大家都應該知道,鎖機制可以控制并發操作,但是其系統開銷較大,而MVCC可以在大多數情況下代替行級鎖,使用MVCC,能降低其系統開銷。

MVCC實現分析

MVCC是一種多版本并發控制機制。

解決的問題?

大多數的MYSQL事務型存儲引擎,如,InnoDB,Falcon以及PBXT都不使用一種簡單的行鎖機制.事實上,他們都和MVCC–多版本并發控制來一起使用。

大家都應該知道,鎖機制可以控制并發操作,但是其系統開銷較大,而MVCC可以在大多數情況下代替行級鎖,使用MVCC,能降低其系統開銷。

MVCC是通過保存數據在某個時間點的快照來實現的,不同存儲引擎的MVCC實現是不同的,典型的有樂觀并發控制和悲觀并發控制。

MVCC的最大好處是讀不加鎖,讀寫不沖突。這樣在讀多寫少的應用中,極大的提高了并發性能。

MVCC中讀又分為快照讀和當前讀。快照度顧名思義是讀的快照版本,有可能是歷史版本,不需要加鎖。當前讀是讀取的當前版本,當前讀返回的記錄需要加鎖,以保證其他事務不會并發修改。?

那么什時候是當前讀,什么時候是快照讀??

快照讀:簡單的select語句。?

select * from table where 不加鎖?

當前讀:特殊的select語句。?

select * from table where .. for update X鎖(排它鎖)?

select * from table where .. lock in share mode S鎖(共享鎖)?

增刪改操作。?

insert into table values (…) X鎖?

update table set .. where .. X鎖?

delete from table where .. X鎖

下面我們通過InnoDB的MVCC實現來分析MVCC使怎樣進行并發控制的.?

InnoDB的MVCC,是通過在每行記錄后面保存兩個隱藏的列來實現的,這兩個列,分別保存了這個行的創建時間(create_time)和行的刪除時間(delete_time)。這里存儲的并不是實際的時間值,而是系統版本號(可以理解為事務的ID),每開始一個新的事務,系統版本號就會自動遞增,事務開始時刻的系統版本號會作為事務的ID。下面看一下在REPEATABLE READ隔離級別下,MVCC具體是如何操作的。

假設有如下表test_mvcc:

idnamecreate_timedelete_time

1aaa1undefined

2bbb1undefined

3ccc1undefined

create_time和detete_time是隱藏的,通過select語句并看不到。

這里假設系統的版本號從1開始。(已經插入的3條數據,在一個事務中完成的)

初始狀態:delete_time(刪除版本)是未定義的,既沒有被刪除過。

select語句,InnoDB會根據以下兩個條件檢查每行記錄:

a.InnoDB只會查找版本早于當前事務版本的數據行(也就是,行的系統版本號小于或等于事務的系統版本號),這樣可以確保事務讀取的行,要么是在事務開始前已經存在的,要么是事務自身插入或者修改過的。

b.行的刪除版本要么未定義,要么大于當前事務版本號,這可以確保事務讀取到的行,在事務開始之前未被刪除.。

只有a,b同時滿足的記錄,才能返回作為查詢結果。

delete語句,InnoDB會為刪除的每一行保存當前系統的版本號(事務的ID)作為刪除標識。

下面開始具體分析:

第二個事務,ID為2;

start transaction;

select * from test_mvcc;? //(1)

select * from test_mvcc;? //(2)

commit;

假設1

假設在執行這個事務ID為2的過程中,剛執行到(1),這時,有另一個事務ID為3往這個表里插入了一條數據;?

第三個事務ID為3;

start transaction;

insert into test_mvcc values(NULL,'ddd');//主鍵是自增的,所以用NULL

commit;

這時表中的數據如下:

idnamecreate_timedelete_time

1aaa1undefined

2bbb1undefined

3ccc1undefined

4ddd3undefined

然后接著執行事務2中的(2),由于id=4的數據的創建時間(事務ID為3),執行當前事務的ID為2,而InnoDB只會查找事務ID小于等于當前事務ID的數據行,所以id=4的數據行并不會在執行事務2中的(2)被檢索出來,在事務2中的兩條select 語句檢索出來的數據都只會是下表:

idnamecreate_timedelete_time

1aaa1undefined

2bbb1undefined

3ccc1undefined

假設2

假設在執行事務ID為2的過程中,剛執行到(1),事務3執行完后,接著又執行了事務4;?

第四個事務:

start? transaction;?

delete from test_mvcc where id=1;

commit;

此時數據庫中的表如下:

idnamecreate_timedelete_time

1aaa14

2bbb1undefined

3ccc1undefined

4ddd3undefined

接著執行事務ID為2的事務(2),根據SELECT 檢索條件可以知道,它會檢索創建時間(創建事務的ID)小于當前事務ID的行和刪除時間(刪除事務的ID)大于當前事務的行,而id=4的行上面已經說過,而id=1的行由于刪除時間(刪除事務的ID)大于當前事務的ID,所以事務2的(2)select * from test_mvcc也會把id=1的數據檢索出來。所以事務2中的兩條select 語句檢索出來的數據都如下:

idnamecreate_timedelete_time

1aaa14

2bbb1undefined

3ccc1undefined

假設3

假設在執行完事務2的(1)后又執行,其它用戶執行了事務3、4,這時又有一個用戶對這張表執行了update操作:?

InnoDB執行update,實際上是新插入了一行記錄,并保存其創建時間為當前事務的ID,同時保存當前事務ID到要update的行的刪除時間。

第5個事務:

start? transaction;

update test_mvcc set name='eee' where id=2;

commit;

根據update的更新原則:會生成新的一行,并在原來要修改的列的刪除時間列上添加本事務ID,得到表如下:

idnamecreate_timedelete_time

1aaa14

2bbb15

3ccc1undefined

4ddd3undefined

2eee5undefined

繼續執行事務2的(2),根據select 語句的檢索條件,得到下表:

idnamecreate_timedelete_time

1aaa14

2bbb15

3ccc1undefined

到此整個MVCC的實現分析就完了。

(3)相關鎖

寫鎖:又稱排他鎖、X鎖。若事務T對數據對象A加上X鎖,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到T釋放A上的鎖。這保證了其他事務在T釋放A上的鎖之前不能再讀取和修改A。

讀鎖:也叫共享鎖、S鎖,若事務T對數據對象A加上S鎖,則事務T可以讀A但不能修改A,其他事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S 鎖。這保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改。

表鎖:mysql大多數存儲引擎都支持,是系統開銷最低但并發性最低的一個鎖策略。事務t對整個表加讀鎖,則其他事務可讀不可寫,若加寫鎖,則其他事務增刪改都不行。

行級鎖:又叫記錄鎖,操作對象是表中的一行,是MVCC技術用的比較多的。行級鎖是在存儲引擎中實現的,而不是在mysql服務器。行級鎖對系統開銷較大,處理高并發較好。我們常用的存儲引擎innodb是支持行級鎖的。

間隙鎖:編程的思想源于生活,生活中的例子能幫助我們更好的理解一些編程中的思想。

生活中排隊的場景,小明,小紅,小花三個人依次站成一排,此時,如何讓新來的小剛不能站在小紅旁邊,這時候只要將小紅和她前面的小明之間的空隙封鎖,將小紅和她后面的小花之間的空隙封鎖,那么小剛就不能站到小紅的旁邊。

這里的小紅,小明,小花,小剛就是數據庫的一條條記錄。

他們之間的空隙也就是間隙,而封鎖他們之間距離的鎖,叫做間隙鎖。

next-key鎖:next-key鎖其實包含了記錄鎖和間隙鎖,即鎖定一個范圍,并且鎖定記錄本身,InnoDB默認加鎖方式是next-key 鎖。

(4)mysql中幾個重要的log

binlog、redo log、undo log,其中redo和undo是跟事務緊密相關的。

binlog

binlog日志用于記錄所有更新且提交了數據或者已經潛在更新提交了數據(例如,沒有匹配任何行的一個DELETE)的所有語句。語句以“事件”的形式保存,它描述數據更改。

binlog是MySQL Server層記錄的日志, redo log是InnoDB存儲引擎層的日志。 兩者都是記錄了某些操作的日志,自然有些重復,但兩者記錄的格式不同。

-- binlog的作用:

a、恢復使能夠最大可能地更新數據庫,因為二進制日志包含備份后進行的所有更新。

b、在主復制服務器上記錄所有將發送給從服務器的語句

undo log

Undo Log是為了實現事務的原子性,在MySQL數據庫InnoDB存儲引擎中,還用UndoLog來實現多版本并發控制(簡稱:MVCC)。

-- 原理:

Undo Log的原理很簡單,為了滿足事務的原子性,在操作任何數據之前,首先將數據備份到一個地方(這個存儲數據備份的地方稱為UndoLo)。

然后進行數據的修改。如果出現了錯誤或者用戶執行了ROLLBACK語句,系統可以利用UndoLog中的備份將數據恢復到事務開始之前的狀態。

除了可以保證事務的原子性,Undo Log也可以用來輔助完成事務的持久化。

-- 用Undo Log實現原子性和持久化的事務的簡化過程:

假設有A、B兩個數據,值分別為1,2。

A.事務開始.

B.記錄A=1到undolog.

C.修改A=3.

D.記錄B=2到undolog.

E.修改B=4.

F.將undolog寫到磁盤。

G.將數據寫到磁盤。

H.事務提交

這里有一個隱含的前提條件:數據都是先讀到內存中,然后修改內存中的數據,最后將數據寫回磁盤。

之所以能同時保證原子性和持久化,是因為以下特點:

A.更新數據前記錄Undo log。

B.為了保證持久性,必須將數據在事務提交前寫到磁盤。只要事務成功提交,數據必然已經持久化。

C.Undo log必須先于數據持久化到磁盤。如果在G,H之間系統崩潰,undo log是完整的,可以用來回滾事務。

D.如果在A-F之間系統崩潰,因為數據沒有持久化到磁盤。所以磁盤上的數據還是保持在事務開始前的狀態。

缺陷:每個事務提交前將數據和Undo Log寫入磁盤,這樣會導致大量的磁盤IO,因此性能很低。

如果能夠將數據緩存一段時間,就能減少IO提高性能。但是這樣就會喪失事務的持久性。因此引入了另外一種機制來實現持久化,即redo log

redo log

記錄的是新數據的備份。在事務提交前,只要將Redo Log持久化即可,不需要將數據持久化。當系統崩潰時,雖然數據沒有持久化,但是RedoLog已經持久化。系統可以根據RedoLog的內容,將所有數據恢復到最新的狀態。

-- undo+redo聯合后,事務的簡化過程:

假設有A、B兩個數據,值分別為1,2.

A.事務開始.

B.記錄A=1到undolog.

C.修改A=3.

D.記錄A=3到redolog.

E.記錄B=2到undolog.

F.修改B=4.

G.記錄B=4到redolog.

H.將redolog寫入磁盤。

I.事務提交

-- undo+redo聯合后,事務的特點:

A.為了保證持久性,必須在事務提交前將redoLog持久化。

B.數據不需要在事務提交前寫入磁盤,而是緩存在內存中。

C.RedoLog保證事務的持久性。

D.UndoLog保證事務的原子性。

E.有一個隱含的特點,數據必須要晚于redolog寫入持久存

2、非常規方案實現分布式鎖

(1)for update方案

start transaction;

select id,name,application_count?

from campaign

Where id=1 for update;//X鎖

...

...

...

執行一大堆業務邏輯;

...

...

update campaign?

Set application_count =application_count -1

Where id=1;

commit;

(2)類似樂觀鎖的方案

start transaction;

select id,name,application_count?

from campaign

Where id=1;

記錄application_count到變量a中

...

...

...

執行一大堆業務邏輯;

...

...

update campaign?

Set application_count =application_count -1

Where id=1 and application_count=#{a};

commit;

3、樂觀鎖實現分布式鎖

(1)定義

樂觀的認為沒有人使用對應的資源。

樂觀鎖是基于數據的版本號實現的。

在創建數據表的時候,給表增加一個字段version,每次讀取的時候,把version讀取出來,更新的時候,比較version是否一致,并把version加1。

有的時候,不用增加version字段,通過某個業務字段也可以做到。

但是如果不用version字段,有可能出現ABA的問題,如果你的業務字段不會出現這種情況或者業務場景允許出現ABA的問題,那就沒有必要增加version字段。

(2)簡單例子

假如有如下一張表user:

idnameageversion

1xxx201

2yyy201

更新user的業務場景如下:?你需要先從user表把數據查詢出來,然后做一系列復雜的操作,最后更新對應的記錄

查詢:select id,name,age,version from user where id=1;

更新:update set name='zwf',version=2 where id=1 and version=1;如果發現version已經不是1了,說明已經有其他的事務進行了更新,id=1的這條記錄并不會因為并發而被修改。

假如你的業務場景跟例子中的是一樣的,而且也不需要增加version字段就可以實現樂觀鎖,那么用這種方式是最簡單的。(不需要專門生成一個資源表,來映射user表中的每條記錄,把記錄當成資源;不用增加version字段,對user表的侵入性為0)

(3)通用實現方式,生成資源表

資源表定義

idresourcegmt_creategmt_modifystatusversion

112017-08-08 11:11:112017-08-08 11:11:11210

222017-08-08 11:11:112017-08-08 11:11:11111

resource:代表一個資源,具體代表什么看你的業務場景了(比如某個類中的某個方法、比如上面提到的user表中的一條記錄)。

status:資源是否被鎖定,1代表未被鎖定,2代表鎖定。

執行流程?a、先執行select操作查詢當前數據的數據版本號,比如當前數據版本號是11:

select id, resource, state,version from resource where state=1 and id=2;

b、執行更新操作:

update resoure set state=2, version=12, update_time=now() where resource=2 and state=1 and version=11

c、如果上述update語句真正更新影響到了一行數據,那就說明占位成功。如果沒有更新影響到一行數據,則說明這個資源已經被別人占位了。

d、如果已經占位成功,想要再次進入,可以通過select * from where resource=2 and state=2 and version=12,有數據代表可以進入。

(3)缺點

a、單點風險,一旦數據庫掛掉,會導致業務系統不可用

b、這種操作方式,使原本一次的update操作,必須變為2次操作: select版本號一次;update一次。增加了數據庫操作的次數。

c、如果業務場景中的一次業務流程中,多個資源都需要用保證數據一致性,那么如果全部使用基于數據庫資源表的樂觀鎖,在高并發的要求下,對數據庫連接的開銷一定是無法忍受的。

d、樂觀鎖機制往往基于系統中的數據存儲邏輯,因此可能會造成臟數據被更新到數據庫中。在系統設計階段,我們應該充分考慮到這些情況出現的可能性,并進行相應調整,如將樂觀鎖策略在數據庫存儲過程中實現,對外只開放基于此存儲過程的數據更新途徑,而不是將數據庫表直接對外公開。

e、這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖。(可以通過定時任務來解決)

f、這把鎖只能是非阻塞的,沒有獲得鎖的線程并不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作。

5、排他鎖(悲觀鎖)實現分布式鎖

(1)定義

悲觀鎖是與樂觀鎖對應的,悲觀的認為資源已經被別人搶占了。

(2)實現方式

首先創建一張數據表:

CREATE TABLE `methodLock` (

? `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',

? `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '鎖定的方法名',

? `desc` varchar(1024) NOT NULL DEFAULT '備注信息',

? `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存數據時間,自動生成',

? PRIMARY KEY (`id`),

? UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';

方式一

當我們想要鎖住某個方法時,執行以下SQL:

insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)

因為我們對method_name做了唯一性約束,這里如果有多個請求同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,那么我們就可以認為操作成功的那個線程獲得了該方法的鎖,可以執行方法體內容。

當方法執行完畢之后,想要釋放鎖的話,需要執行以下Sql:

delete from methodLock where method_name ='method_name'

上面這種簡單的實現有以下幾個問題:

單點,一旦數據庫掛掉,會導致業務系統不可用(解決:搞兩個數據庫,數據之前雙向同步,一旦掛掉快速切換到備庫上)。

這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖(解決:定時任務)。

這把鎖只能是非阻塞的,因為數據的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程并不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖操作(解決:while循環,直到insert成功再返回成功)。

這把鎖是不可重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因為數據中數據已經存在了(解決:在數據庫表中加個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那么下次再獲取鎖的時候先查詢數據庫,如果當前機器的主機信息和線程信息在數據庫可以查到的話,直接把鎖分配給他就可以了)。

方式二

X鎖實現: for update

我們還用剛剛創建的那張數據庫表。可以通過數據庫的排他鎖來實現分布式鎖。

基于MySql的InnoDB引擎,可以使用以下方法來實現加鎖操作:

start transaction

select * from methodLock where method_name=xxx for update;

在查詢語句后面增加for update,數據庫會在查詢過程中給數據庫表增加排他鎖。當某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖。(InnoDB引擎在加鎖的時候,只有通過索引進行檢索的時候才會使用行級鎖,否則會使用表級鎖。這里我們希望使用行級鎖,就要給method_name添加索引,值得注意的是,這個索引一定要創建成唯一索引,否則會出現多個重載方法之間無法同時被訪問的問題。重載方法的話建議把參數類型也加上)

解鎖:commit transaction

使用這種方式可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題。

阻塞鎖? for update語句會在執行成功后立即返回,在執行失敗時一直處于阻塞狀態,直到成功。

鎖定之后服務宕機,無法釋放?使用這種方式,服務宕機之后數據庫會自己把鎖釋放掉。

但是還是無法直接解決數據庫單點和可重入問題。

這里還可能存在另外一個問題,雖然我們對method_name 使用了唯一索引,并且顯示使用for update來使用行級鎖。但是,MySql會對查詢進行優化,即便在條件中使用了索引字段,但是否使用索引來檢索數據是由 MySQL 通過判斷不同執行計劃的代價來決定的,如果 MySQL 認為全表掃效率更高,比如對一些很小的表,它就不會使用索引,這種情況下 InnoDB 將使用表鎖,而不是行鎖。如果發生這種情況就悲劇了。。。

還有一個問題,就是我們要使用排他鎖來進行分布式鎖的lock,那么一個排他鎖長時間不提交,就會占用數據庫連接。一旦類似的連接變得多了,就可能把數據庫連接池撐爆。

4、數據庫實現分布式鎖-總結

優點:

簡單、容易理解

缺點:

在高并發的時候,數據庫連接數會不夠用,性能上限很容易觸及

處理阻塞、單點、鎖超時等問題,會使方案非常復雜。

二、基于Zookeeper實現分布式鎖

1、zookeeper簡單介紹

ZooKeeper是一個分布式的,開放源碼的分布式應用程序協調服務,是Google的Chubby一個開源的實現,是Hadoop和Hbase的重要組件。它是一個為分布式應用提供一致性服務的軟件,提供的功能包括:配置維護、域名服務、分布式同步、組服務等。

https://zookeeper.apache.org/

2、實現原理

基于zookeeper臨時有序節點可以實現的分布式鎖。

大致思想即為:每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。

判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。

當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。

3、實現方式

可以直接使用zookeeper第三方庫Curator客戶端,這個客戶端中封裝了一個可重入的鎖服務(你也可以自己封裝)。

public boolean tryLock(long timeout, TimeUnit unit) throws Exception {

? ? ? ? return interProcessMutex.acquire(timeout, unit);

}

public void unlock() throws Exception {

? ? ? ? interProcessMutex.release();

}

Curator提供的InterProcessMutex是分布式鎖的實現。acquire方法用戶獲取鎖,release方法用于釋放鎖。

使用ZK實現的分布式鎖好像完全符合了本文開頭我們對一個分布式鎖的所有期望。但是,其實并不是,Zookeeper實現的分布式鎖其實存在一個缺點,那就是性能上可能并沒有緩存服務那么高。因為每次在創建鎖和釋放鎖的過程中,都要動態創建、銷毀瞬時節點來實現鎖功能。ZK中創建和刪除節點只能通過Leader服務器來執行,然后將數據同不到所有的Follower機器上。

其實,使用Zookeeper也有可能帶來并發問題,只是并不常見而已。考慮這樣的情況,由于網絡抖動,客戶端可ZK集群的session連接斷了,那么zk以為客戶端掛了,就會刪除臨時節點,這時候其他客戶端就可以獲取到分布式鎖了。就可能產生并發問題。這個問題不常見是因為zk有重試機制,一旦zk集群檢測不到客戶端的心跳,就會重試,Curator客戶端支持多種重試策略。多次重試之后還不行的話才會刪除臨時節點。(所以,選擇一個合適的重試策略也比較重要,要在鎖的粒度和并發之間找一個平衡。)

4、總結

優點:

鎖無法釋放問題解決,使用Zookeeper可以有效的解決鎖無法釋放的問題,因為在創建鎖的時候,客戶端會在ZK中創建一個臨時節點,一旦客戶端獲取到鎖之后突然掛掉(Session連接斷開),那么這個臨時節點就會自動刪除掉。其他客戶端就可以再次獲得鎖。

非阻塞鎖問題解決,使用Zookeeper可以實現阻塞的鎖,客戶端可以通過在ZK中創建順序節點,并且在節點上綁定監聽器,一旦節點有變化,Zookeeper會通知客戶端,客戶端可以檢查自己創建的節點是不是當前所有節點中序號最小的,如果是,那么自己就獲取到鎖,便可以執行業務邏輯了。

不可重入問題解決,使用Zookeeper也可以有效的解決不可重入的問題,客戶端在創建節點的時候,把當前客戶端的主機信息和線程信息直接寫入到節點中,下次想要獲取鎖的時候和當前最小的節點中的數據比對一下就可以了。如果和自己的信息一樣,那么自己直接獲取到鎖,如果不一樣就再創建一個臨時的順序節點,參與排隊。

單點問題解決,使用Zookeeper可以有效的解決單點問題,ZK是集群部署的,只要集群中有半數以上的機器存活,就可以對外提供服務。

實現簡單

缺點,性能不如緩存高

三、基于Tair實現分布式鎖

1、tair簡單介紹

https://www.atatech.org/articles/4743

2、實現

參考:https://www.atatech.org/articles/30653

本人沒有在實際環境中使用過。

import com.taobao.tair.DataEntry;

import com.taobao.tair.Result;

import com.taobao.tair.ResultCode;

import com.taobao.tair.TairManager;

import org.apache.commons.lang.NotImplementedException;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.slf4j.helpers.FormattingTuple;

import org.slf4j.helpers.MessageFormatter;

import javax.annotation.Resource;

import java.net.InetAddress;

import java.net.UnknownHostException;

import java.util.concurrent.TimeUnit;

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.Lock;

public class CommonLocker {

? ? private static final Logger logger = LoggerFactory.getLogger(CommonLocker.class);

? ? @Resource

? ? private TairManager ldbTairManager;

? ? private static final short NAMESPACE = 1310;

? ? private static CommonLocker locker;

? ? public void init() {

? ? ? ? if (locker != null) return;

? ? ? ? synchronized (CommonLocker.class) {

? ? ? ? ? ? if (locker == null)

? ? ? ? ? ? ? ? locker = this;

? ? ? ? }

? ? }

? ? public static Lock newLock(String format, Object... argArray) {

? ? ? ? FormattingTuple ft = MessageFormatter.arrayFormat(format, argArray);

? ? ? ? return newLock(ft.getMessage());

? ? }

? ? public static Lock newLock(String strKey) {

? ? ? ? String key = "_tl_" + strKey;

? ? ? ? return new TairLock(key, CommonConfig.lock_default_timeout);

? ? }

? ? public static Lock newLock(String strKey, int timeout) {

? ? ? ? String key = "_tl_" + strKey;

? ? ? ? return new TairLock(key, timeout);

? ? }

? ? private static class TairLock implements Lock {

? ? ? ? private String lockKey;

? ? ? ? private boolean gotLock = false;

? ? ? ? private int retryGet = 0;

? ? ? ? private int retryPut = 0;

? ? ? ? private int timeout;

? ? ? ? public TairLock(String key, int timeout) {

? ? ? ? ? ? this.lockKey = tokey(key);

? ? ? ? ? ? this.timeout = timeout;

? ? ? ? }

? ? ? ? public boolean tryLock() {

? ? ? ? ? ? return tryLock(timeout);

? ? ? ? }

? ? ? ? /**

? ? ? ? * need finally do unlock

? ? ? ? *

? ? ? ? * @return

? ? ? ? */

? ? ? ? public boolean tryLock(int timeout) {

? ? ? ? ? ? Result result = locker.ldbTairManager.get(NAMESPACE, lockKey);

? ? ? ? ? ? while (retryGet++ < CommonConfig.lock_get_max_retry &&

? ? ? ? ? ? ? ? ? ? (result == null || ResultCode.CONNERROR.equals(result.getRc()) || ResultCode.TIMEOUT.equals(result.getRc()) || ResultCode.UNKNOW.equals(result.getRc()))) // 重試一次

? ? ? ? ? ? ? ? result = locker.ldbTairManager.get(NAMESPACE, lockKey);

? ? ? ? ? ? if (ResultCode.DATANOTEXSITS.equals(result.getRc())) { // lock is free

? ? ? ? ? ? ? ? // 已驗證version 2表示為空,若不是為空,則返回version error

? ? ? ? ? ? ? ? ResultCode code = locker.ldbTairManager.put(NAMESPACE, lockKey, locker.getValue(), 2, timeout);

? ? ? ? ? ? ? ? if (ResultCode.SUCCESS.equals(code)) {

? ? ? ? ? ? ? ? ? ? gotLock = true;

? ? ? ? ? ? ? ? ? ? return true;

? ? ? ? ? ? ? ? } else if (retryPut++ < CommonConfig.lock_put_max_retry &&

? ? ? ? ? ? ? ? ? ? ? ? (code == null || ResultCode.CONNERROR.equals(code) || ResultCode.TIMEOUT.equals(code) || ResultCode.UNKNOW.equals(code))) { // 感謝劍癡指出錯誤

? ? ? ? ? ? ? ? ? ? return tryLock(timeout);

? ? ? ? ? ? ? ? }

? ? ? ? ? ? } else if (result.getValue() != null && locker.getValue().equals(result.getValue().getValue())) {

// 【注意】其實這里線程復用時,ThreadName有相同風險,可以改為uuid邏輯,復用鎖傳入uuid。

? ? ? ? ? ? ? ? // 若是自己的鎖,自己繼續用

? ? ? ? ? ? ? ? gotLock = true;

? ? ? ? ? ? ? ? return true;

? ? ? ? ? ? }

? ? ? ? ? ? // 到這里表示沒有拿到鎖

? ? ? ? ? ? return false;

? ? ? ? }

? ? ? ? public void unlock() {

? ? ? ? ? ? if (gotLock) {

? ? ? ? ? ? ? ? ResultCode invalidCode = locker.ldbTairManager.invalid(NAMESPACE, lockKey);

? ? ? ? ? ? ? ? gotLock = false;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? public void lock() {

? ? ? ? ? ? throw new NotImplementedException();

? ? ? ? }

? ? ? ? public void lockInterruptibly() throws InterruptedException {

? ? ? ? ? ? throw new NotImplementedException();

? ? ? ? }

? ? ? ? public boolean tryLock(long l, TimeUnit timeUnit) throws InterruptedException {

? ? ? ? ? ? throw new NotImplementedException();

? ? ? ? }

? ? ? ? public Condition newCondition() {

? ? ? ? ? ? throw new NotImplementedException();

? ? ? ? }

? ? }

// 【注意】其實這里線程復用時,ThreadName有相同風險,可以改為uuid邏輯,復用鎖傳入uuid。

? ? private String getValue() {

? ? ? ? return getHostname() + ":" + Thread.currentThread().getName();

? ? }

? ? /**

? ? * 獲得機器名

? ? *

? ? * @return

? ? */

? ? public static String getHostname() {

? ? ? ? try {

? ? ? ? ? ? return InetAddress.getLocalHost().getHostName();

? ? ? ? } catch (UnknownHostException e) {

? ? ? ? ? ? return "[unknown]";

? ? ? ? }

? ? }

? ? public void setLdbTairManager(TairManager ldbTairManager) {

? ? ? ? this.ldbTairManager = ldbTairManager;

? ? }

}

使用樣例

Lock lockA = CommonLocker.newLock("hs_room_{}_uid_{}", roomDo.getUuid(), roomDo.getMaster().getUid());

Lock lockB = CommonLocker.newLock("hs_room_{}_uid_{}", roomDo.getUuid(), roomDo.getPartnerList().get(0).getUid());

try {

? ? if (lockA.tryLock() && lockB.tryLock()) {// 分布式鎖定本任務

? ? ? ? // do something....

? ? }

} finally {

? ? lockB.unlock();

? ? lockA.unlock();

}

3、總結

整體上用到了tair的get、put、invalid三個方法,如果put失敗或get失敗,重試即可(根據業務場景來判斷重試幾次,不建議重試太多次,容易造成雪崩)

優點:

鎖無法釋放問題解決,通過tair的超時問題機制解決,不過超時時間需要根據業務場景來判斷

可重入問題解決,讓tair的value包含機器ip+線程name,獲取鎖的時候,先get value做檢查是不是已經獲取了鎖

非阻塞問題解決(while重復執行)

高性能、高可用

缺點:通過時間控制失效不太靠譜

有寫的不對的地方,歡迎大家拍磚

分布式與單機情況下最大的不同在于其不是多線程而是多進程

事務是不能解決分布式鎖要解決的問題的,不過在某些情況下事務也可以解決分布式鎖要解決的問題。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容