分布式鎖的技術選型及思考

本文來自作者 一行 在 GitChat 分享的{分布式鎖的技術選型及思考}

鎖和分布式鎖

在計算機中项乒,鎖的作用是解決在并發(fā)狀態(tài)下的共享資源互斥問題趁冈,保證在同一時間只有一個進程/線程可以掌握資源的控制權攒至。

例如以下幾種情況:

  1. 文件鎖的實現(xiàn)是為了解決不同用戶同時讀寫同一文件的并發(fā)問題而出現(xiàn)的,防止導致文件的內容被破壞蜡饵。

  2. 使用數(shù)組實現(xiàn)的隊列精盅,在 push 操作的地方一般需要加鎖來解決槽位的爭奪問題,防止出現(xiàn)多次 push 沖突從而導致數(shù)據(jù)丟失問題奇适。

  3. 對于12306來說坟比,火車票就是他的資源,最終放票的時候需要鎖來保證票嚷往、人葛账、座位唯一對應。

  4. ……

上面的例子中其實就包含了我們通常講的傳統(tǒng)單機鎖和我要講的分布式鎖皮仁。

單機環(huán)境下籍琳,資源競爭者都是來自機器內部((進程/線程)菲宴,那么實現(xiàn)鎖的方案只需要借助單機資源就可以了,比如借助磁盤趋急、內存喝峦、寄存器來實現(xiàn)。

但是對于分布式環(huán)境下呜达,資源競爭者生存環(huán)境更復雜了谣蠢,原有依賴單機的方案不再發(fā)揮作用,這時候就需要一個大家都認可的協(xié)調者出來查近,幫助解決競爭問題眉踱,那這個協(xié)調者稱之為分布式鎖。

上面這個例子就像兩個職員產(chǎn)生的矛盾霜威,只要公司的領導出面就可以解決谈喳。而當兩個公司產(chǎn)生競爭矛盾的時候,就需要司法機關出面侥祭,是同一個道理叁执。

簡單的說,分布式鎖就是解決分布式環(huán)境下資源競爭問題的手段矮冬。

分布式鎖的應用場景

所有分布式環(huán)境下會出現(xiàn)資源競爭的地方都需要分布式鎖的協(xié)調,除了上面介紹的 12306 放票次哈,還有類似共享文檔平臺編輯問題胎署、王者榮耀選擇英雄、全局自增主鍵等應用需要用到窑滞。簡單介紹一下在類似公司內部 Wiki 等多人協(xié)作編輯平臺的使用場景琼牧。

Wiki 中的多人在線編輯

場景1:清明節(jié)前,團隊要求我們在 Wiki 登記自己的休假情況哀卫,假設我們在 id=1 這個文檔上記錄我們的休假時間和聯(lián)系電話巨坊。A、C 兩個同學同時開始編輯此改,并且 A 和 C 在同一時間提交了結果趾撵,他們在提交前文檔是空的。服務需要如何處理這兩個請求呢共啃?以誰的為準呢占调?會不會產(chǎn)生覆蓋現(xiàn)象導致 A 的記錄丟失了?

場景2:另一個 case移剪,我是 Z 同學究珊,在我前面別人都已經(jīng)填完了,我有一個陋習纵苛,喜歡在保存的時候連續(xù)按3-5下 Ctrl+s剿涮,而每一個 Ctrl+s 都會觸發(fā)一個請求言津,但是每個請求處理大概1s鐘,但是實際請求都在 20ms 內發(fā)出去了取试。

問題同上面纺念,如何保證不重復的追加記錄呢?

假設你的存儲服務和存儲架構是這樣的:

image

一般的處理代碼是這樣的:

<pre style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

 //根據(jù)docid獲取文件內容想括,從分布式文件系統(tǒng)取陷谱,時間不可控
 nowFileContent = getFileByDocId(docId) //do something,類似diff瑟蜈,追加操作
 newFileContent = doSomeThing() //存儲到文件系統(tǒng)
 setNewFileContent(docId,newFileContent)

</pre>

對于場景1講到的 A烟逊、C 兩個請求同時到達代碼段,但是由于網(wǎng)絡原因铺根,A 先拿到文檔內容宪躯,C 在 A 寫入前讀到文件內容,所以最終的結果是兩者會丟失一個寫入位迂。

image

所以需要對讀寫操作做一次加鎖访雪,保證事務的完整、一致掂林。

下圖是《現(xiàn)代操作系統(tǒng)》中的插圖臣缀,這里的效果也希望如此。

image

Wiki 這類場景屬于長耗時事務的資源處理問題泻帮,鎖的出現(xiàn)保證不會因為事務中的讀寫間跨度耗時大導致寫覆蓋的情況精置,使得請求排隊,順序處理锣杂。

解決方案選擇

我遇到的問題也是類 Wiki 這類長事務的問題脂倦,遇到問題第一想法是去看網(wǎng)上的解決方案。

網(wǎng)上 MySQL元莫、ZK赖阻、Redis 各種實現(xiàn)方式很多,我需要選擇哪種踱蠢?怎么選擇火欧?我需要權衡哪些方面?

以前看分布式書的時候朽基,一個被提到很多次的詞是:trade-off布隔,我理解是取舍或者是權衡吧。

作為一個 Web 開發(fā)者稼虎,我需要考慮的主要包含下面幾個部分:

  1. 實現(xiàn)我的功能是否 OK衅檀,耗時是否滿足在線需求?

  2. 實現(xiàn)難度霎俩、學習成本哀军;

  3. 運維成本沉眶。

那么按照這幾個標準來看一下現(xiàn)在的可選方案:

實現(xiàn)方式 功能要求 實現(xiàn)難度 學習成本 運維成本
MySQL 的方案借助表鎖/行鎖實現(xiàn) 滿足基本要求 不難 熟悉 小量OK、大量影響現(xiàn)有業(yè)務杉适、1主多從架構谎倔,不方便擴容
通過 ZK 創(chuàng)建數(shù)據(jù)節(jié)點的方式實現(xiàn) 滿足要求 熟悉 ZK API 即可 需要學習 重,需要堆機器猿推,有跨機房請求
Redis 使用 setnxex 基本要求 不難 熟悉 擴容方便片习、現(xiàn)有服務

MySQL 單主架構,寫都會到 master蹬叭,有瓶頸藕咏。ZK 的方式需要自己搭建、運維秽五,而且需要堆機器孽查,利用率不高。最終采用了 Redis 來實現(xiàn)坦喘,流量/存儲都可以擴容盲再,運維也不需要自己。

實現(xiàn)

選好了方案瓣铣,下面就是實現(xiàn)了答朋。如果我們最終實現(xiàn)了這個鎖,對它的要求是什么呢坯沪?

  1. lock 實現(xiàn)必須要是原子操作绿映,同時保證任何時候只有一個競爭者是獨占的;

  2. unlock 必須是原子的腐晾,同時保證只有自己可以解鎖自己;

  3. 不能出現(xiàn)死鎖丐一,當進程掛掉之后不影響其他的加鎖行為寄纵;

  4. 支持 Twemproxy 模式下的架構和單機嵌屎;

  5. 耗時可以接受。

基于上述要求我的實現(xiàn)如下(只提供了大致,刪除了敏感信息):

<pre style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

<?phpclass LockUtility{ const DEFAULT_UNLOCK_TIME = 4 ; const COMMON_REDISKEY_PREFIX = 'xxxxx' ; /**
 * @brief 
 *
 * @param $ukey 需要加鎖的key
 * @param $unlockTime 鎖持有時長
 *
 * @return 
 */
 public function __construct($ukey,$unlockTime=self::DEFAULT_UNLOCK_TIME){ $this->_objRedis = RedisFactory::getRedis(); $this->_redisKey = self::COMMON_REDISKEY_PREFIX.$ukey; $this->_unLockTime = $unlockTime ; //為單次加鎖生成唯一guid
 $this->_guid = genGuid(); 
 } /**
 * @brief 對給定的key進行加鎖處理
 *
 * @return 
 *
 * true 表示加鎖成功
 * 
 * 拋出異常則表示加鎖未成功,根據(jù)業(yè)務選擇自己的care的級別
 * 異常錯誤碼 :
 * 1.網(wǎng)絡錯誤: ErrorCodes::REDIS_ERROR 視業(yè)務嚴謹度脾还,這個錯誤是否忽略
 * 2.鎖被占用: ErrorCodes::LOCK_IS_USED 明確確定鎖被別人占有
 */
 public function lock(){ /* 
 * 設置鎖的過程需要是原子的,所以采用了set來操作
 * SET key value [EX seconds] [PX milliseconds] [NX|XX]
 * Redis 2.6.12 版本開始支持通過set 指定參數(shù)完成setexnx功能
 *
 * php 語法 : $redis->set('key', 'value', Array('xx', 'px'=>1000));
 *
 */
 $setRet = $this->_objRedis->set($this->_redisKey,$this->_guid,array('nx', 'ex' => $this->_unLockTime)); //返回false表示請求鎖失敗
 if(false === $setRet){ //鎖被占用柒傻,拋異常
 throw new Exception("get Lock Failed!Locking",Constants_ErrorCodes::LOCK_IS_USED);
 } //redis返回null,是網(wǎng)絡剩晴、機器授權、語法錯誤等等
 if(is_null($setRet)){ //網(wǎng)絡錯誤珍坊、異常
 throw new Exception("Request Redis Failed",Constants_ErrorCodes::REDIS_ERROR); 
 } return $setRet ;
 } /**
 * @brief 解除對某個key的鎖定牺勾,原則上不需要關心返回值,可以多次調用
 *
 * @return 
 * 1 redis會話成功阵漏,并且成功刪除了key
 * 0 redis會話成功驻民,但是待刪除的key已經(jīng)不存在
 *
 */
 public function unlock(){ //Reids 2.6 版本增加了對 Lua 環(huán)境的支持,解決了長久以來不能高效地處理 CAS (check-and-set)命令的缺點
 $luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" ;
 $delRet = $this->_objRedis->eval($luaScript,array($this->_redisKey,$this->_guid),1); if(is_null($delRet)){ //redis返回null,是網(wǎng)絡翻具、機器授權、語法錯誤等等
 throw new Exception("Request Redis Failed",Constants_ErrorCodes::REDIS_ERROR); 
 } return $delRet ;
 }
}

</pre>

代碼寫出來之后是否解決了上面的問題呢回还?我們來看一下單機和集群 Redis 方案下的使用裆泳。

單機 Redis 架構

image

對于上圖的單點架構,讀寫不分離柠硕。

那么上面的代碼對于上面要求是否滿足工禾?

  1. lock 采用了set + nx + ex 參數(shù) + redis 單線程可以保證 lock 是個原子操作,加鎖成功即成功蝗柔,失敗即失敗闻葵,滿足要求1和要求3死鎖處理,超時 key 失效诫咱;

  2. unlock 采用 Lua 保證了 compare and del 這個操作是原子的笙隙,同時解決了自己刪除自己的需求;

  3. 耗時上呢坎缭?都是一次請求竟痰,可以接受,同機房在 ms 級掏呼。

Twemproxy 模式下的多地域多分片主從架構

image

Twemproxy 是對 Redis/Memcache 的代理坏快,主要負責根據(jù) key 路由到分片的功能,存在它不支持的操作憎夷,例如 keys *莽鸿。不支持的原因是它需要遍歷所有分片才能完成操作,對于簡單的 set/get 還是路由到相應的分片拾给,工作原理一致祥得。

對于 Lua 腳本呢? Lua 腳本是怎么路由的蒋得?支持嗎级及?

我們使用 eval 來執(zhí)行的時候,我發(fā)現(xiàn)我們集群的文檔里這么寫:

必須至少有一個 key 在 script 后面额衙。命令將發(fā)往第一個 key 所在的分片饮焦。

也就是說使用 eval 來完成工作,命令是發(fā)向第一個 key 的窍侧,而我們的第一個 key 就是我們要處理的 key县踢,所以這套代碼在集群模式也是支持的。

但是對于集群來說伟件,現(xiàn)在都是采用的最終一致性硼啤、單地域主多地域從、寫走主地域的模式锋爪。

那么就是說寫請求是跨地域的丙曙?這個我使用了多一步操作讀來優(yōu)化爸业,因為讀不跨地域、寫跨地域亏镰,但是99%以上的請求主從延時都沒這么大扯旷,當然99%這個比例是我猜測的。

具體代碼如下:

<pre style="margin: 0px; padding: 0px; border: 0px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-variant-numeric: inherit; font-weight: 400; font-stretch: inherit; font-size: 18px; line-height: inherit; font-family: inherit; vertical-align: baseline; word-break: break-word; color: rgb(93, 93, 93); letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration-style: initial; text-decoration-color: initial;">

function lock(){ //首先采用exist來看指定key是不是存在了
 if($objRedis->exist($key)){ //key存在一定是被占了索抓,拋異常
 } //if not exist钧忽,并不能代表這個鎖真的沒被占用,可能是主從延時逼肯,這時候復用上面的代碼更安全耸黑,減少一次跨機房寫}

</pre>

使用注意事項如下:

  1. 使用時候需要控制好自己的 lockTime,需要長于你的事務執(zhí)行時間篮幢;

  2. 上層在獲取鎖失敗的時候大刊,需要自己去選擇是阻塞還是拋棄這次請求,讓用戶端重試三椿。

目前待解決問題有:

  1. 如果你的進程因為 CUP 吃緊而被掛起缺菌,而且掛起的時間超過了你設置的鎖的失效時間,是不是仍然會出現(xiàn)問題搜锰?

  2. 如果集群模式一個分片掛了伴郁,會發(fā)生什么?

  3. 你有什么辦法解決嗎蛋叼?歡迎留言討論焊傅。

總結

總結一下我這次的分享,主要有以下幾點總結:

  1. 分布式鎖是指分布式業(yè)務環(huán)境下需要的鎖狈涮,對支持鎖的服務沒有要求要分布式狐胎;

  2. 鎖實際上是一個資源協(xié)調者的角色,管理并發(fā)態(tài)下的資源控制權歌馍;

  3. 方案選擇就像投資顽爹,需要考慮投入產(chǎn)出比;

  4. Redis 單機和集群方案有自己的優(yōu)化點骆姐,根據(jù)場景做優(yōu)化;

  5. 在寫完文章后發(fā)現(xiàn)我的題目有點問題捏题,更準確的叫法應該是《 Redis 實現(xiàn)分布式鎖的思考》玻褪,如果騙了你,請告訴我公荧。

參考

  1. 吳大山的博客 :提醒了我解鈴還需系鈴人(Lua腳本)

  2. Twemproxy:Twemproxy 的代碼带射,我沒看完,但是搭建了服務測試循狰。

架構技術是程序員繞不開的話題窟社,關于分布式券勺,微服務,源碼灿里,框架結構关炼,設計模式等這些技術我都分享在群619881427,可免費下載匣吊。希望可以幫助在這個行業(yè)發(fā)展的朋友和童鞋們儒拂,在論壇博客等地方少花些時間找資料,把有限的時間色鸳,真正花在學習上社痛,我把這些視頻分享出來。相信對于已經(jīng)工作和遇到技術瓶頸的碼友命雀,在這個群里一定有你需要的內容蒜哀。

image
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市吏砂,隨后出現(xiàn)的幾起案子撵儿,更是在濱河造成了極大的恐慌,老刑警劉巖赊抖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件统倒,死亡現(xiàn)場離奇詭異,居然都是意外死亡氛雪,警方通過查閱死者的電腦和手機房匆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來报亩,“玉大人浴鸿,你說我怎么就攤上這事∠易罚” “怎么了岳链?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長劲件。 經(jīng)常有香客問我掸哑,道長,這世上最難降的妖魔是什么零远? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任苗分,我火速辦了婚禮,結果婚禮上牵辣,老公的妹妹穿的比我還像新娘摔癣。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布择浊。 她就那樣靜靜地躺著戴卜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪琢岩。 梳的紋絲不亂的頭發(fā)上投剥,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音粘捎,去河邊找鬼薇缅。 笑死,一個胖子當著我的面吹牛攒磨,可吹牛的內容都是我干的泳桦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼娩缰,長吁一口氣:“原來是場噩夢啊……” “哼灸撰!你這毒婦竟也來了?” 一聲冷哼從身側響起拼坎,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤浮毯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后泰鸡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體债蓝,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年盛龄,在試婚紗的時候發(fā)現(xiàn)自己被綠了饰迹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡余舶,死狀恐怖啊鸭,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情匿值,我是刑警寧澤赠制,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站挟憔,受9級特大地震影響钟些,放射性物質發(fā)生泄漏。R本人自食惡果不足惜绊谭,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一厘唾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧龙誊,春花似錦、人聲如沸喷楣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至逊朽,卻和暖如春罕伯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背叽讳。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工追他, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人岛蚤。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓邑狸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親涤妒。 傳聞我的和親對象是個殘疾皇子单雾,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內容

  • 單機/單點 單點故障/瓶頸:多個節(jié)點負載:面向數(shù)據(jù):一變多(一致性<弱一致,最終一致性>)》可用性最終一致性:一部...
    壹點零閱讀 790評論 0 3
  • 【轉】緩存在分布式系統(tǒng)中的應用 緩存在分布式系統(tǒng)中的應用 摘要 緩存是分布式系統(tǒng)中的重要組件她紫,主要解決高并發(fā)硅堆,大數(shù)...
    武漢蘇乞兒閱讀 860評論 0 10
  • 本文轉載自http://geek.csdn.net/news/detail/112672 WeTest導讀 我們常...
    shineegirl閱讀 1,546評論 0 26
  • 曾幾何時 撥動了心弦 美麗的遇見 羞澀里帶甜 曾幾何時 定格了時間 幸福的畫面 縈繞在心田 曾幾何時 錯過了期限 ...
    小小芝麻閱讀 340評論 3 4
  • 你一定得娶我 無論生老病死 永遠愛我 除了你沒人會那么寵我 沒人敢那么寵我 我要把你紋身上 提醒自己多好 你對我多...
    會死的鳥閱讀 166評論 0 0