多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
# [**分布式锁**](https://baike.baidu.com/item/分布式锁/10459578?fr=aladdin) 分布式锁可以理解为:控制分布式系统`有序`的对`共享资源`进行操作,通过`互斥`来保持一致性。 举例:假设共享的资源就是一个房子,里面有各种书,分布式系统就是要进屋看书的人,分布式锁就是保证这个房子只有一个门并且一次只有一个人可以进,而且门只有一把钥匙。然后许多人要去看书,可以,排队,第一个人拿着钥匙把门打开进屋看书并且把门锁上,然后第二个人没有钥匙,那就等着,等第一个出来,然后你在拿着钥匙进去,然后就是以此类推 ### **Redis 分布式锁作用:** Redis 写入时不带锁定功能,为防止多个进程同时进行一个操作,出现意想不到的结果,对插入更新操作时自定义加锁功能。 ### **Redis 分布式锁实现原理:** * 互斥性 保证同一时间只有一个客户端可以拿到锁,也就是可以对共享资源进行操作 * 安全性 只有加锁的服务才能解锁权限 * 避免死锁 出现死锁就会导致后续的任何服务都拿不到锁 * 保证加锁与解锁操作是原子性操作 ***** 在进程请求执行操作前进行判断,加锁是否成功,执行如下操作: 加锁不成功,则判读锁的值(时间戳)(设定一般为加锁的时候时间戳+自定义过期时间)是否大于当前时间,则获取锁失败不允许执行下步操作; 如果锁的值(时间戳)小于当前时间,并且`GETSET`命令获取到的锁旧值依然小于当前时间,则获取锁成功允许执行下步操作; 如果锁的值(时间戳)小于当前时间,并且`GETSET`命令获取到的锁旧值依然大于当前时间,则获取锁失败不允许执行下步操作; ***** ### **$redis->setnx() 设置锁:** ~~~ $expire = 10; // 有效期10秒 $key = 'lock'; // key $value = time() + $expire; // 锁的值 = Unix时间戳 + 锁的有效期 $lock = $redis->setnx($key, $value); /** * $lock * 如果返回 1,则表示当前进程获得锁,并获得了当前插入/更新缓存的操作权限 * 如果返回 0,表示锁已被其他进程获取,这是进程可以返回结果或者等待当前锁失效再请求。 */ if (!empty($lock)) { // 下步操作 } ~~~ ### **解决死锁:** ***** 如果只用`SETNX`命令设置锁的话,如果当持有锁的进程崩溃或删除锁失败时,其他进程将无法获取到锁,问题就大了。 解决方法是在获取锁失败的同时获取锁的值,并将值与当前时间进行对比,如果值小于当前时间说明锁以过期失效,进程可运用`Redis`的`DEL`命令删除该锁 ***** ~~~ $expire = 10; // 有效期10秒 $key = 'lock'; // key $value = time() + $expire; // 锁的值 = Unix时间戳 + 锁的有效期 $status = true; while ($status) { $lock = $redis->setnx($key, $value); if (empty($lock)) { $value = $redis->get($key); if ($value < time()) { $redis->del($key); // 删除过期锁 } else { usleep(1000); // 睡眠等待会儿 } } else { $status = false; // 后续操作 } } ~~~ ***** 但是,简单粗暴的用`DEL`命令删除锁再`SETNX`命令上锁也会出现问题。比如,`进程1`获得锁后崩溃或删除锁失败,这时`进程2`检测到锁存在当已过期,用`DEL`命令删除锁并用`SETNX`命令设置锁,`进程3`也检测到锁过期,也用`DEL`命令删除锁也用`SETNX`命令设置了锁,这时`进程2`和`进程3`同时获得了锁。这就出现问题。 为了解决这个问题,这里用到了`Redis`的`GETSET`命令,`GETSET`命令在给锁设置新值的同时返回锁的旧值,这里利用了`GETSET`命令同时获取和赋值的特性,在此期间其他进程无法修改锁的值。 比如: `进程1`获得锁后操作超时/崩溃/删除锁失败; `进程2`检测到锁已存在,但获取锁的值对比当前时间发现锁已过期, `进程2`通过`GETSET`命令重新给锁赋予新的值,并获取到的锁的旧值,再次对比锁的旧值与当前时间,如果锁的旧值依然小于当前时间的话,这时`进程2`就可以忽略`进程1`余留下的废锁进行下步操作了。 这里要说明的是,如果有其他进程在`进程2`之前获取到锁,那么`进程2`将获取锁失败,但是`进程2`在用`GETSET`获取锁的旧值时也赋予了锁新的值,改写了其他进程赋予锁的超时值。看到这大家可能会有疑问了,`进程2`没获取到锁怎么能改变锁的值呢?是的,`进程2`改变了锁的原有值,但这一点小小的时间误差带来的影响是可以忽略。 ***** ### **Redis 分布式锁 PHP具体实现:** ~~~ $key = 'test'; // 要更新信息的缓存 key $lockKey = 'lock_' . $key; // 设置锁的 key $lockExpire = 10; // 设置锁的有效时间 $result = $redis->get($key); // 获取缓存信息 if (empty($result)) { $status = true; while ($status) { $lockValue = $lockExpire + time(); // 设置锁的过去时间 /** * 创建锁 * 以 $lockKey 为 key 值, $lockValue 为 value 值 * 由于 setnx() 函数只有在不存在当前 key 的缓存时才会创建成功 * 所以,用此函数就可以判断当前执行的操作是否已经有其他进程在执行了 */ $lock = $redis->setnx($lockKey, $lockValue); /** * 满足一个条件就可以继续进行操作 * 1 上面创建锁成功 * 2 判断锁的值是否小于当前时间 get() 同时给锁设置新值 getset() */ if (!empty($lock)) { // 给锁设置生存时间,避免死锁 $redis->expire($lockKey, $lockExpire); /************************ * 此处进行业务处理 * 执行插入、更新缓存操作... ************************ */ // 业务走完,删除锁 if ($redis->ttl($lockKey)) { $redis->del($lockKey); } $status = false; } else { /** * 如果存在有效锁 * 睡眠会,等前面操作完在继续请求 */ sleep(1); } } } ~~~ ### **Redis 分布式锁 Redis 命令介绍:** **setnx(key, value)**     将`key`的值设为`value`,当且仅当`key`不存在。     若给定的`key`已经存在,则`SETNX`不做任何动作。     `SETNX`是『SET if Not eXists』(如果不存在,则`SET`)的简写。     返回值:         设置成功,返回`1`。         设置失败,返回`0`。  **get(key)**     返回`key`所关联的字符串值。     如果`key`不存在则返回特殊值`nil`。     假如`key`储存的值不是字符串类型,返回一个错误,因为`GET`只能用于处理字符串值。     返回值:         `key`的值。         如果`key`不存在,返回`nil`。 **getset(key, value)**     将给定`key`的值设为`value`,并返回`key`的旧值。     当`key`存在但不是字符串类型时,返回一个错误。     返回值:         返回给定`key`的旧值`old value`。         当`key`没有旧值时,返回`nil`。 **expire(key, seconds)**     为给定`key`设置生存时间。     当`key`过期时,它会被自动删除。     在`Redis`中,带有生存时间的`key`被称作“易失的”(volatile)。     返回值:         设置成功返回`1`。         当`key`不存在或者不能为`key`设置生存时间时(比如在低于2.1.3中你尝试更新`key`的生存时间),返回`0`。 **ttl(key)**     返回给定`key`的剩余生存时间(time to live)(以秒为单位)。     返回值:         `key`的剩余生存时间(以秒为单位)。         当`key`不存在或没有设置生存时间时,返回`-1` 。 **del(key)**     移除给定的一个或多个`key`。     返回值:         被移除`key`的数量。 ## [**类实现参考**](https://www.cnblogs.com/syhx/p/9753433.html) > 来源 https://www.cnblogs.com/wenxiong/p/3954174.html