Redis实现分布式锁

·  阅读 806

鲁班锁

相信很多小伙伴面试时都被问过“Redis怎么实现分布式锁?”,今天我们就来说一下Redis实现分布式锁。

分布式锁

在计算机科学中,锁是在执行多线程时用于强行限制资源访问的同步机制,即用于在并发控制中保证对互斥要求的满足。

分布式锁需要满足三个要求:

  • 互斥性,在同一时刻,只能有一个客户端获得锁
  • 无死锁,即使客户端崩溃等其他因素,锁最终都会被释放
  • 容错能力,只要大部分节点都可用,客户端就能正常获得和释放锁

Redis分布式锁

获得锁

当我们需要对某个资源加锁时,只需要在Redis设置一个变量,将该资源的名称作为key,一个唯一值作为value即可,

getLock(resourceName, myRandomValue) {
    val = redis.get(resourceName)
    if (!val) {
        redis.set(resourceName, myRandomValue)
        redis.expire(resourceName, 1s)
        return true
    }
    
    return false
}
复制代码

大致的逻辑是这样的。

SETNX(错误做法)

又因为上述逻辑中:

val = redis.get(resourceName)
if (!val) {
    redis.set(resourceName, myRandomValue)
    ...
}
复制代码

这段逻辑可以使用Redis的setnx命令代替,

SETNX key value

Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed. SETNX is short for "SET if Not eXists".

所以网上有不是文章直接这样写:

getLock(resourceName, myRandomValue) {
    result = redis.setnx(resourceName, myRandomValue)
    if (result) {
        redis.expire(resourceName, 1s)
        return true
    }
    
    return false
}
复制代码

这种做法是错误的。因为这个操作不是原子性的,如果客户端还没来得及执行expire命令,就宕机了,这时,这个锁就变成了死锁,违背了上面说的第二条规则。

Lua脚本(不推荐)

使用Lua脚本执行setnxexpire命令,使其变成原子性操作

if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then 
    redis.call('expire',KEYS[1],ARGV[2]) 
    return 1 
else 
    return 0 
end
复制代码

这样虽然能解决问题,但是不推荐,因为将问题复杂化了。

SET key value [EX seconds|PX milliseconds] [NX|XX] 命令(推荐做法)

  • EX seconds -- 设置过期时间,单位秒.
  • PX milliseconds -- 设置过期时间, 单位毫秒.
  • NX -- 当key不存在时,设置值.
  • XX -- 当key存在时,设置值.

所以获得锁时,我们只需要:

getLock(resourceName, myRandomValue) {
    return "OK" == redis.command(SET resourceName myRandomValue NX PX 1000)
}
复制代码

锁释放

释放锁时不能直接del,直接这样的话,可能会把别人的锁删除了,所以需要进行身份识别,怎么识别这个锁是否属于你的呢?这就需要通过我们之前设置的myRandomValue来识别了。

releaseLock(resourceName, myRandomValue) {
    val = redis.get(resourceName)
    if (val == myRandomValue) {
        redis.del(resourceName)
    }
}
复制代码

还是那个问题,整个操作不是原子操作

releaseLock(resourceName, myRandomValue) {
    val = redis.get(resourceName)
    if (val == myRandomValue) {
        // 如果这时resourceName刚好过期
        // 可能把别人的锁删除了
        redis.del(resourceName)
    }
}
复制代码

这时,我们就需要借助Lua脚本了

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

分布式锁

上面说的一堆,都是基于单个Redis实例,分布式锁的实现也是基于上面逻辑实现。获取分布式锁时,需要下列几步:

1、获取当前时间,使用毫秒级时间

2、使用相同的键名和随机值,尝试从所有N个实例中顺序获取锁。在每个实例中设置锁时,客户端使用的超时时间需小于总锁自动释放时间,例如,如果自动释放时间为10秒,则超时时间可能在5到50毫秒之间。 这样可以防止客户端长时间与处于故障状态的Redis节点进行通信:如果某个实例不可用,我们应该尽快尝试与下一个实例进行通信。

3、再次获取当前毫秒级时间,减去在步骤1中获得的时间戳,来计算获取锁所花费的时间。当且仅当客户端能够在大多数实例(至少N/2+1)中获取锁,并且获取锁所花费的总时间小于锁有效时间时,则认为已获取锁。

4、如果获取了锁,则将其有效时间视为初始有效时间减去步骤3中计算的花费时间,即有效时间=初始有效时间-花费时间。

5、如果客户端由于某种原因(如无法锁定N/2+1个实例)而未能获得该锁,必须解锁所有实例。

当客户端无法获取锁时,应在随机延迟后重试,防止其他客户端也在尝试获取锁。 同样,客户端在大多数Redis实例中尝试获取锁的速度越快,失败的情况就越低,因此,客户端应尽快将SET命令发送到所有实例。

这就是Redlock算法,是不是觉得很麻烦?!不用担心,伟大的开源社区已经有很多实现方案了,实际工作中直接拿来用就行了。

后记

使用Redlock算法时,需要注意,当义务逻辑耗时较长时,超出了Redis设置的过期时间,导致锁自动释放掉了。所以这时需要我们自己延长锁的时间,需要延长多久呢?这些你只需要知道就行了,很多开源的实现方案中都已经考虑到了,所以在选择开源包时,需要注意一下是否实现了这些。

关于Redlock算法,Martin Kleppmann大神提出了好几点异议,Martin Kleppmann何许人也?

他的这本书相信很多人应该听过吧?!!

然后Redis的作者针对Martin Kleppmann的几点异议,做出了反驳,双方都有理有据,堪称神仙打架。有兴趣的同学可以看一下。

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改