web程序一般都分布在不同的服务器上,如果需要对外部资源进行独占操作就必须使用锁机制,这时候就需要使用分布式锁。一般来说,使用zookeeper、etcd这种强一致性的服务来实现分布式锁是最可靠的。但是有时候条件受限,仅仅为了分布式锁就引入zookeeper还是太奢侈了,反而缓存是比较容易获得的资源。
一般我们使用Redis或者Memcached来作为缓存服务,下面分析一下缓存的分布式锁实现。
Memcached
一般网上搜索,大多数使用Memcached来实现分布式锁的一般方法就是:
使用add命令加锁,同时设置超时时间,只有当key不存在时才会进行添加。因为memcached的命令是原子性的,所以只有一个客户端的add命令会执行成功,也就表示获取了锁。
解锁操作就是直接删除这个key。可以在解锁前判断当前是否已经超时,如果超时则不删除。
但是这种实现方式是有问题的。在第2步,可能会遇到超时的临界条件。解锁前判断没有超时,发送了删除请求,但是删除请求到达memcached服务器的时候key超时了,这时就可能将其他客户端刚刚获取到的锁给误删。
而且,Memcached实现分布式锁还有一些缺点:
Memcached集群的分布式实现是依靠客户端的一致性hash,如果此时增加或删除机器,就有可能key对应的机器发生变化,导致锁丢失。
Memcached采用LRU置换淘汰算法,内存不够后,可能将锁给丢弃。
Memcached无法持久化,重启后锁会丢失。
所以,使用Memcached实现分布式锁是不可靠的,在高并发场景下不建议使用。
Redis
Redis根据部署情况,实现分布式锁的方法也略有不同。
单点Redis
单点Redis实现分布式锁的思路和Memcached类似,也是使用一个key作为锁,只有获取锁的客户端才能创建key。但是不同的是,redis可以处理超时的临界条件。首先一个客户端通过SET命令放置一把锁:
1
|
SET resource_name my_random_value NX PX 30000 |
分析一下这个SET命令,有以下几点注意的地方:
- key为resource_name,即标识这把锁的名字
- value为my_random_value,这是一个随机值,由客户端生成,用于标识放置锁的客户端
- NX参数确保只有这个key不存在才能SET成功,即如果已经有锁,则创建锁就会失败
- PX是锁过期时间(ms),保证客户端发生崩溃后锁能自动释放
解锁的操作是通过eval一段Lua脚本实现的:
|
|
使用Lua脚本的原因是要保证get和del操作是原子性的,这样就不会被其他客户端的释放锁请求插入打断,造成竞争。Lua脚本中的ARGV[1]参数是当时设置锁的my_random_value,要和Redis中存储的值一致,这样就能保证删除锁的客户端就是当时加锁的客户端,否则可能出现其他客户端误删锁的情况。
在单点Redis上实现分布式锁是可行的,通过上面的加锁和解锁操作能保证同一时间只有一个客户端获取锁。但是要保证系统的可用性,是不能有单点存在的。而对于集群Redis,分布式锁的实现就变复杂了。
集群Redis
对于集群Redis,官方提出了一种称为 Redlock 的分布式锁算法。与强一致性的zookeeper、etcd服务不同,Redlock分布式锁是在客户端实现的。
在Redis集群上实现分布式锁是以单点Redis为基础的,具体到集群上的某一台Redis,加锁和解锁的操作还是一样的。假设我们有一个由5台Redis组成的集群,客户端通过以下操作获取锁:
客户端获取当前时间t0。
客户端使用同一个key和随机值,尝试在所有的N台Redis实例上获取锁。
客户端获取当前时间t1,由t1-t0得到获取每个实例锁使用了多少时间。只有客户端从多数的Redis实例(N/2+1)上获取了锁,并且获取锁花费的时间小于锁过期时间,才能被认为真正获取了这把分布式锁。
如果真正获取了分布式锁,那么锁的有效时间是锁过期时间减去获取锁花费的时间。
如果没有获取到分布式锁,需要在所有的Redis实例上执行解锁操作,即使有部分Redis实例并没有被上锁。
如果需要进行获取锁失败后的重试,那么重试需要等待一个随机的时间,这是为了防止所有客户端同时获取锁失败后又同时重试,导致谁都获取不了锁(脑裂)。而且为了防止出现脑裂,客户端获取锁的时间越快越好,所以最好的方式是并发的去请求所有的Redis实例获取单个实例锁。获取锁失败后,也需要尽快的释放锁住的Redis实例。
Redis作者证明了Redlock的安全性,在工程运用上,Redlock应该是没有问题的。但是因为Redis是一个内存服务,还存在主从同步等数据一致性的问题,所以比起zookeeper还是稍有逊色。
总结
如果非常关心分布式锁的安全性和可靠性,zookeeper、etcd等强一致性服务是首选。Redis资源因为获取简单,在一般情况下也可以选择。Memcached问题较多,不建议使用。