7

《十个为什么》之五:为什么要有分布式锁?

 3 years ago
source link: https://my.oschina.net/watsonos/blog/5011882
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
《十个为什么》之五:为什么要有分布式锁? - 小木开源 - OSCHINA - 中文开源技术交流社区

我在《十个为什么》之三:为什么要有同步线程锁?中讨论了同步线程锁:synchronized,它是多线程不安全的解决办法——简单说就是用它来保证某段代码的串行化执行,避免并发带来的诸多问题。但是,当我们的项目不足以提供足够的性能时,最常用的方法就是使用集群的方式提高,通过增加部署的节点,来获得更大的处理能力。

        因为集群部署下的多个节点互相之间是隔离的,甚至他们都不在同一台服务器上,这时候的同步线程锁显然就无法控制多个节点上的同一段代码能够串行化执行了,可见同步线程锁的适用范围是一个JVM。这时候如果依然希望分布式下能够实现同步线程锁的效果,那么就需要使用到分布式锁了。

        通常使用 redis 来实现分布式锁,不仅因为 redis 是内存型数据库,性能可观,更主要的是 redis 是单线程的,每个 redis 的操作都是顺序执行的,不存在并发的可能性,使用 redis 来实现分布式锁是再适合不过了。

        使用 redis 实现分布式锁的原理非常简单, 基本都是使用 Redis Setnx(SET if Not eXists) 命令,它是在指定的 key 不存在时,为 key 设置指定的值。我们只需要在需要加锁的地方使用该命令来判断设置的结果就行,使用 setnx 时的 key 值必须能够代表当前业务的唯一值,这样,如果返回 true 则表示没有其他线程在操作此数据,否则表示此数据正被其他线程使用,回滚并退出方法,提示用户重新提交。可见,redis 实现分布式锁的原理其实只是给 redis 设置一个简单的数据,利用其单线程的特性,让应用中多个线程可以做基本的通信,从而决定是否继续执行代码而已。

        使用 setnx 后,需要在完成相关指令后删除该 key,否则会一直占用该数据的锁,使得该数据一直不可用;同时,为了避免因为重启或其他未知原因导致删除key失败,需要设置 key 的失效时间。

        如果仅仅是上述做法,只是完成了低配版的分布式锁,试想一下这样的场景,某次加锁失败,可能占用该锁的其他线程只需要1秒钟甚至更少的时间即可完成操作并返还锁了,可是我们却在此时返回失败给调用方,需要客户重新点击一次。如果我们能够减少这种失败返回,必然大大提高系统的可用性。这时,我们可以给出一个等待时间,将 setnx 操作设计成在等待时间内自旋——简单说就是:如果 setnx 返回 false 则继续 setnx,直到返回 true 或超出等待时间。

        以上便是分布式锁的实现原理,但是这里面依然存在一些问题,因为这个锁是不可以重入的(如果有重入的需求的话,就有问题),即使是拿到锁的线程,也不能对该锁声明第二次——如果说,我这个线程已经拿到了该锁,因为某些特别的需求而需要再拿一次锁时,现在的锁明明就在我的手上,我为什么不能重新拿一次呢?如果允许这么做的话,必然对编码更加友好。

        同时,因为我们设置的 key 会给出失效时间,所以被锁住的代码必须在失效时间内完成,否则再有其他线程去获取锁时会成功,这时并发就发生了。可见,设置失效时间有利也有弊,自行权衡。

        此外,分布式锁的简单实现通常用于实现接口的幂等性,实际加锁的业务才需要考虑权衡各方问题,使得代码更加健壮。

附《十个为什么》系列相关文章:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK