$redis-setNX($key, $value); $redis-expire($key, $ttl);
使用 SET resource-name anystring NX EX max-lock-time 实现

该方案在 Redis 官方 SET
命令页有详尽介绍。
在介绍该分布式锁设计前边,大家先来看一下在从 Redis 2.6.12 开头 SET
提供的新特征,命令 SET key value [EX seconds] [PX milliseconds]
[NX|XX]
,其中:

  1. EX seconds — 以秒为单位设置 key 的过期时间;
  2. PX milliseconds — 以飞秒为单位安装 key 的超时时间;
  3. NX — 将key 的值设为value ,当且仅当key 不真实,等效于 SETNX。
  4. XX — 将key 的值设为value ,当且仅当key 存在,等效于 SETEX。

注:鉴于 SET 已经能够替代 SETNX, SETEX, PSETEX
命令,所以在今后的版本中,官方将渐次扬弃那3个指令,并最后移除。

选取 SET 的新特色,改革旧版的遍及式锁设计,首要有三个优化:

  1. 客商端通过 SET 命令能够况且形成获取锁和安装锁的超时时间:SET
    lock.foo token NX EX max-lock-time(原子操作,未有INCTiguan 、
    EXPIRE多个操作的事情难题),锁将要逾期后自动过期,不担心早前设计的
    “死锁” 难点,也从没多服务器时间同步兵高校准的难点。

  2. 使用 Lua 脚本完结 CAS 删除,使锁越来越硬朗。获取锁时为锁设置五个 token
    (叁个不可能推测的随便字符串),释放锁时先比较 token
    的值以保证只释放具备的有效锁。释放锁的 Lua 代码示例:

    1
    if redis.call("get",KEYS[1]) == ARGV[1]
    then
        return redis.call("del",KEYS[1])
    else
        return 0
    end

2、 客商端B也去伏乞服务器获取key的值为2象征收获锁败北

运用 Redis 完毕遍及式锁

选择 Redis 完成布满式锁最简易方法是创设一对 key-value 值,key
被创立为有必然的生存期,由此它最终会被放走。而当客商端想要释放时,则一直删除
key 。基于分歧的 Redis 命令,有二种完毕形式:

  1. Redis 官方开始时期给的一个兑现,使用 SETNX,将 value
    设置为超时时间,由代码完结锁超时的检验[有难点,有约束,并发不高时可用];
  2. 有同学团结的兑现:使用 INC福特Explorer + EXPIRE,利用 Redis
    的逾期机制调节锁的生存期[不提出使用];
  3. Redis 官方给的三个更进一竿达成:使用 SET resource-name anystring NX EX
    max-lock-time(Redis 2.6.12 后帮忙) 完成, 利用 Redis
    的逾期机制调控锁的生存期[Redis 2.6.12 以往建议使用]。

总结

背景

在日常的遍布式应用中,要安全有效地联手多服务器多进度之间的分享财富访谈,将在涉及到遍及式锁。方今项目是基于
Tornado 完结的布满式安排,同期也采取了 Redis
作为缓存。参照他事他说加以侦察了有些材质并构成项目自身的必要后,决定直接动用Redis完成全局的遍布式锁。

即使如此下面一步已经满意了大家的需要,但是依旧要思谋任何难点?

单点难点

上述完毕都有贰个单点难点: Redis
节点挂了如何是好?那是个很艰难的题目,何况由于 Redis
主从复制是异步的,大家便不也许轻巧地促成互斥锁在节点间的平安迁移。当然日常的项目不会有这么高的渴求,就当下大家的品种来讲,本人Redis已然是单点。。。

对此那个单点难题,Redis
上有一篇小说提供了二个算法来清除,不过完成相比较复杂:

《Distributed locks with
Redis(原文)》 / 《使用 Redis
落成布满式锁(译文)》

来源: 

源点为知笔记(Wiz卡塔尔国

本着难题3:在加锁的时候存入的key是轻易的。那样的话,每便在剔除key的时等候法庭判果断下存入的key里的value和和煦存的是还是不是相通

使用 INCR + EXPIRE 实现

该方案的落到实处来源那篇
blog 《Redis实现分布式全局锁》

  1. 顾客端A通过 INC昂Cora locker.foo 获取名称叫 locker.foo
    的锁,若赢得的值为1,则意味收获成功,转入下一步,不然获取失利;
  2. 奉行 EXPIRE locker.foo seconds
    设置锁的晚点时间,设置成功转入下一步;
  3. 推行分享能源访谈;
  4. 执行 DEL locker.foo 释放锁。

伪代码如下所示:

1
if(INCR('locker.foo') == 1)
{
     // 设置锁的超时时间为1分钟,这个可以设置为一个较大的值来避免锁提前过期释放。
     EXPIRE(60)

     // 执行共享资源访问
     DO_SOMETHING()

     // 释放锁
     DEL('locker.foo')}
}

该兑现存贰个严重的“死锁”难题:假使 INC宝马X5 命令获取锁成功后,EXPIRE
失利,会引致锁无法平常释放。可用的缓慢解决方案是:依靠 EVAL 命令,将 INC智跑 、
EXPIRE 操作封装在贰个 Lua 脚本中推行,先举行 INCRAV4命令,成功收获锁后再执行 EXPIRE。以下是躬行实行 Lua 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- Set a lock
-- KEYS[1]   - key
-- KEYS[2]   - ttl in ms

local key     = KEYS[1]
local ttl     = KEYS[2]

local lockSet = redis.call('incr', key)

if lockSet == 1 then
  redis.call('pexpire', key, ttl)
end

return lockSet

注: 由于 EVAL 命令仅在 Redis 2.6
版本后提供,对于在此以前的版本只好通过 MULTI/EXEC 将 INC本田UR-V 、 EXPIRE
封装在三个事务中来拍卖。可是由于 MULTI/EXEC 的限量,未有主意和行使 Lua
脚本相像基于 INCR 推行结果来实践 EXPIRE ,所以一旦获得锁战败,会变成 TTL
不断被延长,在高并发的意况里假设取得锁的进程意外挂掉而未有健康释放锁,锁便只好等到过期才具被此外客商端持有,而以此过期岁月的长度决意于获取锁时的角逐剧烈场合。该施工方案有生死攸关破绽,不相符高并发蒙受

++实际上,由于不可能经过三个原语完结获取锁和设置锁过期时间的操作,就算通过上述
Lua 脚本来获取锁,仍然为有题指标。由于 Redis 事务的特征,只保障 INC路虎极光 、
EXPIRE 两条命令在 Redis 上是接连实行的,但当 EXPIRE 命令战败后并不会回滚
INCENCORE 命令,所以 “死锁” 难题依然未有消除(决定于 Redis
的波平浪静)。同期,也设有锁过期后地下释放别的顾客端持有的锁的标题,且由于正视redis 的机关过期机制,便不能检查评定到此主题素材。++

就算 key 已存在,则 SETNX 不做任何动作

使用 SETNX 实现

Redis 官方最初在 SETNX
命令页给了三个依照该命令的分布式锁达成。

1
Acquire lock: SETNX lock.foo <current Unix time + lock timeout + 1>
1
Release lock: DEL lock.foo
  1. 假如 SETNX 再次来到 1,则注解顾客端获取锁成功, lock.foo 被安装为使得
    Unix time。顾客端操作达成后调用 DEL 命令释放锁。

  2. 就算 SETNX 重返0,则注解锁已经被其它顾客端持有。那时候大家能够先回去或进行重试等对方达成或等待锁超时。

管理死锁难题:
上述算法中,若是具有锁的顾客端发生故障、意外崩溃、或许此外因素因素形成未有释放锁,该怎么消除?。大家得以经过锁的键对应的时辰戳来判别这种气象是还是不是产生了,假若当前的时间已经超(jīng chāo卡塔尔越lock.foo的值,说明该锁已失效,能够被另行利用。
发出这种气象时,可不得不难的通过DEL来删除锁,然后再SETNX三次,当多少个顾客端检验到锁超时后都会尝试去放活它,这里就大概现身八个竞态条件:

  1. C1 和 C2 读取 lock.foo 检查时间戳,前后相继发掘过期了。
  2. C1 发送DEL lock.foo。
  3. C1 发送SETNX lock.foo 而且成功了。
  4. C2 发送DEL lock.foo
  5. C2 发送SETNX lock.foo 而且成功了。
  6. ERROR: 由于竞态的标题,C1 和 C2 都拿走了锁,那下子难题大了。

恰恰的是,使用上边包车型客车算法能够制止这几个标题。大家看看客商端 C4 是怎么办的:

  1. C4 发送 SETNX lock.foo 想要获取锁。
  2. 只是由于爆发故障的客商端 C3 依然有着锁,所以回来 0 给 C4。
  3. C4 发送 GET lock.foo 来检查锁是或不是过期, 假使没超时,则等待或重试。
  4. 反过来讲,借使已经晚点, C4 则尝试进行上面包车型大巴授命来博取锁:

    1
    Acquire lock when time expired: GETSET lock.foo <current Unix timestamp + lock timeout + 1>
  5. 因此 GETSET ,C4 获得的时光戳要是照旧是逾期的,这就注脚 C4
    快心满意得到锁了。

  6. 万一在 C4 以前,有个叫 C5 的客户端比 C4 快一步施行了上边的操作,那么
    C4 获得的时光戳是个未超时的值,这个时候,C4
    未有按时收获锁,要求重新等待或重试。留意一下,即便 C4
    没得到锁,但它改写了 C5
    设置的锁的超时值,但是这一点一线的截断误差(平时境况下锁的享有的时辰非常的短,所以在该竞态下冒出的标称误差是能够忍受的)是可以忍受的。(Note
    that even if C4 set the key a bit a few seconds in the future this
    is not a problem
    )。

为了这么些锁的算法更康健一些,持有锁的客商端在解锁从前应当再自己商量一次和煦的锁是没有过期,再去做
DEL
操作,因为顾客端失利的缘故很复杂,不只有是崩溃也恐怕是因为有些耗费时间的操作而挂起,操作完的时候锁因为超时已经锁已经被人家得到,当时就不要解锁了。

周全的剖析这几个方案,大家就能够意识这里有二个尾巴:Release lock 使用的 DEL
命令不支持 CAS 删除(check-and-set,delete if current value equals old
value),在高并发动静下就能有一点点题目:承认持有的锁未有过期后实行DEL 释放锁,由于竞态的留存 Redis 服务器施行命令时锁只怕已过期( “真的”
刚巧过期只怕被别的客商端竞争锁时设置了多个很小的超时时间而招致过期)且被其余客商端持有。这种场馆下将会(违法)释放别的顾客端持有的锁。

斩草除根方案: 先分明锁未有过期,再经过 EVAL 命令(在 Redis 2.6
及以上版本提供) 在实践 Lua 脚本:先履行 GET
指令获取锁的日子戳,确认和和睦的日子戳一致后再施行 DEL 释放锁。

布署缺欠:

  1. 上述建设方案并不周详,只肃清了过期锁的获释难题,但是由于这么些方案本身的短处,客户端获取锁时产生竞争(C4
    改写 C5 时间戳的例证),那么 lock.foo 的 “时间戳”
    将与地面包车型客车不平等,那个时候不会实践 DEL
    命令,而是等待锁失效,那在高并发的景况下是无用的。
  2. 虚拟多服务器意况下,要求服务器进行时间一齐校准。

在我们的品种中采用了 tornadoredis
库,那些库达成的遍布式锁便接受了上述算法。可是在出狱锁时微微节制,可是并发量不高的事态下不会有太大的难题,详细的解析参照他事他说加以考察下述代码注释。实现代码如下所示:

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
class Lock(object):
    """
    A shared, distributed Lock that uses a Redis server to hold its state.
    This Lock can be shared across processes and/or machines. It works
    asynchronously and plays nice with the Tornado IOLoop.
    """

    LOCK_FOREVER = float(2 ** 31 + 1)  # 1 past max unix time

    def __init__(self, redis_client, lock_name, lock_ttl=None, polling_interval=0.1):
        """
        Create a new Lock object using the Redis key ``lock_name`` for
        state, that behaves like a threading.Lock.
        This method is synchronous, and returns immediately. It doesn't acquire the
        Lock or in fact trigger any sort of communications with the Redis server.
        This must be done using the Lock object itself.
        If specified, ``lock_ttl`` indicates the maximum life time for the lock.
        If none is specified, it will remain locked until release() is called.
        ``polling_interval`` indicates the time between acquire attempts (polling)
        when the lock is in blocking mode and another client is currently
        holding the lock.
        Note: If using ``lock_ttl``, you should make sure all the hosts
        that are running clients have their time synchronized with a network
        time service like ntp.
        """
        self.redis_client = redis_client
        self.lock_name = lock_name
        self.acquired_until = None
        self.lock_ttl = lock_ttl
        self.polling_interval = polling_interval
        if self.lock_ttl and self.polling_interval > self.lock_ttl:
            raise LockError("'polling_interval' must be less than 'lock_ttl'")

    @gen.engine
    def acquire(self, blocking=True, callback=None):
        """
        Acquire the lock.
        Returns True once the lock is acquired.
        If ``blocking`` is False, always return immediately. If the lock
        was acquired, return True, otherwise return False.
        Otherwise, block until the lock is acquired (or an error occurs).
        If ``callback`` is supplied, it is called with the result.
        """

        # Loop until we have a conclusive result
        while 1:

            # Get the current time
            unixtime = int(mod_time.time())

            # If the lock has a limited lifetime, create a timeout value
            if self.lock_ttl:
                timeout_at = unixtime + self.lock_ttl
            # Otherwise, set the timeout value at forever (dangerous)
            else:
                timeout_at = Lock.LOCK_FOREVER
            timeout_at = float(timeout_at)

            # Try and get the lock, setting the timeout value in the appropriate key,
            # but only if a previous value does not exist in Redis
            result = yield gen.Task(self.redis_client.setnx, self.lock_name, timeout_at)

            # If we managed to get the lock
            if result:

                # We successfully acquired the lock!
                self.acquired_until = timeout_at
                if callback:
                    callback(True)
                return

            # We didn't get the lock, another value is already there
            # Check to see if the current lock timeout value has already expired
            result = yield gen.Task(self.redis_client.get, self.lock_name)
            existing = float(result or 1)

            # Has it expired?
            if existing < unixtime:

                # The previous lock is expired. We attempt to overwrite it, getting the current value
                # in the server, just in case someone tried to get the lock at the same time
                result = yield gen.Task(self.redis_client.getset,
                                        self.lock_name,
                                        timeout_at)
                existing = float(result or 1)

                # If the value we read is older than our own current timestamp, we managed to get the
                # lock with no issues - the timeout has indeed expired
                if existing < unixtime:

                    # We successfully acquired the lock!
                    self.acquired_until = timeout_at
                    if callback:
                        callback(True)
                    return

                # However, if we got here, then the value read from the Redis server is newer than
                # our own current timestamp - meaning someone already got the lock before us.
                # We failed getting the lock.

            # If we are not signalled to block
            if not blocking:

                # We failed acquiring the lock...
                if callback:
                    callback(False)
                return

            # Otherwise, we "sleep" for an amount of time equal to the polling interval, after which
            # we will try getting the lock again.
            yield gen.Task(self.redis_client._io_loop.add_timeout,
                           self.redis_client._io_loop.time() + self.polling_interval)

    @gen.engine
    def release(self, callback=None):
        """
        Releases the already acquired lock.
        If ``callback`` is supplied, it is called with True when finished.
        """

        if self.acquired_until is None:
            raise ValueError("Cannot release an unlocked lock")

        # Get the current lock value
        result = yield gen.Task(self.redis_client.get, self.lock_name)
        existing = float(result or 1)

        # 从上下文代码中可以看出,在这个实现中,有一个限制:获取锁的时候设置的 lock_ttl 必须能够保证释放锁时,锁未过期。
        # 否则,当前锁过期后,将会非法释放其他客户端持有的锁。如果无法估计持有锁后代码的执行时间,则可以增加当前锁的过期检测,
        # 当 self.acquired_until <= int(mod_time.time()) 时不执行 DEL 命令。不过,这个限制在一般的应用中倒是可以满足,
        # 所以这个实现不会有太大的问题。
        # 由于 GET、DEL 之间的时间差,以及 DEL 命令发出到 执行 之间的时间差,高并发情况下,锁过期释放的问题依然存在,这个是
        # 算法缺陷。并发不大的情况下,问题不大。
        #
        # 注:这个条件判断 existing >= self.acquired_until 是有这样一个潜在的前提,使用锁的客户端代码正常运行的情况下,
        # 考虑到并发代码使用相同的 lock_ttl 获取锁,竞争失败的客户端将会把锁的过期时间设置的更长一些,这里的判断是有意义的。
        # If the lock time is in the future, delete the lock
        if existing >= self.acquired_until:
            yield gen.Task(self.redis_client.delete, self.lock_name)
        self.acquired_until = None

        # That is it.
        if callback:
            callback(True)

2、
循环诉求的话,假使有叁个收获了锁,此外的在去赢得锁的时候,是还是不是便于爆发抢锁的只怕?

2、 顾客端B也去恳求服务器设置key的值,假如回到退步,那么就表示加锁战败

正文首要给我们介绍了关于redis实现加锁的二种办法,分享出去供我们参谋学习,上边话没有多少说了,来协同会见详细的牵线吧。

redis能用的的加锁命令分表是INCLacrosse、SETNX、SET

4、 顾客端B在等候一段时间后在去哀告设置key的值,设置成功

前言

这种加锁的思路是, key 不设有,那么 key 的值会先被开端化为 0
,然后再进行 INCENVISION 操作进行加一。然后其它顾客在实行 INCPAJERO操作进行加不时,要是回去的数超越 1 ,表明那个锁正在被接收个中。

4. 第两种锁SET

4、 顾客端B在伺机一段时间后在去乞求的时候拿到key的值为1意味收获锁成功

 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);

5、 顾客端B施行代码实现,删除锁

 $redis-set($key, $value, array('nx', 'ex' = $ttl)); //ex表示秒

3、 客户端A试行代码完成,删除锁

1. redis加锁分拣

3. 次之种锁SETNX

6. 化解办法

5、 客商端B施行代码实现,删除锁