使用redis实现分布式锁

背景

在类似秒杀这样的并发场景下,为了确保同一时刻只能允许一个用户访问资源,需要利用加锁的机制控制资源的访问权。如果服务只在单台机器上运行,可以简单地用一个内存变量进行控制。而在多台机器的系统上,则需要用分布式锁的机制进行并发控制。基于redis的一些特性,利用redis可以既方便又高效地模拟锁的实现。

一个简单方案

让我们先从一个简单的实现说起,这里用到了redis的两个命令,SETNXEXPIRE。如果lock_key不存在,那么就设置lock_key的值为1,并且设置过期时间;如果lock_key存在,说明已经有人在使用这把锁,访问失败。

def acquire_lock(lock_key, expire_timeout=60):
    if redis.setnx(lock_key, 1):
        redis.expire(lock_key, expire_timeout)
        return True
    return False

逻辑上看似乎没有问题,但是考虑一下异常情况:如果setnx设置成功,但expire由于某些原因(比如超时)操作失败,那么这把锁就永远存在了,也就是所谓的死锁,后面的人永远无法访问这个资源。

利用时间戳取值的方案

为了解决死锁,我们可以利用setnx的value来做文章。上例中的我们设的value是1,其实并没有派上用场。因此可以考虑将value设为当前时间加上expire_timeout,当setnx设置失败后,我们去读lock_key的value,并且和当前时间作比对,如果当前时间大于value,那么资源理当被释放。代码示例如下:

def acquire_lock(lock_key, expire_timeout=60):
    expire_time = int(time.time()) + expire_timeout
    if redis.setnx(lock_key, expire_time):
        redis.expire(lock_key, expire_timeout)
        return True
    redis_value = redis.get(lock_key)
    if redis_value and int(time.time()) > int(redis_value):
        redis.delete(lock_key)
    return False

然而仔细推敲下这段代码仍然能发现一些问题。第一,这个方案依赖时间,如果在分布式系统中的时间没有同步,则会对方案产生一定偏差。第二,假设C1和C2都没拿到锁,它们都去读value并对比时间,在竞态条件(race condition)下可能产生如下的时序:C1删除lock_key,C1获得锁,C2删除lock_key,C2获得锁。这样C1和C2同时拿到了锁,显然是不对的。

改进后的方案

幸运的是,redis里还有一个指令可以帮助我们解决这个问题。GETSET指令在set新值的同时会返回老的值,这样的话我们可以检查返回的值,如果该值和之前读出来的值相同,那么这次操作有效,反之则无效。代码示例如下:

def acquire_lock(lock_key, expire_timeout=60):
    expire_time = int(time.time()) + expire_timeout
    if redis.setnx(lock_key, expire_time):
        redis.expire(lock_key, expire_timeout)
        return True
    redis_value = redis.get(lock_key)
    if redis_value and int(time.time()) > int(redis_value):
        expire_time = int(time.time()) + expire_timeout
        old_value = redis.getset(lock_key, expire_time)
        if int(old_value) == int(redis_value):
            return True
    return False

这个方案基本可以满足要求,除了有一个小瑕疵,由于getset会去修改value,在竞态条件下可能会被修改多次导致timeout有细微的误差,但这个对结果影响不大。

最终方案

以上方案实现起来略显繁琐,但从redis 2.6.12版本开始有一个更为简便的方法。我们可以使用SET指令的扩展 ** SET key value [EX seconds] [PX milliseconds] [NX|XX] **,这个指令相当于对SETNX和EXPIRES进行了合并,因而我们的算法可以简化为如下一行:

def acquire_lock(lock_key, expire_timeout=60):
    ret = redis.set(lock_key, int(time.time()), nx=True, ex=expire_timeout):
    return ret

总结

在redis 2.6.12版本之后我们可以用一个简单的SET命令实现分布式锁,而在此版本之前则需要将SETNX和GETSET配合使用一个较为繁琐的方案。简化后的方案对于开发者来说当然是好事,但通过学习这一演变过程我们会对问题有更深刻的印象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容