• Linux
  • apache
  • centos
  • Git版本管理
  • Linux基本命令
  • linux配置与优化
  • Nginx
  • PHP
  • Redis
  • Supervisor
  • Swoole
  • windows
  • THINKPHP
  • 开发者手册
  • Chat GPT / Open Ai
  • PHP+Redis实现分布式锁

    全屏阅读
  • 基本信息
  • 作者:
  • 作者已发布:925篇文章
  • 发布时间:2023年01月06日 19:35:48
  • 所属分类:PHP+MySql, PHP原生源码, Redis
  • 阅读次数:403次阅读
  • 标签:
  • 一、分布式锁概述

            在分布式环境下,各个线程通过对公共资源的抢占,能够使一个代码块在同一时间只能被一个机器的一个线程执行,这个机制就是分布式锁。

            分布式锁主要用于在分布式环境中保护跨进程、跨主机、跨网络的共享资源实现互斥访问,以达到保证数据的一致性

    实现锁的操作主要有两个,即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节点, 分别部署在不同的主机上

    客户端获取锁的操作:

    使用相同key和唯一值(作为value)同时向这N个redis节点请求锁, 锁的超时时间应该 >> 超时时间(考虑到请求耗时), 若某个节点阻塞了了应尽快跳过

    计算步骤1消耗的时间, 若总消耗时间超过超时时间, 则认为锁失败. 客户端需在大多数(超过一半)的节点上成功获取锁, 才认为是锁成功.

    如果锁成功了, 则该锁有效时间就是 锁原始有效时间 - 步骤1消耗的时间

    如果锁失败了(超时或无法获取超过一半 N/2 + 1 实例的锁), 客户端会到每个节点释放锁(是每个, 即使之前认为加锁失败的节点)

    顶一下
    (0)
    100%
    订阅 回复
    踩一下
    (0)
    100%
    » 郑重声明:本文由mpxq168发布,所有内容仅代表个人观点。版权归恒富网mpxq168共有,欢迎转载, 但未经作者同意必须保留此段声明,并给出文章连接,否则保留追究法律责任的权利! 如果本文侵犯了您的权益,请留言。
  • 【上一篇】
  • 【下一篇】
  • 目前有 0 条留言 其中:访客:0 条, 博主:0 条

    给我留言

    您必须 [ 登录 ] 才能发表留言!