Redis实现分布式锁要注意哪些事项

67次阅读
没有评论

共计 4296 个字符,预计需要花费 11 分钟才能阅读完成。

本文丸趣 TV 小编为大家详细介绍“Redis 实现分布式锁要注意哪些事项”,内容详细,步骤清晰,细节处理妥当,希望这篇“Redis 实现分布式锁要注意哪些事项”文章能帮助大家解决疑惑,下面跟着丸趣 TV 小编的思路慢慢深入,一起来学习新知识吧。

Redis 实现分布式锁

最近看分布式锁的过程中看到一篇不错的文章,特地的加工一番自己的理解:

Redis 分布式锁实现的三个核心要素:

1. 加锁

最简单的方法是使用 setnx 命令。key 是锁的唯一标识,按业务来决定命名,value 为当前线程的线程 ID。

比如想要给一种商品的秒杀活动加锁,可以给 key 命名为“lock_sale_ID”。而 value 设置成什么呢?我们可以姑且设置成 1。加锁的伪代码如下:

setnx(key,1)当一个线程执行 setnx 返回 1,说明 key 原本不存在,该线程成功得到了锁,当其他线程执行 setnx 返回 0,说明 key 已经存在,该线程抢锁失败。

2. 解锁

有加锁就得有解锁。当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行 del 指令,伪代码如下:

del(key)释放锁之后,其他线程就可以继续执行 setnx 命令来获得锁。

3. 锁超时

锁超时是什么意思呢?如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程再也别想进来。

所以,setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。setnx 不支持超时参数,所以需要额外的指令,伪代码如下:

expire(key,30)综合起来,我们分布式锁实现的第一版伪代码如下:

if(setnx(key,1) == 1){
 expire(key,30) try {
 do something ......
 }catch() {} finally {
 del(key) }
}

因为上面的伪代码中,存在着三个致命问题:

1. setnx 和 expire 的非原子性

设想一个极端场景,当某线程执行 setnx,成功得到了锁:

setnx 刚执行成功,还未来得及执行 expire 指令,节点 1 Duang 的一声挂掉了。

if(setnx(key,1) == 1){ // 此处挂掉了.....
 expire(key,30) try {
 do something ......
 }catch()
 finally {
 del(key) }
 
}

这样一来,这把锁就没有设置过期时间,变得“长生不老”,别的线程再也无法获得锁了。

怎么解决呢?setnx 指令本身是不支持传入超时时间的,Redis 2.6.12 以上版本为 set 指令增加了可选参数,伪代码如下:set(key,1,30,NX), 这样就可以取代 setnx 指令。

2. 超时后使用 del 导致误删其他线程的锁

又是一个极端场景,假如某线程成功得到了锁,并且设置的超时时间是 30 秒。

如果某些原因导致线程 A 执行的很慢很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。

随后,线程 A 执行完了任务,线程 A 接着执行 del 指令来释放锁。但这时候线程 B 还没执行完,线程 A 实际上删除的是线程 B 加的锁。

怎么避免这种情况呢?可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。

至于具体的实现,可以在加锁的时候把当前的线程 ID 当做 value,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。

加锁:String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)doSomething.....
 
if(threadId .equals(redisClient.get(key))){ del(key)
}

但是,这样做又隐含了一个新的问题,if 判断和释放锁是两个独立操作,不是原子性。

我们都是追求极致的程序员,所以这一块要用 Lua 脚本来实现:

String luaScript = if redis.call(get , KEYS[1]) == ARGV[1] then return redis.call(del , KEYS[1]) else return 0 end

redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));

这样一来,验证和删除过程就是原子操作了。

3. 出现并发的可能性

还是刚才第二点所描述的场景,虽然我们避免了线程 A 误删掉 key 的情况,但是同一时间有 A,B 两个线程在访问代码块,仍然是不完美的。

怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。

当过去了 29 秒,线程 A 还没执行完,这时候守护线程会执行 expire 指令,为这把锁“续命”20 秒。守护线程从第 29 秒开始执行,每 20 秒执行一次。

当线程 A 执行完任务,会显式关掉守护线程。

另一种情况,如果节点 1 忽然断电,由于线程 A 和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。

memcache 实现分布式锁

首页 top 10, 由数据库加载到 memcache 缓存 n 分钟
微博中名人的 content cache, 一旦不存在会大量请求不能命中并加载数据库
需要执行多个 IO 操作生成的数据存在 cache 中, 比如查询 db 多次
问题
在大并发的场合,当 cache 失效时,大量并发同时取不到 cache,会同一瞬间去访问 db 并回设 cache,可能会给系统带来潜在的超负荷风险。我们曾经在线上系统出现过类似故障。

解决方法

if (memcache.get(key) == null) {
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
 
sleep(50);
retry();}

在 load db 之前先 add 一个 mutex key, mutex key add 成功之后再去做加载 db, 如果 add 失败则 sleep 之后重试读取原 cache 数据。为了防止死锁,mutex key 也需要设置过期时间。伪代码如下

Zookeeper 实现分布式缓存

Zookeeper 的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做 Znode。

Znode 分为四种类型:

1. 持久节点(PERSISTENT)

默认的节点类型。创建节点的客户端与 zookeeper 断开连接后,该节点依旧存在。

2. 持久节点顺序节点(PERSISTENT_SEQUENTIAL)

所谓顺序节点,就是在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号:

3. 临时节点(EPHEMERAL)

和持久节点相反,当创建节点的客户端与 zookeeper 断开连接后,临时节点会被删除:

4. 临时顺序节点(EPHEMERAL_SEQUENTIAL)

顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与 zookeeper 断开连接后,临时节点会被删除。

Zookeeper 分布式锁恰恰应用了临时顺序节点。具体如何实现呢?让我们来看一看详细步骤:

获取锁

首先,在 Zookeeper 当中创建一个持久节点 ParentLock。当第一个客户端想要获得锁时,需要在 ParentLock 这个节点下面创建一个临时顺序节点 Lock1。

之后,Client1 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock1 是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。

这时候,如果再有一个客户端 Client2 前来获取锁,则在 ParentLock 下载再创建一个临时顺序节点 Lock2。

Client2 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock2 是不是顺序最靠前的一个,结果发现节点 Lock2 并不是最小的。

于是,Client2 向排序仅比它靠前的节点 Lock1 注册 Watcher,用于监听 Lock1 节点是否存在。这意味着 Client2 抢锁失败,进入了等待状态。

这时候,如果又有一个客户端 Client3 前来获取锁,则在 ParentLock 下载再创建一个临时顺序节点 Lock3。

Client3 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock3 是不是顺序最靠前的一个,结果同样发现节点 Lock3 并不是最小的。

于是,Client3 向排序仅比它靠前的节点 Lock2 注册 Watcher,用于监听 Lock2 节点是否存在。这意味着 Client3 同样抢锁失败,进入了等待状态。

Redis 实现分布式锁要注意哪些事项

这样一来,Client1 得到了锁,Client2 监听了 Lock1,Client3 监听了 Lock2。这恰恰形成了一个等待队列,很像是 Java 当中 ReentrantLock 所依赖的 AQS(AbstractQueuedSynchronizer)。

Redis 实现分布式锁要注意哪些事项

Redis 实现分布式锁要注意哪些事项

释放锁

释放锁分为两种情况:

1. 任务完成,客户端显示释放

当任务完成时,Client1 会显示调用删除节点 Lock1 的指令。

Redis 实现分布式锁要注意哪些事项

2. 任务执行过程中,客户端崩溃

获得锁的 Client1 在任务执行过程中,如果 Duang 的一声崩溃,则会断开与 Zookeeper 服务端的链接。根据临时节点的特性,相关联的节点 Lock1 会随之自动删除。

Redis 实现分布式锁要注意哪些事项

由于 Client2 一直监听着 Lock1 的存在状态,当 Lock1 节点被删除,Client2 会立刻收到通知。这时候 Client2 会再次查询 ParentLock 下面的所有节点,确认自己创建的节点 Lock2 是不是目前最小的节点。如果是最小,则 Client2 顺理成章获得了锁。

Redis 实现分布式锁要注意哪些事项

同理,如果 Client2 也因为任务完成或者节点崩溃而删除了节点 Lock2,那么 Cient3 就会接到通知。

Redis 实现分布式锁要注意哪些事项

最终,Client3 成功得到了锁。

Redis 实现分布式锁要注意哪些事项

Redis 实现分布式锁要注意哪些事项

Redis 实现分布式锁要注意哪些事项

Zookeeper 和 Redis 分布式锁的比较

下面的表格总结了 Zookeeper 和 Redis 分布式锁的优缺点:

Redis 实现分布式锁要注意哪些事项

Redis 实现分布式锁要注意哪些事项

读到这里,这篇“Redis 实现分布式锁要注意哪些事项”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注丸趣 TV 行业资讯频道。

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-15发表,共计4296字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)