在这个技术不断更新迭代的情况下,分布式这个概念,在企业中的权重越来越高!谈及分布式时,不可避免一定会提到分布式锁,现阶段分布式锁的实现方式主流的有三种实现方式,Zookeeper
、DB
、Redis
,我们本篇文章以Redis为例!
从我们的角度来看,这三个属性是有效使用分布式锁所需的最低保证。
我们使用Redis锁定资源的最简单方法是:
乍一看,似乎并没有什么问题。但是不妨我们深究一下,这种实现方案在redis单机环境下似乎并没有什么问题!但是如果节点宕了呢?好吧,那么让我们添加一个slave
节点!如果主服务器宕机了,就使用这个节点!但是我们不妨来看看她真的能保证可用吗?
在谈论这个的致命缺陷时,我们需要了解一个知识点,Redis复制是异步的。
slave
晋升为master
。显然,这样是不对的,主节点因为没来得及同步数据就宕机了,所以从节点没有该数据,从而造成分布式锁的失效,那么作者antirez
的观点是如何解决这个呢?
作者认为,我们应该使用多个Redis
,这些节点是完全独立的,不需要使用复制或者任何协调数据的系统,多个redis系统获取锁的过程就变成了如下步骤:
设置的有效时间-获取锁花费的时间
Martin Kleppmann发表文章任务,Redlock并不能保证该锁的安全性!
他认为锁的用途无非两种
- 提升效率,用锁来保证一个任务没有必要被执行两次。比如(很昂贵的计算)
- 保证正确,使用锁来保证任务按照正常的步骤执行,防止两个节点同时操作一份数据,造成文件冲突,数据丢失。
对于第一种原因,我们对锁是有一定宽容度的,就算发生了两个节点同时工作,对系统的影响也仅仅是多付出了一些计算的成本,没什么额外的影响。这个时候 使用单点的 Redis 就能很好的解决问题,没有必要使用RedLock,维护那么多的Redis实例,提升系统的维护成本。
但是对于第二种场景来说,就比较慎重了,因为很可能涉及到一些金钱交易,如果锁定失败,并且两个节点同时处理同一数据,则结果将导致文件损坏,数据丢失,永久性不一致,或者金钱方面的损失!
我们假设一种场景,我们有两个客户端,每一个客户端必须拿到锁之后才能去保存数据到数据库,我们使用RedLock算法实现会出现什么问题呢?RedLock中,为了防止死锁,锁是具有过期时间的,但是Martin
认为这是不安全的!该流程图类似于这样!
客户端1获取到锁成功后,开始执行,执行到一半系统发生Full GC ,系统服务被挂起,过段时间锁超时了。
客户端2等待客户端1的锁超时后,成功的获取到锁,开始执行入库操作,完成后,客户端1完成了Full GC,又做了一次入库操作!这是不安全的!如何解决呢?
Martin
提出来一种类似乐观锁的实现机制,示例图如下:
客户端1长时间被挂起后,客户端2获取到锁,开始写库操作,同时携带令牌 34
,写库完成后,客户端1苏醒,开始进行入库操作,但是因为携带的令牌为33 小于最新令牌,该次提交就被拒绝!
这个想法听起来似乎时很完备的思路,这样即使系统因为某些原因被挂起,数据也能够被正确的处理。但是仔细想一下:
回想一下Redlock算法
获取锁的几个步骤,你会发现锁的有效性是与当前的系统时钟强依赖,我们假设:
我们有,A B C D E 五个redis节点:
如果C在将锁持久保存到磁盘之前崩溃并立即重新启动,则可能会发生类似的问题。
Martin认为系统时间的阶跃主要来自两个方面(以及作者给出的解决方案):
我们回顾 1 观点,深究抽象出现这个缺陷的根本原因,就是为了解决由于系统宕机带来的锁失效而给锁强加了一个失效时间,异常情况下,程序(业务)执行的时间大于锁失效时间从而造成的一系列的问题,我们能否从这方面去考虑,从而用程序来解决这个样一个死局 呢?
既然是因为锁的失效时间小于业务时间,那么我们想办法保证业务程序执行时间绝对小于锁超时时间不久解决了?
java语言中redisson
实现了一种保证锁失效时间绝对大于业务程序执行时间的机制。官方叫做看门狗机制(Watchdog),他的主要原理是,在程序成功获取锁之后,会fork一条子线程去不断的给该锁续期,直至该锁释放为止!他的原理图大概如下所示:
redisson使用守护线程来进行锁的续期,(守护线程的作用:当主线程销毁,会和主线程一起销毁。)防止程序宕机后,线程依旧不断续命,造成死锁!
另外,Redisson还实现并且优化了 RedLock算法、公平锁、可重入锁、连锁等操作,使Redis分布式锁的实现方式更加简便高效!
才疏学浅,如果文章中理解有误,欢迎大佬们私聊指正!欢迎关注作者的公众号,一起进步,一起学习!