分布式系统 - 锁

分布式锁的两种实现方式。

Redis实现

可使用setnx , expire指令简单实现,但是无法保证原子性,以及保证各个节点的时钟同步

  • lua脚本

    -- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。

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

redis保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。

另一方面,这也意味着,执行一个运行缓慢的脚本并不是一个好主意。写一个跑得很快很顺溜的脚本并不难,因为脚本的运行开销(overhead)非常少,但是当你不得不使用一些跑得比较慢的脚本时,请小心,因为当这些蜗牛脚本在慢吞吞地运行的时候,其他客户端会因为服务器正忙而无法执行命令。

  • redlock

    redisson 官网推荐

      1. 获取当期毫秒时间,轮流用相同的key和随机值在N个节点上请求锁,在这一步里客户端在每个master上请求锁时,会有一个比总释放时间小的多的超时时间,这样可以防止客户端在某个master 宕机时堵塞过长时间,如果这个master节点不可用我们应尽快尝试下一个master
      1. 客户端计算获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
      1. 如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
      1. 如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。

Zookeeper实现

创建一个永久节点作为锁节点,试图加锁的客户端在锁节点下创建临时顺序节点。Zookeeper会保证子节点的有序性。若锁节点下id最小的节点是为当前客户端创建的节点,说明当前客户端成功加锁。否则加锁失败,订阅上一个顺序节点。当上一个节点被删除时,当前节点为最小,说明加锁成功。操作完成后,删除锁节点释放锁。

  1. zk的底层数据结构是树形结构,由一个一个的数据节点组成;

  2. zk的节点分为永久节点和临时节点,客户端可以创建临时节点,当客户端会话终止或超时后,zk会自动删除临时节点,该特性可以避免死锁;

  3. 当节点的状态发生变化时,zk的watch机制会通知监听相应事件的客户端,该特性可以用来实现阻塞等待加锁;

  4. 客户端可以在某个节点下创建子节点,Zookeeper会根据子节点数量自动生成整数序号,类似于数据库的自增主键;

由于需要频繁的新增和删除节点,性能比较差,不推荐使用。