一、为什么我们需要一把分布式锁?
- 为了效率(efficiency),协调各个客户端避免做重复的工作。即使锁偶尔失效了,只是可能把某些操作多做一遍而已,不会产生其它的不良后果。比如重复发送了一封同样的 email(当然这取决于业务应用的容忍度)。
- 为了正确性(correctness)。在任何情况下都不允许锁失效的情况发生,因为一旦发生,就可能意味着数据不一致(inconsistency),数据丢失,文件损坏,订单重复,超卖或者其它严重的问题。
二、分布式锁的三个属性
- 互斥(Mutual Exclusion),这是锁最基本的功能,同一时刻只能有一个客户端持有锁;
- 避免死锁(Dead lock free),如果某个客户端获得锁之后花了太长时间处理,或者客户端发生了故障,锁无法释放会导致整个处理流程无法进行下去,所以要避免死锁。最常见的是通过设置一个TTL(Time To Live,存活时间) 来避免死锁。
- 容错(Fault tolerance),为避免单点故障,锁服务需要具有一定容错性。大体有两种容错方式,一种是锁服务本身是一个集群,能够自动故障切换(ZooKeeper、etcd);另一种是客户端向多个独立的锁服务发起请求,其中某个锁服务故障时仍然可以从其他锁服务读取到锁信息(Redlock),代价是一个客户端要获取多把锁,并且要求每台机器的时钟都是一样的,否则 TTL 会不一致,可能有的机器会提前释放锁,有的机器会太晚释放锁,导致出现问题。
核心:解决的是多线程或多进程情况下的数据一致性问题;分布式锁,解决的是分布式集群下的数据一致性问题。
三、JVM的锁无法解决分布式环境下数据一致性的问题
为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。
在单机环境中,Java中提供了很多并发处理相关的API。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
举个例子:
机器A , 机器B是一个集群, A, B两台机器上的程序都是一样的, 具备高可用性能.
A, B机器都有一个定时任务, 每天晚上凌晨2点需要执行一个定时任务, 但是这个定时任务只能执行一遍, 否则的话就会报错, 那A,B两台机器在执行的时候, 就需要抢锁, 谁抢到锁, 谁执行, 谁抢不到, 就不用执行了!
四、Redis分布式锁和zookeeper分布式锁的区别
优点:
- ZooKeeper 分布式锁基于分布式一致性算法实现,能有效的解决分布式问题,不受时钟变迁影响,不可重入问题,使用起来也较为简单;
- 当锁持有方发生异常的时候,它和 ZooKeeper 之间的 session 无法维护。ZooKeeper 会在 Session 租约到期后,自动删除该 Client 持有的锁,以避免锁长时间无法释放而导致死锁。
缺点:
ZooKeeper 实现的分布式锁,性能并不太高。为啥呢?
因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK 中创建和删除节点只能通过 Leader 服务器来执行,然后 Leader 服务器还需要将数据同步到所有的 Follower 机器上,这样频繁的网络通信,性能的短板是非常突出的。
总之,在高性能,高并发的场景下,不建议使用 ZooKeeper 的分布式锁。而由于 ZooKeeper 的高可用特性,所以在并发量不是太高的场景,推荐使用 ZooKeeper 的分布式锁。
小结一下,基于 ZooKeeper 的锁和基于 Redis 的锁相比在实现特性上有两个不同:
- 在正常情况下,客户端可以持有锁任意长的时间,这可以确保它做完所有需要的资源访问操作之后再释放锁。这避免了基于 Redis 的锁对于有效时间(lock validity time)到底设置多长的两难问题。实际上,基于 ZooKeeper 的锁是依靠 Session(心跳)来维持锁的持有状态的,而 Redis 不支持 Sesion。
- 基于 ZooKeeper 的锁支持在获取锁失败之后等待锁重新释放的事件。这让客户端对锁的使用更加灵活。
Redis分布式锁的难点:
对于有效时间的设置,可以通过RedLock 来动态增加锁的时间
Zookeeper:CP
Redis集群模式:AP
总结
- 基于ZooKeeper 的分布式锁,适用于高可靠(高可用)而并发量不是太大的场景;
- 基于Redis 的分布式锁,适用于并发量很大、性能要求很高的、而可靠性问题可以通过其他方案去弥补的场景。