1、redis实现
在 JUC 包中除了阻塞锁外还有一种叫 CAS 的无阻塞锁,CAS 操作本身是原子性的,多个线程操作同一个变量的 CAS 时候只有一个线程能进行 CAS 成功,失败的线程接下来那么使用乐观锁机制直接失败要么使用自旋方式使用 CPU 资源重复进行 CAS 尝试。
那么在分布式锁的实现中我们也可以使用类似的方式,比如 Redis 提供了一个保证原子性的 setnx 函数,多个线程调用该函数操作同一个 key 的时候,只有一个线程会返回 OK,其他线程返回 null,那么多个 JVM 中的线程同时设置同一个 key 时候只有一个 JVM 里面的一个线程可以返回 OK,返回 OK 的线程就相当于获取了全局锁,返回 null 的线程则可以选择自旋重试。获取到锁的线程使用完毕后调用 del 函数删除对应的 key,然后自旋的线程就会有一个返回 OK…
2、zookeeper实现
在 ZK 中是使用文件目录的格式存放节点内容,其中节点类型分为:
持久节点(PERSISTENT ):节点创建后,一直存在,直到主动删除了该节点。
临时节点(EPHEMERAL):生命周期和客户端会话绑定,一旦客户端会话失效,这个节点就会自动删除。
序列节点(SEQUENTIAL ):多个线程创建同一个顺序节点时候,每个线程会得到一个带有编号的节点,节点编号是递增不重复
创建临时顺序节点(EPHEMERAL_SEQUENTIAL),这里我们就使用临时顺序节点来实现分布式锁。
分布式锁实现步骤,每个想要获取锁的线程都要执行下面步骤:
创建临时顺序节点,比如 /locks/lockOne,假设返回结果为 /locks/lockOne000000000*。
获取 /locks下所有孩子节点,用自己创建的节点 /locks/lockOne000000000* 的序号 lockOne000000000* 与所有子节点比较,看看自己是不是编号最小的。如果是最小的则就相当于获取到了锁;如果自己不是最小的,则从所有子节点里面获取比自己次小的一个节点,然后设置监听该节点的事件,然后挂起当前线程。
当最小编号的线程获取锁,处理完业务后删除自己对应的节点,删除后会激活比自己大一号的节点的线程从阻塞变为运行态,被激活的线程应该就是当前 node 序列号最小的了,然后就会获取到锁。整个过程是一个类似循环监听的模式。
3、对比
使用 Redis 来实现分布式锁优点是实现简单,并且获取锁的 setnx 方法使用 cas 算法来判断获取锁是否成功,吞吐量不错;另外 setnx 方法自带了超时参数,这可以有效避免当一个线程获取到锁后,在释放锁前机器挂了后,其他线程一直阻塞到获取锁的情况,等超时时间过了,锁会被自动释放;缺点也很明显,本文例子获取锁时候是类似 CAS 自旋重试的,在高并发情况下会造成大量线程共同竞争锁时候的本地自旋,这很像 JUC 中的 AtomicLong 一样,在高并发下多个线程竞争同一个资源时候造成大量线程占用 cpu 进行重试操作。这时候其实可以随机生成一个等待时间,等时间到后在进行重试,以减少潜在的同时对一个资源进行竞争的并发量;另外如果Redis是单实例挂了,也会存在问题;如果是sentinel模式,在master节点获取锁之后还未同步的状态,master故障,发生故障转移,slave节点升级为master节点,导致锁丢失,也会存在问题。
使用 Zookeeper 实现分布式锁优点是可以对节点进行监听,多个线程获取锁时候没有获取到锁的线程不需要本地自旋重试,而是挂起自己,等待获取锁的线程释放锁后发送事件激活自己;由于线程阻塞自己使用的是 JUC 包的 CountDownLatch,在调用 await 的时候是可以添加超时时间的,所以 zk 方式也可以在实现获取锁时候超时候自动返回;缺点是使用 zk 实现比较重,实现起来稍微麻烦点。( Apache Curator 对 zk 进行了封装)