使用 Redis 如何设计一个分布式锁

前言
现在的业务应用通常都是微服务架构,如果一个应用部署多个进程,那这多个进程如果需要修改操作同一行记录时,为了避免操作乱序导致数据错误,此时,我们就需要引入分布式锁来解决这个问题了。

而实现分布式锁,大多有以下三种方式实现:

使用 MySQL 实现
使用 Redis 等缓存系统实现
使用 Zookeeper 实现
下面我们以 Redis 来讲解如何实现分布式锁,以及分布式锁的各种安全性问题。

想要实现分布式锁,关键是使用 SETNX 指令。

SETNX
SETNX key value
1
这个命令执行时,如果 key 不存在,则设置 key 值为 value;如果 key 已经存在,则不执行赋值操作。并使用不同的返回值标识

SETNX 实现分布式锁
接下来我们对比下面的几种实现分布式锁的方式:

方式 1、SETNX + DEL
客户端 A 申请加锁,加锁成功:

> setnx name 1
(integer) 1

1
2
3
客户端 B 申请加锁,加锁失败:

> setnx name 1
(integer) 0
1
2
这时加锁成功的客户端就去操作数据,操作成功之后,需要释放锁给后面的客户端操作,这里使用 DEL 命令删除这个 key就可以。

> del name
(integer) 1
1
2
但是这个实现方式会有个问题,一旦服务获取锁之后,因某种原因挂掉,则锁一直无法自动释放。从而导致死锁。

那么怎么解决这个问题呢?

方式 2、SETNX + EXPIRE
服务某种原因挂掉,导致无法释放锁,这时候我们能想到的就是给这把锁加个时间,在 Redis 中,给 key 设置一个过期时间。

> setnx name 1
(integer) 1
> expire name 5
(integer) 1
1
2
3
4
这样的话,无论是否异常,我们设置的这个锁都会在 5 秒之后自动释放锁,其他客户端还是可以获取到锁的。

此方式解决了方式 1 死锁的问题,但同时引入了新的死锁问题,因为我们设置过期时间是经过 2 条命令来执行的,可能发生以下的情况:

SETNX 成功以后,因为各种原因(网络、Redis异常、宕机崩溃),都会导致陷入死锁,两条命令不能保证原子操作,就会导致过期时间设置失败的问题。依然会发生死锁。

那么怎么解决这个问题呢?

方式 3、SET EX NX
> set name 1 ex 10 nx
OK
1
2
这个方式通过 set 的 EX/NX 选项,将加锁、设置超时两个步骤合并为一个原子操作,从而解决方式 1、2 的问题,但是此方式还是会出现问题,什么问题呢?

如果锁被错误的释放(如超时),或被错误的抢占,或因 Redis 问题等导致锁丢失,无法很快的感知到。

比如 客户端 A 去加锁成功去操作资源,超过锁的过期时间自动释放锁,这时候客户端 B 加锁成功去操作资源,这时候客户端 A 操作资源完成,释放锁,可能释放的是客户端 B 的锁。

如何解决这个问题呢?

SET name uuid EX 10 NX
客户端在加锁时,设置一个只有自己知道的唯一标识进去。在释放锁时,要先判断这把锁是否自己持有的。

if redis.get("lock") == $uuid:
    redis.del("lock")

1
2
3
但是这里释放锁,使用的是 GET + DEL 两条命令,又回出现我们前面所讲的的原子性问题,为保证原子性,需要通过 lua 脚本实现。

if redis.call("GET",KEYS[1]) == ARGV[1]
then
    return redis.call("DEL",KEYS[1])
else
    return 0
end
1
2
3
4
5
6
此方案更严谨,即使因为某些异常导致锁被错误的抢占,也能部分保证锁的正确释放。并且在释放锁时能检测到锁是否被错误抢占、错误释放,从而进行特殊处理。

项目我们总结一下,基于 Redis 实现的分布式锁,严谨的的流程如下所示:

加锁:SET name uuid EX time NX
操作共享资源
释放锁:Lua 脚本,先 GET 判断锁是否自己持有的,再 DEL 释放锁
————————————————
版权声明:本文为CSDN博主「华少聊编程」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wuhuayangs/article/details/122499190

上一篇:redis实现分布式锁天然的缺陷


下一篇:redis访问击穿/穿透/雪崩