PHP+Redis实现分布式锁

一、分布式锁概述
二、redis实现锁的命令
1、redis实现锁的命令
3、释放锁的步骤
三、PHP+redis分布式锁示例
四、redis集群分布式锁

一、分布式锁概述

    在分布式环境下,各个线程通过对公共资源的抢占,能够使一个代码块在同一时间只能被一个机器的一个线程执行,这个机制就是分布式锁。
    分布式锁主要用于在分布式环境中保护跨进程、跨主机、跨网络的共享资源实现互斥访问,以达到保证数据的一致性

实现锁的操作主要有两个,即lock()和unlock()。
分布式锁实现的注意点:
1)互斥: 任意时刻, 只能有一个客户端获得锁
2)不会死锁: 客户端持有锁期间崩溃, 没有主动解除锁, 能保证后续的其他客户端获得锁
3)锁归属标识: 加锁和解锁的必须是同一个客户端, 客户端不能解掉非自己持有的锁(锁应具备标识)
4)如果是Redis集群, 还得考虑具有容错性: 只要大部分Redis节点正常运行, 客户端就可以加锁和解锁.

二、redis实现锁的命令

1、redis实现锁的命令

set key value NX EX

“NX” 仅在key不存在时加锁, 满足条件1: 互斥型
“EX” 设置锁过期时间(秒), 满足条件2: 避免死锁
上面这个set命令拆解开就是:

setnx cache_key random_value 
expire cache_key 30

虽然这两组命令执行的效果一样,但是第二个是非原子性操作,如果执行了setnx成功,但是expire失败的话,就会造成这个key一直存在了,无法释放的情况(死锁)
2、使用随机数增加锁标识

public function lock(){
    // 生成随机值
    $this->lockValue = md5(uniqid());
    return $this->redis->set($this->scene, $this->lockValue , ['NX', 'EX' => $this->expire]);
}

使用锁标识(增加随机数)的原因:避免某个客户端获取锁后做其他操作过久而导致锁被自动释放,但该客户端以为还获得锁,这时假设其他客户端获取了锁,但上个客户端删除锁继而导致本客户端的锁被删除,总而言之客户端只能删除自己的锁,通过锁标识进行判断
3、释放锁的步骤

  • get 所持有锁
  • 判断这个锁是否自己所持有
  • 删除持有锁

所以,这三步要保证原子性,用lua脚本来执行,redis官方已经提供脚本文件。

if redis.call("get",KEYS[1]) == ARGV[1] then
  return redis.call("del",KEYS[1])
else
  return 0
end
    lua 脚本中执行 get 和 del 是原子性的, 整个lua脚本会被当做一条命令来执行,即使 get 后锁刚好过期, 此时也不会被其他客户端加锁,直到eval命令执行完成,Redis才会执行其他命令。如果不用原子性,还是会导致该锁被其他客户端获得,但上客户端已经判断过(自己所持有),进一步会删除该锁。
 * 解锁
 */
public function unLock(){
    $script = <<<LUA
        local key=KEYS[1]
        local value=ARGV[1]
        if(redis.call('get', key) == value)
        then
            return redis.call('del', key)
        end
    LUA;
    
    // 执行lua脚本
    $this->redis->eval($script, [$this->scene, $this->lockValue], 1);
}

eval方法的参数 3个,第一个是脚本代码(具体执行的代码),第二个是一个数组,参数数组,第三个参数是个整数,表示第key参数的数量,lua代码中的KEYS数量(不包括ARGV数量)

三、PHP+redis分布式锁示例

<?php

/**
 * redis 分布式锁
 */

class Lock{

    private $config;
    private $redis;

    public function __construct($config = []){
        $this->config = $config ? $config : ['host' => '127.0.0.1', 'port' => 6379];
        $this->redis = $this->connect();
    }

    public function connect(){
        $redis = new Redis();
        $redis->connect($this->config['host'], $this->config['port']);
        return $redis;
    }

    /**
     * @param $scene 锁场景
     * @param $expire 锁有效期
     * @return bool
     */
    public function lock($scene = null , $expire = 10){
        if (!$scene || !$expire){
            return false;
        }

        // 生成随机值,锁标识
        $lockId = md5(uniqid());
        $result = $this->redis->set($scene, $lockId, ['NX', 'EX' => $expire]) ;

        if($result)
            return $lockId;
        else
            return $result;
    }

    /**
     * 解锁
     */
    public function unLock($scene, $lockId){

        $lua = <<<SCRIPT
       local key=KEYS[1]
            local value=ARGV[1]
            if(redis.call('get', key) == value)
            then
                return redis.call('del', key)
            end
SCRIPT;
        // 执行lua脚本
        return $this->redis->eval($lua, [$scene, $lockId], 1);
    }
}


$lock = new Lock();
//第一次加锁
$res = $lock->Lock("test", 30);
var_dump($res); // 返回lockId

echo "<br>";
//第二次加锁
$res1 = $lock->Lock("test", 25);
var_dump($res1); //加锁失败 false

//解锁
if($res){
    $lock -> unLock("test", $res);
}

四、redis集群分布式锁

Redis 集群相对单机来说, 需要考虑一个 容错性, 设计上更为复杂

RedLock 算法:官方给出了一个 RedLock 算法

情景: 当前有N个完全独立的Redis master节点, 分别部署在不同的主机上

客户端获取锁的操作:

  1. 使用相同key和唯一值(作为value)同时向这N个redis节点请求锁, 锁的超时时间应该 >> 超时时间(考虑到请求耗时),
    若某个节点阻塞了了应尽快跳过
  2. 计算步骤1消耗的时间, 若总消耗时间超过超时时间, 则认为锁失败. 客户端需在大多数(超过一半)的节点上成功获取锁, 才认为是锁成功.
  3. 如果锁成功了, 则该锁有效时间就是 锁原始有效时间 - 步骤1消耗的时间
  4. 如果锁失败了(超时或无法获取超过一半 N/2 + 1 实例的锁), 客户端会到每个节点释放锁(是每个, 即使之前认为加锁失败的节点)
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PHPRedis 的结合可以实现很多实用的功能,例如缓存、队列、计数器、分布式锁等。以下是一个简单的 PHP + Redis 实战示例,用于实现一个简单的缓存功能。 首先需要安装 Redis 扩展,可以使用 PECL 安装: ``` pecl install redis ``` 安装完成后,在 php.ini 中添加以下行启用扩展: ``` extension=redis.so ``` 接下来是实现缓存功能的示例代码: ```php <?php // 连接 Redis $redis = new Redis(); $redis->connect('127.0.0.1', 6379); // 尝试从缓存中获取数据 $key = 'my_cache_key'; $data = $redis->get($key); // 如果缓存中没有数据,则从数据库中获取并缓存 if (!$data) { $data = fetch_data_from_database(); $redis->set($key, $data); } // 使用数据 echo $data; // 从数据库中获取数据的函数 function fetch_data_from_database() { // ... } ``` 以上代码首先连接 Redis,然后尝试从缓存中获取数据,如果缓存中没有数据,则从数据库中获取并缓存。最后使用获取的数据。 在实际应用中,可以将以上代码封装成一个缓存类,以便更方便地使用。例如: ```php <?php class Cache { private $redis; public function __construct() { $this->redis = new Redis(); $this->redis->connect('127.0.0.1', 6379); } public function get($key) { $data = $this->redis->get($key); if (!$data) { $data = $this->fetch_data_from_database(); $this->redis->set($key, $data); } return $data; } private function fetch_data_from_database() { // ... } } // 使用缓存 $cache = new Cache(); $data = $cache->get('my_cache_key'); echo $data; ``` 以上代码实现了一个简单的缓存类,使用方式更加简单,只需要实例化一个 Cache 对象并调用 get 方法即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值