分布式锁(3)--基于Etcd的分布式锁实现
前记
基于Redis
实现的分布式锁的性能强,功能多,但是由于Redis
本身的特性决定了基于Redis
实现的分布式锁可能会不唯一,所以无法应用在要求分布式锁必须绝对唯一的场景。
为此,只能基于其他服务来实现分布式锁,不过它们实现的总体思路是一致的,只是调用方式有一些区别,本文将介绍如何基于Etcd
实现具有完备性的分布式锁。
1.为何基于Etcd实现分布式锁
目前常见的分布式锁实现除了Redis
外还有Etcd
和Zookeeper
,由于Redis
的一致性只满足了AP,所以基于Redis
实现的分布式锁无法保证绝对唯一,而Etcd
和Zookeeper
的一致性都满足了CP,都是通过类似的一致性算法确保了数据的一致性,这样基于Etcd
和Zookeeper
实现的分布式锁能保证绝对唯一。
其中Etcd
和Zookeeper
的功能类似,但有一些方面中Etcd
甚至超越了ZooKeeper
,如Etcd
采用的Raft
协议就要比ZooKeeper
采用的Zab
协议简单、易理解,同时它的性能也比Zookeeper
高,所以通常会选择Etcd
作为分布式锁的实现。
官方提供的Etcd
与其他同类型的组件对比如下图:
通过图可以看到Etcd
略胜于其他组件,此外它提供的Watch
,Lease
,Revision
以及Prefix
机制使基于Etcd
实现分布式锁会非常的方便,同时它是通过HTTP/gRPC提供对应的API,所以它的客户端实现也比较方便,当然如果当前系统依赖的是Zookepeer
,那么肯定还是选择Zookerpeer
,除非有性能要求。
在
Python
中Etcd
的客户端并没有官方的维护,目前维护的比较好的是aetcd,接下来将使用aetcd实现一个简单的分布式锁。
2.分布式锁的实现
前面说到,Etcd
的Watch
,Lease
,Revision
以及Prefix
机制赋予Etcd
分布式锁的能力,要实现分布式锁则需要了解它们的作用,它们的作用如下:
- Lease机制:它类似于
Redis
中的过期,Etcd
可以通过Lease
为Key-Value
设置租约时间(也就是过期时间),当租约到期时Key-Value
会被删除。 - Revision机制:
Etcd
会为每个Key-Value
赋予一个版本,同时更新Key-Value
的时候,它的版本也会发生变化。比如插入Key1
时,它的版本为0,而在后续插入Key2
时它的版本为1,通过这样的机制,就可以知道写操作的顺序。 - Prefix机制:
Etcd
通过Prefix
机制提供了对前缀相同的Key执行同一操作的能力,例如一个名为/lock
的锁,当有两个客户端进行写操作时,实际上写入的Key
分别为key1="/lock/UUID1"
和key2=/lock/UUID2""
,其中UUID就是token
的含义,用于确保每个锁的唯一性。
不过与Redis
中实现的分布式锁不一样的是在Etcd
中这两个锁都会写入成功,但是返回的Revision
是不一样的,需要应用程序通过/lock/
前缀去查询数据,然后会获得到key1
和key2
的结果和Revision
,接着再根据Revision
的结果进行判断哪个Key
才是获取到锁。 - Watch机制:
Etcd
的Watch
机制可以批量的监听一批Key
,当被监听的Key
发生变化时,客户端会收到通知。在实现分布式锁的时候,如果抢锁失败,可通过Prefix
机制返回的Key
列表获得Revision
比自己小且相差最小的Key
(称为Pre-Key
),对Pre-Key
进行监听,因为只有它释放锁,自己才能获得锁,如果监听到Pre-Key
的删除事件,则说明Pre-Key
已经释放,自己已经持有锁。
了解完这些机制,可以发现通过这些机制可以非常快的基于Etcd
实现一个简单的分布式锁,如下图:
图中红色方块代表Etcd
服务端,蓝色方块代表Etcd
客户端,白色方块代表当前Etcd
服务端存的数据,图中总共分为4个步骤,用虚线隔开。
首先是步骤1,client1
和client2
会通过put
命令把/demo/{uuid}
推送到Etcd
中。
然后是步骤2,这里通过Prefix
获取/demo
开头的键值对的信息。
接着就是步骤3,这个步骤会判断步骤2返回的键值对信息中Revision
版本最小的是不是自己,如果是那就意味着自己获取到锁了,如果不是则会通过Watch
机制监听Revision
比自己小1的键值对的删除事件。
最后是步骤4,当获取到锁的client1
执行完任务后删除了/demo/uuid1
,这时Etcd
会通知到client2
客户端,它已经获取锁了,可以执行任务。
通过这些机制,可以发现基于Etcd
实现的分布式锁会非常的方便,同时由于Revision
的作用,实现的分布式锁默认带有了公平锁的功能,接下来开始手撸Etcd
分布式锁的实现,首先是获取锁的逻辑:
1 |
|
示例代码中会先进行初始化,其中传递进来的name
参数会取名为prefix
,而锁内的name
为prefix
和token
的拼接,这样会方便通过Prefix
机制去查找相同锁的内容,也可以让Watch
机制单独的去监听对应锁的状态,以及防止发生别的锁释放到自己的锁的情况。
在初始化之后就是获取锁的实现了,获取锁的第一步是创建续约的相关方法,Etcd
的续约机制与Redis
的过期时间不同的是,Etcd
是需要先创建一个租约,然后再用租约去绑定对应的Key
,如果这个租约过期了,那么租约对应的Key
都会同时过期,所有要先创建一个租约,然后在put
方法中与Key
绑定。
接着是与图中的步骤一样,会先推送数据到Etcd
中,然后根据get_prefix
获取所有锁的数据,需要注意的是这里需要定义sort_target
为create
,这样返回的结果集会按照Key
创建的版本号排序。使后续能快速的判断自己获取锁是否成功,也容易推断出上一个Key的值是什么。
最后就是通过watch
方法,监听上一个Key的删除事件,如果收到删除事件那么意味获取到锁。
可以看到由于Etcd
的Revision
和Watch
机制以及它的一致性,基于Etcd
实现的获取锁不需要通过While
循环去循环获取锁,也不需要考虑到网络原因导致客户端和服务端的数据不同步的因素,因此实现的代码 非常的简单。同时这个简单还不止提现在获取锁的方法中,释放锁和WatchDog
的实现也非常简单,如下:
1 |
|
通过代码可以看到,释放锁的操作非常简单只需要调用delete
方法即可,而WatchDog
与之前Redis
分布式锁的机制类似,只是核心的锁续约交给了Lease
机制去实现,Lease
机制的refresh
方法会重置当前续约的时间为一开始定义的ttl。
接下来运行测试代码,这个代码会按顺序执行相同的任务,它们的锁超时时间为1秒,但是执行的时间为2秒,代码如下:
1 |
|
在运行测试代码后可以看到如下输出:
1 |
|
通过输出可以发现,每个任务的执行时间都在2秒左右,而且任务2和任务3虽然在同一时间内等待执行,但是任务1执行完毕后,只有任务2会执行,任务3需要等待任务2执行完毕后才能执行。
3.总结
通过实现过程了解到由于Etcd
本身拥有的各种机制,基于Etcd
实现的分布式锁非常简单,同时由于Etcd
与Redis
一样都是Key-Value
数据库,它也能通过数据结构拓展锁的功能。
- 本文作者:So1n
- 本文链接:http://so1n.me/2023/09/10/distributed_lock_by_etcd/index.html
- 版权声明:本博客所有文章均采用 BY-NC-SA 许可协议,转载请注明出处!