Redis奇幻之旅(三)7.分布式锁

7.分布式锁

7.1 单机Redis的分布式锁

​ 分布式锁的终极奥义就是在一个地方有一个唯一资源,当多个客户端过来时,谁先抢到这个资源,谁就获得了赢取其他公共资源的资格。常见的分布式锁可以拿这些技术来实现:redis、mysql、linux file、zookeeper。这些技术各有利弊,我就不在这分析了。

​ 用redis做分布式锁其实有三个阶段,也是我自己在开发过程中亲身经历过的。

  • 最早的版本

    redis2.8之前一般是用setnx命令和expire命令来实现分布式锁,但是这是两个命令,无论我们用pipeline还是redis事务,都解决不了它们是两条命令的事实,既然是两条命令,那么就有可能第一条执行成功,第二条执行失败。给个场景:客户端A连续发送了setnx和expire给redis,redis执行setnx成功并返回success,但是执行expire失败。客户端拿到setnx的回执继续往下执行代码,但是到某处报错了,导致代码中最后的释放锁未生效。这时,若无人工干预,这个分布式锁将无法被其他客户端获得,进入了死锁状态...

  • 中间的版本

    在redis2.8之后,set命令加入了nx和ex一起配置的功能,也就是说,上述的两个命令变成了一个命令。也就很好的解决了上述提到的死锁问题。不过这时还有其他的问题暴露出来。还是给个场景:当客户端A设置好了分布式锁并且设置了3秒的有效时间,但3秒过后A并未执行完代码,分布式锁被释放。这时客户端B获得了分布式锁,B还未执行完代码,但是A执行完了业务代码然后del了分布式锁。这时我们看见A把B的锁给删了...这里就存在了两个隐患,一是A未执行完的时候B拿到了锁,二是A把B的锁给删了。

  • 后来的版本

    上述的第一个问题只能通过合理的设置ex时间来规避。所以焦点就到了上述的第二个问题,如果我们在设置lock的时候将value设置成一个唯一值,当删除命令触发时,先进行value是否相等的判断,如果相等则删除,如果不相等则不执行删除,这样就解决了A把B的锁给删了的情况。不过获取value,比较value,然后执行del这些步骤并不是原子执行的,如果不是原子执行的就有可能发生资源线程安全问题。这时就想起了在redis中嵌套lua脚本,一个lua脚本中执行的命令对redis来说是原子的。

    这里给一个用Python2.7实现的分布式锁:

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-
    """
        使用lua脚本解决redis分布式锁存在的错误释放问题
    """
    import redis
    import time
    import hashlib
    
    
    class DistributedLock:
        def __init__(self, redis_instance, lock_key, value):
            """
            :param redis_instance:
            :param lock_key:
            :param value: A unique identifier
            """
            self.redis_instance = redis_instance
            self.lock_key = lock_key
            self.value = value
    
        def set_lock(self, ex=3):
            """
            :param ex: sets an expire flag on key ``name`` for ``ex`` seconds.
            :return: True -> success or None -> fail
            """
            return self.redis_instance.set(self.lock_key, self.value, ex=ex, nx=True)
    
        def del_lock(self):
            """
            :return: 1 -> success or 0 -> fail
            """
            try:
                return self.redis_instance.evalsha(self.script_sha1_str(), 1, self.lock_key, self.value)
            except redis.exceptions.NoScriptError:
                return self.redis_instance.eval(self.lua_script(), 1, self.lock_key, self.value)
    
        @staticmethod
        def lua_script():
            lua_str = """
                if redis.call("GET",KEYS[1]) == ARGV[1] then
                    return redis.call("DEL",KEYS[1])
                else
                    return 0
                end
            """
            return lua_str
    
        def load_script(self):
            return self.redis_instance.script_load(self.lua_script())
    
        def script_sha1_str(self):
            script_sha1 = hashlib.sha1(self.lua_script())
            return script_sha1.hexdigest()
    
        def is_script_exist(self):
            """
            :return: True or False
            """
            return self.redis_instance.script_exists(self.script_sha1_str())[0]
    
        def flush_script(self):
            return self.redis_instance.script_flush()
    
        @classmethod
        def unique_value(cls):
            return int(time.time() * 1000)
    
    
    if __name__ == '__main__':
    
        def redis_cache():
          connection_pool = redis.ConnectionPool(
            host='127.0.0.1',
            port='6379',
            db=0,
          )
          return redis.Redis(connection_pool=connection_pool)
    
        distributed_lock = DistributedLock(
          redis_instance=redis_cache(),
          lock_key='distributed_lock',
          value=DistributedLock.unique_value()
        )
        print(distributed_lock.set_lock(ex=30))
        print(distributed_lock.del_lock())
    
    

7.2 Redlock

贴一个官网地址:Distributed locks with Redis

​ 其原理大致就是给定两个时间,一个是设置锁的超时时间,一个是分布式锁的超时时间。前者用来在多台机器上设置锁,当设置时间超出,那么这台机器就算失败,后者就是我们设置释放分布式锁的超时时间。当过半的机器设置成功,那么这个分布式锁就算成功设置上了,它的超时时间为用户设置的超时时间减去在每台机器上设置所花的时间。

详细的说明(tielei公众号的文章):

  1. 获取当前时间(毫秒数)。
  2. 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。
  3. 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>= N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。
  4. 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。
  5. 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。

当然,上面描述的只是获取锁的过程,而释放锁的过程比较简单:客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。

由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作,因此理论上它的可用性更高。我们前面讨论的单Redis节点的分布式锁在failover的时候锁失效的问题,在Redlock中不存在了,但如果有节点发生崩溃重启,还是会对锁的安全性有影响的。具体的影响程度跟Redis对数据的持久化程度有关。

假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:

  1. 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
  2. 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。
  3. 节点C重启后,客户端2锁住了C, D, E,获取锁成功。

这样,客户端1和客户端2同时获得了锁(针对同一资源)。

​ 在默认情况下,Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync),因此最坏情况下可能丢失1秒的数据。为了尽可能不丢数据,Redis允许设置成每次修改数据都进行fsync,但这会降低性能。当然,即使执行了fsync也仍然有可能丢失数据(这取决于系统而不是Redis的实现)。所以,上面分析的由于节点重启引发的锁失效问题,总是有可能出现的。为了应对这一问题,antirez又提出了延迟重启(delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。

​ 关于Redlock还有一点细节值得拿出来分析一下:在最后释放锁的时候,antirez在算法描述中特别强调,客户端应该向所有Redis节点发起释放锁的操作。也就是说,即使当时向某个节点获取锁没有成功,在释放锁的时候也不应该漏掉这个节点。这是为什么呢?设想这样一种情况,客户端发给某个Redis节点的获取锁的请求成功到达了该Redis节点,这个节点也成功执行了SET操作,但是它返回给客户端的响应包却丢失了。这在客户端看来,获取锁的请求由于超时而失败了,但在Redis这边看来,加锁已经成功了。因此,释放锁的时候,客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。实际上,这种情况在异步通信模型中是有可能发生的:客户端向服务器通信是正常的,但反方向却是有问题的。

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

推荐阅读更多精彩内容