背景
分布式锁,是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。
分布式锁要满足哪些要求呢?
-
排他性:在同一时间只会有一个客户端能获取到锁,其它客户端无法同时获取
-
避免死锁:这把锁在一段有限的时间之后,一定会被释放(正常释放或异常释放)
-
高可用:获取或释放锁的机制必须高可用且性能佳
分布式锁的实现方式
目前主流的有三种,从实现的复杂度上来看,从上往下难度依次增加:
-
基于数据库实现
-
基于Redis实现
-
基于ZooKeeper实现
无论哪种方式,其实都不完美,依旧要根据咱们业务的实际场景来选择。
为什么用分布式锁?
在讨论这个问题之前,我们先来看一个业务场景:
系统A是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,该接口的流程是用户下订单之前一定要去检查一下库存,确保库存足够了才会给用户下单。
由于系统有一定的并发,所以会预先将商品的库存保存在redis中,用户下单的时候会更新redis的库存。
此时系统架构如下:
image但是这样一来会产生一个问题:假如某个时刻,redis里面的某个商品库存为1,此时两个请求同时到来,其中一个请求执行到上图的第3步,更新数据库的库存为0,但是第4步还没有执行。
而另外一个请求执行到了第2步,发现库存还是1,就继续执行第3步。
这样的结果,是导致卖出了2个商品,然而其实库存只有1个。
很明显不对啊!这就是典型的问题,产生这种问题实质是2 3 4步骤不是原子操作
此时,我们很容易想到解决方案:用锁把2、3、4步锁住(其实是把2步骤锁住),让他们执行完4步骤之后把锁解开,另一个线程才能进来执行第2步。这里又会涉及到另一个问题,数据库和缓存一致性的问题,即3 4步骤,这里不做讨论。
image按照上面的图,在执行第2步时,使用Java提供的synchronized或者ReentrantLock来锁住,然后在第4步执行完之后才释放锁。
这样一来,2、3、4 这3个步骤就被“锁”住了,多个线程之间只能串行化执行。
但是好景不长,整个系统的并发飙升,一台机器扛不住了。现在要增加一台机器,如下图:
image增加机器之后,系统变成上图所示,我的天!
假设此时两个用户的请求同时到来,但是落在了不同的机器上,那么这两个请求是可以同时执行了,还是会出现库存超卖的问题。
为什么呢?因为上图中的两个A系统,运行在两个不同的JVM里面,他们加的锁只对属于自己JVM里面的线程有效,对于其他JVM的线程是无效的。
因此,这里的问题是:Java提供的原生锁机制在多机部署场景下失效了
这是因为两台机器加的锁不是同一个锁(两个锁在不同的JVM里面)。
那么,我们只要保证两台机器加的锁是同一个锁,问题不就解决了吗?
此时,就该分布式锁隆重登场了,分布式锁的思路是:
在整个系统提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。
至于这个“东西”,可以是Redis、Zookeeper,也可以是数据库。
文字描述不太直观,我们来看下图:
image通过上面的分析,我们知道了库存超卖场景在分布式部署系统的情况下使用Java原生的锁机制无法保证线程安全,所以我们需要用到分布式锁的方案。
那么,如何实现分布式锁呢?接着往下看!
基于数据库实现分布式锁(悲观锁)
拿换领码换领优惠券按理来说
- 查该码状态是否已经被换取,如果未换取执行下一步
- 换取优惠券
- 更新该码状态
看似逻辑很正常,但如果用户同一时间并发提交换领码,在没有加锁限制的情况下,用户则可以使用同一个换领码同时兑换到多张优惠券。比如用户A在执行完第二步但还没执行第三步时,用户B也使用该码进行兑换,在第一步是查询到的是该码未被兑换。
所以在第一步的时候就要加锁。为代码如下
start transaction;
//加排他锁,有别的线程并发读的时候,此处就处于加锁状态(阻塞状态)
select state from coupon where number='11111143' for update;
....
换取优惠券
....
//更新状态码状态
update coupon set state=1 where number='1111143';
这样在第一步查询时就加锁,这样就可以防止同一码被多次兑换。如果使用正常的select state from coupon where number='11111143',在mvcc中读是不加锁的。如果使用共享锁select state from coupon where number='11111143' lock in share mode的话,并发时其他线程也是可以获得该共享锁的。所以这里只能用排他锁。
总结:如果第一步查询时是使用redis查询的话,就使用redis来实现分布式锁,如果是数据库查询时就可以使用数据库锁来实现。
基于Redis实现分布式锁
上面分析为啥要使用分布式锁了,这里我们来具体看看分布式锁落地的时候应该怎么样处理。
最常见的一种方案就是使用Redis做分布式锁
使用Redis做分布式锁的思路大概是这样的:在redis中设置一个值表示加了锁,然后释放锁的时候就把这个key删除。
具体代码是这样的:
这种方式有几大要点:
-
一定要用使用SET key value NX PX milliseconds 命令(原子操作)
如果不用这种方式,采用先设置了值,再设置过期时间,这个不是原子性操作,有可能在设置过期时间之前宕机,会造成死锁(key永久存在)。
这种方式一定要设置过期时间,同样也是避免死锁。如果没设置过期时间的情况下,机器(不是redis服务器,指api服务器)获取锁成功,但是删除锁时机器宕机,这是造成死锁,其他线程将一直获取不到该锁。
-
value要具有唯一性
这个是为了在解锁的时候,需要验证value是和加锁的一致才删除key。
这是避免了一种情况:假设A获取了锁,过期时间30s,此时35s之后,锁已经自动释放了,A去释放锁,但是此时可能B获取了锁。A客户端就不能删除B的锁了。
如果真出现A超过过期时间还没有执行完的情况,这里也会出现一个很严重的问题,也就是线程安全问题,同样会出现上述线程超卖的情况。所以要使用这种方式的话,过期时间一定要合理控制。
除了要考虑客户端要怎么实现分布式锁之外,还需要考虑redis的部署问题。
redis有3种部署方式:
-
单机模式
-
master-slave + sentinel选举模式
-
redis cluster模式
使用redis做分布式锁的缺点在于:如果采用单机部署模式,会存在单点问题,只要redis故障了。加锁就不行了。
采用master-slave模式,加锁的时候只对一个节点加锁,即便通过sentinel做了高可用,但是如果master节点故障了,发生主从切换,此时就会有可能出现锁丢失的问题。
如果采用redis cluster模式,同样会有上述问题,加锁会先对集群中的某个主节点加锁,然后异步复制到其备节点。如果还没复制到备节点时,主节点宕机,这时备节点上选举为主节点。这把锁已经丢失。
所以这种方式有以下俩个缺点:
- 会有锁丢失的问题
- set key value nx px 3000 如果3000毫秒之后业务逻辑还没执行完,key过期,锁释放。其他线程获取到锁。这样一来也会造成线程安全问题。
php代码实现如下:模拟下单间库存,避免库存超卖现象
public function lock(Request $request, Response $response)
{
$data = $this->validate($request, [
'shop_id' => 'required',
'num' => 'required'
]);
$lockState = false;
$lockValue = microtime(true);
while (!$lockState) {
//加锁 set aaa 2324242 px 60000 nx
$lockState = Redis::set($data['shop_id'] . "lock", $lockValue, 'px', 60000, 'nx');
//避免重复无用循环,浪费cpu等资源
sleep(1)
}
//获取库存缓存,要预热,或者后台添加商品时添加缓存
$inventory = Redis::get($data['shop_id'] . "sku");
if ($inventory <= 0) {
//库存为0 已经卖完
return $response->setStatusCode(401);
}
// 下单
//修改库存,涉及到双写一致问题
try {
//加事务
DB::beginTransaction();
//修改数据库库存
$updateInventory = Shop::where('id', $data['shop_id'])->decrement('inventory', $data['num']);
if ($updateInventory) {
//库存存入
Redis::decrby($data['shop_id'] . "sku", $data['num']);
}
DB::commit();
} catch (\Exception $e) {
$this->addError('sku error', ['error' => $e->getMessage()]);
DB::rollBack();
}
//解锁 用到了get和delete操作 使用lua脚本
Redis::eval(file_get_contents(storage_path("app/lock.lua")), 1, $data['shop_id'] . "lock", $lockValue);
}
lua脚本如下
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
要注意:获取锁是一个阻塞的过程,如果锁被线程A占用,这时线程B来获取时会一直处于阻塞状态,直到线程A释放锁时线程B才会获取锁。
使用这种方法需要注意几个问题:
-
首先第一个问题,这个锁必须要设置一个过期时间。否则的话,当一个客户端获取锁成功之后,假如它崩溃了,或者由于发生了网络分割(network partition)导致它再也无法和Redis节点通信了,那么它就会一直持有这个锁,而其它客户端永远无法获得锁了。antirez在后面的分析中也特别强调了这一点,而且把这个过期时间称为锁的有效时间(lock validity time)。获得锁的客户端必须在这个时间之内完成对共享资源的访问。
-
第二个问题,第一步获取锁的操作,网上不少文章把它实现成了两个Redis命令:
SETNX resource_name my_random_value
EXPIRE resource_name 30
虽然这两个命令和前面算法描述中的一个SET
命令执行效果相同,但却不是原子的。如果客户端在执行完SETNX
后崩溃了,那么就没有机会执行EXPIRE
了,导致它一直持有这个锁。
- 第三个问题,也是antirez指出的,设置一个随机字符串
my_random_value
是很有必要的,它保证了一个客户端释放的锁必须是自己持有的那个锁。假如获取锁时SET
的不是一个随机字符串,而是一个固定值,那么可能会发生下面的执行序列:
-
客户端1获取锁成功。
-
客户端1在某个操作上阻塞了很长时间。
-
过期时间到了,锁自动释放了。
-
客户端2获取到了对应同一个资源的锁。
-
客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。
之后,客户端2在访问共享资源的时候,就没有锁为它提供保护了。
- 第四个问题,释放锁的操作必须使用Lua脚本来实现。释放锁其实包含三步操作:'GET'、判断和'DEL',用Lua脚本来实现能保证这三步的原子性。否则,如果把这三步操作放到客户端逻辑中去执行的话,就有可能发生与前面第三个问题类似的执行序列:
- 客户端1获取锁成功。
-
客户端1访问共享资源。
-
客户端1为了释放锁,先执行'GET'操作获取随机字符串的值。
-
客户端1判断随机字符串的值,与预期的值相等。
-
客户端1由于某个原因阻塞住了很长时间。
-
过期时间到了,锁自动释放了。
-
客户端2获取到了对应同一个资源的锁。
*客户端1从阻塞中恢复过来,执行DEL
操纵,释放掉了客户端2持有的锁。
实际上,在上述第三个问题和第四个问题的分析中,如果不是客户端阻塞住了,而是出现了大的网络延迟,也有可能导致类似的执行序列发生。
基于以上的考虑,其实redis的作者也考虑到这个问题,他提出了一个RedLock算法
假设redis的部署模式是redis cluster,总共有5个master节点,通过以下步骤获取一把锁:
-
获取当前时间戳,单位是毫秒
-
它尝试按顺序获取所有5个实例中的锁,在所有实例中使用相同的键名和随机值,并设置一个锁的过期时间。在步骤2期间,客户端使用与锁的过期时间相比较小的时间以获取锁,这个时间叫做的锁的获取时间。例如,如果过期时间是10秒,则锁的获取时间可以在~5-50毫秒范围内(如果超时则获取失败,从而尝试获取下个锁)。这可以防止客户端长时间保持阻塞,试图与Redis节点进行通信,如果实例不可用,我们应该尝试尽快与下一个实例通话。
-
客户端通过从当前时间中减去在步骤1中获得的时间戳来计算锁的获取时间。当且仅当客户端能够在大多数实例中获取锁时(至少3个)并且获取锁时间小于锁定锁的过期时间,认为锁定被获取。
-
如果获得了锁,则其有效时间被认为是锁的过期时间减去获取锁时间,如步骤3中计算的。
-
如果客户端由于某种原因(无法锁定5 / 2 + 1实例或有效时间为负)未能获取锁定,它将尝试解锁所有实例(即使它认为不是能够锁定)。
-
只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁
当然,上面描述的只是获取锁的过程,而释放锁的过程比较简单:客户端向所有Redis节点发起释放锁的操作,不管这些节点当时在获取锁的时候成功与否。
具体步骤可参考redis 官网
但是这样的这种算法还是颇具争议的,可能还会存在不少的问题,无法保证加锁的过程一定正确。
imageredLock锁真的万无一失吗?下面是需要思考的几个问题
-
未解决执行时间大于过期时间问题
单节点时存在如果线程A的执行时间或者发生阻塞以至于大于锁的加锁时间,这时线程A还没有执行完,由于锁已经自动释放,这时线程B获得锁。同一资源被俩线程执行,出现安全问题。redLock同样未解决该问题,所以锁的过期时间时一个很关键的问题。 -
RedLock性能及崩溃恢复的相关解决方法
假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:-
客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
-
节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。
-
节点C重启后,客户端2锁住了C, D, E,获取锁成功。
这样,客户端1和客户端2同时获得了锁(针对同一资源)。在默认情况下,Redis的AOF持久化方式是每秒写一次磁盘(即执行fsync),因此最坏情况下可能丢失1秒的数据。为了尽可能不丢数据,Redis允许设置成每次修改数据都进行fsync,但这会降低性能。当然,即使执行了fsync也仍然有可能丢失数据(这取决于系统而不是Redis的实现)。所以,上面分析的由于节点重启引发的锁失效问题,总是有可能出现的。为了应对这一问题,antirez又提出了
延迟重启
(delayed restarts)的概念。也就是说,一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,这段时间应该大于锁的有效时间(lock validity time)。这样的话,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。 -
-
基于故障转移实现的redis主从无法真正实现Redlock
假设redis cluster同样有5个master节点,A, B, C, D, E。设想发生了如下的事件序列:
-
客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住)。
-
节点C崩溃,并且崩溃前锁未同步到slave节点锁,slave节点升级为master节点,这时该master节点并没有加锁。
-
这时客户端2锁住了C, D, E,获取锁成功。
这样,客户端1和客户端2同时获得了锁(针对同一资源),导致资源共享,没有进行互斥。这个失败的原因是因为从redis立刻升级为主redis,如果能够过TTL时间再升级为主redis
(延迟升级)
后,或者立刻升级为主redis的节点只有过TTL的时间后再执行获取锁的任务,就能成功产生互斥效果;是不是这样就能实现基于redis主从的Redlock了 -
RedLock算法是否是异步算法?
可以看成是同步算法;因为 即使进程间(多个电脑间)没有同步时钟,但是每个进程时间流速大致相同;并且时钟漂移相对于TTL叫小,可以忽略,所以可以看成同步算法;(不够严谨,算法上要算上时钟漂移,因为如果两个电脑在地球两端,则时钟漂移非常大)
RedLock失败重试
当client不能获取锁时,应该在随机时间后重试获取锁;并且最好在同一时刻并发的把set命令发送给所有redis实例;而且对于已经获取锁的client在完成任务后要及时释放锁,这是为了节省时间;
RedLock释放锁
由于释放锁时会判断这个锁的value是不是自己设置的,如果是才删除;所以在释放锁时非常简单,只要向所有实例都发出释放锁的命令,不用考虑能否成功释放锁;
RedLock注意点(Safety arguments):
1.先假设client获取所有实例,所有实例包含相同的key和过期时间(TTL) ,但每个实例set命令时间不同导致不能同时过期,第一个set命令之前是T1,最后一个set命令后为T2,则此client有效获取锁的最小时间为TTL-(T2-T1)-时钟漂移;
2.对于以N/2+ 1(也就是一半以 上)的方式判断获取锁成功,是因为如果小于一半判断为成功的话,有可能出现多个client都成功获取锁的情况, 从而使锁失效
3.一个client锁定大多数事例耗费的时间大于或接近锁的过期时间,就认为锁无效,并且解锁这个redis实例(不执行业务) ;只要在TTL时间内成功获取一半以上的锁便是有效锁;否则无效
总结
1.TTL时长 要大于正常业务执行的时间+获取所有redis服务消耗时间+时钟漂移
2.获取redis所有服务消耗时间要 远小于TTL时间,并且获取成功的锁个数要 在总数的一般以上:N/2+1
3.尝试获取每个redis实例锁时的时间要 远小于TTL时间
4.尝试获取所有锁失败后 重新尝试一定要有一定次数限制
5.在redis崩溃后(无论一个还是所有),要延迟TTL时间重启redis
6.在实现多redis节点时要结合单节点分布式锁算法 共同实现
最后,Martin得出了如下的结论:
-
如果是为了效率(efficiency)而使用分布式锁,允许锁的偶尔失效,那么使用单Redis节点的锁方案就足够了,简单而且效率高。Redlock则是个过重的实现(heavyweight)。
-
如果是为了正确性(correctness)在很严肃的场合使用分布式锁,那么不要使用Redlock。它不是建立在异步模型上的一个足够强的算法,它对于系统模型的假设中包含很多危险的成分(对于timing)。而且,它没有一个机制能够提供fencing token。那应该使用什么技术呢?Martin认为,应该考虑类似Zookeeper的方案,或者支持事务的数据库。
另一种方式:Redisson
此外,实现Redis的分布式锁,除了自己基于redis client原生api来实现之外,还可以使用开源框架:Redission
Redisson是一个企业级的开源Redis Client,也提供了分布式锁的支持。我也非常推荐大家使用,为什么呢?
回想一下上面说的,如果自己写代码来通过redis设置一个值,是通过下面这个命令设置的。
- SET anyLock unique_value NX PX 30000
这里设置的超时时间是30s,假如我超过30s都还没有完成业务逻辑的情况下,key会过期,其他线程有可能会获取到锁。
这样一来的话,第一个线程还没执行完业务逻辑,第二个线程进来了也会出现线程安全问题。所以我们还需要额外的去维护这个过期时间,太麻烦了~
我们来看看redisson是怎么实现的?先感受一下使用redission的爽:
Config config = new Config();
config . useClusterServers()
. addNodeAddress("redis://192.168.31.101:7001")
. addNodeAddress("redis://192.168.31.101:7002")
. addNodeAddress("redis://192.168.31.101:7003")
. addNodeAddress("redis://192.168.31.101:7004")
. addNodeAddress("redis://192.168.31.101:7005")
. addNodeAddress("redis://192.168.31.101:7006")
RedissonClient redisson = Redisson.create(config)
RLock lock=redisson.getLock("anyLock");
lock.lock();
lock.unlock();
就是这么简单,我们只需要通过它的api中的lock和unlock即可完成分布式锁,他帮我们考虑了很多细节:
-
redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行
-
redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s怎么办?
redisson中有一个
watchdog
的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间重新延后30s这样的话,就不会出现业务没执行完而key过期了,其他线程获取到锁的问题了。
-
redisson的“看门狗”逻辑保证了没有死锁发生。
(如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁)
这里稍微贴出来其实现代码:
这里的代码请看下面参考连接中的
另外,redisson还提供了对redlock算法的支持,
它的用法也很简单:
这里的代码请看下面参考连接中的
小结:
本节分析了使用redis作为分布式锁的具体落地方案
以及其一些局限性
然后介绍了一个redis的客户端框架redisson,
这也是我推荐大家使用的,
比自己写代码实现会少care很多细节。
基于zookeeper实现分布式锁
常见的分布式锁实现方案里面,除了使用redis来实现之外,使用zookeeper也可以实现分布式锁。
在介绍zookeeper(下文用zk代替)实现分布式锁的机制之前,先粗略介绍一下zk是什么东西:
Zookeeper是一种提供配置管理、分布式协同以及命名的中心化服务。
zk的模型是这样的:zk包含一系列的节点,叫做znode,就好像文件系统一样每个znode表示一个目录,然后znode有一些特性:
-
有序节点:假如当前有一个父节点为
/lock
,我们可以在这个父节点下面创建子节点;zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号
也就是说,如果是第一个创建的子节点,那么生成的子节点为
/lock/node-0000000000
,下一个节点则为/lock/node-0000000001
,依次类推。 -
临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点。
-
事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:
-
节点创建
-
节点删除
-
节点数据修改
-
子节点变更
基于以上的一些zk的特性,我们很容易得出使用zk实现分布式锁的落地方案:
-
使用zk的临时节点和有序节点,每个线程获取锁就是在zk创建一个临时有序的节点,比如在/lock/目录下。
-
创建节点成功后,获取/lock目录下的所有临时节点,再判断当前线程创建的节点是否是所有的节点的序号最小的节点
-
如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。
-
如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听。
比如当前线程获取到的节点序号为
/lock/003
,然后所有的节点列表为[/lock/001,/lock/002,/lock/003]
,则对/lock/002
这个节点添加一个事件监听器。
如果锁释放了,会唤醒下一个序号的节点,然后重新执行第3步,判断是否自己的节点序号是最小。
比如/lock/001
释放了,/lock/002
监听到时间,此时节点集合为[/lock/002,/lock/003]
,则/lock/002
为最小序号节点,获取到锁。
整个过程如下:
image具体的实现思路就是这样,至于代码怎么写,这里比较复杂就不贴出来了。
Curator介绍
Curator是一个zookeeper的开源客户端,也提供了分布式锁的实现。
他的使用方式也比较简单:
InterProcessMutex interProcessMutex = new InterProcessMutex(client,"/anyLock");`
interProcessMutex.acquire();`
interProcessMutex.release();`
其实现分布式锁的核心源码如下:
这里的代码请看下下面的连接
其实curator实现分布式锁的底层原理和上面分析的是差不多的。这里我们用一张图详细描述其原理:
image小结:
本节介绍了zookeeperr实现分布式锁的方案以及zk的开源客户端的基本使用,简要的介绍了其实现原理。
两种方案的优缺点比较
学完了两种分布式锁的实现方案之后,本节需要讨论的是redis和zk的实现方案中各自的优缺点。
对于redis的分布式锁而言,它有以下缺点:
-
它获取锁的方式简单粗暴,获取不到锁直接不断尝试获取锁,比较消耗性能。
-
另外来说的话,redis的设计定位决定了它的数据并不是强一致性的,在某些极端情况下,可能会出现问题。锁的模型不够健壮
-
即便使用redlock算法来实现,在某些复杂场景下,也无法保证其实现100%没有问题,关于redlock的讨论可以看How to do distributed locking
-
redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
但是另一方面使用redis实现分布式锁在很多企业中非常常见,而且大部分情况下都不会遇到所谓的“极端复杂场景”
所以使用redis作为分布式锁也不失为一种好的方案,最重要的一点是redis的性能很高,可以支撑高并发的获取、释放锁操作。
对于zk分布式锁而言:
-
zookeeper天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。
-
如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。
但是zk也有其缺点:如果有较多的客户端频繁的申请加锁、释放锁,对于zk集群的压力会比较大。
小结:
综上所述,redis和zookeeper都有其优缺点。我们在做技术选型的时候可以根据这些问题作为参考因素。
作者的一些建议
通过前面的分析,实现分布式锁的两种常见方案:redis和zookeeper,他们各有千秋。应该如何选型呢?
就个人而言的话,我比较推崇zk实现的锁:
因为redis是有可能存在隐患的,可能会导致数据不对的情况。但是,怎么选用要看具体在公司的场景了。
如果公司里面有zk集群条件,优先选用zk实现,但是如果说公司里面只有redis集群,没有条件搭建zk集群。
那么其实用redis来实现也可以,另外还可能是系统设计者考虑到了系统已经有redis,但是又不希望再次引入一些外部依赖的情况下,可以选用redis。
这个是要系统设计者基于架构的考虑了
思考
加锁是为了资源不能同时被共享,那么队列是单个消费的,能保证资源不被共享。 那么能使用队列代替吗?
网友评论