Redis上實(shí)現(xiàn)分布式鎖以提高性能的方案研究
背景:
在很多互聯(lián)網(wǎng)產(chǎn)品應(yīng)用中,有些場(chǎng)景需要加鎖處理,比如:秒殺,全局遞增ID,樓層生成等等。大部分是解決方案基于DB實(shí)現(xiàn)的,Redis為單進(jìn)程單線程模式,采用隊(duì)列模式將并發(fā)訪問(wèn)變成串行訪問(wèn),且多客戶端對(duì)Redis的連接并不存在競(jìng)爭(zhēng)關(guān)系。
項(xiàng)目實(shí)踐
任務(wù)隊(duì)列用到分布式鎖的情況比較多,在將業(yè)務(wù)邏輯中可以異步處理的操作放入隊(duì)列,在其他線程中處理后出隊(duì),此時(shí)隊(duì)列中使用了分布式鎖,保證入隊(duì)和出隊(duì)的一致性。關(guān)于redis隊(duì)列這塊的邏輯分析,我將在下一次對(duì)其進(jìn)行總結(jié),此處先略過(guò)。
接下來(lái)對(duì)redis實(shí)現(xiàn)的分布式鎖的邏輯代碼進(jìn)行詳細(xì)的分析和理解:
1、為避免特殊原因?qū)е骆i無(wú)法釋放, 在加鎖成功后, 鎖會(huì)被賦予一個(gè)生存時(shí)間(通過(guò) lock 方法的參數(shù)設(shè)置或者使用默認(rèn)值), 超出生存時(shí)間鎖將被自動(dòng)釋放.
2、鎖的生存時(shí)間默認(rèn)比較短(秒級(jí), 具體見 lock 方法), 因此若需要長(zhǎng)時(shí)間加鎖, 可以通過(guò) expire 方法延長(zhǎng)鎖的生存時(shí)間為適當(dāng)?shù)臅r(shí)間. 比如在循環(huán)內(nèi)調(diào)用 expire
3、系統(tǒng)級(jí)的鎖當(dāng)進(jìn)程無(wú)論因?yàn)槿魏卧虺霈F(xiàn)crash,操作系統(tǒng)會(huì)自己回收鎖,所以不會(huì)出現(xiàn)資源丟失。
4、但分布式鎖不同。若一次性設(shè)置很長(zhǎng)的時(shí)間,一旦由于各種原因進(jìn)程 crash 或其他異常導(dǎo)致 unlock 未被調(diào)用,則該鎖在剩下的時(shí)間就變成了垃圾鎖,導(dǎo)致其他進(jìn)程或進(jìn)程重啟后無(wú)法進(jìn)入加鎖區(qū)域。
<?php require_once 'RedisFactory.php'; /** * 在 Redis 上實(shí)現(xiàn)的分布式鎖 */ class RedisLock { //單例模式 private static $_instance = null; public static function instance() { if(self::$_instance == null) { self::$_instance = new RedisLock(); } return self::$_instance; } //redis對(duì)象變量 private $redis; //存放被鎖的標(biāo)志名的數(shù)組 private $lockedNames = array(); public function __construct() { //獲取一個(gè) RedisString 實(shí)例 $this->redis = RedisFactory::instance()->getString(); } /** * 加鎖 * * @param string 鎖的標(biāo)識(shí)名 * @param int 獲取鎖失敗時(shí)的等待超時(shí)時(shí)間(秒), 在此時(shí)間之內(nèi)會(huì)一直嘗試獲取鎖直到超時(shí). 為 0 表示失敗后直接返回不等待 * @param int 當(dāng)前鎖的最大生存時(shí)間(秒), 必須大于 0 . 如果超過(guò)生存時(shí)間后鎖仍未被釋放, 則系統(tǒng)會(huì)自動(dòng)將其強(qiáng)制釋放 * @param int 獲取鎖失敗后掛起再試的時(shí)間間隔(微秒) */ public function lock($name, $timeout = 0, $expire = 15, $waitIntervalUs = 100000) { if(empty($name)) return false; $timeout = (int)$timeout; $expire = max((int)$expire, 5); $now = microtime(true); $timeoutAt = $now + $timeout; $expireAt = $now + $expire; $redisKey = "Lock:$name"; while(true) { $result = $this->redis->setnx($redisKey, (string)$expireAt); if($result !== false) { //對(duì)$redisKey設(shè)置生存時(shí)間 $this->redis->expire($redisKey, $expire); //將最大生存時(shí)刻記錄在一個(gè)數(shù)組里面 $this->lockedNames[$name] = $expireAt; return true; } //以秒為單位,返回$redisKey 的剩余生存時(shí)間 $ttl = $this->redis->ttl($redisKey); // TTL 小于 0 表示 key 上沒(méi)有設(shè)置生存時(shí)間(key 不會(huì)不存在, 因?yàn)榍懊?setnx 會(huì)自動(dòng)創(chuàng)建) // 如果出現(xiàn)這種情況, 那就是進(jìn)程在某個(gè)實(shí)例 setnx 成功后 crash 導(dǎo)致緊跟著的 expire 沒(méi)有被調(diào)用. 這時(shí)可以直接設(shè)置 expire 并把鎖納為己用 if($ttl < 0) { $this->redis->set($redisKey, (string)$expireAt, $expire); $this->lockedNames[$name] = $expireAt; return true; } // 設(shè)置了不等待或者已超時(shí) if($timeout <= 0 || microtime(true) > $timeoutAt) break; // 掛起一段時(shí)間再試 usleep($waitIntervalUs); } return false; } /** * 給當(dāng)前鎖增加指定的生存時(shí)間(秒), 必須大于 0 * * @param string 鎖的標(biāo)識(shí)名 * @param int 生存時(shí)間(秒), 必須大于 0 */ public function expire($name, $expire) { if($this->isLocking($name)) { if($this->redis->expire("Lock:$name", max($expire, 1))) { return true; } } return false; } /** * 判斷當(dāng)前是否擁有指定名稱的鎖 * * @param mixed $name */ public function isLocking($name) { if(isset($this->lockedNames[$name])) { return (string)$this->lockedNames[$name] == (string)$this->redis->get("Lock:$name"); } return false; } /** * 釋放鎖 * * @param string 鎖的標(biāo)識(shí)名 */ public function unlock($name) { if($this->isLocking($name)) { if($this->redis->deleteKey("Lock:$name")) { unset($this->lockedNames[$name]); return true; } } return false; } /** 釋放當(dāng)前已經(jīng)獲取到的所有鎖 */ public function unlockAll() { $allSuccess = true; foreach($this->lockedNames as $name => $item) { if(false === $this->unlock($name)) { $allSuccess = false; } } return $allSuccess; } }
此類很多代碼都寫上了注釋,只要認(rèn)真理解下,就很容易懂得如何在redis實(shí)現(xiàn)分布式鎖了。
相關(guān)文章
Redis數(shù)據(jù)庫(kù)安裝部署及基本操作詳解
這篇文章主要介紹了Redis數(shù)據(jù)庫(kù)安裝部署及基本操作,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08Redis有序集合類型的操作_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
今天通過(guò)本文給大家說(shuō)一下Redis中最后一個(gè)數(shù)據(jù)類型 “有序集合類型”,需要的的朋友參考下吧2017-08-08Redis中ServiceStack.Redis和StackExchange.Redis區(qū)別詳解
本文主要介紹了Redis中ServiceStack.Redis和StackExchange.Redis區(qū)別詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05Redis sentinel節(jié)點(diǎn)如何修改密碼
這篇文章主要介紹了Redis sentinel節(jié)點(diǎn)如何修改密碼問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01詳解redis是如何實(shí)現(xiàn)隊(duì)列消息的ack
這篇文章主要介紹了關(guān)于redis是如何實(shí)現(xiàn)隊(duì)列消息的ack的相關(guān)資料,文中介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-04-04Redis實(shí)戰(zhàn)之商城購(gòu)物車功能的實(shí)現(xiàn)代碼
這篇文章主要介紹了Redis實(shí)戰(zhàn)之商城購(gòu)物車功能的實(shí)現(xiàn)代碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02