背景:分布式锁本质上就是在redis里面占一个坑,当别的进程进来的时候,发现坑里有人了,就放弃或者等待。
分布式锁
分布式锁实现一
redis核心指令:setnx(set if not exists) 更多说明
操作代码:
> setnx lock:codehole true
OK
> expire lock:codehole 5
... do something critical ...
> del lock:codehole
(integer) 1
存在问题:
setnx 和 expire 执行之间服务出现问题,会导致expire不被执行,造成死锁。本质上是因为这俩个指令执行不是原子性。
图例说明
解决方案:作者在后续的版本中对setnx指令添加 expire 指令参数。
> set lock:codehole true ex 5 nx
OK
... do something critical ...
> del lock:codehole
SET 指令解析 SET
EX second
:设置键的过期时间为second
秒。SET key value EX second
效果等同于SETEX key second value
。PX millisecond
:设置键的过期时间为millisecond
毫秒。SET key value PX millisecond
效果等同于PSETEX key millisecond value
。NX
:只在键不存在时,才对键进行设置操作。SET key value NX
效果等同于SETNX key value
。XX
:只在键已经存在时,才对键进行设置操作。
实现三大要点:
-
set命令要用set key value px milliseconds nx;
-
value要具有唯一性;
-
释放锁时要验证value值,不能误解锁;
引入问题
超时问题:服务的过长影响其他系统处理能力或者过短造成线程并发
一种方式是:添加value随机数,谁加锁,谁来解。存在问题是判断value值和del不是原子的,还会失败,所以要使用lua脚本解决方案。
主从问题:redis主从复制机制导致数据丢失,会出现如下场景:A 线程获得了锁,但锁数据还未同步到 slave 上,master 挂了,slave 顶成主,线程 B 尝试加锁,仍然能够成功,造成 A、B 两个线程并发访问同一个资源。
分布式锁实现二
结合导读链接效果更佳
Redlock 源码分析
前置说明:引入redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.3.2</version>
</dependency>
扩展阅读
如何预防死锁
- Client 定时向 Server 发送心跳包,Server 收到心跳包之后,维护 Server 端 Session 并立即回复,Client 收到心跳包响应后,维护 Client 端 Session。心跳包同时承担了延长 Session 租约的功能。
- 当锁持有方发生故障时,Server 会在 Session 租约到期后,自动删除该 Client 持有的锁,以避免锁长时间无法释放而导致死锁。Client 会在 Session 租约到期后,进行回调,可选择性的决策是否要结束对当前持有资源的访问。
- 对于未设置过期的锁,也就意味着无法通过租约自动释放故障 Client 持有的锁。因此额外提供了一种协商机制,在加锁的时候传递一些 condition 到服务端,用于约定 Client 端期望 Server 端对异常情况的处理,包括什么情况下能够释放锁。譬如可以通过这种机制实现 Server 端在未收到十个心跳请求后自动释放锁,Client 端在未收到五个心跳响应后主动结束对共享资源的访问。
- 尽最大程度保证锁被加锁进程主动释放。
a)进程正常关闭时调用钩子来尝试释放锁。
b)未释放的锁信息写文件,进程重启后读取锁信息,并尝试释放锁。
如何确保锁的安全性
1. 尽量不打破谁加锁谁解锁的约束,尽最大程度保证锁被加锁进程主动释放。a)进程正常关闭时调用钩子来尝试释放锁。
b)未释放的锁信息写文件,进程重启后读取锁信息,并尝试释放锁。
2. 依靠自动续约来维持锁的持有状态,在正常情况下,客户端可以持有锁任意长的时间,这可以确保它做完所有需要的资源访问操作之后再释放锁。一定程度上防止如下情况发生。
a)线程 A 获取锁,进行资源访问。
b)锁已经过期,但 A 线程未执行完成。
c)线程 B 获得了锁,导致同时有两个线程在访问共享资源。
3. 提供一种安全检测机制,用于对安全性要求极高的业务场景。
a)对于同一把锁,每一次获取锁操作,都会得到一个全局增长的版本号。
b)对外暴露检测 API checkVersion(lock_name,version),用于检测持锁进程的锁是不是已经被其他进程抢占(锁已经有了更新的版本号)。
c)加锁成功的客户端与后端资源服务器通信的时候可带上版本号,后端资源服务器处理请求前,调用 checkVersion 去检查锁是否依然有效。有效则认为此客户端依旧是锁的持有者,可以为其提供服务。
d)该机制能在一定程度上解决持锁 A 线程发生故障,Server 主动释放锁,线程 B 获取锁成功,A 恢复了认为自己仍旧持有锁而发起修改资源的请求,会因为锁的版本号已经过期而失败,从而保障了锁的安全性。