redis加锁实现方式

思考

  • 是否有官方推荐(自己先思考如何实现,然后再参考其他人的实践,总结优缺点)
  • 通过哪些方式可以实现锁
  • 锁是否具有原子性
  • 锁请求失败了如何处理
  • 如果避免发生死锁
  • 如果避免发生资源抢占
  • 如果避免锁的误删

官方实现策略

  • 安全性能:互斥。在任何给定的时刻,只有一个客户可以持有锁
  • 活性属性A:无死锁。最终,即使锁定资源的客户端崩溃或被分区,也始终有可能获取锁
  • 活性性质B:容错性。只要大多数Redis节点都启动了,客户端就可以获取和释放锁

储备知识

原子操作

解释一

原子操作是指不会被线程调度机制打断操作,这种操作一旦开始,就一直到结束,
中间不会有任何context switch(切换到另外一个线程)

解释二

原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。
原子操作可以是一个步骤,也可以是多个步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。
将操作视作一个整体,资源在该次操作中保持一致,这是原子性的核心特征。

可用加锁命令

INCR SETNS SET

加锁命令优劣分析

INCR

实现思路

key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。
然后其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中

实现方式

  • 客户端A请求服务器获取key的值为1表示获取了锁
  • 客户端B也去请求服务器获取key的值为2表示获取锁失败
  • 客户端A执行代码完成,删除锁
  • 客户端B在等待一段时间后在去请求的时候获取key的值为1表示获取锁成功
  • 客户端B执行代码完成,删除锁

remark

加入过期时间是为了防止意外退出,锁没有删除,锁一直存在,以至于无法再次获取锁资源

demo

  • r e d i s − > i n c r ( redis->incr( redis>incr(key);
  • r e d i s − > e x p i r e ( redis->expire( redis>expire(key, $ttl); //设置生成时间为1秒

缺点
借住Expire设置,不再是原子操作。

SETNX

实现思路

如果key不存在,则将key设置为value,如果key已存在,则SETNX不做任何操作

实现方式

  • 客户端A请求服务端设置key的值,如果设置成功,表示加锁成功
  • 客户端B请求服务器设置key的值,如果返回失败,表示加锁失败
  • 客户端A执行代码完成,删除锁
  • 客户端B在等待一段时间后再去请求设置key值,设置成功
  • 客户端B执行代码完成,删除锁

demo

  • r e d i s − > s e t N X ( redis->setNX( redis>setNX(Kkey, $value);
  • r e d i s − > e x p i r e ( redis->expire( redis>expire(key, $ttl);

缺点
不是原子操作

非原子性潜在问题:无法实现互斥,出现单点故障时,比如说如果Redis主机坏了怎么办?好吧,
让我们增加一个假设!如果主机不可用,我们增加一个备机,请使用它。不幸的是,这是不可行的。
这样做我们就无法实现互斥的安全属性,因为Redis复制是异步的。

场景描述

  • 客户机A获取主机中的锁。
  • 在向从机发送对密钥的写入之前,主机崩溃。
  • 备机被提升为主机。
  • 客户端B获取已为其持有锁的同一资源A的锁。违反安全规定!(在A拥有锁的同时,B也用用了锁)
  • 有时,在特殊情况下,比如在故障期间,多个客户机可以同时持有锁,这是非常好的。
    如果是这种情况,则可以使用基于复制的解决方案。否则,我们建议实现本文档中描述的解决方案。

SET

实现思路

设置key的同时,这只过期时间

实现方式

  • 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
  • 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
  • 客户端A执行代码完成,删除锁
  • 客户端B在等待一段时间后在去请求设置key的值,设置成功
  • 客户端B执行代码完成,删除锁

demo
r e d i s − > s e t ( redis->set( redis>set(key, $value, array(‘nx’, ‘ex’ => $ttl)); //ex表示秒

存在问题

以上几种方式仍存在的问题
  1. redis发现锁失败了要怎么办?中断请求还是循环请求?
  2. 循环请求的话,如果有一个获取了锁,其它的在去获取锁的时候,是不是容易发生抢锁的可能?
  3. 锁提前过期后,客户端A还没执行完,然后客户端B获取到了锁,这时候客户端A执行完了,会不会在删锁的时候把B的锁给删(非原子操作的影响)
解决办法

针对问题1:使用循环请求,循环请求去获取锁
  针对问题2:针对第二个问题,在循环请求获取锁的时候,加入睡眠功能,等待几毫秒在执行循环
  针对问题3:在加锁的时候存入的key是随机的。这样的话,每次在删除key的时候判断下存入的key里的value和自己存的是否一样

 do { //针对问题1,使用循环
    $timeout = 10;
    $roomid = 10001;
    $key = 'room_lock';
    $value = 'room_'.$roomid; //分配一个随机的值针对问题3
    $isLock = Redis::set($key, $value, 'ex', $timeout, 'nx');//ex 秒
    if ($isLock) {
        if (Redis::get($key) == $value) { //防止提前过期,误删其它请求创建的锁
          //执行内部代码
          Redis::del($key);
          continue;//执行成功删除key并跳出循环
       }
  } else {
      usleep(5000); //睡眠,降低抢锁频率,缓解redis压力,针对问题2
  }
} while(!$isLock);

官方提供分布式redis锁说明

参考

【锁】redis加锁的几种方法
线程安全之原子操作
原子操作

你可能感兴趣的:(redis,bootstrap,前端)