抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

分布式锁的实现

分布式锁应具备以下特点:

  • 互斥:在任意时刻,锁最多只能被同一个客户端(进程所持有)
  • 防死锁:避免死锁情况,当一个客户端在持有锁期间内,由于意外崩溃而导致锁未能主动解锁,其持有的锁也能够被正确释放,并保证后续其它客户端也能正常加锁
  • 高可用:分布式锁需要有一定的高可用能力,当提供锁的服务节点故障(宕机)时不影响服务运行,避免单点风险,如Redis的集群模式、哨兵模式,ETCD/zookeeper的集群选主能力等保证HA,保证自身持有的数据与故障节点一致。
  • 可重入性:对同一个锁,加锁和解锁必须是同一个进程。该进程持有的锁不能被其他进程所释放,也不能释放其他进程所持有的锁。

1. 基于Mysql

{note, info red, 基于Mysql的分布式锁性能较差,实际上使用的不多。}

2. 基于Redis

2.1 setnx+lua

  • lock:SET resource_name unique_value NX PX timeout

  • unlock

    1
    2
    3
    4
    5
    if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
    else
    return 0
    end
  • lock时,需要通过设置timeout防死锁

  • unlock时,需要先比较value,如果value相同才能删除,是为了防止误解锁,解了其他客户端(进程)加的锁。因此,value需要具有唯一性。

问题:

  • Redis的加锁只是在单个节点写入。如果在主从复制的过程中,master节点发生故障,可能无法把锁同步给slave,导致锁丢失。

2.2 RedLock

该方案也是基于set命令枷锁和lua脚本实现的。假设有N个Redis节点,例如N=5,这些节点之间是完全独立,我们不适用复制或者任何协调系统,客户端要获取锁的步骤如下:

  • 获取当前时间,以毫秒为单位。
  • 依次尝试从5个实例中,使用相同的key和随机值获取锁。当向Redis请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如锁的自动失效时间为10秒时,则超时时间因该在5-50毫秒之间。这样可以防止客户端在试图与一个当即的Redis节点对话长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去与另外一个Redis实例请求获取锁。
  • 客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功。
  • 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。
  • 如果由于某些原因未能获得锁(无法在至少 N/2 + 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

评论