在 Redis 上实现的分布式锁

  由于近排很忙,忙各种事情,还有工作上的项目,已经超过一个月没写博客了,确实有点惭愧啊,没能每天或者至少每周坚持写一篇博客。这一个月里面接触到很多新知识,同时也遇到很多技术上的难点,在这我将对每一个有用的技术点做一个小小的分析理解和总结。每天去学会总结,才会有进步。

  本次对我在工作上的项目中用到的技术---在redis上实现分布式锁,进行一个分析和总结。

  先了解下什么时分布式锁,在百度上是这么定义的:

    分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

  简单的理解就是:分布式锁是一个在很多环境中非常有用的原语,它是不同的系统或是同一个系统的不同主机之间互斥操作共享资源的有效方法。

  背景:

    在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等。大部分是解决方案基于DB实现的,Redis为单进程单线程模式采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。

  我们的项目:

  我们现在的项目中,任务队列用到分布式锁的情况比较多,在将业务逻辑中可以异步处理的操作放入队列,在其他线程中处理后出队,此时队列中使用了分布式锁,保证入队和出队的一致性。关于redis队列这块的逻辑分析,我将在下一次对其进行总结,此处先略过。

 

  接下来对redis实现的分布式锁的逻辑代码进行详细的分析和理解:

  1、为避免特殊原因导致锁无法释放, 在加锁成功后, 锁会被赋予一个生存时间(通过 lock 方法的参数设置或者使用默认值), 超出生存时间锁将被自动释放.
  2、锁的生存时间默认比较短(秒级, 具体见 lock 方法), 因此若需要长时间加锁, 可以通过 expire 方法延长锁的生存时间为适当的时间. 比如在循环内调用 expire
  3、系统级的锁当进程无论因为任何原因出现crash,操作系统会自己回收锁,所以不会出现资源丢失。
  4、但分布式锁不同。若一次性设置很长的时间,一旦由于各种原因进程 crash 或其他异常导致 unlock 未被调用,则该锁在剩下的时间就变成了垃圾锁,导致其他进程或进程重启后无法进入加锁区域。

 

  1 <?php
  2 
  3 require_once 'RedisFactory.php';
  4 
  5 /**
  6 * 在 Redis 上实现的分布式锁
  7 */
  8 class RedisLock {
  9     //单例模式
 10     private static $_instance = null;
 11     public static function instance() {
 12         if(self::$_instance == null) {
 13             self::$_instance = new RedisLock();
 14         }
 15         return self::$_instance;
 16     }
 17 
 18     //redis对象变量
 19     private $redis;
 20     //存放被锁的标志名的数组
 21     private $lockedNames = array();
 22 
 23     public function __construct() {
 24         //获取一个 RedisString 实例
 25         $this->redis = RedisFactory::instance()->getString();
 26     }
 27 
 28     /** 
 29     * 加锁
 30     *
 31     * @param string 锁的标识名
 32     * @param int 获取锁失败时的等待超时时间(秒), 在此时间之内会一直尝试获取锁直到超时. 为 0 表示失败后直接返回不等待
 33     * @param int 当前锁的最大生存时间(秒), 必须大于 0 . 如果超过生存时间后锁仍未被释放, 则系统会自动将其强制释放
 34     * @param int 获取锁失败后挂起再试的时间间隔(微秒)
 35     */
 36     public function lock($name, $timeout = 0, $expire = 15, $waitIntervalUs = 100000) {
 37         if(empty($name)) return false;
 38 
 39         $timeout = (int)$timeout;
 40         $expire = max((int)$expire, 5);
 41         $now = microtime(true);
 42         $timeoutAt = $now + $timeout;
 43         $expireAt = $now + $expire;
 44 
 45         $redisKey = "Lock:$name";
 46         while(true) {
 47             $result = $this->redis->setnx($redisKey, (string)$expireAt);
 48             if($result !== false) {
 49                 //对$redisKey设置生存时间
 50                 $this->redis->expire($redisKey, $expire);
 51                 //将最大生存时刻记录在一个数组里面
 52                 $this->lockedNames[$name] = $expireAt;
 53                 return true;
 54             }
 55 
 56             //以秒为单位,返回$redisKey 的剩余生存时间
 57             $ttl = $this->redis->ttl($redisKey);
 58             // TTL 小于 0 表示 key 上没有设置生存时间(key 不会不存在, 因为前面 setnx 会自动创建)
 59             // 如果出现这种情况, 那就是进程在某个实例 setnx 成功后 crash 导致紧跟着的 expire 没有被调用. 这时可以直接设置 expire 并把锁纳为己用
 60             if($ttl < 0) {
 61                 $this->redis->set($redisKey, (string)$expireAt, $expire);
 62                 $this->lockedNames[$name] = $expireAt;
 63                 return true;
 64             }
 65 
 66             // 设置了不等待或者已超时
 67             if($timeout <= 0 || microtime(true) > $timeoutAt) break;
 68 
 69             // 挂起一段时间再试
 70             usleep($waitIntervalUs);
 71         }
 72 
 73         return false;
 74     }
 75 
 76     /**
 77     * 给当前锁增加指定的生存时间(秒), 必须大于 0
 78     *
 79     * @param string 锁的标识名
 80     * @param int 生存时间(秒), 必须大于 0
 81     */
 82     public function expire($name, $expire) {
 83         if($this->isLocking($name)) {
 84             if($this->redis->expire("Lock:$name", max($expire, 1))) {
 85                 return true;
 86             }
 87         }
 88         return false;
 89     }
 90 
 91     /**
 92     * 判断当前是否拥有指定名称的锁
 93     *
 94     * @param mixed $name
 95     */
 96     public function isLocking($name) {
 97         if(isset($this->lockedNames[$name])) {
 98             return (string)$this->lockedNames[$name] == (string)$this->redis->get("Lock:$name");
 99         }
100         return false;
101     }
102 
103     /**
104     * 释放锁
105     *
106     * @param string 锁的标识名
107     */
108     public function unlock($name) {
109         if($this->isLocking($name)) {
110             if($this->redis->deleteKey("Lock:$name")) {
111                 unset($this->lockedNames[$name]);
112                 return true;
113             }
114         }
115         return false;
116     }
117 
118     /** 释放当前已经获取到的所有锁 */
119     public function unlockAll() {
120         $allSuccess = true;
121         foreach($this->lockedNames as $name => $item) {
122             if(false === $this->unlock($name)) {
123                 $allSuccess = false;
124             }
125         }
126         return $allSuccess;
127     }
128 }

  此类很多代码都写上了注释,只要认真理解下,就很容易懂得如何在redis实现分布式锁了。

  另外,我在网上找到另一篇关于redis实现分布式锁的文章,我感觉挺不错的,推荐给大家:

  网址: http://www.oschina.net/translate/redis-distlock

  结合我所总结的和我推荐的文章做对比,基本上能理解清楚是如何在redis实现分布式锁的了。

  如果此博文中有哪里讲得让人难以理解,欢迎留言交流,若有讲解错的地方欢迎指出。

 

 

 

  如果您觉得您能在此博文学到了新知识,请为我顶一个,如文章中有解释错的地方,欢迎指出。

  互相学习,共同进步!

 

    

  

posted @ 2015-04-20 13:15  __kelly_  阅读(5788)  评论(7编辑  收藏  举报