由於近排很忙,忙各種事情,還有工作上的專案,已經超過一個月沒寫部落格了,確實有點慚愧啊,沒能每天或者至少每週堅持寫一篇部落格。這一個月裡面接觸到很多新知識,同時也遇到很多技術上的難點,在這我將對每一個有用的技術點做一個小小的分析理解和總結。每天去學會總結,才會有進步。
本次對我在工作上的專案中用到的技術,在Redis上實現分散式鎖,進行一個分析和總結。
先了解下什麼是分散式鎖,在百科上是這麼定義的:
分散式鎖是控制分散式系統之間同步訪問共享資源的一種方式。在分散式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那麼訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分散式鎖。
簡單的理解就是:分散式鎖是一個在很多環境中非常有用的原語,它是不同的系統或是同一個系統的不同主機之間互斥操作共享資源的有效方法。
背景:
在很多網際網路產品應用中,有些場景需要加鎖處理,比如:秒殺,全域性遞增ID,樓層生成等等。大部分是解決方案基於DB實現的,Redis為單程式單執行緒模式,採用佇列模式將併發訪問變成序列訪問,且多客戶端對Redis的連線並不存在競爭關係。
我們的專案:
我們現在的專案中,任務佇列用到分散式鎖的情況比較多,在將業務邏輯中可以非同步處理的操作放入佇列,在其他執行緒中處理後出隊,此時佇列中使用了分散式鎖,保證入隊和出隊的一致性。關於redis佇列這塊的邏輯分析,我將在下一次對其進行總結,此處先略過。
接下來對redis實現的分散式鎖的邏輯程式碼進行詳細的分析和理解:
1、為避免特殊原因導致鎖無法釋放, 在加鎖成功後, 鎖會被賦予一個生存時間(通過 lock 方法的引數設定或者使用預設值), 超出生存時間鎖將被自動釋放.
2、鎖的生存時間預設比較短(秒級, 具體見 lock 方法), 因此若需要長時間加鎖, 可以通過 expire 方法延長鎖的生存時間為適當的時間. 比如在迴圈內呼叫 expire
3、系統級的鎖當程式無論因為任何原因出現crash,作業系統會自己回收鎖,所以不會出現資源丟失。
4、但分散式鎖不同。若一次性設定很長的時間,一旦由於各種原因程式 crash 或其他異常導致 unlock 未被呼叫,則該鎖在剩下的時間就變成了垃圾鎖,導致其他程式或程式重啟後無法進入加鎖區域。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
<?php require_once 'RedisFactory.php'; /** * 在 Redis 上實現的分散式鎖 */ class RedisLock { //單例模式 private static $_instance = null; public static function instance() { if(self::$_instance == null) { self::$_instance = new RedisLock(); } return self::$_instance; } //redis物件變數 private $redis; //存放被鎖的標誌名的陣列 private $lockedNames = array(); public function __construct() { //獲取一個 RedisString 例項 $this->redis = RedisFactory::instance()->getString(); } /** * 加鎖 * * @param string 鎖的標識名 * @param int 獲取鎖失敗時的等待超時時間(秒), 在此時間之內會一直嘗試獲取鎖直到超時. 為 0 表示失敗後直接返回不等待 * @param int 當前鎖的最大生存時間(秒), 必須大於 0 . 如果超過生存時間後鎖仍未被釋放, 則系統會自動將其強制釋放 * @param int 獲取鎖失敗後掛起再試的時間間隔(微秒) */ 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) { //對$redisKey設定生存時間 $this->redis->expire($redisKey, $expire); //將最大生存時刻記錄在一個陣列裡面 $this->lockedNames[$name] = $expireAt; return true; } //以秒為單位,返回$redisKey 的剩餘生存時間 $ttl = $this->redis->ttl($redisKey); // TTL 小於 0 表示 key 上沒有設定生存時間(key 不會不存在, 因為前面 setnx 會自動建立) // 如果出現這種情況, 那就是程式在某個例項 setnx 成功後 crash 導致緊跟著的 expire 沒有被呼叫. 這時可以直接設定 expire 並把鎖納為己用 if($ttl < 0) { $this->redis->set($redisKey, (string)$expireAt, $expire); $this->lockedNames[$name] = $expireAt; return true; } // 設定了不等待或者已超時 if($timeout <= 0 || microtime(true) > $timeoutAt) break; // 掛起一段時間再試 usleep($waitIntervalUs); } return false; } /** * 給當前鎖增加指定的生存時間(秒), 必須大於 0 * * @param string 鎖的標識名 * @param int 生存時間(秒), 必須大於 0 */ public function expire($name, $expire) { if($this->isLocking($name)) { if($this->redis->expire("Lock:$name", max($expire, 1))) { return true; } } return false; } /** * 判斷當前是否擁有指定名稱的鎖 * * @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 鎖的標識名 */ public function unlock($name) { if($this->isLocking($name)) { if($this->redis->deleteKey("Lock:$name")) { unset($this->lockedNames[$name]); return true; } } return false; } /** 釋放當前已經獲取到的所有鎖 */ public function unlockAll() { $allSuccess = true; foreach($this->lockedNames as $name => $item) { if(false === $this->unlock($name)) { $allSuccess = false; } } return $allSuccess; } } |
此類很多程式碼都寫上了註釋,只要認真理解下,就很容易懂得如何在redis實現分散式鎖了。
另外,我在網上找到另一篇關於redis實現分散式鎖的文章:《使用 Redis 實現分散式鎖》。我感覺挺不錯的,推薦給大家。結合我所總結的和我推薦的文章做對比,基本上能理解清楚是如何在redis實現分散式鎖的了。
如果此博文中有哪裡講得讓人難以理解,歡迎留言交流,若有講解錯的地方歡迎指出。