简单易懂的分布式锁实现方案

为什么需要分布式锁

为了保证一个资源再同一时间只有一个线程访问,传统单机部署的模式下,可以使用java自带的并发包来控制线程间的互斥。但是随着业务逐渐发展,原单机部署的系统演化成分布式系统后,使得原单机部署情况下的并发控制策略失效。为了解决这一问题就,就需要提供一种跨JVM,适用于分布式系统的互斥机制,这就是分布式锁要解决的问题。

MySQL分布式锁

假设在我们的项目在创建初期,并没有引入其他中间件。就可以使用MySQL来完成分布式锁。

MySQL分布式锁主要有两种实现方式:

悲观锁

具体实现是我们会加一个数据库级别的select ... for update排他锁。当某个client的事务成功获取到数据后,就代表成功获取锁。并阻塞其他想要获取锁的client事务。

这个做法其实很简单,但缺点也比较明显。首先是高并发环境下,会占用MySQL珍贵的连接资源,耗费CPU。并且如果某个获取锁的client在释放锁前宕机,就会发生死锁,导致其他获取资源的client不可用。而对于MySQL而言,锁的不释放意味着长时间占用mysql连接池资源,影响mysql并发执行效率。

乐观锁

乐观锁是基于版本号以及轮询+CAS的思想创建的锁。

在事务执行前,我们先查出锁的版本。然后执行我们的业务逻辑。最后我们根据事务开始时查出的锁版本,去更新锁的新版本。若版本不一致,则更新失败,代表本次加锁失败。若更新成功,则代表本次加锁成功。

由此可见,乐观锁的实现方式并不是基于数据库的锁来实现的。因此不会产生死锁问题。

但是问题也很明显,我们每个client都会频繁的请求数据库,造成性能的严重消耗。并且由于我们业务代码的耗时时间不一样,因此抢到锁的概率也无法收到保证。因此MySQL实现分布式锁的缺点是相当多的。市面几乎没有公司会去用MySQL实现分布式锁。

Redis分布式锁

Redis最普通的分布式锁

SET resource_name my_random_value NX PX 30000
  • NX:表示只有 key 不存在的时候才会设置成功。(如果此时 redis 中存在这个 key,那么设置失败,返回 nil)
  •  PX 30000:意思是 30s 后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。

假设有三个服务器,服务器A,B,C。假设服务器A获取到了锁,那么服务器B和C只能通过轮询+cas的方式进行获取锁。

如果A服务器获取到锁,并且因为各种原因导致持有锁的时间超过了我们设置的redis过期时间,那么A此时redis锁就会失效。其他服务器就再次拥有了获取锁资源的资格,假设B服务器此时获取到了锁。在服务器B执行期间,假设服务器A忙忘完了,开始执行主动释放锁操作了。此时就会释放掉服务器B锁持有的redis锁。为了避免这种情况出现,我们会在加锁的时候会设置一个随机值。在释放锁的时候会与加锁时的随机值进行对比。保证每个服务器只负责释放自己加的锁。最后,为了保证操作的原子性,我们的删除锁都会基于lua脚本进行操作。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

我们发现,这个是基于单机的redis锁的实现。我们知道主从复制,或者集群脑裂都有可能导致数据丢失的情况。为了解决这个问题,redis还有第二种实现分布式锁的方法。

多节点redis实现的分布式锁(RedLock)

这个锁的算法实现了多redis实例的情况,相对于单redis节点来说,优点在于防止了单节点数据丢失的情况。

实现原理是这样的:假设有5个完全独立的redis主服务器

  1. 获取当前时间戳,单位是毫秒;
  2. 轮流尝试在每个 master 节点上创建锁,并根据当前时间戳,推算出一个加锁超时时间;
  3. 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点 n / 2 + 1;
  4. 如果建立锁的时间小于超时时间,就算建立成功了;
  5. 要是锁建立失败了,那么就依次之前建立过的锁删除;
  6. 当某个服务器成功使用ReadLock算法创建了reids分布式锁后,其他服务器就有需要使用轮询+cas的方式去获取锁。

我们发现,这种一redLock算法建立的锁。在集群中的多个master提供了冗余数据,从而保证了锁的可靠性,但是这种锁的实现方式强依赖于redis集群,成本较高。并且在查看了redlock官网后,这个算法貌似本身也是存在问题的。最关键的是无论用任何一种redis的模式来创建分布锁,似乎总太不过未成果获取锁的服务器总需要使用轮询+cas的这种比较消耗CPU的方式去获取锁,也没有办法解决某个持有锁的client宕机后,只有硬等该redis过期后,别的client才能具备获取锁资格问题。

zk分布式锁

zk 分布式锁,其实可以做的比较简单。就是所有的客户端会尝试创建临时 znode,此时创建成功了,就获取了这个锁;(为什么说是临时节点呢?假设某个client持有锁后发生了宕机,那么zk中的临时节点也会随之销毁,从而释放锁。这解决了redis只能通过过期机制来释放锁的问题)。当某个client成功创建锁后,其他client来创建锁会失败,失败不要紧,他们会顺便在zk中注册个监听器监听这个锁。

释放锁就是持有锁的客户端在zk中删除这个 znode。一旦释放掉就会通知其他注册了监听的客户端,然后其他客户端会开始进行抢锁操作。

那么这样就会有一个问题,当锁释放的时候会通知所有注册监听的客户端,假设现在监听的客户端有上千个呢?

这对zk而言就是一个很大的一个挑战,一个释放锁,就可能唤醒大量的客户端来竞争锁。这也是我们常说的羊群效应。会占用服务资源,网络带宽,甚至有可能让服务器宕机的风险。

创建临时顺序节点

因此,我们往往会创建临时顺序节点。也就是如果有一把锁,被多个人给竞争,那么来访问的客户端就会根据访问顺序排队。后面的每个客户端会去监听自己前面的客户端状态。一旦前面的客户端释放锁,那么排在后面的客户端就会被zk通知。我们发现,这种形式由原来的批量唤醒,改为了顺序唤醒。这个模式与多线程AQS框架内部维护的队列有异曲同工之妙。

redis 分布式锁和 zk 分布式锁的对比

  • redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
  • redis分布式锁,有一个比较致命的缺点,就是万一某个获取锁的服务在自己释放锁前宕机了,那么其他服务器只能等到redis锁过期后,才能获取锁资源。为了解决这个问题,redis还提供了一个RedLock的解决方案。
  • zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。
  • zk 分布式锁,仅创建临时znode,当某个持有锁的client宕机后,zk的中的临时znode也会随之销毁。避免了长时间无效的占用锁。

同时,普通redis分布式锁的实现方式,如果某个持有锁的client宕机,那么释放锁只能等待redis过期才能释放锁。而redLock算法实现的也需要依赖redis集群实现。因此,基于以上两点,我个人实践认为 zk 的分布式锁比 redis 的分布式锁牢靠、而且模型简单易用。

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大将黄猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值