实现分布式锁

1、Redis实现

  • 不论是本地锁还是分布式锁,核心都在于"互斥"。

    • 在 Redis 中, SETNX 命令是可以帮助我们实现互斥。SETNXSET if N ot eX ists (对应 Java 中的 setIfAbsent 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, SETNX 啥也不做。

    • 释放锁的话,直接通过 DEL 命令删除对应的 key 即可。

    • 为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。

      Lua 复制代码
      // 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
      if redis.call("get",KEYS[1]) == ARGV[1] then
          return redis.call("del",KEYS[1])
      else
          return 0
      end
    • 这种方式实现分布式锁存在一些问题。如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问

  • 给锁设置一个过期时间:为避免锁无法被释放

    • 如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能
  • 锁续期:Redisson:提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗) ,如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。

  • 集群分布式锁的可靠性:由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

    • Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。

2、Zk实现

  • ZooKeeper 相比于 Redis 实现分布式锁,除了提供相对更高的可靠性之外,在功能层面还有一个非常有用的特性:Watch 机制 。这个机制可以用来实现公平的分布式锁。不过,使用 ZooKeeper 实现的分布式锁在性能方面相对较差,因此如果对性能要求比较高的话,ZooKeeper 可能就不太适合了

  • 具体实现

    • 获取锁
      • 客户端获取锁就是在locks下创建临时顺序节点。
      • 假设客户端 1 创建了/locks/lock1节点,创建成功之后,会判断 lock1是否是 /locks 下最小的子节点。
      • 如果 lock1是最小的子节点,则获取锁成功。否则,获取锁失败。
      • 如果获取锁失败,则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如/locks/lock0上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1(避免无效自旋),这样客户端 1 就加锁成功了。
    • 释放锁
      • 成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。
      • 成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。
      • 我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。
  • 为什么使用临时节点:临时节点相比持久节点,最主要的是对会话失效的情况处理不一样,临时节点会话消失则对应的节点消失。这样的话,如果客户端发生异常导致没来得及释放锁也没关系,会话失效节点自动被删除,不会发生死锁的问题。使用 Redis 实现分布式锁的时候,我们是通过过期时间来避免锁无法被释放导致死锁问题的,而 ZooKeeper 直接利用临时节点的特性即可。假设不使用顺序节点的话,所有尝试获取锁的客户端都会对持有锁的子节点加监听器。当该锁被释放之后,势必会造成所有尝试获取锁的客户端来争夺锁,这样对性能不友好。使用顺序节点之后,只需要监听前一个节点就好了,对性能更友好。

  • 为什么监听前一个节点

    • 同一时间段内,可能会有很多客户端同时获取锁,但只有一个可以获取成功。如果获取锁失败,则说明有其他的客户端已经成功获取锁。获取锁失败的客户端并不会不停地循环去尝试加锁,而是在前一个节点注册一个事件监听器。这个事件监听器的作用是:当前一个节点对应的客户端释放锁之后(也就是前一个节点被删除之后,监听的是删除事件),通知获取锁失败的客户端(唤醒等待的线程,Java 中的 wait/notifyAll ),让它尝试去获取锁,然后就成功获取锁了。

3、总结

  • 如果对性能要求比较高的话,建议使用 Redis 实现分布式锁。推荐优先选择 Redisson 提供的现成分布式锁,而不是自己实现。实际项目中不建议使用 Redlock 算法,成本和收益不成正比,可以考虑基于 Redis 主从复制+哨兵模式实现分布式锁。
  • 如果对可靠性要求比较高,建议使用 ZooKeeper 实现分布式锁,推荐基于 Curator 框架来实现。不过,现在很多项目都不会用到 ZooKeeper,如果单纯是因为分布式锁而引入 ZooKeeper 的话,那是不太可取的,不建议这样做,为了一个小小的功能增加了系统的复杂度。
相关推荐
m0_5719575810 分钟前
Java | Leetcode Java题解之第538题把二叉搜索树转换为累加树
java·leetcode·题解
CAORENZHU11 分钟前
Java NIO 核心知识.下
java
程序员徐师兄13 分钟前
基于 JavaWeb 的宠物商城系统(附源码,文档)
java·vue·springboot·宠物·宠物商城
小鸡脚来咯34 分钟前
java 中List 的使用
java·开发语言
南棱笑笑生40 分钟前
20241105编译Rockchip原厂的Android13并给荣品PRO-RK3566开发板刷机
java·开发语言·前端
Erorrs1 小时前
Android13 系统/用户证书安装相关分析总结(二) 如何增加一个安装系统证书的接口
android·java·数据库
余华余华1 小时前
Ngnix
java
昂子的博客1 小时前
通过mybatis和mybatis plus 实现用户注册功能和基础的增删改查
java·开发语言·mybatis
菜菜-plus1 小时前
java设计模式之工厂模式
java·设计模式
哎呦没1 小时前
健身行业创新:SpringBoot管理系统应用
java·spring boot·后端