简介

分布式锁是分布式系统中必要且重要的控制资源并发访问的手段

要求

  • 互斥性
  • 不会发生死锁
  • 可重入性
  • 高效、高可用

固有缺陷

  1. 加锁不设置超时时间:客户端阻塞或宕机锁无法释放
  2. 加锁不设置线程标记:解锁时会发送误操作
  3. 超时时间设置:过长阻塞、过短锁泄漏
  4. 主从集群:主节点在确认加锁后宕机,从节点未确认加锁后升级成主节点发生锁泄漏
  5. 客户端发生阻塞:客户端发生GC或者网络等阻塞时,若此期间锁超时释放被其他客户端获取则发送锁泄漏

实现方式

  • 完整实现:在获取锁的同时生成一个递增的序列号,每次资源发生修改的时候比较客户端所携带的序列号,早于当前资源版本的修改不被允许。
  • 简易实现:在锁超时后还有一个lock-delay的时长,在lock-delay时间内其获取同一把锁会被拒绝,正常的释放锁则可以立刻被再次获取。

实现方案

  • 数据库
  • Redis
  • ZooKeeper
  • Google Chubby

数据库

使用数据库实现作为分布式锁大家都比较熟悉了,我们可以建立一块表来记录锁的资源、线程标记、重入次数、超时时间。

使用事务来保证客户端加解锁过程的原子性,使用定时任务来删除超时的锁。

  • 优点
    • 简单
    • 无需额外的第三方组件
  • 缺点
    • 性能限制于数据库
    • 强依赖单点服务
    • 具有分布式锁固有缺陷3、4、5

Redis

使用Redis实现分布式锁大家也比较熟悉了,我们可以用常见的基于单机的setNxEx,也可以用基于集群的RedLock。下面简单介绍一下两种方式中比较重要的操作。

  • setNxEx

    • 加锁时set if not exist 和set expire是原子操作
    • 解锁时使用以下lua脚本保证原子操作
      1
      2
      3
      4
      5
      if redis.call("get",KEYS[1]) == ARGV[1] then
      return redis.call("del",KEYS[1])
      else
      return 0
      end

    此种的优缺点与数据库实现基本相同,但是性能更好。

  • RedLock

    1. 获取当前时间
    2. 轮流在每台master上获取锁,获取锁超时时间应远小于锁超时时间M
    3. 计算获取锁成功的机器以及总的获取锁时间N,获取锁成功机器大于设置个数才成功
    4. 如果锁获取成功了,锁自动释放时间为M-N
    5. 获取锁失败了应在每台master上执行释放锁操作
    6. 锁使用完成释放也应该在每台master上执行释放操作,即使获取锁时失败的机器
    • 优点
      • 简单
      • 使用集群可靠性更高
    • 缺点
      • 当获取锁成功的某台机器宕机后重启,会发送锁泄漏
      • 当发生时间跳跃时,会发生锁泄漏

    关于RedLock的有名的讨论,详见参考文档1、2中的说明。

ZooKeeper

使用ZooKeeper实现分布式锁也是常见的手段,这里同样只介绍比较重要的操作。

  1. 客户端尝试创建一个临时且有序的子节点
  2. 判断当前子节点是否为序号最小的子节点,是则获取锁成功,否则监听节点变更直到获取锁成功
  3. 当客户端失去心跳则临时子节点会被删除
  4. 监听节点变化应当只监听有序子节点列表的前一个,如果监听子节点则会发送羊群效应
  • 优点
    • 使用集群可靠性更高
  • 缺点
    • 当发生时间跳跃时,会发生锁泄漏
    • 具有分布式锁固有缺陷5

Google Chubby

Chubby是Google内部使用的分布式锁系统,也是较全面的实现了两种方式的系统,实现方式章节的内容就是取自Chubby系统的思路。

具体的实现操作这里不做介绍,详见参考文档6中的说明。

参考文档

1 基于Redis的分布式锁到底安全吗(上)
2 基于Redis的分布式锁到底安全吗(下)
3 分布式之抉择分布式锁
4 Redis 分布式锁的正确实现方式( Java 版 )
5 基于Zookeeper的分布式锁
6 Chubby的锁服务
7 再有人问你分布式锁,这篇文章扔给他