写在前面
我们知道在同一个JVM中,可以通过Volatile、Synchronized、ReentrantLock 三个关键字来实现线程的安全。那么在分布式系统中这些是无法保证的,所以要通过分布式锁来实现。
基于分布式锁的实现有多种方案,现针对Redis 和Zookeeper 这两种方式聊一聊功能具体实现方式、优缺点以及各自适用的业务场景。
基于setnx 锁实现
正常基于Redis 自带的setnx 命令就可以实现简单的加锁功能:
setnx key value
Key 作为锁的唯一标识,当线程setnx返回1时,说明原本key不存在,该线程成功得到了锁;如果返回的结果为0,则说明key已经存在,线程获取锁失败。
当线程获取锁,执行完任务后,需要释放锁,以便后续线程使用。可以通过del 这个key来实现:
del key
但是由于加锁和解锁是分为两步实现,不是原子操作,所以可能会出现中间状态:
基于此有这种方案:
expire key
显而易见,这边也有一个上述的风险点:就是非原子操作,可能存在中间状态。
所以我们引入第二种方案。
set key value [EX seconds] [PX milliseconds] [NX|XX]
通过 set (key, value, EX a,NX) 取代setnx 来实现原子加锁(自动释放锁)操作。(redis-client中的API setnx 即是基于此实现,所以在使用中直接调用API的setnx)
针对上述问题需要另外服务来保证实现:
Java中的Lock对象以及Synchronized关键字语块都可具有可重入性,可以实现同一个线程共用同一把锁;避免死锁发生的可能。
而在Redis上述实现中则没有相应的功能,如果业务上需要,则需在业务代码中实现其逻辑。
Redis Cluster 在master异常情况下,会发生主从切换,而主从是异步复制,极大可能导致数据丢失,从而导致锁的失效。这块安全性方面Redis Cluster 无法保证。
但是Redis 作者实现了基于多节点的高可用分布式锁的算法 RedLock。有兴趣的可以了解一下。
Zookeeper 通过临时有序(顺序)节点实现分布式锁。
所谓临时顺序节点就是Zookeeper根据创建的时间给该节点名称进行编号,当创建节点的客户端与Zookeeper 断开连接后,临时节点就被删除。
Zookeeper 通过创建临时有序节点实现上锁,只有序号最小(或顺序最靠前)的可以成功获取到锁;
如果该序号不是最小(或不是顺序最靠前)则向它前一个节点注册Watcher,通过watch来监听前一个节点是否存在,等待watch事件(即监听节点的状态变化),如果监听到watch事件发生,则再次判断该节点是否为序号最小节点,如果是则成功获取锁,否则,继续监听等待。
Zookeeper 实现锁的方式上较为简单。
每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上。
所以性能开销这块比Redis 大,即并发性能不如Redis。
优点:实现指令性能较高
缺点:
适用场景:
高并发的分布式锁实现
优点:
缺点:
添加和删除节点性能较低
适用场景:
并发量小,安全性要求较高的业务场景