一、分布式锁的作用:

同一时间只允许一个用户操作,

一般情况下,我们使用分布式锁主要有两个场景:

  1. 避免不同节点重复相同的工作:比如用户执行了某个操作有可能不同节点会发送多封邮件;
  2. 避免破坏数据的正确性:如果两个节点在同一条数据上同时进行操作,可能会造成数据错误或不一致的情况出现

二、分布式锁的实现方式

  1. 基于 MySQL 中的锁:MySQL 本身有自带的悲观锁 ​​for update​​ 关键字,也可以自己实现悲观/乐观锁来达到目的;
  2. 基于 Zookeeper 有序节点:Zookeeper 允许临时创建有序的子节点,这样客户端获取节点列表时,就能够当前子节点列表中的序号判断是否能够获得锁;
  3. 基于 Redis 的单线程:由于 Redis 是单线程,所以命令会以串行的方式执行,并且本身提供了像 ​​SETNX(set if not exists)​​ 、Expire这样的指令,本身具有互斥性(因为SETNX和EXPIRE组合不是原子的,因此出现了SET key value EX 6 NX指令是原子的);

三、redis分布式锁的问题

(1)锁超时:

假设现在我们有两台平行的服务 A B,其中 A 服务在 获取锁之后 突然 挂了,那么 B 服务就永远无法获取到锁了:

如果在加锁和释放锁之间的逻辑执行得太长,以至于超出了锁的超时限制,也会出现问题。因为这时候第一个线程持有锁过期了,而临界区的逻辑还没有执行完,与此同时第二个线程就提前拥有了这把锁,导致临界区的代码不能得到严格的串行执行。因此Redis 分布式锁不要用于较长时间的任务

GC可能引发的问题:服务 A 获取了锁并设置了超时时间,但是服务 A 出现了 STW 且时间较长,导致了分布式锁进行了超时释放,在这个期间服务 B 获取到了锁,待服务 A STW 结束之后又恢复了锁,这就导致了 服务 A 和服务 B 同时获取到了锁,这个时候分布式锁就不安全了。

(2)单点和多点宕机问题对分布式锁的影响:

如果 Redis 采用单机部署模式也就是单点锁,那就意味着当 Redis 故障了,就会导致整个服务不可用。

而如果采用主从模式部署,我们想象一个这样的场景:服务 A 申请到一把锁之后,如果作为主机的 Redis 宕机了,那么 服务 B 在申请锁的时候就会从 从机那里获取到这把锁,从而导致服务AB同时获取了锁,为了解决这个问题,Redis 作者提出了一种 RedLock 红锁 的算法 (Redission 同 Jedis)

四、redLock红锁

一种基于 Redis 实现分布式锁的方式。

比原先的单节点的方法更安全。它可以保证以下特性:

  1. 安全特性:互斥访问,即永远只有一个 client 能拿到锁
  2. 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
  3. 容错性:只要大部分 Redis 节点存活就可以正常提供服务

redlock算法:

假设有5个完全独立的redis主服务器,为了获得锁,client 会进行如下操作

1.获取当前时间戳

2.client尝试顺序的使用相同的key,value申请所有redis服务的锁,在获取锁的过程中的获取时间比锁过期时间timeout短很多,是为了不要过长时间的等待已经关闭的redis服务。并且试着获取下一个redis实例。

   比如:timeout为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁

3.当 client 在大于等于 3 个 master 上成功申请到锁的时候,会计算申请锁消耗了多少时间,这部分消耗的时间采用获得锁的当下时间戳减去第一步获得的时间戳得到,如果申请花费的时间小于timeout的时间,那么锁就真正获取到了。

4.如果成功获取锁,则锁的真正有效时间是 timeout时间减去申请锁花费的时间;比如:timeout是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);

5.如果 client 申请分布式锁失败了,那么它就会在少部分申请成功锁的 master 节点上执行释放锁的操作,重置状态

6.失败重试:如果一个 client 申请锁失败了,那么它需要稍等一会再重试避免多个 client 同时申请锁的情况,另外就是如果 client 申请锁失败了它需要尽快在它曾经申请到锁的 master 上执行 unlock 操作,便于其他 client 获得这把锁,避免这些锁过期造成的时间浪费

7.崩溃恢复:如果我们的节点没有持久化机制,client 从 5 个 master 中的 3 个处获得了锁,然后其中一个重启了,这是注意 整个环境中又出现了 3 个 master 可供另一个 client 申请同一把锁! 违反了互斥性。

如果我们开启了 AOF 持久化那么情况会稍微好转一些,因为 Redis 的过期机制是语义层面实现的,所以在 server 挂了的时候时间依旧在流逝,重启之后锁状态不会受到污染。但是考虑断电之后呢,AOF部分命令没来得及刷回磁盘直接丢失了,除非我们配置刷回策略为 fsnyc = always,但这会损伤性能。

解决这个问题的方法是,当一个节点重启之后,我们规定在 max timeout期间它是不可用的(不可以被获取锁的),这样它就不会干扰原本已经申请到的锁,等到它 crash 前的那部分锁都过期了,环境不存在历史锁了,那么再把这个节点加进来正常工作。