使用Redis实现分布式锁

112 views
Skip to first unread message

jeff

unread,
Jul 13, 2011, 9:30:46 AM7/13/11
to guangzhou-...@googlegroups.com

一边翻译手册一边创作的一篇关于Redis的学习文档,发上来与大家分享:

============================================

Redis有一系列的命令,特点是以NX结尾,NX是Not eXists的缩写,如SETNX命令就应该理解为:SET if Not eXists。这系列的命令非常有用,这里讲使用SETNX来实现分布式锁。

用SETNX实现分布式锁

利用SETNX非常简单地实现分布式锁。例如:某客户端要获得一个名字foo的锁,客户端使用下面的命令进行获取:

SETNX lock.foo <current Unix time + lock timeout + 1>

  •  如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁。
  •  如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。

解决死锁

上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。

发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次,当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:

  1.  C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。
  2.  C1 发送DEL lock.foo
  3.  C1 发送SETNX lock.foo 并且成功了。
  4.  C2 发送DEL lock.foo
  5.  C2 发送SETNX lock.foo 并且成功了。

这样一来,C1,C2都拿到了锁!问题大了!

幸好这种问题是可以避免D,让我们来看看C3这个客户端是怎样做的:

  1. C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0
  2. C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。
  3. 反之,如果已超时,C3通过下面的操作来尝试获得锁:
    GETSET lock.foo <current Unix time + lock timeout + 1>
  4. 通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。
  5. 如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。

注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。

示例伪代码

根据上面的代码,我写了一小段Fake代码来描述使用分布式锁的全过程:

  1. # get lock
  2. lock = 0
  3. while lock !1:
  4.     timestamp = current Unix time + lock timeout + 1
  5.     lock = SETNX lock.foo timestamp
  6.     if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)):
  7.         break;
  8.     else:
  9.         sleep(10ms)
  10.  
  11. # do your job
  12. do_job()
  13.  
  14. # release
  15. if now() < GET lock.foo:
  16.     DEL lock.foo

是的,要想这段逻辑可以重用,使用python的你马上就想到了Decorator,而用Java的你是不是也想到了那谁?AOP + annotation?行,怎样舒服怎样用吧,别重复代码就行。


--
风路过的时候,没能吹走,这座城市太厚的灰尘
多少次的雨水,从来没有,冲掉你那沉重的忧伤
珠三角技术沙龙: http://techparty.org

Tim

unread,
Jul 13, 2011, 10:37:25 AM7/13/11
to guangzhou-...@googlegroups.com
这个不是Redis独有的优势,memcached 也可以的,http://timyang.net/programming/memcache-mutex/

2011/7/13 jeff <je...@fallever.com>

--
您收到此信息是由于您订阅了 珠三角技术沙龙(http://techparty.org) 论坛。
要在此论坛发帖,请发电子邮件到 guangzhou-...@googlegroups.com
要退订此论坛,请发邮件至
guangzhou-tech-p...@googlegroups.com
更多选项,请通过
http://groups.google.com/group/guangzhou-tech-party?hl=zh-CN 访问该论坛

jeff

unread,
Jul 13, 2011, 10:57:37 AM7/13/11
to guangzhou-...@googlegroups.com
是的,这的确并不是Redis独有,实际上通过锁定一个文件也可以达到同样的目的。

不过相比之下,Redis由于提供了非常丰富和强大的原子操作,使用用它来达到这个目的时可以写少很多代码(代码少的好处大家都懂的)。正式的代码的量与文中的伪代码量基本相等的。

2011/7/13 Tim <iso...@gmail.com>

Yongchao Lao

unread,
Jul 13, 2011, 12:51:14 PM7/13/11
to guangzhou-...@googlegroups.com

自己检查超市和自己del并不是原子操作吧,这是有问题的呀。

On Jul 13, 2011 9:30 PM, "jeff" <je...@fallever.com> wrote:
> 一边翻译手册一边创作的一篇关于Redis的学习文档,发上来与大家分享:
>
> ============================================
>
> Redis有一系列的命令,特点是以NX结尾,NX是Not eXists的缩写,如SETNX命令就应该理解为:SET if Not
> eXists。这系列的命令非常有用,这里讲使用SETNX来实现分布式锁。
> 用SETNX实现分布式锁
>
> 利用SETNX非常简单地实现分布式锁。例如:某客户端要获得一个名字foo的锁,客户端使用下面的命令进行获取:
>
> SETNX lock.foo <current Unix time + lock timeout + 1>
>
> - 如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁。
> - 如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。

>
> 解决死锁
>
> 上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。
>
> 发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次,当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:
>
> 1. C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。
> 2. C1 发送DEL lock.foo
> 3. C1 发送SETNX lock.foo 并且成功了。
> 4. C2 发送DEL lock.foo
> 5. C2 发送SETNX lock.foo 并且成功了。

>
> 这样一来,C1,C2都拿到了锁!问题大了!
>
> 幸好这种问题是可以避免D,让我们来看看C3这个客户端是怎样做的:
>
> 1. C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0
> 2. C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。
> 3. 反之,如果已超时,C3通过下面的操作来尝试获得锁:

> GETSET lock.foo <current Unix time + lock timeout + 1>
> 4. 通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。
> 5.

> 如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。
>
> *注意:*

> 为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。
> 示例伪代码
>
> 根据上面的代码,我写了一小段Fake代码来描述使用分布式锁的全过程:
>
> 1. # get lock
> 2. lock = 0
> 3. while lock != 1:
> 4. timestamp = current Unix time + lock timeout + 1
> 5. lock = SETNX lock.foo timestamp
> 6. if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.
> foo timestamp)):
> 7. break;
> 8. else:
> 9. sleep(10ms)
> 10.
> 11. # do your job
> 12. do_job()
> 13.
> 14. # release
> 15. if now() < GET lock.foo:
> 16. DEL lock.foo

>
> 是的,要想这段逻辑可以重用,使用python的你马上就想到了Decorator,而用Java的你是不是也想到了那谁?AOP +
> annotation?行,怎样舒服怎样用吧,别重复代码就行。
>
> --
> 风路过的时候,没能吹走,这座城市太厚的灰尘
> 多少次的雨水,从来没有,冲掉你那沉重的忧伤
>
> 我的妙想奇境:http://jeffkit.info
> twitter: http://twitter.com/jeff_kit
> 微勃:http://t.sina.com.cn/jeffjie
> github: http://github.com/jeffkit
> 珠三角技术沙龙: http://techparty.org
>

jeff

unread,
Jul 13, 2011, 9:50:56 PM7/13/11
to guangzhou-...@googlegroups.com
DEL语句不一定被执行,要根据情况:
- 如果未超时,说明当前的客户端是持有锁的,进行DEL操作是在锁内执行的,所以可以看作是原子的。
- 如果已超时,DEL语句已不会被执行了,没有两步操作,所以也不存在原子性的要求了。

2011/7/14 Yongchao Lao <flu...@gmail.com>

B.Tag

unread,
Jul 13, 2011, 11:21:06 AM7/13/11
to guangzhou-...@googlegroups.com
有 有关mongodb的中文 或更多相关的文章 么!~~~

在 2011年7月13日 下午11:20,B.Tag <bb....@gmail.com>写道:
new-school 的东西  这个  好文章!!谢谢分享!~



--
-------------------------------------------------------------------------
        learn log: http://me.boolsir.com 
        my life log: http://www.boolsir.com
-------------------------------------------------------------------------




--
-------------------------------------------------------------------------
        learn log: http://me.boolsir.com 
        my life log: http://www.boolsir.com
-------------------------------------------------------------------------

B.Tag

unread,
Jul 13, 2011, 11:20:25 AM7/13/11
to guangzhou-...@googlegroups.com
new-school 的东西  这个  好文章!!谢谢分享!~

在 2011年7月13日 下午10:57,jeff <je...@fallever.com>写道:



--

jeff

unread,
Jul 13, 2011, 10:04:05 PM7/13/11
to guangzhou-...@googlegroups.com


2011/7/14 jeff <je...@fallever.com>

DEL语句不一定被执行,要根据情况:
- 如果未超时,说明当前的客户端是持有锁的,进行DEL操作是在锁内执行的,所以可以看作是原子的。

这也是为什么要设置lock.foo的值是“ 当前时间 + 超时时间 + 1”的原因,最后那个1(可以设再大一点)的时间是用来保证检查和DEL操作是在同一个锁里面的。这是我觉得非常巧妙的一个地方。
 
也就是说,哪怕客户端在检查的那一刻刚好是超时的最后一MS,它还有1(或N)MS的时间来排他地执行DEL操作。

Zoom.Quiet

unread,
Jul 13, 2011, 10:45:30 PM7/13/11
to guangzhou-...@googlegroups.com
既然是转发也应该给出自个儿的URI 哪
http://www.jeffkit.info/2011/07/1000/

在 2011年7月13日 下午10:57,jeff <je...@fallever.com> 写道:

> 是的,这的确并不是Redis独有,实际上通过锁定一个文件也可以达到同样的目的。
> 不过相比之下,Redis由于提供了非常丰富和强大的原子操作,使用用它来达到这个目的时可以写少很多代码(代码少的好处大家都懂的)。正式的代码的量与文中的伪代码量基本相等的。
> 2011/7/13 Tim <iso...@gmail.com>
>>
>> 这个不是Redis独有的优势,memcached
>> 也可以的,http://timyang.net/programming/memcache-mutex/

俺的直觉问题:
这样只能作同一主机的不同进程/线程的锁吧?
Redis 自个儿不是分布式的哪,这怎么分布式锁呢?
jeff 说到:
2011-07-13 @ 10:11 下午
我所定义的分布式锁是指支持分布式应用(多个进程,多个主机)共享的锁。而不是说锁本身是分布式的。
锁本身若是分布式,那它的可靠性也就值得商榷了。。
我使用Redis这个机制实现锁,就是为了可以让程序可以实现分布式部署啊。

对于 redis 使用方式的挖掘,无论如何是得肯定的,
- 俺的疑问也是从实际部署出发的:
- 基于这一自身不分布的"分布式锁"的其它应用,是否可以分布式部署在不同主机
- 然后通过 http 接口来共同调度一些业务?
- 如果是这样,和 "RestMQ"
http://www.restmq.com/
有什么差别?

>> 2011/7/13 jeff <je...@fallever.com>
>>>
>>> 一边翻译手册一边创作的一篇关于Redis的学习文档,发上来与大家分享:
>>>

...

--
人生苦短, Pythonic! 冗余不做,日子甭过!备份不做,十恶不赦!
俺: http://about.me/zoom.quiet
哲: http://www.zeuux.org/home/zoomquiet
豆: http://www.douban.com/group/zoomquiet
书: http://code.google.com/p/openbookproject
营: http://code.google.com/p/kcpycamp/wiki/PythoniCamp
文字协议: http://creativecommons.org/licenses/by-sa/2.5/cn/

jeff

unread,
Jul 13, 2011, 10:56:57 PM7/13/11
to guangzhou-...@googlegroups.com


2011/7/14 Zoom.Quiet <zoom....@gmail.com>
既然是转发也应该给出自个儿的URI 哪
   http://www.jeffkit.info/2011/07/1000/

谢谢大妈!
 


在 2011年7月13日 下午10:57,jeff <je...@fallever.com> 写道:
> 是的,这的确并不是Redis独有,实际上通过锁定一个文件也可以达到同样的目的。
> 不过相比之下,Redis由于提供了非常丰富和强大的原子操作,使用用它来达到这个目的时可以写少很多代码(代码少的好处大家都懂的)。正式的代码的量与文中的伪代码量基本相等的。
> 2011/7/13 Tim <iso...@gmail.com>
>>
>> 这个不是Redis独有的优势,memcached
>> 也可以的,http://timyang.net/programming/memcache-mutex/

俺的直觉问题:
   这样只能作同一主机的不同进程/线程的锁吧?
   Redis 自个儿不是分布式的哪,这怎么分布式锁呢?
jeff 说到:
2011-07-13 @ 10:11 下午
     我所定义的分布式锁是指支持分布式应用(多个进程,多个主机)共享的锁。而不是说锁本身是分布式的。
     锁本身若是分布式,那它的可靠性也就值得商榷了。。
     我使用Redis这个机制实现锁,就是为了可以让程序可以实现分布式部署啊。

再补上随后回复的一条:

 我理解的分布式锁中的锁是指存在应用进程外的锁,传统的锁是虚拟机(或进程)级别的锁,是活在某个进程内的,使用虚拟机级别的锁的程序是没办法或很难大规模化和实现分布式部署的。

对于 redis 使用方式的挖掘,无论如何是得肯定的,
- 俺的疑问也是从实际部署出发的:
   - 基于这一自身不分布的"分布式锁"的其它应用,是否可以分布式部署在不同主机

绝对可以呀,把某台Redis当作一个锁服务器来理解就OK了。上面例子讲的C0-C4都可以理解为位于不同主机的Redis客户端。
 
   - 然后通过 http 接口来共同调度一些业务?

通过Http接口来调度业务,似乎与“锁”的使用场景没太大关系?
 
   - 如果是这样,和 "RestMQ"
       http://www.restmq.com/
       有什么差别?

RestMQ提供的是Rest化的消息队列方案,有点像HttpSQS了,但这是队列,从队列的几种使用模式上来讲都不能用来当锁啊。我认为这是两个不同的概念了哇。

 

>> 2011/7/13 jeff <je...@fallever.com>
>>>
>>> 一边翻译手册一边创作的一篇关于Redis的学习文档,发上来与大家分享:
>>>

...

--
人生苦短, Pythonic! 冗余不做,日子甭过!备份不做,十恶不赦!
俺: http://about.me/zoom.quiet
哲: http://www.zeuux.org/home/zoomquiet
豆: http://www.douban.com/group/zoomquiet
书: http://code.google.com/p/openbookproject
营: http://code.google.com/p/kcpycamp/wiki/PythoniCamp
文字协议: http://creativecommons.org/licenses/by-sa/2.5/cn/
--
您收到此信息是由于您订阅了 珠三角技术沙龙(http://techparty.org) 论坛。
要在此论坛发帖,请发电子邮件到 guangzhou-...@googlegroups.com
要退订此论坛,请发邮件至
guangzhou-tech-p...@googlegroups.com
更多选项,请通过
http://groups.google.com/group/guangzhou-tech-party?hl=zh-CN 访问该论坛

Zoom.Quiet

unread,
Jul 13, 2011, 11:11:25 PM7/13/11
to guangzhou-...@googlegroups.com

- 有点明白了,俺想差了
- Jeff 这是要直接增强 redis 的内存数据管理策略
+ 通过分布式的原生锁调度服务
+ 来直接对分布式的同类数据缓存的 redis 实例进行协同
+ 利用 redis 自身,完成 memcache 天然的分布式缓存機能
是也乎?
这的确很好用哪,省得 app 自个儿管理怎么同步/清理神马的了...
期待正式插件释放,以及教程文档...

>>
>>    - 然后通过 http 接口来共同调度一些业务?
>
> 通过Http接口来调度业务,似乎与“锁”的使用场景没太大关系?
>
>>
>>    - 如果是这样,和 "RestMQ"
>>        http://www.restmq.com/
>>        有什么差别?
>
> RestMQ提供的是Rest化的消息队列方案,有点像HttpSQS了,但这是队列,从队列的几种使用模式上来讲都不能用来当锁啊。我认为这是两个不同的概念了哇。

...

jeff

unread,
Jul 13, 2011, 11:29:21 PM7/13/11
to guangzhou-...@googlegroups.com


2011/7/14 Zoom.Quiet <zoom....@gmail.com>
呵,Redis对有着一堆APP实例的应用而言,它本身就是一个分布式的缓存了。我只是把原来进程内的锁移出来,存在redis里,所以就变成了分布式锁。

原生的锁,使用编程语言来操作起来,那是相当的麻烦,吃力之余又不讨好──你的程序还要只能够单机运行。

分布式锁,以往很多人通用锁定文件来实现,但这又会引入一堆解决死锁的定时器啊之类的程序,低效并复杂。

使用redis来管理的锁,借助于Redis的特性,使锁的获取、共享、死锁解决等都得简单地得到解决。更重要的,你的应用程序可以由原来的一个复杂的拆成多个简单的,甚至可以部署任意多个实例来跑了。

这里并没有什么插件啊,所有的内容就只有伪代码里面那么一点东西。Redis的威力啊。
 

>>
>>    - 然后通过 http 接口来共同调度一些业务?
>
> 通过Http接口来调度业务,似乎与"锁"的使用场景没太大关系?
>
>>
>>    - 如果是这样,和 "RestMQ"
>>        http://www.restmq.com/
>>        有什么差别?
>
> RestMQ提供的是Rest化的消息队列方案,有点像HttpSQS了,但这是队列,从队列的几种使用模式上来讲都不能用来当锁啊。我认为这是两个不同的概念了哇。

...
--
--
您收到此信息是由于您订阅了 珠三角技术沙龙(http://techparty.org) 论坛。
要在此论坛发帖,请发电子邮件到 guangzhou-...@googlegroups.com
要退订此论坛,请发邮件至
guangzhou-tech-p...@googlegroups.com
更多选项,请通过
http://groups.google.com/group/guangzhou-tech-party?hl=zh-CN 访问该论坛

Yongchao Lao

unread,
Jul 14, 2011, 10:10:47 PM7/14/11
to guangzhou-...@googlegroups.com
手机上面的消息竟然没有发出,难道是要改host才能访问了?

我在这里重新说一遍吧,check_expiration,和del不是原子操作。如果没有CAD操作的话,那可能出现如下的情况:

1. 我自己检查超时,确定没超时。
  a. 别人来了,发现你超时了,还没释放,那清掉。加上自己的锁。
2. 我自己del锁foo
  b. 这下好了,别人在做事情,不知道自己的锁被删除了,同时有另外一个过来拿锁,成功。

这是你预想的结果么?

我觉得这是关键的问题。不过不清楚Jeff兄的实际应用是什么,可否讲下?

2011/7/14 jeff <je...@fallever.com>

Yongchao Lao

unread,
Jul 14, 2011, 10:28:51 PM7/14/11
to guangzhou-...@googlegroups.com

坚持和删除不是原子的呀,没有一个checkanddel原子操作。

另外,我看你后面提及时间设置长一些,更加保险 但是完美的解决方案是不依赖多一点时间来保证什么的呀。

如果没有cad操作,那c的时候没超市,试图去删除,但实际上 锁被别人取了,然后你删到的是别人的锁哦。

from my phone.

jeff

unread,
Jul 14, 2011, 10:44:16 PM7/14/11
to guangzhou-...@googlegroups.com


2011/7/15 Yongchao Lao <flu...@gmail.com>

手机上面的消息竟然没有发出,难道是要改host才能访问了?

我在这里重新说一遍吧,check_expiration,和del不是原子操作。如果没有CAD操作的话,那可能出现如下的情况:

1. 我自己检查超时,确定没超时。
  a. 别人来了,发现你超时了,还没释放,那清掉。加上自己的锁。
2. 我自己del锁foo
  b. 这下好了,别人在做事情,不知道自己的锁被删除了,同时有另外一个过来拿锁,成功。 

这是你预想的结果么?

不是的,Check_expration和Del的确不是一个原子操作,但是通过一些技巧可以保证这两句都在锁定过程中执行的,也就是说,你上面说的1,2之间不会出现a。
技巧就是设置给foo的超时时间为:<当前时间 + 超时时间 + 一定的容忍时间>

我这样解释一下,我们假定一条进程拿到foo锁后,会有三个时刻,分别是:

----锁定开始------

------逻辑锁超时------

-----foo超时--------

从”锁定开始“到”Foo超时“这段时间内,都是该进程独占了foo锁,可把在这段时间做的操作都视为原子操作。这个能理解吧?如果没问题的话,那么也就是说在这段时间内进程进行Check_expration和Del操作也是不会受到其他进程的干扰的。

Check_expration是检查时间是否到了”------逻辑锁超时------“这个时刻
- 如果没有到这个时刻(或刚好到这个时刻),它仍然有一段时间(从“------逻辑锁超时------”到“-----foo超时--------”)独占式地进行Del操作
- 如果超过了这个时刻(进入了”------逻辑锁超时------“与“-----foo超时--------”时段),说明它自己的操作已超时,它就不再进行Del操作了,把锁让给其他进程来解。
- 其他进程是在“-----foo超时--------”的时刻过去后才有机会取得锁的。

所以绝大多数的情况下不会出现永超兄上面的假设,但不排除极端的情况就是由于特殊的原因(例如突然网掉断开),Check超时时间到Del操作之间的时间被拉得很长,这时就会出现上面的情况。

嗯,可以说这个方案不是完美的方案,但能满足大多数使用独占锁的场景了。

不知道我这样讲是否表达清楚了。
 

我觉得这是关键的问题。不过不清楚Jeff兄的实际应用是什么,可否讲下?

实际应用就是比较常见的多进程并发读写某个资源时,需要加锁的场景。当然,我这里只实现了排他锁,还没细到读锁写锁的级别,比较粗糙。
Reply all
Reply to author
Forward
0 new messages