? ? ? 最近碰到幾個(gè)業(yè)務(wù)場(chǎng)景竭宰,會(huì)遇到并發(fā)的問題。在單實(shí)例情況下松蒜,我們會(huì)通過java.util.concurrent包下的API或者synchronized關(guān)鍵字來處理丢氢。但是集群部署情況奕翔,我們就要利用分布式鎖來解決了裕寨。
分布式與單機(jī)情況下最大的不同在于其不是多線程而是多進(jìn)程。
多線程由于可以共享堆內(nèi)存派继,因此可以簡(jiǎn)單的采取內(nèi)存作為標(biāo)記存儲(chǔ)位置宾袜。而進(jìn)程之間甚至可能都不在同一臺(tái)物理機(jī)上,因此需要將標(biāo)記存儲(chǔ)在一個(gè)所有進(jìn)程都能看到的地方驾窟。
與單機(jī)模式下的鎖不僅需要保證進(jìn)程可見庆猫,還需要考慮進(jìn)程與鎖之間的網(wǎng)絡(luò)問題。(我覺得分布式情況下之所以問題變得復(fù)雜绅络,主要就是需要考慮到網(wǎng)絡(luò)的延時(shí)和不可靠)
分布式鎖的要求:
可以保證在分布式部署的應(yīng)用集群中月培,同一個(gè)方法在同一時(shí)間只能被一臺(tái)機(jī)器上的一個(gè)線程執(zhí)行嘁字。
這把鎖要是一把可重入鎖(避免死鎖)
這把鎖最好是一把阻塞鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
有高可用的獲取鎖和釋放鎖功能
獲取鎖和釋放鎖的性能要好
這篇文章把我以前用到分布式鎖方案和現(xiàn)在調(diào)研到的方案做一個(gè)總結(jié)。
基于數(shù)據(jù)庫(kù)
基于分布式緩存(redis杉畜、memcached纪蜒、日本人Mikio Hirabayashi 開發(fā)的ttserver、公司的tair等等)
基于zookeeper
本人目前只用過基于數(shù)據(jù)庫(kù)此叠、zookeeper纯续、redis的分布式鎖
再說一句,要基于你的業(yè)務(wù)場(chǎng)景選擇合適方案灭袁。
再談數(shù)據(jù)庫(kù)的分布式鎖之前茸歧,先了解一下相關(guān)基礎(chǔ)知識(shí)倦炒。
事務(wù)是不能解決分布式鎖要解決的問題的,不過在某些情況下事務(wù)也可以解決分布式鎖要解決的問題举娩。
事務(wù)是一組原子性sql查詢語句析校,被當(dāng)作一個(gè)工作單元。若MySQL對(duì)改事務(wù)單元內(nèi)的所有sql語句都正常的執(zhí)行完铜涉,則事務(wù)操作視為成功智玻,所有的sql語句才對(duì)數(shù)據(jù)生效,若sql中任意不能執(zhí)行或出錯(cuò)則事務(wù)操作失敗芙代,所有對(duì)數(shù)據(jù)的操作則無效(通過回滾恢復(fù)數(shù)據(jù))吊奢。事務(wù)有四個(gè)屬性:
a、原子性:事務(wù)被認(rèn)為不可分的一個(gè)工作單元纹烹,要么全部正常執(zhí)行页滚,要么全部不執(zhí)行。
b铺呵、一致性:事務(wù)操作對(duì)數(shù)據(jù)庫(kù)總是從一種一致性的狀態(tài)轉(zhuǎn)換成另外一種一致性狀態(tài)裹驰。
c、隔離性:一個(gè)事務(wù)的操作結(jié)果在內(nèi)部一致片挂,可見幻林,而對(duì)除自己以外的事務(wù)是不可見的。
d音念、永久性:事務(wù)在未提交前數(shù)據(jù)一般情況下可以回滾恢復(fù)數(shù)據(jù)沪饺,一旦提交(commit)數(shù)據(jù)的改變則變成永久(當(dāng)然用update肯定還能修改)。
事務(wù)的4個(gè)隔離級(jí)別:
a闷愤、Read uncommitted
讀未提交整葡,顧名思義,就是一個(gè)事務(wù)可以讀取另一個(gè)未提交事務(wù)的數(shù)據(jù)讥脐。
實(shí)際中不會(huì)使用
b遭居、Read committed
讀提交啼器,顧名思義,就是一個(gè)事務(wù)要等另一個(gè)事務(wù)提交后才能讀取數(shù)據(jù)俱萍。
針對(duì)當(dāng)前讀镀首,加行鎖。存在幻讀情況鼠次。?
c更哄、Repeatable read
讀提交,顧名思義腥寇,就是一個(gè)事務(wù)要等另一個(gè)事務(wù)提交后才能讀取數(shù)據(jù)成翩。
針對(duì)當(dāng)前讀,加行鎖赦役,加間隙鎖(GAP鎖)麻敌。所以不存在幻讀情況。
d掂摔、Serializable
Serializable 是最高的事務(wù)隔離級(jí)別术羔,在該級(jí)別下,事務(wù)串行化順序執(zhí)行乙漓,可以避免臟讀级历、不可重復(fù)讀與幻讀。但是這種事務(wù)隔離級(jí)別效率低下叭披,比較耗數(shù)據(jù)庫(kù)性能寥殖,一般不使用。
從MVCC退回到基于鎖的并發(fā)控制涩蜘。讀加共享鎖嚼贡,寫加排它鎖,且讀寫沖突同诫,還會(huì)鎖定這個(gè)范圍粤策。并發(fā)性能急劇下降,實(shí)際中不會(huì)使用误窖。
MYSAM 引擎的數(shù)據(jù)庫(kù)不支持事務(wù)叮盘,所以事務(wù)最好不要對(duì)混合引擎(如INNODB 、MYISAM)操作贩猎,若能正常運(yùn)行且是你想要的最好熊户,否則事務(wù)中對(duì)非支持事務(wù)表的操作是不能回滾恢復(fù)的萍膛。
MVCC是一種多版本并發(fā)控制機(jī)制吭服。
大多數(shù)的MYSQL事務(wù)型存儲(chǔ)引擎,如,InnoDB蝗罗,F(xiàn)alcon以及PBXT都不使用一種簡(jiǎn)單的行鎖機(jī)制.事實(shí)上,他們都和MVCC–多版本并發(fā)控制來一起使用艇棕。
大家都應(yīng)該知道,鎖機(jī)制可以控制并發(fā)操作,但是其系統(tǒng)開銷較大,而MVCC可以在大多數(shù)情況下代替行級(jí)鎖,使用MVCC,能降低其系統(tǒng)開銷蝌戒。
MVCC是一種多版本并發(fā)控制機(jī)制。
解決的問題沼琉?
大多數(shù)的MYSQL事務(wù)型存儲(chǔ)引擎,如,InnoDB北苟,F(xiàn)alcon以及PBXT都不使用一種簡(jiǎn)單的行鎖機(jī)制.事實(shí)上,他們都和MVCC–多版本并發(fā)控制來一起使用。
大家都應(yīng)該知道,鎖機(jī)制可以控制并發(fā)操作,但是其系統(tǒng)開銷較大,而MVCC可以在大多數(shù)情況下代替行級(jí)鎖,使用MVCC,能降低其系統(tǒng)開銷打瘪。
MVCC是通過保存數(shù)據(jù)在某個(gè)時(shí)間點(diǎn)的快照來實(shí)現(xiàn)的友鼻,不同存儲(chǔ)引擎的MVCC實(shí)現(xiàn)是不同的,典型的有樂觀并發(fā)控制和悲觀并發(fā)控制。
MVCC的最大好處是讀不加鎖闺骚,讀寫不沖突彩扔。這樣在讀多寫少的應(yīng)用中,極大的提高了并發(fā)性能僻爽。
MVCC中讀又分為快照讀和當(dāng)前讀虫碉。快照度顧名思義是讀的快照版本胸梆,有可能是歷史版本敦捧,不需要加鎖。當(dāng)前讀是讀取的當(dāng)前版本碰镜,當(dāng)前讀返回的記錄需要加鎖兢卵,以保證其他事務(wù)不會(huì)并發(fā)修改。?
那么什時(shí)候是當(dāng)前讀绪颖,什么時(shí)候是快照讀济蝉??
快照讀:簡(jiǎn)單的select語句。?
select * from table where 不加鎖?
當(dāng)前讀:特殊的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實(shí)現(xiàn)來分析MVCC使怎樣進(jìn)行并發(fā)控制的.?
InnoDB的MVCC,是通過在每行記錄后面保存兩個(gè)隱藏的列來實(shí)現(xiàn)的滓鸠,這兩個(gè)列雁乡,分別保存了這個(gè)行的創(chuàng)建時(shí)間(create_time)和行的刪除時(shí)間(delete_time)。這里存儲(chǔ)的并不是實(shí)際的時(shí)間值,而是系統(tǒng)版本號(hào)(可以理解為事務(wù)的ID)糜俗,每開始一個(gè)新的事務(wù)踱稍,系統(tǒng)版本號(hào)就會(huì)自動(dòng)遞增,事務(wù)開始時(shí)刻的系統(tǒng)版本號(hào)會(huì)作為事務(wù)的ID悠抹。下面看一下在REPEATABLE READ隔離級(jí)別下珠月,MVCC具體是如何操作的。
假設(shè)有如下表test_mvcc:
idnamecreate_timedelete_time
1aaa1undefined
2bbb1undefined
3ccc1undefined
create_time和detete_time是隱藏的楔敌,通過select語句并看不到啤挎。
這里假設(shè)系統(tǒng)的版本號(hào)從1開始。(已經(jīng)插入的3條數(shù)據(jù)卵凑,在一個(gè)事務(wù)中完成的)
初始狀態(tài):delete_time(刪除版本)是未定義的庆聘,既沒有被刪除過胜臊。
select語句,InnoDB會(huì)根據(jù)以下兩個(gè)條件檢查每行記錄:
a.InnoDB只會(huì)查找版本早于當(dāng)前事務(wù)版本的數(shù)據(jù)行(也就是,行的系統(tǒng)版本號(hào)小于或等于事務(wù)的系統(tǒng)版本號(hào))伙判,這樣可以確保事務(wù)讀取的行象对,要么是在事務(wù)開始前已經(jīng)存在的,要么是事務(wù)自身插入或者修改過的宴抚。
b.行的刪除版本要么未定義,要么大于當(dāng)前事務(wù)版本號(hào),這可以確保事務(wù)讀取到的行勒魔,在事務(wù)開始之前未被刪除.。
只有a,b同時(shí)滿足的記錄菇曲,才能返回作為查詢結(jié)果沥邻。
delete語句,InnoDB會(huì)為刪除的每一行保存當(dāng)前系統(tǒng)的版本號(hào)(事務(wù)的ID)作為刪除標(biāo)識(shí)羊娃。
下面開始具體分析:
第二個(gè)事務(wù),ID為2;
start transaction;
select * from test_mvcc;? //(1)
select * from test_mvcc;? //(2)
commit;
假設(shè)1
假設(shè)在執(zhí)行這個(gè)事務(wù)ID為2的過程中唐全,剛執(zhí)行到(1),這時(shí)蕊玷,有另一個(gè)事務(wù)ID為3往這個(gè)表里插入了一條數(shù)據(jù);?
第三個(gè)事務(wù)ID為3;
start transaction;
insert into test_mvcc values(NULL,'ddd');//主鍵是自增的邮利,所以用NULL
commit;
這時(shí)表中的數(shù)據(jù)如下:
idnamecreate_timedelete_time
1aaa1undefined
2bbb1undefined
3ccc1undefined
4ddd3undefined
然后接著執(zhí)行事務(wù)2中的(2),由于id=4的數(shù)據(jù)的創(chuàng)建時(shí)間(事務(wù)ID為3)垃帅,執(zhí)行當(dāng)前事務(wù)的ID為2延届,而InnoDB只會(huì)查找事務(wù)ID小于等于當(dāng)前事務(wù)ID的數(shù)據(jù)行,所以id=4的數(shù)據(jù)行并不會(huì)在執(zhí)行事務(wù)2中的(2)被檢索出來贸诚,在事務(wù)2中的兩條select 語句檢索出來的數(shù)據(jù)都只會(huì)是下表:
idnamecreate_timedelete_time
1aaa1undefined
2bbb1undefined
3ccc1undefined
假設(shè)2
假設(shè)在執(zhí)行事務(wù)ID為2的過程中方庭,剛執(zhí)行到(1),事務(wù)3執(zhí)行完后酱固,接著又執(zhí)行了事務(wù)4;?
第四個(gè)事務(wù):
start? transaction;?
delete from test_mvcc where id=1;
commit;
此時(shí)數(shù)據(jù)庫(kù)中的表如下:
idnamecreate_timedelete_time
1aaa14
2bbb1undefined
3ccc1undefined
4ddd3undefined
接著執(zhí)行事務(wù)ID為2的事務(wù)(2),根據(jù)SELECT 檢索條件可以知道械念,它會(huì)檢索創(chuàng)建時(shí)間(創(chuàng)建事務(wù)的ID)小于當(dāng)前事務(wù)ID的行和刪除時(shí)間(刪除事務(wù)的ID)大于當(dāng)前事務(wù)的行,而id=4的行上面已經(jīng)說過运悲,而id=1的行由于刪除時(shí)間(刪除事務(wù)的ID)大于當(dāng)前事務(wù)的ID龄减,所以事務(wù)2的(2)select * from test_mvcc也會(huì)把id=1的數(shù)據(jù)檢索出來。所以事務(wù)2中的兩條select 語句檢索出來的數(shù)據(jù)都如下:
idnamecreate_timedelete_time
1aaa14
2bbb1undefined
3ccc1undefined
假設(shè)3
假設(shè)在執(zhí)行完事務(wù)2的(1)后又執(zhí)行班眯,其它用戶執(zhí)行了事務(wù)3希停、4,這時(shí)又有一個(gè)用戶對(duì)這張表執(zhí)行了update操作:?
InnoDB執(zhí)行update署隘,實(shí)際上是新插入了一行記錄宠能,并保存其創(chuàng)建時(shí)間為當(dāng)前事務(wù)的ID,同時(shí)保存當(dāng)前事務(wù)ID到要update的行的刪除時(shí)間磁餐。
第5個(gè)事務(wù):
start? transaction;
update test_mvcc set name='eee' where id=2;
commit;
根據(jù)update的更新原則:會(huì)生成新的一行违崇,并在原來要修改的列的刪除時(shí)間列上添加本事務(wù)ID,得到表如下:
idnamecreate_timedelete_time
1aaa14
2bbb15
3ccc1undefined
4ddd3undefined
2eee5undefined
繼續(xù)執(zhí)行事務(wù)2的(2),根據(jù)select 語句的檢索條件,得到下表:
idnamecreate_timedelete_time
1aaa14
2bbb15
3ccc1undefined
到此整個(gè)MVCC的實(shí)現(xiàn)分析就完了。
寫鎖:又稱排他鎖亦歉、X鎖。若事務(wù)T對(duì)數(shù)據(jù)對(duì)象A加上X鎖畅哑,事務(wù)T可以讀A也可以修改A肴楷,其他事務(wù)不能再對(duì)A加任何鎖,直到T釋放A上的鎖荠呐。這保證了其他事務(wù)在T釋放A上的鎖之前不能再讀取和修改A赛蔫。
讀鎖:也叫共享鎖、S鎖泥张,若事務(wù)T對(duì)數(shù)據(jù)對(duì)象A加上S鎖呵恢,則事務(wù)T可以讀A但不能修改A,其他事務(wù)只能再對(duì)A加S鎖媚创,而不能加X鎖渗钉,直到T釋放A上的S 鎖。這保證了其他事務(wù)可以讀A钞钙,但在T釋放A上的S鎖之前不能對(duì)A做任何修改鳄橘。
表鎖:mysql大多數(shù)存儲(chǔ)引擎都支持,是系統(tǒng)開銷最低但并發(fā)性最低的一個(gè)鎖策略。事務(wù)t對(duì)整個(gè)表加讀鎖,則其他事務(wù)可讀不可寫推穷,若加寫鎖岛宦,則其他事務(wù)增刪改都不行。
行級(jí)鎖:又叫記錄鎖浓镜,操作對(duì)象是表中的一行,是MVCC技術(shù)用的比較多的。行級(jí)鎖是在存儲(chǔ)引擎中實(shí)現(xiàn)的暗挑,而不是在mysql服務(wù)器。行級(jí)鎖對(duì)系統(tǒng)開銷較大斜友,處理高并發(fā)較好窿祥。我們常用的存儲(chǔ)引擎innodb是支持行級(jí)鎖的。
間隙鎖:編程的思想源于生活蝙寨,生活中的例子能幫助我們更好的理解一些編程中的思想晒衩。
生活中排隊(duì)的場(chǎng)景,小明墙歪,小紅听系,小花三個(gè)人依次站成一排,此時(shí)虹菲,如何讓新來的小剛不能站在小紅旁邊靠胜,這時(shí)候只要將小紅和她前面的小明之間的空隙封鎖,將小紅和她后面的小花之間的空隙封鎖,那么小剛就不能站到小紅的旁邊浪漠。
這里的小紅陕习,小明,小花址愿,小剛就是數(shù)據(jù)庫(kù)的一條條記錄该镣。
他們之間的空隙也就是間隙,而封鎖他們之間距離的鎖响谓,叫做間隙鎖损合。
next-key鎖:next-key鎖其實(shí)包含了記錄鎖和間隙鎖,即鎖定一個(gè)范圍娘纷,并且鎖定記錄本身嫁审,InnoDB默認(rèn)加鎖方式是next-key 鎖。
binlog赖晶、redo log律适、undo log,其中redo和undo是跟事務(wù)緊密相關(guān)的遏插。
binlog日志用于記錄所有更新且提交了數(shù)據(jù)或者已經(jīng)潛在更新提交了數(shù)據(jù)(例如擦耀,沒有匹配任何行的一個(gè)DELETE)的所有語句。語句以“事件”的形式保存涩堤,它描述數(shù)據(jù)更改眷蜓。
binlog是MySQL Server層記錄的日志, redo log是InnoDB存儲(chǔ)引擎層的日志胎围。 兩者都是記錄了某些操作的日志吁系,自然有些重復(fù),但兩者記錄的格式不同白魂。
-- binlog的作用:
a汽纤、恢復(fù)使能夠最大可能地更新數(shù)據(jù)庫(kù),因?yàn)槎M(jìn)制日志包含備份后進(jìn)行的所有更新福荸。
b蕴坪、在主復(fù)制服務(wù)器上記錄所有將發(fā)送給從服務(wù)器的語句
Undo Log是為了實(shí)現(xiàn)事務(wù)的原子性,在MySQL數(shù)據(jù)庫(kù)InnoDB存儲(chǔ)引擎中敬锐,還用UndoLog來實(shí)現(xiàn)多版本并發(fā)控制(簡(jiǎn)稱:MVCC)背传。
-- 原理:
Undo Log的原理很簡(jiǎn)單,為了滿足事務(wù)的原子性台夺,在操作任何數(shù)據(jù)之前径玖,首先將數(shù)據(jù)備份到一個(gè)地方(這個(gè)存儲(chǔ)數(shù)據(jù)備份的地方稱為UndoLo)。
然后進(jìn)行數(shù)據(jù)的修改颤介。如果出現(xiàn)了錯(cuò)誤或者用戶執(zhí)行了ROLLBACK語句梳星,系統(tǒng)可以利用UndoLog中的備份將數(shù)據(jù)恢復(fù)到事務(wù)開始之前的狀態(tài)赞赖。
除了可以保證事務(wù)的原子性,Undo Log也可以用來輔助完成事務(wù)的持久化冤灾。
-- 用Undo Log實(shí)現(xiàn)原子性和持久化的事務(wù)的簡(jiǎn)化過程:
假設(shè)有A前域、B兩個(gè)數(shù)據(jù),值分別為1,2韵吨。
A.事務(wù)開始.
B.記錄A=1到undolog.
C.修改A=3.
D.記錄B=2到undolog.
E.修改B=4.
F.將undolog寫到磁盤匿垄。
G.將數(shù)據(jù)寫到磁盤。
H.事務(wù)提交
這里有一個(gè)隱含的前提條件:數(shù)據(jù)都是先讀到內(nèi)存中学赛,然后修改內(nèi)存中的數(shù)據(jù)年堆,最后將數(shù)據(jù)寫回磁盤吞杭。
之所以能同時(shí)保證原子性和持久化盏浇,是因?yàn)橐韵绿攸c(diǎn):
A.更新數(shù)據(jù)前記錄Undo log。
B.為了保證持久性芽狗,必須將數(shù)據(jù)在事務(wù)提交前寫到磁盤绢掰。只要事務(wù)成功提交,數(shù)據(jù)必然已經(jīng)持久化童擎。
C.Undo log必須先于數(shù)據(jù)持久化到磁盤滴劲。如果在G,H之間系統(tǒng)崩潰,undo log是完整的顾复,可以用來回滾事務(wù)班挖。
D.如果在A-F之間系統(tǒng)崩潰,因?yàn)閿?shù)據(jù)沒有持久化到磁盤。所以磁盤上的數(shù)據(jù)還是保持在事務(wù)開始前的狀態(tài)芯砸。
缺陷:每個(gè)事務(wù)提交前將數(shù)據(jù)和Undo Log寫入磁盤萧芙,這樣會(huì)導(dǎo)致大量的磁盤IO,因此性能很低假丧。
如果能夠?qū)?shù)據(jù)緩存一段時(shí)間双揪,就能減少IO提高性能。但是這樣就會(huì)喪失事務(wù)的持久性包帚。因此引入了另外一種機(jī)制來實(shí)現(xiàn)持久化渔期,即redo log
記錄的是新數(shù)據(jù)的備份。在事務(wù)提交前渴邦,只要將Redo Log持久化即可疯趟,不需要將數(shù)據(jù)持久化。當(dāng)系統(tǒng)崩潰時(shí)谋梭,雖然數(shù)據(jù)沒有持久化迅办,但是RedoLog已經(jīng)持久化。系統(tǒng)可以根據(jù)RedoLog的內(nèi)容章蚣,將所有數(shù)據(jù)恢復(fù)到最新的狀態(tài)站欺。
-- undo+redo聯(lián)合后姨夹,事務(wù)的簡(jiǎn)化過程:
假設(shè)有A、B兩個(gè)數(shù)據(jù)矾策,值分別為1,2.
A.事務(wù)開始.
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.事務(wù)提交
-- undo+redo聯(lián)合后,事務(wù)的特點(diǎn):
A.為了保證持久性贾虽,必須在事務(wù)提交前將redoLog持久化逃糟。
B.數(shù)據(jù)不需要在事務(wù)提交前寫入磁盤,而是緩存在內(nèi)存中蓬豁。
C.RedoLog保證事務(wù)的持久性绰咽。
D.UndoLog保證事務(wù)的原子性。
E.有一個(gè)隱含的特點(diǎn)地粪,數(shù)據(jù)必須要晚于redolog寫入持久存
2取募、非常規(guī)方案實(shí)現(xiàn)分布式鎖
start transaction;
select id,name,application_count?
from campaign
Where id=1 for update;//X鎖
...
...
...
執(zhí)行一大堆業(yè)務(wù)邏輯;
…
...
...
update campaign?
Set application_count =application_count -1
Where id=1;
commit;
start transaction;
select id,name,application_count?
from campaign
Where id=1;
記錄application_count到變量a中
...
...
...
執(zhí)行一大堆業(yè)務(wù)邏輯蟆技;
…
...
...
update campaign?
Set application_count =application_count -1
Where id=1 and application_count=#{a};
commit;
樂觀的認(rèn)為沒有人使用對(duì)應(yīng)的資源。
樂觀鎖是基于數(shù)據(jù)的版本號(hào)實(shí)現(xiàn)的质礼。
在創(chuàng)建數(shù)據(jù)表的時(shí)候旺聚,給表增加一個(gè)字段version,每次讀取的時(shí)候眶蕉,把version讀取出來砰粹,更新的時(shí)候,比較version是否一致造挽,并把version加1碱璃。
有的時(shí)候,不用增加version字段刽宪,通過某個(gè)業(yè)務(wù)字段也可以做到厘贼。
但是如果不用version字段,有可能出現(xiàn)ABA的問題圣拄,如果你的業(yè)務(wù)字段不會(huì)出現(xiàn)這種情況或者業(yè)務(wù)場(chǎng)景允許出現(xiàn)ABA的問題嘴秸,那就沒有必要增加version字段。
假如有如下一張表user:
idnameageversion
1xxx201
2yyy201
更新user的業(yè)務(wù)場(chǎng)景如下:?你需要先從user表把數(shù)據(jù)查詢出來庇谆,然后做一系列復(fù)雜的操作岳掐,最后更新對(duì)應(yīng)的記錄
查詢:select id,name,age,version from user where id=1;
更新:update set name='zwf',version=2 where id=1 and version=1;如果發(fā)現(xiàn)version已經(jīng)不是1了,說明已經(jīng)有其他的事務(wù)進(jìn)行了更新饭耳,id=1的這條記錄并不會(huì)因?yàn)椴l(fā)而被修改串述。
假如你的業(yè)務(wù)場(chǎng)景跟例子中的是一樣的,而且也不需要增加version字段就可以實(shí)現(xiàn)樂觀鎖寞肖,那么用這種方式是最簡(jiǎn)單的纲酗。(不需要專門生成一個(gè)資源表衰腌,來映射user表中的每條記錄,把記錄當(dāng)成資源觅赊;不用增加version字段右蕊,對(duì)user表的侵入性為0)
資源表定義
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:代表一個(gè)資源吮螺,具體代表什么看你的業(yè)務(wù)場(chǎng)景了(比如某個(gè)類中的某個(gè)方法饶囚、比如上面提到的user表中的一條記錄)。
status:資源是否被鎖定鸠补,1代表未被鎖定萝风,2代表鎖定。
執(zhí)行流程?a紫岩、先執(zhí)行select操作查詢當(dāng)前數(shù)據(jù)的數(shù)據(jù)版本號(hào),比如當(dāng)前數(shù)據(jù)版本號(hào)是11:
select id, resource, state,version from resource where state=1 and id=2;
b规惰、執(zhí)行更新操作:
update resoure set state=2, version=12, update_time=now() where resource=2 and state=1 and version=11
c、如果上述update語句真正更新影響到了一行數(shù)據(jù)被因,那就說明占位成功卿拴。如果沒有更新影響到一行數(shù)據(jù)衫仑,則說明這個(gè)資源已經(jīng)被別人占位了梨与。
d、如果已經(jīng)占位成功文狱,想要再次進(jìn)入粥鞋,可以通過select * from where resource=2 and state=2 and version=12,有數(shù)據(jù)代表可以進(jìn)入瞄崇。
a呻粹、單點(diǎn)風(fēng)險(xiǎn),一旦數(shù)據(jù)庫(kù)掛掉苏研,會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)不可用
b等浊、這種操作方式,使原本一次的update操作摹蘑,必須變?yōu)?次操作: select版本號(hào)一次筹燕;update一次。增加了數(shù)據(jù)庫(kù)操作的次數(shù)衅鹿。
c撒踪、如果業(yè)務(wù)場(chǎng)景中的一次業(yè)務(wù)流程中,多個(gè)資源都需要用保證數(shù)據(jù)一致性大渤,那么如果全部使用基于數(shù)據(jù)庫(kù)資源表的樂觀鎖制妄,在高并發(fā)的要求下,對(duì)數(shù)據(jù)庫(kù)連接的開銷一定是無法忍受的泵三。
d耕捞、樂觀鎖機(jī)制往往基于系統(tǒng)中的數(shù)據(jù)存儲(chǔ)邏輯衔掸,因此可能會(huì)造成臟數(shù)據(jù)被更新到數(shù)據(jù)庫(kù)中。在系統(tǒng)設(shè)計(jì)階段俺抽,我們應(yīng)該充分考慮到這些情況出現(xiàn)的可能性具篇,并進(jìn)行相應(yīng)調(diào)整,如將樂觀鎖策略在數(shù)據(jù)庫(kù)存儲(chǔ)過程中實(shí)現(xiàn)凌埂,對(duì)外只開放基于此存儲(chǔ)過程的數(shù)據(jù)更新途徑驱显,而不是將數(shù)據(jù)庫(kù)表直接對(duì)外公開。
e瞳抓、這把鎖沒有失效時(shí)間埃疫,一旦解鎖操作失敗,就會(huì)導(dǎo)致鎖記錄一直在數(shù)據(jù)庫(kù)中孩哑,其他線程無法再獲得到鎖栓霜。(可以通過定時(shí)任務(wù)來解決)
f、這把鎖只能是非阻塞的横蜒,沒有獲得鎖的線程并不會(huì)進(jìn)入排隊(duì)隊(duì)列胳蛮,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作。
悲觀鎖是與樂觀鎖對(duì)應(yīng)的仅炊,悲觀的認(rèn)為資源已經(jīng)被別人搶占了。
首先創(chuàng)建一張數(shù)據(jù)表:
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 '保存數(shù)據(jù)時(shí)間澎蛛,自動(dòng)生成',
? PRIMARY KEY (`id`),
? UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='鎖定中的方法';
方式一
當(dāng)我們想要鎖住某個(gè)方法時(shí)抚垄,執(zhí)行以下SQL:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
因?yàn)槲覀儗?duì)method_name做了唯一性約束,這里如果有多個(gè)請(qǐng)求同時(shí)提交到數(shù)據(jù)庫(kù)的話谋逻,數(shù)據(jù)庫(kù)會(huì)保證只有一個(gè)操作可以成功呆馁,那么我們就可以認(rèn)為操作成功的那個(gè)線程獲得了該方法的鎖,可以執(zhí)行方法體內(nèi)容毁兆。
當(dāng)方法執(zhí)行完畢之后浙滤,想要釋放鎖的話,需要執(zhí)行以下Sql:
delete from methodLock where method_name ='method_name'
上面這種簡(jiǎn)單的實(shí)現(xiàn)有以下幾個(gè)問題:
單點(diǎn)气堕,一旦數(shù)據(jù)庫(kù)掛掉纺腊,會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)不可用(解決:搞兩個(gè)數(shù)據(jù)庫(kù),數(shù)據(jù)之前雙向同步送巡,一旦掛掉快速切換到備庫(kù)上)摹菠。
這把鎖沒有失效時(shí)間,一旦解鎖操作失敗骗爆,就會(huì)導(dǎo)致鎖記錄一直在數(shù)據(jù)庫(kù)中次氨,其他線程無法再獲得到鎖(解決:定時(shí)任務(wù))。
這把鎖只能是非阻塞的摘投,因?yàn)閿?shù)據(jù)的insert操作煮寡,一旦插入失敗就會(huì)直接報(bào)錯(cuò)虹蓄。沒有獲得鎖的線程并不會(huì)進(jìn)入排隊(duì)隊(duì)列,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作(解決:while循環(huán)幸撕,直到insert成功再返回成功)薇组。
這把鎖是不可重入的,同一個(gè)線程在沒有釋放鎖之前無法再次獲得該鎖坐儿。因?yàn)閿?shù)據(jù)中數(shù)據(jù)已經(jīng)存在了(解決:在數(shù)據(jù)庫(kù)表中加個(gè)字段律胀,記錄當(dāng)前獲得鎖的機(jī)器的主機(jī)信息和線程信息,那么下次再獲取鎖的時(shí)候先查詢數(shù)據(jù)庫(kù)貌矿,如果當(dāng)前機(jī)器的主機(jī)信息和線程信息在數(shù)據(jù)庫(kù)可以查到的話炭菌,直接把鎖分配給他就可以了)。
方式二
X鎖實(shí)現(xiàn): for update
我們還用剛剛創(chuàng)建的那張數(shù)據(jù)庫(kù)表逛漫『诘停可以通過數(shù)據(jù)庫(kù)的排他鎖來實(shí)現(xiàn)分布式鎖。
基于MySql的InnoDB引擎酌毡,可以使用以下方法來實(shí)現(xiàn)加鎖操作:
start transaction
select * from methodLock where method_name=xxx for update;
在查詢語句后面增加for update克握,數(shù)據(jù)庫(kù)會(huì)在查詢過程中給數(shù)據(jù)庫(kù)表增加排他鎖。當(dāng)某條記錄被加上排他鎖之后枷踏,其他線程無法再在該行記錄上增加排他鎖菩暗。(InnoDB引擎在加鎖的時(shí)候,只有通過索引進(jìn)行檢索的時(shí)候才會(huì)使用行級(jí)鎖呕寝,否則會(huì)使用表級(jí)鎖勋眯。這里我們希望使用行級(jí)鎖婴梧,就要給method_name添加索引下梢,值得注意的是,這個(gè)索引一定要?jiǎng)?chuàng)建成唯一索引塞蹭,否則會(huì)出現(xiàn)多個(gè)重載方法之間無法同時(shí)被訪問的問題孽江。重載方法的話建議把參數(shù)類型也加上)
解鎖:commit transaction
使用這種方式可以有效的解決上面提到的無法釋放鎖和阻塞鎖的問題。
阻塞鎖番电? for update語句會(huì)在執(zhí)行成功后立即返回岗屏,在執(zhí)行失敗時(shí)一直處于阻塞狀態(tài),直到成功漱办。
鎖定之后服務(wù)宕機(jī)这刷,無法釋放?使用這種方式娩井,服務(wù)宕機(jī)之后數(shù)據(jù)庫(kù)會(huì)自己把鎖釋放掉暇屋。
但是還是無法直接解決數(shù)據(jù)庫(kù)單點(diǎn)和可重入問題。
這里還可能存在另外一個(gè)問題洞辣,雖然我們對(duì)method_name 使用了唯一索引咐刨,并且顯示使用for update來使用行級(jí)鎖昙衅。但是,MySql會(huì)對(duì)查詢進(jìn)行優(yōu)化定鸟,即便在條件中使用了索引字段而涉,但是否使用索引來檢索數(shù)據(jù)是由 MySQL 通過判斷不同執(zhí)行計(jì)劃的代價(jià)來決定的,如果 MySQL 認(rèn)為全表掃效率更高联予,比如對(duì)一些很小的表啼县,它就不會(huì)使用索引,這種情況下 InnoDB 將使用表鎖沸久,而不是行鎖谭羔。如果發(fā)生這種情況就悲劇了。麦向。瘟裸。
還有一個(gè)問題,就是我們要使用排他鎖來進(jìn)行分布式鎖的lock诵竭,那么一個(gè)排他鎖長(zhǎng)時(shí)間不提交话告,就會(huì)占用數(shù)據(jù)庫(kù)連接。一旦類似的連接變得多了卵慰,就可能把數(shù)據(jù)庫(kù)連接池?fù)伪?/p>
4沙郭、數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖-總結(jié)
優(yōu)點(diǎn):
簡(jiǎn)單、容易理解
缺點(diǎn):
在高并發(fā)的時(shí)候裳朋,數(shù)據(jù)庫(kù)連接數(shù)會(huì)不夠用病线,性能上限很容易觸及
處理阻塞、單點(diǎn)鲤嫡、鎖超時(shí)等問題送挑,會(huì)使方案非常復(fù)雜。
二暖眼、基于Zookeeper實(shí)現(xiàn)分布式鎖
ZooKeeper是一個(gè)分布式的,開放源碼的分布式應(yīng)用程序協(xié)調(diào)服務(wù)诫肠,是Google的Chubby一個(gè)開源的實(shí)現(xiàn)司澎,是Hadoop和Hbase的重要組件。它是一個(gè)為分布式應(yīng)用提供一致性服務(wù)的軟件栋豫,提供的功能包括:配置維護(hù)挤安、域名服務(wù)、分布式同步丧鸯、組服務(wù)等蛤铜。
基于zookeeper臨時(shí)有序節(jié)點(diǎn)可以實(shí)現(xiàn)的分布式鎖。
大致思想即為:每個(gè)客戶端對(duì)某個(gè)方法加鎖時(shí)昂羡,在zookeeper上的與該方法對(duì)應(yīng)的指定節(jié)點(diǎn)的目錄下絮记,生成一個(gè)唯一的瞬時(shí)有序節(jié)點(diǎn)。
判斷是否獲取鎖的方式很簡(jiǎn)單虐先,只需要判斷有序節(jié)點(diǎn)中序號(hào)最小的一個(gè)怨愤。
當(dāng)釋放鎖的時(shí)候,只需將這個(gè)瞬時(shí)節(jié)點(diǎn)刪除即可蛹批。同時(shí)撰洗,其可以避免服務(wù)宕機(jī)導(dǎo)致的鎖無法釋放,而產(chǎn)生的死鎖問題腐芍。
可以直接使用zookeeper第三方庫(kù)Curator客戶端,這個(gè)客戶端中封裝了一個(gè)可重入的鎖服務(wù)(你也可以自己封裝)猪勇。
public boolean tryLock(long timeout, TimeUnit unit) throws Exception {
? ? ? ? return interProcessMutex.acquire(timeout, unit);
}
public void unlock() throws Exception {
? ? ? ? interProcessMutex.release();
}
Curator提供的InterProcessMutex是分布式鎖的實(shí)現(xiàn)设褐。acquire方法用戶獲取鎖,release方法用于釋放鎖泣刹。
使用ZK實(shí)現(xiàn)的分布式鎖好像完全符合了本文開頭我們對(duì)一個(gè)分布式鎖的所有期望助析。但是,其實(shí)并不是椅您,Zookeeper實(shí)現(xiàn)的分布式鎖其實(shí)存在一個(gè)缺點(diǎn)外冀,那就是性能上可能并沒有緩存服務(wù)那么高。因?yàn)槊看卧趧?chuàng)建鎖和釋放鎖的過程中掀泳,都要?jiǎng)討B(tài)創(chuàng)建雪隧、銷毀瞬時(shí)節(jié)點(diǎn)來實(shí)現(xiàn)鎖功能。ZK中創(chuàng)建和刪除節(jié)點(diǎn)只能通過Leader服務(wù)器來執(zhí)行员舵,然后將數(shù)據(jù)同不到所有的Follower機(jī)器上脑沿。
其實(shí),使用Zookeeper也有可能帶來并發(fā)問題固灵,只是并不常見而已捅伤。考慮這樣的情況巫玻,由于網(wǎng)絡(luò)抖動(dòng),客戶端可ZK集群的session連接斷了祠汇,那么zk以為客戶端掛了仍秤,就會(huì)刪除臨時(shí)節(jié)點(diǎn),這時(shí)候其他客戶端就可以獲取到分布式鎖了可很。就可能產(chǎn)生并發(fā)問題诗力。這個(gè)問題不常見是因?yàn)閦k有重試機(jī)制,一旦zk集群檢測(cè)不到客戶端的心跳,就會(huì)重試苇本,Curator客戶端支持多種重試策略袜茧。多次重試之后還不行的話才會(huì)刪除臨時(shí)節(jié)點(diǎn)。(所以瓣窄,選擇一個(gè)合適的重試策略也比較重要笛厦,要在鎖的粒度和并發(fā)之間找一個(gè)平衡。)
優(yōu)點(diǎn):
鎖無法釋放問題解決裳凸,使用Zookeeper可以有效的解決鎖無法釋放的問題,因?yàn)樵趧?chuàng)建鎖的時(shí)候劝贸,客戶端會(huì)在ZK中創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn)姨谷,一旦客戶端獲取到鎖之后突然掛掉(Session連接斷開),那么這個(gè)臨時(shí)節(jié)點(diǎn)就會(huì)自動(dòng)刪除掉映九。其他客戶端就可以再次獲得鎖梦湘。
非阻塞鎖問題解決,使用Zookeeper可以實(shí)現(xiàn)阻塞的鎖件甥,客戶端可以通過在ZK中創(chuàng)建順序節(jié)點(diǎn)践叠,并且在節(jié)點(diǎn)上綁定監(jiān)聽器,一旦節(jié)點(diǎn)有變化嚼蚀,Zookeeper會(huì)通知客戶端禁灼,客戶端可以檢查自己創(chuàng)建的節(jié)點(diǎn)是不是當(dāng)前所有節(jié)點(diǎn)中序號(hào)最小的,如果是轿曙,那么自己就獲取到鎖弄捕,便可以執(zhí)行業(yè)務(wù)邏輯了。
不可重入問題解決导帝,使用Zookeeper也可以有效的解決不可重入的問題守谓,客戶端在創(chuàng)建節(jié)點(diǎn)的時(shí)候,把當(dāng)前客戶端的主機(jī)信息和線程信息直接寫入到節(jié)點(diǎn)中您单,下次想要獲取鎖的時(shí)候和當(dāng)前最小的節(jié)點(diǎn)中的數(shù)據(jù)比對(duì)一下就可以了斋荞。如果和自己的信息一樣,那么自己直接獲取到鎖虐秦,如果不一樣就再創(chuàng)建一個(gè)臨時(shí)的順序節(jié)點(diǎn)平酿,參與排隊(duì)。
單點(diǎn)問題解決悦陋,使用Zookeeper可以有效的解決單點(diǎn)問題蜈彼,ZK是集群部署的,只要集群中有半數(shù)以上的機(jī)器存活俺驶,就可以對(duì)外提供服務(wù)幸逆。
實(shí)現(xiàn)簡(jiǎn)單
缺點(diǎn),性能不如緩存高
https://www.atatech.org/articles/4743
參考:https://www.atatech.org/articles/30653
本人沒有在實(shí)際環(huán)境中使用過。
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
? ? ? ? ? ? ? ? // 已驗(yàn)證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))) { // 感謝劍癡指出錯(cuò)誤
? ? ? ? ? ? ? ? ? ? return tryLock(timeout);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? } else if (result.getValue() != null && locker.getValue().equals(result.getValue().getValue())) {
// 【注意】其實(shí)這里線程復(fù)用時(shí),ThreadName有相同風(fēng)險(xiǎn)菇怀,可以改為uuid邏輯凭舶,復(fù)用鎖傳入uuid。
? ? ? ? ? ? ? ? // 若是自己的鎖爱沟,自己繼續(xù)用
? ? ? ? ? ? ? ? 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();
? ? ? ? }
? ? }
// 【注意】其實(shí)這里線程復(fù)用時(shí)帅霜,ThreadName有相同風(fēng)險(xiǎn),可以改為uuid邏輯呼伸,復(fù)用鎖傳入uuid身冀。
? ? private String getValue() {
? ? ? ? return getHostname() + ":" + Thread.currentThread().getName();
? ? }
? ? /**
? ? * 獲得機(jī)器名
? ? *
? ? * @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()) {// 分布式鎖定本任務(wù)
? ? ? ? // do something....
? ? }
} finally {
? ? lockB.unlock();
? ? lockA.unlock();
}
整體上用到了tair的get括享、put搂根、invalid三個(gè)方法,如果put失敗或get失敗铃辖,重試即可(根據(jù)業(yè)務(wù)場(chǎng)景來判斷重試幾次剩愧,不建議重試太多次,容易造成雪崩)
優(yōu)點(diǎn):
鎖無法釋放問題解決娇斩,通過tair的超時(shí)問題機(jī)制解決仁卷,不過超時(shí)時(shí)間需要根據(jù)業(yè)務(wù)場(chǎng)景來判斷
可重入問題解決,讓tair的value包含機(jī)器ip+線程name犬第,獲取鎖的時(shí)候锦积,先get value做檢查是不是已經(jīng)獲取了鎖
非阻塞問題解決(while重復(fù)執(zhí)行)
高性能、高可用
缺點(diǎn):通過時(shí)間控制失效不太靠譜
有寫的不對(duì)的地方歉嗓,歡迎大家拍磚
分布式與單機(jī)情況下最大的不同在于其不是多線程而是多進(jìn)程
事務(wù)是不能解決分布式鎖要解決的問題的丰介,不過在某些情況下事務(wù)也可以解決分布式鎖要解決的問題。