阿里Java架構師分享Redis實現(xiàn)分布式鎖的技術選型及思考

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

鎖和分布式鎖

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

例如以下幾種情況:

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

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

對于12306來說,火車票就是他的資源幽七,最終放票的時候需要鎖來保證票、人溅呢、座位唯一對應澡屡。

……

上面的例子中其實就包含了我們通常講的傳統(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ā)出去了。

問題同上面哩盲,如何保證不重復的追加記錄呢前方?

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

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

//根據(jù)docid獲取文件內容,從分布式文件系統(tǒng)取廉油,時間不可控

nowFileContent = getFileByDocId(docId) //do something惠险,類似diff,追加操作

newFileContent = doSomeThing() //存儲到文件系統(tǒng)

setNewFileContent(docId,newFileContent)

對于場景1講到的 A娱两、C 兩個請求同時到達代碼段莺匠,但是由于網(wǎng)絡原因,A 先拿到文檔內容十兢,C 在 A 寫入前讀到文件內容趣竣,所以最終的結果是兩者會丟失一個寫入。

所以需要對讀寫操作做一次加鎖旱物,保證事務的完整遥缕、一致。

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

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

解決方案選擇

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

網(wǎng)上 MySQL、ZK须鼎、Redis 各種實現(xiàn)方式很多鲸伴,我需要選擇哪種府蔗?怎么選擇?我需要權衡哪些方面汞窗?

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

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

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

實現(xiàn)難度狂男、學習成本;

運維成本品腹。

那么按照這幾個標準來看一下現(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)了這個鎖,對它的要求是什么呢聂薪?

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

unlock 必須是原子的藏澳,同時保證只有自己可以解鎖自己仁锯;

不能出現(xiàn)死鎖,當進程掛掉之后不影響其他的加鎖行為笆载;

支持 Twemproxy 模式下的架構和單機扑馁;

耗時可以接受涯呻。

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

<?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 ;

}

}

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

單機 Redis 架構

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

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

lock 采用了set + nx + ex 參數(shù) + redis 單線程可以保證 lock 是個原子操作茅糜,加鎖成功即成功,失敗即失敗素挽,滿足要求1和要求3死鎖處理蔑赘,超時 key 失效;

unlock 采用 Lua 保證了 compare and del 這個操作是原子的预明,同時解決了自己刪除自己的需求缩赛;

耗時上呢?都是一次請求贮庞,可以接受峦筒,同機房在 ms 級。

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

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%這個比例是我猜測的汇跨。

具體代碼如下:

function lock(){ //首先采用exist來看指定key是不是存在了

if($objRedis->exist($key)){ //key存在一定是被占了,拋異常

} //if not exist妆距,并不能代表這個鎖真的沒被占用,可能是主從延時函匕,這時候復用上面的代碼更安全娱据,減少一次跨機房寫}

使用注意事項如下:

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

上層在獲取鎖失敗的時候中剩,需要自己去選擇是阻塞還是拋棄這次請求,讓用戶端重試抒寂。

目前待解決問題有:

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

如果集群模式一個分片掛了郊愧,會發(fā)生什么?

你有什么辦法解決嗎井佑?歡迎留言討論属铁。

總結

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

分布式鎖是指分布式業(yè)務環(huán)境下需要的鎖躬翁,對支持鎖的服務沒有要求要分布式焦蘑;

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

方案選擇就像投資例嘱,需要考慮投入產(chǎn)出比狡逢;

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

參考

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

Twemproxy:Twemproxy 的代碼奢浑,我沒看完,但是搭建了服務測試间学。

架構技術是程序員繞不開的話題殷费,關于分布式,微服務低葫,源碼详羡,框架結構,設計模式等這些技術我都收藏了些視頻嘿悬,進群:710373545就能免費獲得实柠。希望可以幫助在這個行業(yè)發(fā)展的朋友和童鞋們,在論壇博客等地方少花些時間找資料善涨,把有限的時間窒盐,真正花在學習上,我把這些視頻分享出來钢拧。相信對于已經(jīng)工作和遇到技術瓶頸的碼友蟹漓,在這個群里一定有你需要的內容。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末源内,一起剝皮案震驚了整個濱河市葡粒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌膜钓,老刑警劉巖嗽交,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異颂斜,居然都是意外死亡夫壁,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門沃疮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盒让,“玉大人,你說我怎么就攤上這事司蔬∨幢颍” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵葱她,是天一觀的道長撩扒。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么搓谆? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任炒辉,我火速辦了婚禮,結果婚禮上泉手,老公的妹妹穿的比我還像新娘黔寇。我一直安慰自己,他們只是感情好斩萌,可當我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布缝裤。 她就那樣靜靜地躺著,像睡著了一般颊郎。 火紅的嫁衣襯著肌膚如雪憋飞。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天姆吭,我揣著相機與錄音榛做,去河邊找鬼。 笑死内狸,一個胖子當著我的面吹牛检眯,可吹牛的內容都是我干的。 我是一名探鬼主播昆淡,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼锰瘸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了昂灵?” 一聲冷哼從身側響起获茬,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎倔既,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鹏氧,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡渤涌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了把还。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片实蓬。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吊履,靈堂內的尸體忽然破棺而出安皱,到底是詐尸還是另有隱情,我是刑警寧澤艇炎,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布酌伊,位于F島的核電站,受9級特大地震影響缀踪,放射性物質發(fā)生泄漏居砖。R本人自食惡果不足惜虹脯,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望奏候。 院中可真熱鬧循集,春花似錦、人聲如沸蔗草。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咒精。三九已至镶柱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間狠轻,已是汗流浹背奸例。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留向楼,地道東北人查吊。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像湖蜕,于是被迫代替她去往敵國和親逻卖。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內容

  • 鎖和分布式鎖 在計算機中昭抒,鎖的作用是解決在并發(fā)狀態(tài)下的共享資源互斥問題评也,保證在同一時間只有一個進程/線程可以掌握資...
    Java架構師Carl閱讀 838評論 0 2
  • 原理分析 最近看到好多博主都在推分布式鎖,實現(xiàn)方式很多灭返,基于db盗迟、redis、zookeeper熙含。zookeepe...
    ongahong閱讀 684評論 0 0
  • 出行的這幾天交通工具多數(shù)都是出租車罚缕,司機師傅便成了我們的免費導游,了解獅城的市民生活全靠他們了怎静。機票是21號凌晨0...
    夏yan閱讀 422評論 0 1
  • 迎著蒙蒙細雨邮弹,我們一行人帶著興奮坐上了上山的纜車。但是沒過多久蚓聘,我便十分后悔腌乡。懸空的座椅,身旁躁動的伙伴夜牡,...
    yee只閱讀 551評論 0 0
  • 六月十八日剛剛過去的父親節(jié) 有喜也有悲 當然有爸爸媽媽的都會收到這一天的一個特殊的禮物——父親節(jié) 而對于我們這些早...
    子涵貝貝閱讀 259評論 0 1