使用分布式锁的目的/意义:
1、efficiency:为了提高效率,节约资源,避免重复操作。
2、correctness:为了数据的一致性,保证业务正确。
redis分布式锁(single instance)
基本特征
- 独占互斥
- 无死锁
- 有一定容错性
推荐用法
1 | // 通过set nx达到原子操作 |
redlock
定义
- 在n个redis节点上同时上锁,只有在N/2 +1(至少3个)个节点同时加锁成功
- 且加锁时间小于锁的有效期,则认为上锁成功
- 上锁失败则所有节点解锁
失败重试
- 在获取锁失败后应该在随机时间后重新获取锁,减少并发竞争
特性
- 锁的自动释放(因为key失效了):最终锁可以再次被使用.
- 客户端通常会将没有获取到的锁删除,或者锁被取到后,使用完后,客户端会主动(提前)释放锁,而不是等到锁失效另外的客户端才能取到锁。.
- 当客户端重试获取锁时,需要等待一段时间,这个时间必须大于从大多数Redis实例成功获取锁使用的时间,以最大限度地避免脑裂。
Martin提出的RedLock存在的问题
问:redis分布式锁无法避免当客户端处理锁对应的逻辑超过来锁过期时间后,锁被其他客户端占有导致的资源冲突问题。(这个问题大多数分布式锁实现都无法避免,也比较难解决)。Martin提出应该提供fencing(栅栏)机制
答:
首先分布式锁需要自动失效,否则会有死锁问题。
redis中的random value可以起到token的作用。
且如果有fencing机制来保证race condition的话,分布式锁存在的意义是什么?
并没有正面回答这个问题,可能确实比较难解决。
zookeeper
实现方式一
- 客户端尝试创建一个znode节点,比如
/lock
。那么第一个客户端就创建成功了,相当于拿到了锁;而其它的客户端会创建失败(znode已存在),获取锁失败。 - 持有锁的客户端访问共享资源完成后,将znode删掉,这样其它客户端接下来就能来获取锁了。
- znode应该被创建成ephemeral的。这是znode的一个特性,它保证如果创建znode的那个客户端崩溃了,那么相应的znode会被自动删除。这保证了锁一定会被释放。
实现方式二
- 创建一个临时顺序节点,通过临时顺序节点的序号来判断是否获取锁成功
- 获取锁失败则监听上一个临时顺序节点(可以避免herd effect)
- 解锁后通过watcher机制通知下一个节点获取锁成功
对比redlock
- 没有redlock的过期时间问题,能在需要的时候让锁自动释放。但是可能会由于session过期的问题,导致锁被其他客户端占有产生冲突。
- 在正常情况下,客户端可以持有锁任意长的时间,这可以确保它做完所有需要的资源访问操作之后再释放锁。这避免了基于Redis的锁对于有效时间(lock validity time)到底设置多长的两难问题。实际上,基于ZooKeeper的锁是依靠Session(心跳)来维持锁的持有状态的,而Redis不支持Sesion。
- 基于ZooKeeper的锁支持在获取锁失败之后等待锁重新释放的事件。这让客户端对锁的使用更加灵活。
chubby
特性
- advisory lock
- coarse-grained(粗粒度的)
- paxos algo
- file system
对比zookeeper、redlock
- 使用paxos协议保证一致性,只有一个master节点可以读写
- 封装了lock api,使用起来更方便
- 线性一致性
chubby解决延迟造成锁失效的机制
- sequencer,类似于fencing token机制,锁的持有者可以随时请求一个sequencer,这是一个字节串,它由三部分组成:
- 锁的名字。
- 锁的获取模式(排他锁还是共享锁)。
- lock generation number(一个64bit的单调递增数字)。作用相当于fencing token或epoch number。
客户端拿到sequencer之后,在操作资源的时候把它传给资源服务器。然后,资源服务器负责对sequencer的有效性进行检查。检查可以有两种方式:
1、调用Chubby提供的API,CheckSequencer(),将整个sequencer传进去进行检查。这个检查是为了保证客户端持有的锁在进行资源访问的时候仍然有效。
2、将客户端传来的sequencer与资源服务器当前观察到的最新的sequencer进行对比检查。可以理解为与Martin描述的对于fencing token的检查类似。
- lock-delay
- Chubby允许客户端为持有的锁指定一个lock-delay的时间值(默认是1分钟)。当Chubby发现客户端被动失去联系的时候,并不会立即释放锁,而是会在lock-delay指定的时间内阻止其它客户端获得这个锁。这是为了在把锁分配给新的客户端之前,让之前持有锁的客户端有充分的时间把请求队列排空(draining the queue),尽量防止出现延迟到达的未处理请求。