使用php實現redis分布式鎖

redis來實現分布式鎖的原理就是將程序中一個唯一的key寫入redis中,當有其他分布式應用要訪問時候此key時,就去redis中讀取,讀取到了則說明此數據正在被處理,讀取不到則說明可以進行處理;

但是,想將分布式鎖處理的妥當,還真不是一件輕松地事情,繼續往后看。

在redis實現的分布式鎖中,我們需要強調以下幾點,只有保證了以下幾點,才可說是確保了鎖的實現:

(1)互斥,在任何時刻,對于同一條數據,只有一臺應用可以獲取到分布式鎖;

(2)不能發生死鎖,一臺服務器掛了,程序沒有執行完,但是redis中的鎖卻永久存在了,那么已加鎖未執行完的數據,就永遠得不到處理了,直到人工發現,或者監控發現;

(3)高可用性,可以保證程序的正常加鎖,正常解鎖;

(4) 加鎖解鎖必須由同一臺服務器進行,不能出現你加的鎖,別人給你解鎖了。

多數實現使用的是setnx()方法和expire()方法實現,而在redis最新版本中使用的是set()方法;

在分布式系統環境下,val的值可以設置成該機器的唯一標識,例如時間+請求號。為什么這么說,當一個服務器向redis加鎖時候,我們需要確定這個key是來自于哪臺服務器,在解鎖時需要校驗是不是解鎖的請求來自于同一個服務器;

SET(key value [EX seconds] [PX milliseconds] [NX|XX]) 方法:

(1)key,我們使用key來當鎖,key是唯一的。 (2)value,我們傳的是“時間+請求號”,通過給value賦值我們在解鎖的時候就會傳遞同樣的數據進行解鎖。不至于出現不同的服務器對key進行解鎖。為什么說,不允許出現不同的服務器對一個key進行解鎖?我們后面講解。 (3)nxxx,NX意思為SET IF NOT EXIST,即當key不存在時,我們進行set操作;若key已經存在,則不做任何操作; (4)expx,PX意思是給這個key加一個過期設置,具體時間由第五個參數決定。 (5)time,代表key的過期時間,單位毫秒。

$redis = new \Redis();

$rt = $redis->connect('127.0.0.1', 6379, 0.01);

$redis->set('test','hello word',['NX', 'PX' => 10]);

說完了上鎖,接下來說說解鎖:

解鎖,就是將key刪除,你可能會覺得調用redis刪除方法就行了唄,事實并不是如此;

$redis->del($key);

我們前面說了,在分布式環境中,哪臺服務器加的鎖,在解鎖時候,還讓那臺服務器來解鎖。不能出現A服務器加鎖,而B服務器解鎖的情況;而上面的代碼就會出現這種情況。

當A服務器將一個key設置超時時間為5秒鐘,獲取到鎖執行業務邏輯,但是呢,5秒鐘沒有執行完,此時key由于到了過期時間而被刪除了。正好B服務器進行了獲取鎖操作,發現key沒有上鎖,進而加鎖開始執行業務邏輯。過了1秒后,A服務器執行完畢,執行釋放所操作,del(key),將B服務器上的鎖給刪除了。A、B服務器對同一個可以執行了一樣的操作;

由于判斷和del()操作不是原子性的,那么就會存在判斷后,讓其他服務器刪除的情況;

例如:A服務器加鎖,執行業務邏輯,很快執行完畢,進行解鎖操作,解鎖判斷,OK,準備進行del()操作,此時CPU切換到執行別的操作了,或者JVM虛擬機進行垃圾回收操作。這時候,key到了過期時間,B服務器執行獲取到鎖,執行業務邏輯,還沒執行完成,A服務器復活,執行del()操作,刪除key;此時,A服務器上的鎖,超時而被刪除,B服務器加鎖,A服務器將其刪除;

終極大招,lua腳本實現:

$luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";

eval($luaScript,$key,$value);

通過lua腳本,解決了 解鈴還須系鈴人 的問題。

PHP代碼實現

<?php

class RedLock

{

? ? private $retryDelay;

? ? private $retryCount;

? ? private $clockDriftFactor = 0.01;

? ? private $quorum;

? ? private $servers = array();

? ? private $instances = array();

? ? function __construct(array $servers, $retryDelay = 200, $retryCount = 3)

? ? {

? ? ? ? $this->servers = $servers;

? ? ? ? $this->retryDelay = $retryDelay;

? ? ? ? $this->retryCount = $retryCount;

? ? ? ? $this->quorum? = min(count($servers), (count($servers) / 2 + 1));

? ? }

? ? public function lock($resource, $ttl)

? ? {

? ? ? ? $this->initInstances();

? ? ? ? $token = uniqid();

? ? ? ? $retry = $this->retryCount;

? ? ? ? do {

? ? ? ? ? ? $n = 0;

? ? ? ? ? ? $startTime = microtime(true) * 1000;

? ? ? ? ? ? foreach ($this->instances as $instance) {

? ? ? ? ? ? ? ? if ($this->lockInstance($instance, $resource, $token, $ttl)) {

? ? ? ? ? ? ? ? ? ? $n++;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? ? ? # Add 2 milliseconds to the drift to account for Redis expires

? ? ? ? ? ? # precision, which is 1 millisecond, plus 1 millisecond min drift

? ? ? ? ? ? # for small TTLs.

? ? ? ? ? ? $drift = ($ttl * $this->clockDriftFactor) + 2;

? ? ? ? ? ? $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift;

? ? ? ? ? ? if ($n >= $this->quorum && $validityTime > 0) {

? ? ? ? ? ? ? ? return [

? ? ? ? ? ? ? ? ? ? 'validity' => $validityTime,

? ? ? ? ? ? ? ? ? ? 'resource' => $resource,

? ? ? ? ? ? ? ? ? ? 'token'? ? => $token,

? ? ? ? ? ? ? ? ];

? ? ? ? ? ? } else {

? ? ? ? ? ? ? ? foreach ($this->instances as $instance) {

? ? ? ? ? ? ? ? ? ? $this->unlockInstance($instance, $resource, $token);

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? ? ? // Wait a random delay before to retry

? ? ? ? ? ? $delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay);

? ? ? ? ? ? usleep($delay * 1000);

? ? ? ? ? ? $retry--;

? ? ? ? } while ($retry > 0);

? ? ? ? return false;

? ? }

? ? public function unlock(array $lock)

? ? {

? ? ? ? $this->initInstances();

? ? ? ? $resource = $lock['resource'];

? ? ? ? $token? ? = $lock['token'];

? ? ? ? foreach ($this->instances as $instance) {

? ? ? ? ? ? $this->unlockInstance($instance, $resource, $token);

? ? ? ? }

? ? }

? ? private function initInstances()

? ? {

? ? ? ? if (empty($this->instances)) {

? ? ? ? ? ? foreach ($this->servers as $server) {

? ? ? ? ? ? ? ? list($host, $port, $timeout) = $server;

? ? ? ? ? ? ? ? $redis = new \Redis();

? ? ? ? ? ? ? ? $redis->connect($host, $port, $timeout);

? ? ? ? ? ? ? ? $this->instances[] = $redis;

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? private function lockInstance($instance, $resource, $token, $ttl)

? ? {

? ? ? ? return $instance->set($resource, $token, ['NX', 'PX' => $ttl]);

? ? }

? ? private function unlockInstance($instance, $resource, $token)

? ? {

? ? ? ? $script = '

? ? ? ? ? ? if redis.call("GET", KEYS[1]) == ARGV[1] then

? ? ? ? ? ? ? ? return redis.call("DEL", KEYS[1])

? ? ? ? ? ? else

? ? ? ? ? ? ? ? return 0

? ? ? ? ? ? end

? ? ? ? ';

? ? ? ? return $instance->eval($script, [$resource, $token], 1);

? ? }

}

?>

實例

<?php

require_once __DIR__ . '/src/RedLock.php';

$servers = [

? ? ['127.0.0.1', 6379, 0.01],

? ? ['127.0.0.1', 6389, 0.01],

? ? ['127.0.0.1', 6399, 0.01],

];

$redLock = new RedLock($servers);

while (true) {

? ? $lock = $redLock->lock('test', 10000);

? ? if ($lock) {

? ? ? ? print_r($lock);

? ? } else {

? ? ? ? print "Lock not acquired\n";

? ? }

}

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容