前言
现在的业务应用通常都是微服务架构,如果一个应用部署多个进程,那这多个进程如果需要修改操作同一行记录时,为了避免操作乱序导致数据错误,此时,我们就需要引入分布式锁来解决这个问题了。
而实现分布式锁,大多有以下三种方式实现:
使用 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