当前位置:首页 > 教程 > 在 Redis 上实现的分布式锁

在 Redis 上实现的分布式锁

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

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

redis-on-docker1

背景:

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

我们的项目:

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

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

1、为避免特殊原因导致锁无法释放, 在加锁成功后, 锁会被赋予一个生存时间(通过 lock 方法的参数设置或者使用默认值), 超出生存时间锁将被自动释放.

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

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

总结

分布式锁是一个在很多环境中非常有用的原语,它是不同的系统或是同一个系统的不同主机之间互斥操作共享资源的有效方法。合适的利用分布式锁是,可以让你的项目更加稳定。

  • << 关于程序员面试的那些事
  • 人人都应该懂得TCP知识 >>
  • 作者:
    除非注明,本文原创:知道91,欢迎转载!转载请以链接形式注明本文地址,谢谢。
    原文链接:http://www.zhidao91.com/redis-lock/

    相关文章 近期热评 最新文章

    • 怎样在WordPress中安装Google Analytics
      就算你积累了多年网站建设的经验,你都不可能一开始就建一个外观,速度,功能以及转化都很完美的网站。你能做的并且也是各个站长正在做的,无非是通过不断的监测来改进...
    • 怎样在WordPress中安装Google Analytics
      就算你积累了多年网站建设的经验,你都不可能一开始就建一个外观,速度,功能以及转化都很完美的网站。你能做的并且也是各个站长正在做的,无非是通过不断的监测来改进...
    • oracle数据库相关操作注意事项
      修改Oracle SGA(共享内存) 很多网站说修改Oracle的内存通过命令 如果你这么做了,那么恭喜你,你的Oracle数据库无法启动了。如果你已经这么做了,恢复Oracle启动的方...
    • 使用微信JDK实现微信接口签名验证
      要使用微信的接口必须在绑定的域名下测试;签名必须先向微信请求到access_token,然后用access_token再去请求jsapi_ticket,最后用jsapi_ticket和相关的参数按照ASCII码...
    • ABP开发指南系列教程(2) – 多层架构...
      为了减少复杂性和提高代码的可重用性,采用分层架构是一种被广泛接受的技术。为了实现分层的体系结构,ABP遵循DDD(领域驱动设计)的原则,将工程分为四个层: 展现层(...
    • ABP开发指南系列教程(1) – 入...
      ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称。 ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应用程序的新起点,它旨在成为一个通用的WE...
    • Windows下 JIRA + Agile + Mysql 破解...
      本文讲述了Windows下 JIRA + Agile + Mysql 破解安装示例教程