「Redis」redis 分布式锁

不积跬步,无以至千里;不积小流,无以成江海。

分布式锁

分布式锁 是控制 分布式系统 之间 同步 访问 共享资源 的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间 共享 了一个或一组资源,那么访问这些资源的时候,往往需要 互斥 来防止彼此干扰来保证 一致性,在这种情况下,便需要使用到分布式锁。

简单一点:分布式锁的本质其实就是先占一个 “坑”位,当别的进程进来也要持有时,发现已经被占用,而不得不放弃。

抢锁一般是使用 setnx (set if not exists) 指令,只允许被一个客户端抢锁。先来先占, 用完了,再调用 del 指令释放锁。

<code>> setnx 

lock

:key 

true

OK ... 

do

 something ... > del 

lock

:key (integer) 

1

/<code>

但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入 死锁

,锁永远得不到释放。

于是我们在拿到锁之后,再给锁加上一个 过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。

<code>> setnx 

lock

:key 

true

OK > expire 

lock

:key 

5

... 

do

 something  ... > del 

lock

:key (integer) 

1

/<code>

仔细想一下,以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。

这种问题的根源就在于 setnx 和 expire 是两条指令而不是 原子 指令(Wiki 解释:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch 线程切换。)

如果这两条指令可以一起执行就不会出现问题。也许你会想到用 Redis 事务来解决。但是这里不行,因为 expire 是依赖于 setnx 的执行结果的,如果 setnx 没抢到锁,expire 是不应该执行的。事务里没有 if-else 分支逻辑,事务的特点是一口气执行,要么全部执行要么一个都不执行。

Redis 2.8 以上版本中作者加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行,彻底解决了分布式死锁的问题。

SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX second :设置键的过期时间为second秒
  • PX millisecond :设置键的过期时间为millisecond毫秒
  • NX :只在键不存在时,才对键进行设置操作
  • XX:只在键已经存在时,才对键进行设置操作
  • SET操作成功完成时,返回OK ,否则返回nil
<code>> 

set

 

lock

:key 

true

 ex 

5

 nx OK ... 

do

 something  ... > del 

lock

:key/<code>

指令 setnx 和 expire 组合在一起的原子指令,它就是单节点分布式锁的奥义所在。

锁超时问题

如下执行顺序:

  1. 客户端1获取锁成功。
  2. 客户端1在某个操作上 阻塞 了很长时间。
  3. 过期时间到了,锁 自动 释放了。
  4. 客户端2获取到了对应同一个资源的锁。
  5. 客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。

之后,客户端2在访问共享资源的时候,就没有锁为它提供保护了。

这时 为 key 的 value 设置一个 随机数 很有必要的,释放锁时先匹配随机数是否一致,然后再删除 key,它保证了 一个客户端 释放的锁必须是 自己 持有的那个锁。

切记:释放锁的操作 必须 使用 Lua脚本 来实现。因为释放锁其实包含三步操作:’GET’、判断 和 ’DEL’,用Lua脚本来实现能保证这三步的 原子性。否则,如果把这三步操作放到客户端逻辑中去执行的话,还有可能发生以上问题。

  1. 客户端1获取锁成功。
  2. 客户端1访问共享资源。
  3. 客户端1为了释放锁,先执行’GET’操作获取 随机字符串 的值。
  4. 客户端1判断随机字符串的值,与预期的值相等。
  5. 客户端1由于某个原因阻塞住了很长时间。
  6. 过期时间到了,锁自动释放了。
  7. 客户端2获取到了对应同一个资源的锁。
  8. 客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。

实际上,如果不是客户端被阻塞住了,而是出现了大的 网络延迟,也有可能导致类似的执行序列发生。

这样使用也只是相对安全一点,现实中还要从自己的业务出发,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。

Redlock 算法

假如Redis节点 宕机 了,那么 所有 客户端就都无法获得锁了,服务变得不可用。为了提高 可用性,我们可以给这个Redis节点挂一个Slave,当Master节点不可用的时候,系统自动切到Slave上(failover)。但由于Redis的 主从复制(replication)是异步的,这可能导致在failover过程中丧失锁的安全性。考虑下面的执行序列:

  1. 客户端1从Master获取了锁。
  2. Master宕机了,存储锁的key还没有来得及同步到Slave上。
  3. Slave升级为Master。
  4. 客户端2从新的Master获取到了对应同一个资源的锁。

于是,客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。针对这个问题,antirez设计了 Redlock算法

为了使用 Redlock,需要提供多个 Redis 实例,这些实例之前相互独立没有主从关系。同很多分布式算法一样,redlock 也使用「大多数机制」。

加锁时,它会向过半节点发送 set(key, value, nx=True, ex=xxx)

指令,只要 过半 节点 set 成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过 Redlock 算法还需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock 需要向多个节点进行读写,意味着相比单实例 Redis 性能会下降一些。

如果你很在乎高可用性,希望挂了一台 redis 完全不受影响,那就应该考虑 redlock。不过代价也是有的,需要更多的 redis 实例,性能也下降了,这些都是需要考虑的成本,使用前还请想清楚自己想要什么。

点关注 不迷路

以上就是这篇的全部内容,能看到这里的都是 人才。

如果你从本篇内容有收获,求 点赞,求 关注,求 转发 ,让更多的人学习到。

如果本文有任何错误,请批评指教,不胜感激


分享到:


相關文章: