不积跬步,无以至千里;不积小流,无以成江海。
分布式锁
分布式锁 是控制 分布式系统 之间 同步 访问 共享资源 的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间 共享 了一个或一组资源,那么访问这些资源的时候,往往需要 互斥 来防止彼此干扰来保证 一致性,在这种情况下,便需要使用到分布式锁。
简单一点:分布式锁的本质其实就是先占一个 “坑”位,当别的进程进来也要持有时,发现已经被占用,而不得不放弃。
抢锁一般是使用 setnx (set if not exists) 指令,只允许被一个客户端抢锁。先来先占, 用完了,再调用 del 指令释放锁。
<code>> setnxlock
:keytrue
OK ...do
something ... > dellock
:key (integer)1
/<code>
但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入 死锁
,锁永远得不到释放。于是我们在拿到锁之后,再给锁加上一个 过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。
<code>> setnxlock
:keytrue
OK > expirelock
:key5
...do
something ... > dellock
: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
:keytrue
ex5
nx OK ...do
something ... > dellock
:key/<code>
指令 setnx 和 expire 组合在一起的原子指令,它就是单节点分布式锁的奥义所在。
锁超时问题
如下执行顺序:
- 客户端1获取锁成功。
- 客户端1在某个操作上 阻塞 了很长时间。
- 过期时间到了,锁 自动 释放了。
- 客户端2获取到了对应同一个资源的锁。
- 客户端1从阻塞中恢复过来,释放掉了客户端2持有的锁。
之后,客户端2在访问共享资源的时候,就没有锁为它提供保护了。
这时 为 key 的 value 设置一个 随机数 很有必要的,释放锁时先匹配随机数是否一致,然后再删除 key,它保证了 一个客户端 释放的锁必须是 自己 持有的那个锁。
切记:释放锁的操作 必须 使用 Lua脚本 来实现。因为释放锁其实包含三步操作:’GET’、判断 和 ’DEL’,用Lua脚本来实现能保证这三步的 原子性。否则,如果把这三步操作放到客户端逻辑中去执行的话,还有可能发生以上问题。
- 客户端1获取锁成功。
- 客户端1访问共享资源。
- 客户端1为了释放锁,先执行’GET’操作获取 随机字符串 的值。
- 客户端1判断随机字符串的值,与预期的值相等。
- 客户端1由于某个原因阻塞住了很长时间。
- 过期时间到了,锁自动释放了。
- 客户端2获取到了对应同一个资源的锁。
- 客户端1从阻塞中恢复过来,执行DEL操纵,释放掉了客户端2持有的锁。
实际上,如果不是客户端被阻塞住了,而是出现了大的 网络延迟,也有可能导致类似的执行序列发生。
这样使用也只是相对安全一点,现实中还要从自己的业务出发,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。
Redlock 算法
假如Redis节点 宕机 了,那么 所有 客户端就都无法获得锁了,服务变得不可用。为了提高 可用性,我们可以给这个Redis节点挂一个Slave,当Master节点不可用的时候,系统自动切到Slave上(failover)。但由于Redis的 主从复制(replication)是异步的,这可能导致在failover过程中丧失锁的安全性。考虑下面的执行序列:
- 客户端1从Master获取了锁。
- Master宕机了,存储锁的key还没有来得及同步到Slave上。
- Slave升级为Master。
- 客户端2从新的Master获取到了对应同一个资源的锁。
于是,客户端1和客户端2同时持有了同一个资源的锁。锁的安全性被打破。针对这个问题,antirez设计了 Redlock算法
为了使用 Redlock,需要提供多个 Redis 实例,这些实例之前相互独立没有主从关系。同很多分布式算法一样,redlock 也使用「大多数机制」。
加锁时,它会向过半节点发送 set(key, value, nx=True, ex=xxx)
指令,只要 过半 节点 set 成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过 Redlock 算法还需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock 需要向多个节点进行读写,意味着相比单实例 Redis 性能会下降一些。如果你很在乎高可用性,希望挂了一台 redis 完全不受影响,那就应该考虑 redlock。不过代价也是有的,需要更多的 redis 实例,性能也下降了,这些都是需要考虑的成本,使用前还请想清楚自己想要什么。
点关注 不迷路
以上就是这篇的全部内容,能看到这里的都是 人才。
如果你从本篇内容有收获,求 点赞,求 关注,求 转发 ,让更多的人学习到。
如果本文有任何错误,请批评指教,不胜感激 !