溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

面試官:聊聊你對(duì)分布式鎖技術(shù)方案的理解

發(fā)布時(shí)間:2020-06-09 12:30:21 來(lái)源:網(wǎng)絡(luò) 閱讀:341 作者:愛(ài)碼仕i 欄目:編程語(yǔ)言

前言

由于在平時(shí)的工作中,線上服務(wù)器是分布式多臺(tái)部署的,經(jīng)常會(huì)面臨解決分布式場(chǎng)景下數(shù)據(jù)一致性的問(wèn)題,那么就要利用分布式鎖來(lái)解決這些問(wèn)題。

第一步,自身的業(yè)務(wù)場(chǎng)景:

在我日常做的項(xiàng)目中,目前涉及了以下這些業(yè)務(wù)場(chǎng)景:

場(chǎng)景一:比如分配任務(wù)場(chǎng)景。在這個(gè)場(chǎng)景中,由于是公司的業(yè)務(wù)后臺(tái)系統(tǒng),主要是用于審核人員的審核工作,并發(fā)量并不是很高,而且任務(wù)的分配規(guī)則設(shè)計(jì)成了通過(guò)審核人員每次主動(dòng)的請(qǐng)求拉取,然后服務(wù)端從任務(wù)池中隨機(jī)的選取任務(wù)進(jìn)行分配。這個(gè)場(chǎng)景看到這里你會(huì)覺(jué)得比較單一,但是實(shí)際的分配過(guò)程中,由于涉及到了按用戶聚類的問(wèn)題,所以要比我描述的復(fù)雜,但是這里為了說(shuō)明問(wèn)題,大家可以把問(wèn)題簡(jiǎn)單化理解。那么在使用過(guò)程中,主要是為了避免同一個(gè)任務(wù)同時(shí)被兩個(gè)審核人員獲取到的問(wèn)題。我最終使用了基于數(shù)據(jù)庫(kù)資源表的分布式鎖來(lái)解決的問(wèn)題。

場(chǎng)景二:比如支付場(chǎng)景。在這個(gè)場(chǎng)景中,我提供給用戶三個(gè)用于保護(hù)用戶隱私的手機(jī)號(hào)碼(這些號(hào)碼是從運(yùn)營(yíng)商處獲取的,和真實(shí)手機(jī)號(hào)碼看起來(lái)是一樣的),讓用戶選擇其中一個(gè)進(jìn)行購(gòu)買(mǎi),用戶購(gòu)買(mǎi)付款后,我需要將用戶選擇的號(hào)碼分配給用戶使用,同時(shí)也要將沒(méi)有選擇的釋放掉。在這個(gè)過(guò)程中,給用戶篩選的號(hào)碼要在一定時(shí)間內(nèi)(用戶篩選正常時(shí)間范圍內(nèi))讓當(dāng)前用戶對(duì)這個(gè)產(chǎn)品具有獨(dú)占性,以便保證付款后是100%可以拿到;同時(shí)由于產(chǎn)品資源池的資源有限,還要保持資源的流動(dòng)性,即不能讓資源長(zhǎng)時(shí)間被某個(gè)用戶占用著。對(duì)于服務(wù)的設(shè)計(jì)目標(biāo),一期項(xiàng)目上線的時(shí)候至少能夠支持峰值qps為300的請(qǐng)求,同時(shí)在設(shè)計(jì)的過(guò)程中要考慮到用戶體驗(yàn)的問(wèn)題。我最終使用了memecahed的add()方法和基于數(shù)據(jù)庫(kù)資源表的分布式鎖來(lái)解決的問(wèn)題。

場(chǎng)景三:我有一個(gè)數(shù)據(jù)服務(wù),每天調(diào)用量在3億,每天按86400秒計(jì)算的qps在4000左右,由于服務(wù)的白天調(diào)用量要明顯高于晚上,所以白天下午的峰值qps達(dá)到6000的,一共有4臺(tái)服務(wù)器,單臺(tái)qps要能達(dá)到3000以上。我最終使用了redis的setnx()和expire()的分布式鎖解決的問(wèn)題。

場(chǎng)景四:場(chǎng)景一和場(chǎng)景二的升級(jí)版。在這個(gè)場(chǎng)景中,不涉及支付。但是由于資源分配一次過(guò)程中,需要保持涉及一致性的地方增加,而且一期的設(shè)計(jì)目標(biāo)要達(dá)到峰值qps500,所以需要我們對(duì)場(chǎng)景進(jìn)一步的優(yōu)化。我最終使用了redis的setnx()、expire()和基于數(shù)據(jù)庫(kù)表的分布式鎖來(lái)解決的問(wèn)題。

看到這里,不管你覺(jué)得我提出的業(yè)務(wù)場(chǎng)景qps是否足夠大,都希望你能繼續(xù)看下去,因?yàn)闊o(wú)論你身處一個(gè)什么樣的公司,最開(kāi)始的工作可能都需要從最簡(jiǎn)單的做起。不要提阿里和騰訊的業(yè)務(wù)場(chǎng)景qps如何大,因?yàn)樵谶@樣的大場(chǎng)景中你未必能親自參與項(xiàng)目,親自參與項(xiàng)目未必能是核心的設(shè)計(jì)者,是核心的設(shè)計(jì)者未必能獨(dú)自設(shè)計(jì)。如果能真能滿足以上三條,關(guān)閉頁(yè)面可以不看啦,如果不是的話,建議還是看完,我有說(shuō)的不足的地方歡迎提出建議,我說(shuō)的好的地方,也希望給我點(diǎn)個(gè)贊或者評(píng)論一下,算是對(duì)我最大的鼓勵(lì)哈。

第二步,分布式鎖的解決方式:

  1. 首先明確一點(diǎn),有人可能會(huì)問(wèn)是否可以考慮采用ReentrantLock來(lái)實(shí)現(xiàn),但是實(shí)際上去實(shí)現(xiàn)的時(shí)候是有問(wèn)題的,ReentrantLock的lock和unlock要求必須是在同一線程進(jìn)行,而分布式應(yīng)用中,lock和unlock是兩次不相關(guān)的請(qǐng)求,因此肯定不是同一線程,因此導(dǎo)致無(wú)法使用ReentrantLock。
  2. 基于數(shù)據(jù)庫(kù)表做樂(lè)觀鎖,用于分布式鎖。
  3. 使用memcached的add()方法,用于分布式鎖。
  4. 使用memcached的cas()方法,用于分布式鎖。(不常用)
  5. 使用redis的setnx()、expire()方法,用于分布式鎖。
  6. 使用redis的setnx()、get()、getset()方法,用于分布式鎖。
  7. 使用redis的watch、multi、exec命令,用于分布式鎖。(不常用)
  8. 使用zookeeper,用于分布式鎖。(不常用)

第三步,基于數(shù)據(jù)庫(kù)資源表做樂(lè)觀鎖,用于分布式鎖:

1. 首先說(shuō)明樂(lè)觀鎖的含義:

大多數(shù)是基于數(shù)據(jù)版本(version)的記錄機(jī)制實(shí)現(xiàn)的。何謂數(shù)據(jù)版本號(hào)?即為數(shù)據(jù)增加一個(gè)版本標(biāo)識(shí),在基于數(shù)據(jù)庫(kù)表的版本解決方案中,一般是通過(guò)為數(shù)據(jù)庫(kù)表添加一個(gè) “version”字段來(lái)實(shí)現(xiàn)讀取出數(shù)據(jù)時(shí),將此版本號(hào)一同讀出,之后更新時(shí),對(duì)此版本號(hào)加1。

在更新過(guò)程中,會(huì)對(duì)版本號(hào)進(jìn)行比較,如果是一致的,沒(méi)有發(fā)生改變,則會(huì)成功執(zhí)行本次操作;如果版本號(hào)不一致,則會(huì)更新失敗。

2. 對(duì)樂(lè)觀鎖的含義有了一定的了解后,結(jié)合具體的例子,我們來(lái)推演下我們應(yīng)該怎么處理

(1). 假設(shè)我們有一張資源表,如下圖所示: t_resource , 其中有6個(gè)字段id, resoource, state, add_time, update_time, version,分別表示表主鍵、資源、分配狀態(tài)(1未分配 2已分配)、資源創(chuàng)建時(shí)間、資源更新時(shí)間、資源數(shù)據(jù)版本號(hào)。

面試官:聊聊你對(duì)分布式鎖技術(shù)方案的理解

(2). 假設(shè)我們現(xiàn)在我們對(duì)id=5780這條數(shù)據(jù)進(jìn)行分配,那么非分布式場(chǎng)景的情況下,我們一般先查詢出來(lái)state=1(未分配)的數(shù)據(jù),然后從其中選取一條數(shù)據(jù)可以通過(guò)以下語(yǔ)句進(jìn)行,如果可以更新成功,那么就說(shuō)明已經(jīng)占用了這個(gè)資源。

(3). 如果在分布式場(chǎng)景中,由于數(shù)據(jù)庫(kù)的update操作是原子是原子的,其實(shí)上邊這條語(yǔ)句理論上也沒(méi)有問(wèn)題,但是這條語(yǔ)句如果在典型的“ABA”情況下,我們是無(wú)法感知的。有人可能會(huì)問(wèn)什么是“ABA”問(wèn)題呢?大家可以網(wǎng)上搜索一下,這里我說(shuō)簡(jiǎn)單一點(diǎn)就是,如果在你第一次select和第二次update過(guò)程中,由于兩次操作是非原子的,所以這過(guò)程中,如果有一個(gè)線程,先是占用了資源(state=2),然后又釋放了資源(state=1),實(shí)際上最后你執(zhí)行update操作的時(shí)候,是無(wú)法知道這個(gè)資源發(fā)生過(guò)變化的。也許你會(huì)說(shuō)這個(gè)在你說(shuō)的場(chǎng)景中應(yīng)該也還好吧,但是在實(shí)際的使用過(guò)程中,比如銀行賬戶存款或者扣款的過(guò)程中,這種情況是比較恐怖的。

(4).那么如果使用樂(lè)觀鎖我們?nèi)绾谓鉀Q上邊的問(wèn)題呢?

a. 先執(zhí)行select操作查詢當(dāng)前數(shù)據(jù)的數(shù)據(jù)版本號(hào),比如當(dāng)前數(shù)據(jù)版本號(hào)是26:select id, resource, state,version from t_resource where state=1 andid=5780;

b. 執(zhí)行更新操作:update t_resoure set state=2, version=27, update_time=now() where resource=xxxxxx and state=1 and version=26

c. 如果上述update語(yǔ)句真正更新影響到了一行數(shù)據(jù),那就說(shuō)明占位成功。如果沒(méi)有更新影響到一行數(shù)據(jù),則說(shuō)明這個(gè)資源已經(jīng)被別人占位了。

3. 通過(guò)2中的講解,相信大家已經(jīng)對(duì)如何基于數(shù)據(jù)庫(kù)表做樂(lè)觀鎖有有了一定的了解了,但是這里還是需要說(shuō)明一下基于數(shù)據(jù)庫(kù)表做樂(lè)觀鎖的一些缺點(diǎn):

(1). 這種操作方式,使原本一次的update操作,必須變?yōu)?次操作: select版本號(hào)一次;update一次。增加了數(shù)據(jù)庫(kù)操作的次數(shù)。

(2). 如果業(yè)務(wù)場(chǎng)景中的一次業(yè)務(wù)流程中,多個(gè)資源都需要用保證數(shù)據(jù)一致性,那么如果全部使用基于數(shù)據(jù)庫(kù)資源表的樂(lè)觀鎖,就要讓每個(gè)資源都有一張資源表,這個(gè)在實(shí)際使用場(chǎng)景中肯定是無(wú)法滿足的。而且這些都基于數(shù)據(jù)庫(kù)操作,在高并發(fā)的要求下,對(duì)數(shù)據(jù)庫(kù)連接的開(kāi)銷一定是無(wú)法忍受的。

(3). 樂(lè)觀鎖機(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)整,如將樂(lè)觀鎖策略在數(shù)據(jù)庫(kù)存儲(chǔ)過(guò)程中實(shí)現(xiàn),對(duì)外只開(kāi)放基于此存儲(chǔ)過(guò)程的數(shù)據(jù)更新途徑,而不是將數(shù)據(jù)庫(kù)表直接對(duì)外公開(kāi)。

4.講了樂(lè)觀鎖的實(shí)現(xiàn)方式和缺點(diǎn),是不是會(huì)覺(jué)得不敢使用樂(lè)觀鎖了呢???

當(dāng)然不是,在文章開(kāi)頭我自己的業(yè)務(wù)場(chǎng)景中,場(chǎng)景1和場(chǎng)景2的一部分都使用了基于數(shù)據(jù)庫(kù)資源表的樂(lè)觀鎖,已經(jīng)很好的解決了線上問(wèn)題。所以大家要根據(jù)的具體業(yè)務(wù)場(chǎng)景選擇技術(shù)方案,并不是隨便找一個(gè)足夠復(fù)雜、足夠新潮的技術(shù)方案來(lái)解決業(yè)務(wù)問(wèn)題就是好方案?!比如,如果在我的場(chǎng)景一中,我使用zookeeper做鎖,可以這么做,但是真的有必要嗎???答案覺(jué)得是沒(méi)有必要的!?。?/p>

第四步,使用memcached的add()方法,用于分布式鎖:

對(duì)于使用memcached的add()方法做分布式鎖,這個(gè)在互聯(lián)網(wǎng)公司是一種比較常見(jiàn)的方式,而且基本上可以解決自己手頭上的大部分應(yīng)用場(chǎng)景。在使用這個(gè)方法之前,只要能搞明白memcached的add()和set()的區(qū)別,并且知道為什么能用add()方法做分布式鎖就好。如果還不知道add()和set()方法,請(qǐng)直接百度吧,這個(gè)需要自己了解一下。

我在這里想說(shuō)明的是另外一個(gè)問(wèn)題,人們?cè)陉P(guān)注分布式鎖設(shè)計(jì)的好壞時(shí),還會(huì)重點(diǎn)關(guān)注這樣一個(gè)問(wèn)題,那就是是否可以避免死鎖問(wèn)題???!??!

如果使用memcached的add()命令對(duì)資源占位成功了,那么是不是就完事兒了呢?當(dāng)然不是!我們需要在add()的使用指定當(dāng)前添加的這個(gè)key的有效時(shí)間,如果不指定有效時(shí)間,正常情況下,你可以在執(zhí)行完自己的業(yè)務(wù)后,使用delete方法將這個(gè)key刪除掉,也就是釋放了占用的資源。但是,如果在占位成功后,memecached或者自己的業(yè)務(wù)服務(wù)器發(fā)生宕機(jī)了,那么這個(gè)資源將無(wú)法得到釋放。所以通過(guò)對(duì)key設(shè)置超時(shí)時(shí)間,即便發(fā)生了宕機(jī)的情況,也不會(huì)將資源一直占用,可以避免死鎖的問(wèn)題。

第五步,使用memcached的cas()方法,用于分布式鎖: 略

第六步,使用redis的setnx()、expire()方法,用于分布式鎖:

對(duì)于使用redis的setnx()、expire()來(lái)實(shí)現(xiàn)分布式鎖,這個(gè)方案相對(duì)于memcached()的add()方案,redis占優(yōu)勢(shì)的是,其支持的數(shù)據(jù)類型更多,而memcached只支持String一種數(shù)據(jù)類型。除此之外,無(wú)論是從性能上來(lái)說(shuō),還是操作方便性來(lái)說(shuō),其實(shí)都沒(méi)有太多的差異,完全看你的選擇,比如公司中用哪個(gè)比較多,你就可以用哪個(gè)。

首先說(shuō)明一下setnx()命令,setnx的含義就是SET if Not Exists,其主要有兩個(gè)參數(shù) setnx(key, value)。該方法是原子的,如果key不存在,則設(shè)置當(dāng)前key成功,返回1;如果當(dāng)前key已經(jīng)存在,則設(shè)置當(dāng)前key失敗,返回0。但是要注意的是setnx命令不能設(shè)置key的超時(shí)時(shí)間,只能通過(guò)expire()來(lái)對(duì)key設(shè)置。

具體的使用步驟如下:

  1. setnx(lockkey, 1) 如果返回0,則說(shuō)明占位失敗;如果返回1,則說(shuō)明占位成功
  2. expire()命令對(duì)lockkey設(shè)置超時(shí)時(shí)間,為的是避免死鎖問(wèn)題。
  3. 執(zhí)行完業(yè)務(wù)代碼后,可以通過(guò)delete命令刪除key。
    這個(gè)方案其實(shí)是可以解決日常工作中的需求的,但從技術(shù)方案的探討上來(lái)說(shuō),可能還有一些可以完善的地方。比如,如果在第一步setnx執(zhí)行成功后,在expire()命令執(zhí)行成功前,發(fā)生了宕機(jī)的現(xiàn)象,那么就依然會(huì)出現(xiàn)死鎖的問(wèn)題,所以如果要對(duì)其進(jìn)行完善的話,可以使用redis的setnx()、get()和getset()方法來(lái)實(shí)現(xiàn)分布式鎖。

第七步,使用redis的setnx()、get()、getset()方法,用于分布式鎖:

這個(gè)方案的背景主要是在setnx()和expire()的方案上針對(duì)可能存在的死鎖問(wèn)題,做了一版優(yōu)化。

那么先說(shuō)明一下這三個(gè)命令,對(duì)于setnx()和get()這兩個(gè)命令,相信不用再多說(shuō)什么。那么getset()命令?這個(gè)命令主要有兩個(gè)參數(shù)getset(key,newValue)。該方法是原子的,對(duì)key設(shè)置newValue這個(gè)值,并且返回key原來(lái)的舊值。假設(shè)key原來(lái)是不存在的,那么多次執(zhí)行這個(gè)命令,會(huì)出現(xiàn)下邊的效果:

  1. getset(key, "value1") 返回nil 此時(shí)key的值會(huì)被設(shè)置為value1
  2. getset(key, "value2") 返回value1 此時(shí)key的值會(huì)被設(shè)置為value2
  3. 依次類推!

介紹完要使用的命令后,具體的使用步驟如下:

  1. setnx(lockkey, 當(dāng)前時(shí)間+過(guò)期超時(shí)時(shí)間),如果返回1,則獲取鎖成功;
    如果返回0則沒(méi)有獲取到鎖,轉(zhuǎn)向2。
  2. get(lockkey)獲取值oldExpireTime ,并將這個(gè)value值與當(dāng)前的系統(tǒng)時(shí)間進(jìn)行比較,如果小于當(dāng)前系統(tǒng)時(shí)間,則認(rèn)為這個(gè)鎖已經(jīng)超時(shí),可以允許別的請(qǐng)求重新獲取,轉(zhuǎn)向3。
  3. 計(jì)算newExpireTime=當(dāng)前時(shí)間+過(guò)期超時(shí)時(shí)間,然后getset(lockkey, newExpireTime) 會(huì)返回當(dāng)前l(fā)ockkey的值currentExpireTime。
  4. 判斷currentExpireTime與oldExpireTime 是否相等,如果相等,說(shuō)明當(dāng)前getset設(shè)置成功,獲取到了鎖。如果不相等,說(shuō)明這個(gè)鎖又被別的請(qǐng)求獲取走了,那么當(dāng)前請(qǐng)求可以直接返回失敗,或者繼續(xù)重試。
  5. 在獲取到鎖之后,當(dāng)前線程可以開(kāi)始自己的業(yè)務(wù)處理,當(dāng)處理完畢后,比較自己的處理時(shí)間和對(duì)于鎖設(shè)置的超時(shí)時(shí)間,如果小于鎖設(shè)置的超時(shí)時(shí)間,則直接執(zhí)行delete釋放鎖;如果大于鎖設(shè)置的超時(shí)時(shí)間,則不需要再鎖進(jìn)行處理。

注意:這個(gè)方案我當(dāng)初在線上使用的時(shí)候是沒(méi)有問(wèn)題的,所以當(dāng)初寫(xiě)這篇文章時(shí)也認(rèn)為是沒(méi)有問(wèn)題的。但是截止到2017.05.13(周六),自己在重新回顧這篇文章時(shí),看了文章下網(wǎng)友的很多評(píng)論,我發(fā)現(xiàn)有兩個(gè)問(wèn)題比較集中:

問(wèn)題1:在“get(lockkey)獲取值oldExpireTime”這個(gè)操作與“getset(lockkey, newExpireTime)”這個(gè)操作之間,如果有N個(gè)線程在get操作獲取到相同的oldExpireTime后,然后都去getset,會(huì)不會(huì)返回的newExpireTime都是一樣的,都會(huì)是成功,進(jìn)而都獲取到鎖???

我認(rèn)為這套方案是不存在這個(gè)問(wèn)題的。依據(jù)有兩條: 第一,redis是單進(jìn)程單線程模式,串行執(zhí)行命令。 第二,在串行執(zhí)行的前提條件下,getset之后會(huì)比較返回的currentExpireTime與oldExpireTime 是否相等。

問(wèn)題2:在“get(lockkey)獲取值oldExpireTime”這個(gè)操作與“getset(lockkey, newExpireTime)”這個(gè)操作之間,如果有N個(gè)線程在get操作獲取到相同的oldExpireTime后,然后都去getset,假設(shè)第1個(gè)線程獲取鎖成功,其他鎖獲取失敗,但是獲取鎖失敗的線程它發(fā)起的getset命令確實(shí)執(zhí)行了,這樣會(huì)不會(huì)造成第一個(gè)獲取鎖的線程設(shè)置的鎖超時(shí)時(shí)間一直在延長(zhǎng)???

我認(rèn)為這套方案確實(shí)存在這個(gè)問(wèn)題的可能。但我個(gè)人認(rèn)為這個(gè)微笑的誤差是可以忽略的,不過(guò)技術(shù)方案上存在缺陷,大家可以自行抉擇哈。

第八步,使用redis的watch、multi、exec命令,用于分布式鎖:

第九步,使用zookeeper,用于分布式鎖:

第十步,總結(jié)

綜上,關(guān)于分布式鎖的第一篇文章我就寫(xiě)到這兒了,在文章中主要說(shuō)明了日常項(xiàng)目中會(huì)比較常用到四種方案,大家掌握了這四種方案,其實(shí)在日常的工作中就可以解決很多業(yè)務(wù)場(chǎng)景下的分布式鎖的問(wèn)題。從文章開(kāi)頭我自己的實(shí)際使用中,也可以看到,這么說(shuō)完全是有一定的依據(jù)。對(duì)于另外那三種方案,我會(huì)在下一篇關(guān)于分布式鎖的文章中,和大家再探討一下。

常用的四種方案:

  1. 基于數(shù)據(jù)庫(kù)表做樂(lè)觀鎖,用于分布式鎖。
  2. 使用memcached的add()方法,用于分布式鎖。
  3. 使用redis的setnx()、expire()方法,用于分布式鎖。
  4. 使用redis的setnx()、get()、getset()方法,用于分布式鎖。

不常用但是可以用于技術(shù)方案探討的:

  1. 使用memcached的cas()方法,用于分布式鎖。
  2. 使用redis的watch、multi、exec命令,用于分布式鎖。
  3. 使用zookeeper,用于分布式鎖。

寫(xiě)在最后

原文鏈接:https://shimo.im/docs/f2ajdNJBQJItSobT/

面試官:聊聊你對(duì)分布式鎖技術(shù)方案的理解

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI