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";
? ? }
}