本文來自作者 一行 在 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)工作和遇到技術瓶頸的碼友蟹漓,在這個群里一定有你需要的內容。