基于redis的分布式锁分析与实践


声明:本文转载自https://my.oschina.net/wnjustdoit/blog/1606215,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

前言:在分布式环境中,我们经常使用锁来进行并发控制,锁可分为乐观锁和悲观锁,基于数据库版本戳的实现是乐观锁,基于redis或zookeeper的实现可认为是悲观锁了。乐观锁和悲观锁最根本的区别在于线程之间是否相互阻塞。

那么,本文主要来讨论基于redis的分布式锁算法问题。

从2.6.12版本开始,redis为SET命令增加了一系列选项(set [key] NX/XX EX/PX [expiration]):

  • EX seconds – 设置键key的过期时间,单位时秒
  • PX milliseconds – 设置键key的过期时间,单位时毫秒
  • NX – 只有键key不存在的时候才会设置key的值
  • XX – 只有键key存在的时候才会设置key的值

原文地址:https://redis.io/commands/set

中文地址:http://redis.cn/commands/set.html

注意: 由于SET命令加上选项已经可以完全取代SETNXSETEXPSETEX的功能,所以在将来的版本中,redis可能会不推荐使用并且最终抛弃这几个命令。

(这里简单提一下,在旧版本的redis中(指2.6.12版本之前),使用redis实现分布式锁一般需要setNX、expire、getSet、del等命令。)

而在旧版本的redis中,redis的超时时间很难控制,用户迫切需要把setNX和expiration结合为一体的命令,把他们作为一个原子操作,这样新版本的多选项set命令诞生了。然而这并没有完全解决复杂的超时控制带来的问题。

接下来,我们的一切讨论都基于新版redis。

在这里,我先提出几个在实现redis分布式锁中需要考虑的关键问题

1、死锁问题;

1.1、为了防止死锁,redis至少需要设置一个超时时间;

1.2、由1.1引申出来,当锁自动释放了,但是程序并没有执行完毕,这时候其他线程又获取到锁执行同样的程序,可能会造成并发问题,这个问题我们需要考虑一下是否归属于分布式锁带来问题的范畴。

2、锁释放问题,这里会有两个问题;

2.1、每个获取redis锁的线程应该释放自己获取到的锁,而不是其他线程的,所以我们需要在每个线程获取锁的时候给锁做上不同的标记以示区分;

2.2、由2.1带来的问题是线程在释放锁的时候需要判断当前锁是否属于自己,如果属于自己才释放,这里涉及到逻辑判断语句,至少是两个操作在进行,那么我们需要考虑这两个操作要在一个原子内执行,否者在两个行为之间可能会有其他线程插入执行,导致程序紊乱。

3、更可靠的锁;

单实例的redis(这里指只有一个master节点)往往是不可靠的,虽然实现起来相对简单一些,但是会面临着宕机等不可用的场景,即使在主从复制的时候也显得并不可靠(因为redis的主从复制往往是异步的)。

关于Martin Kleppmann的Redlock的分析

原文地址:https://redis.io/topics/distlock

中文地址:http://redis.cn/topics/distlock.html

文章分析得出,这种算法只需具备3个特性就可以实现一个最低保障的分布式锁。

  1. 安全属性(Safety property): 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。
  2. 活性A(Liveness property A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取。
  3. 活性B(Liveness property B): 容错。 只要大部分Redis节点都活着,客户端就可以获取和释放锁.

我们来分析一下:

第一点安全属性意味着悲观锁(互斥锁)是我们做redis分布式锁的前提,否者将可能造成并发;

第二点表明为了避免死锁,我们需要设置锁超时时间,保持在一定的时间过后,锁可以重新被利用;

第三点是说对于客户端来说,获取锁和手动释放锁可以有更高的可靠性。

更进一步分析,结合上文提到的关键问题,这里可以引申出另外的两个问题:

1、怎么有效判断程序真正可以处理的时间范围?(这里有个时间偏移的问题)

2、redis Master节点宕机后恢复(可能还没有持久化到磁盘)、主从节点切换,(N/2)+1这里的N应该怎么动态计算更合理?

接下来再看,redis之父antirez对Redlock的评价

原文地址:http://antirez.com/news/101

文中主要提到了网络延迟和本地时钟的修改(不管是时间服务器或人为修改)对这种算法可能造成的影响。

最后,来点实践吧

I、传统的单实例redis分布式锁实现(关键步骤)

获取锁(含自动释放锁):

SET resource_name my_random_value NX PX 30000

 手动删除锁(Lua脚本):

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

 II、分布式环境的redis(多master节点)的分布式锁实现

为了保证在尽可能短的时间内获取到(N/2)+1个节点的锁,可以并行去获取各个节点的锁(当然,并行可能需要消耗更多的资源,因为串行只需要count到足够数量的锁就可以停止获取了);

另外,怎么动态实时统一获取redis master nodes需要更进一步去思考了。

具体实施未完待续。。

本文发表于2018年01月12日 17:21
(c)注:本文转载自https://my.oschina.net/wnjustdoit/blog/1606215,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 1966 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

这世界真好,吃野东西也要留出这条命来看看

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1