分布式锁

在多线程情况下访问一些共享资源需要加锁,不然就hi出现数据被写乱的问题。一般可以用数据库DB,Redis和Zookeeper实现

分布式锁特点:

  • 安全性(Safety):在任意时刻,只有一个客户端可以获得锁(排他性)
  • 避免死锁:客户端最终一定可以获得锁,即使锁住某个资源的客户端在释放锁之前崩溃或者网络不可达
  • 容错性:只要锁服务集群中的大部分节点存活,Client就可以进行加锁解锁操作

Redis的分布式锁服务

在redis中,我们可以通过以下命令

SET resource_name my_random_value NX PX 30000
  • SET NX只会在key不存在的时候给key赋值,PX通知redis保存这个key30000ms
  • my_random_value必须是全局唯一的值。这个随机数在释放锁时保证释放锁操作的安全性
  • PX操作后面的参数代表的是这个key的存活时间,称为锁过期时间
  • 当资源被锁定超过这个时间时,锁将自动释放
  • 获得锁的客户端如果没有在这个时间窗口内完成操作,就可能会有其他客户端获得锁,引起争用问题

这里的原理是,只有在某个key不存在的情况下才能设置(set)成功该key这就导致多个进程并发设置同一个key

申请成功的锁解锁:

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

如果key对应的value一致,则删除这个key。

通过这个方式释放锁时为了避免Client释放了其他Client申请的锁

例如,这个例子演示了不区分Client出现的问题

  1. Client A获得了一把锁
  2. 当尝试释放锁的请求发送给Redis时被阻塞,没有即使到达Redis
  3. 锁定时间超时,Redis认为锁的租约到期,释放了这个锁
  4. Client B重新申请到了这个锁
  5. Client A的解锁请求到达,释放了Client B的锁
  6. Client C也获得了锁
  7. Client B 和 Client C 同时持有锁

通过上面这种方式,Client的解锁操作只会解锁自己曾经加锁的资源,所以是安全的

关于 value 的生成,官方推荐从 /dev/urandom 中取 20 个 byte 作为随机数。或者采用更加简单的方式,例如使用 RC4 加密算法在 /dev/urandom 中得到一个种子(Seed),然后生成一个伪随机流

也可以采用更简单的方法,使用时间戳 + 客户端编号的方式生成随机数。Redis 的官方文档说:“这种方式的安全性较差一些,但对于绝大多数的场景来说已经足够安全了”。

分布式锁服务的一个问题

为了避免Client端把锁占住不放,然后,Redis在超时后把其释放掉。这样就很不靠谱了

举个例子

  • 如果 Client A 先取得了锁。
  • 其它 Client(比如说 Client B)在等待 Client A 的工作完成。
  • 这个时候,如果 Client A 被挂在了某些事上,比如一个外部的阻塞调用,或是 CPU 被别的进程吃满,或是不巧碰上了 Full GC,导致 Client A 花了超过平时几倍的时间。
  • 然后,我们的锁服务因为怕死锁,就在一定时间后,把锁给释放掉了。
  • 此时,Client B 获得了锁并更新了资源。
  • 这个时候,Client A 服务缓过来了,然后也去更新了资源。于是乎,把 Client B 的更新给冲掉了。
  • 这就造成了数据出错。

图解Redis和Zookeeper分布式锁 redis分布式锁 zookeeper_Redis

要解决这个问题,我们可以引入fence(栅栏)技术。一般来说,这是乐观锁机制,需要一个版本号排它

图解Redis和Zookeeper分布式锁 redis分布式锁 zookeeper_Redis_02

我们从图中可以看到

  • 锁服务需要有一个单调递增的版本号
  • 写数据的时候,也需要带上自己的版本号
  • 数据库服务需要保存数据的版本号,然后对请求做检查

如果使用ZooKeeper做锁服务的话,那么可以使用zxidznode的版本号来做这个fence版本号

从乐观锁到CAS

图解Redis和Zookeeper分布式锁 redis分布式锁 zookeeper_分布式_03

在数据库中可以使用数据版本(Version)记录机制,即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的"version"字段来实现。当读取数据时,将 version 字段的值一同读出,数据每更新一次,对此 version 值加一。

这是乐观锁最常用的一种实现方式。是的,如果我们使用版本号,或是 fence token 这种方式,就不需要使用分布式锁服务了。

分布式锁设计的重点

一般情况下,我们可以使用数据库,Redis或者ZooKeeper来做分布式锁服务

分布式锁的特点是,保证在一个集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。这就是所谓的分布式互斥。

我们需要明确一下分布式锁服务的初衷和几个概念性问题

  • 如果锁的进程挂掉了怎么办?一般的处理方法是在锁服务加上一个过期时间,如果这个时间内锁没有被还回来,那么锁服务要自动解锁,以避免全部锁住
  • 如果锁服务自动解锁了,新的进程拿到锁了,但之前的进程以为自己还有锁,那么就出现了两个进程拿到同一个锁的问题,它们在更新数据的时候就会产生问题:像Redis那样也可以使用Check and Set的方式来保证数据的一致性,这就有点像CAS。那还需要分布式锁服务吗?的确是不需要,但现实生活中也有不需要更新某个数据的场景,只是为了同步或是互斥一下不同机器上的线程,这时候分布式锁就有意义了

如果确定要分布式锁服务,你需要考虑下面几个设计:

  • 需要给一个锁被释放的方式,以避免请求者不把锁还回来导致死锁的问题。Redis使用超时时间,ZooKeeper可以依靠自身的sessionTimeout来删除节点
  • 分布式锁服务应该是高可用的,而且需要持久化
  • 要提供非阻塞方式的锁服务
  • 还要考虑锁的可重入性