本文來自作者 一行 在 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ā)出去了取试。
問題同上面纺念,如何保證不重復的追加記錄呢?
假設你的存儲服務和存儲架構是這樣的:
一般的處理代碼是這樣的:
<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 寫入前讀到文件內容,所以最終的結果是兩者會丟失一個寫入位迂。
所以需要對讀寫操作做一次加鎖访雪,保證事務的完整、一致掂林。
下圖是《現(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)如下(只提供了大致,刪除了敏感信息):
<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 架構
對于上圖的單點架構,讀寫不分離柠硕。
那么上面的代碼對于上面要求是否滿足工禾?
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%這個比例是我猜測的。
具體代碼如下:
<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>
使用注意事項如下:
使用時候需要控制好自己的 lockTime,需要長于你的事務執(zhí)行時間篮幢;
上層在獲取鎖失敗的時候大刊,需要自己去選擇是阻塞還是拋棄這次請求,讓用戶端重試三椿。
目前待解決問題有:
如果你的進程因為 CUP 吃緊而被掛起缺菌,而且掛起的時間超過了你設置的鎖的失效時間,是不是仍然會出現(xiàn)問題搜锰?
如果集群模式一個分片掛了伴郁,會發(fā)生什么?
你有什么辦法解決嗎蛋叼?歡迎留言討論焊傅。
總結
總結一下我這次的分享,主要有以下幾點總結:
分布式鎖是指分布式業(yè)務環(huán)境下需要的鎖狈涮,對支持鎖的服務沒有要求要分布式狐胎;
鎖實際上是一個資源協(xié)調者的角色,管理并發(fā)態(tài)下的資源控制權歌馍;
方案選擇就像投資顽爹,需要考慮投入產(chǎn)出比;
Redis 單機和集群方案有自己的優(yōu)化點骆姐,根據(jù)場景做優(yōu)化;
在寫完文章后發(fā)現(xiàn)我的題目有點問題捏题,更準確的叫法應該是《 Redis 實現(xiàn)分布式鎖的思考》玻褪,如果騙了你,請告訴我公荧。
參考
吳大山的博客 :提醒了我解鈴還需系鈴人(Lua腳本)
Twemproxy:Twemproxy 的代碼带射,我沒看完,但是搭建了服務測試循狰。
架構技術是程序員繞不開的話題窟社,關于分布式券勺,微服務,源碼灿里,框架結構关炼,設計模式等這些技術我都分享在群619881427,可免費下載匣吊。希望可以幫助在這個行業(yè)發(fā)展的朋友和童鞋們儒拂,在論壇博客等地方少花些時間找資料,把有限的時間色鸳,真正花在學習上社痛,我把這些視頻分享出來。相信對于已經(jīng)工作和遇到技術瓶頸的碼友命雀,在這個群里一定有你需要的內容蒜哀。