PHP 使用 Redis 實現分佈式鎖

Last-Modified: 2019年6月5日15:59:34php

參考連接

鎖實現的注意點

  1. 互斥: 任意時刻, 只能有一個客戶端得到鎖
  2. 不會死鎖: 客戶端持有鎖期間崩潰, 沒有主動解除鎖, 能保證後續的其餘客戶端得到鎖
  3. 鎖歸屬標識: 加鎖和解鎖的必須是同一個客戶端, 客戶端不能解掉非本身持有的鎖(鎖應具有標識)

若是是Redis集羣, 還得考慮具備容錯性: 只要大部分Redis節點正常運行, 客戶端就能夠加鎖和解鎖.git

如下只考慮 Redis單機部署的 場景.github

若是是Redis集羣部署, 可使用redis

加鎖

php 加鎖示例算法

$redis = new Redis();
$redis->pconnect("127.0.0.1", 6379);
$redis->auth("password");    // 密碼驗證
$redis->select(1);    // 選擇所使用的數據庫, 默認有16個

$key = "...";
$value = "...";
$expire = 3;

// 參數解釋 ↓
// $value 加鎖的客戶端請求標識, 必須保證在全部獲取鎖清秋的客戶端裏保持惟一, 知足上面的第3個條件: 加鎖/解鎖的是同一客戶端
// "NX" 僅在key不存在時加鎖, 知足條件1: 互斥型
// "EX" 設置鎖過時時間, 知足條件2: 避免死鎖
$redis->set($key, $value, ["NX", "EX" => $expire])

執行上面代碼結果:數據庫

  1. $key 對應的鎖不存在, 進行加鎖操做
  2. $key 對應的鎖已存在, 什麼也不作

加鎖容易錯誤的點:緩存

  • 使用 setnxexpire 的組合

    緣由: 若在 setnx 後腳本崩潰會致使死鎖服務器

$value 客戶端標識的:分佈式

  • 簡單點就用 毫秒級unix時間戳 + 客戶端標識(大部分狀況下夠用了)
  • 使用其餘算法確保生成惟一隨機值

connect 與 pconnect

在php中, 若使用 pconnect 鏈接redis, 則在當前腳本聲明週期結束後, 與redis創建的鏈接仍會保留, 直到對應fpm進程的生命週期結束, 同時在下一次請求時, fpm會重用該鏈接.ide

即該鏈接的生命週期是 fpm 進程的生命週期, 而非一次php腳本的執行.

若代碼使用 pconnect, close 的做用僅是使當前php腳本不能再進行redis請求, 並無真正關閉與redis的鏈接, 鏈接在後續請求中仍然會被重用.

pconnect函數在線程版本中不能被使用

clipboard.png

clipboard.png

上圖中, php-fpm 與redis創建的鏈接並未隨請求結束後立刻斷開

解鎖

php解鎖示例: 使用lua腳本

$key = "...";
$identification = "...";
// KEYS 和 ARGV 是lua腳本中的全局變量
$script = <<< EOF
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end
EOF;
# $result = $redis->eval($script, [$key, $identification], 1);
// 返回結果 >0 表示解鎖成功
// php中參數的傳遞順序與標準不同, 注意區分
// 第2個參數表示傳入的 KEYS 和 ARGV, 經過第3個參數來區分, KEYS 在前, ARGV 在後
// 第3個參數表示傳入的 KEYS 的個數
$result = $redis->evaluate($script, [$key, $identification], 1);

使用Lua腳本的緣由:

  • 避免誤刪其餘客戶端加的鎖

    eg. 某個客戶端獲取鎖後作其餘操做太久致使鎖被自動釋放, 這時候要避免這個客戶端刪除已經被其餘客戶端獲取的鎖, 這就用到了鎖的標識.
  • lua 腳本中執行 getdel 是原子性的, 整個lua腳本會被當作一條命令來執行
  • 即便 get 後鎖恰好過時, 此時也不會被其餘客戶端加鎖
eval命令執行Lua代碼的時候,Lua代碼將被當成一個命令去執行,而且直到eval命令執行完成,Redis纔會執行其餘命令。

因爲 script 執行的原子性, 因此不要在script中執行過長開銷的程序,不然會驗證影響其它請求的執行。

解鎖容易錯誤的點:

  • 直接 del 刪除鍵

    緣由: 可能移除掉其餘客戶端加的鎖(在本身的鎖已過時狀況下)

  • get判斷鎖歸屬, 若符合再 del

    緣由: 非原子性操做, 若在 get 後鎖過時了, 此時別的客戶端進行加鎖操做, 這裏的 del 就會錯誤的將其餘客戶端加的鎖解開.

Redis 中使用 Lua 腳本的注意點

↓ 這一段內容轉載自 https://blog.csdn.net/zhouzme...

注意點:

  1. Redis 會把全部執行過的腳本都緩存在內存中
  2. Redis 在重啓的時候會釋放掉以前保存的腳本
  3. Lua 腳本中所須要用到的鍵名以及參數必定要使用 KEYS 和 ARGV 來替換,千萬不要寫死在代碼中,除非你百分百肯定每次請求時他們是固定不變的值,特別是涉及到 時間,隨機數的,必定要用參數代入,由於 Redis 每次使用 script 都會校驗腳本緩存中是否已存在相同腳本,不然就會存儲到緩存中,若是你的腳本很長,且每次請求存在不一樣的變量值,則會生成無數多個腳本緩存,你將會發現Redis佔用的內存會唰唰唰的往上漲,我一開始由於key 和 參數太多,分開寫太麻煩了,就圖省事方便,直接把變量拼接到腳本里面,結果發現內存不停的漲,非常抓狂,找了很久才發現是這麼個緣由。

    clipboard.png

義變量必定要使用局部變量, 即 local var = 1, 局部變量只在所定義的塊(指控制結構, 函數或chunk等)內有效, 使用局部變量能夠避免命名衝突 而且訪問更快(lua中局部變量和全局變量存儲方式是不同的)

  1. 若是Lua腳本寫的比較長,非本地或局域網的狀況下,建議使用 SHA 簽名的方法來調用,這樣節省帶寬,但對性能彷佛沒什麼直接的提高。這裏對小白普及下我理解的原理就是 Redis 會把每一個腳本都生成惟一簽名,把腳本做爲函數體,並使用該簽名做爲腳本的函數名放到緩存中,因此後面調用就只須要傳一個 SHA 簽名就能夠調用該函數了,精簡不少了。同一個腳本生成的簽名都是相同的,因此SHA簽名能夠先在本地生成,而後在服務器上 script load 一次腳本,程序中只需保存和使用該簽名便可。另外須要注意的是,腳本若是被改動哪怕一個換行或一個空格(這些容易被忽略或誤操做)都必須從新 load 來獲取新的 SHA

    注意:獲取 SHA 簽名是單獨的功能,不要放在你的正常流程中,當本地開發時就能夠生成SHA,把字符串寫死在流程中。一樣的腳本,Reids是始終生成相同的簽名的。

    clipboard.png

  2. 經過 eval 帶入的 ARGV 參數若是原來是數字的,會被轉換爲字符串,若是你的邏輯中須要判斷該變量 > 0 或 < 0 之類的數字判斷則必須進行字符串到數字的轉換,使用 tonumber() 方法 if (tonumber(ARGV[1]) > 0) then return 1; end;
  3. 我測試了幾個 lua script 與 PIPELINE 處理對比,發現 script 的效率通常比 PIPELINE 高 30% ~ 40% 左右

    clipboard.png

Redis集羣分佈式鎖

Redis 集羣相對單機來講, 須要考慮一個 容錯性, 設計上更爲複雜

因爲這個我也從未實踐過, 先貼一個官方的教程貼壓壓驚

https://github.com/antirez/re...

對應的翻譯: http://ifeve.com/redis-lock/

RedLock 算法

官方給出了一個 RedLock 算法

情景: 當前有N個徹底獨立的Redis master節點, 分別部署在不一樣的主機上

客戶端獲取鎖的操做:

  1. 使用相同key和惟一值(做爲value)同時向這N個redis節點請求鎖, 鎖的超時時間應該 >> 超時時間(考慮到請求耗時), 若某個節點阻塞了了應儘快跳過
  2. 計算步驟1消耗的時間, 若總消耗時間超過超時時間, 則認爲鎖失敗. 客戶端需在大多數(超過一半)的節點上成功獲取鎖, 才認爲是鎖成功.
  3. 若是鎖成功了, 則該鎖有效時間就是 鎖原始有效時間 - 步驟1消耗的時間
  4. 若是鎖失敗了(超時或沒法獲取超過一半 N/2 + 1 實例的鎖), 客戶端會到每一個節點釋放鎖(是每一個, 即便以前認爲加鎖失敗的節點)
相關文章
相關標籤/搜索