赞
踩
现在的业务应用通常都是微服务架构,如果一个应用部署多个进程,那这多个进程如果需要修改操作同一行记录时,为了避免操作乱序导致数据错误,此时,我们就需要引入分布式锁来解决这个问题了。
而实现分布式锁,大多有以下三种方式实现:
下面我们以 Redis 来讲解如何实现分布式锁,以及分布式锁的各种安全性问题。
想要实现分布式锁,关键是使用 SETNX 指令。
SETNX key value
这个命令执行时,如果 key 不存在,则设置 key 值为 value;如果 key 已经存在,则不执行赋值操作。并使用不同的返回值标识
接下来我们对比下面的几种实现分布式锁的方式:
客户端 A 申请加锁,加锁成功:
> setnx name 1
(integer) 1
客户端 B 申请加锁,加锁失败:
> setnx name 1
(integer) 0
这时加锁成功的客户端就去操作数据,操作成功之后,需要释放锁给后面的客户端操作,这里使用 DEL 命令删除这个 key就可以。
> del name
(integer) 1
但是这个实现方式会有个问题,一旦服务获取锁之后,因某种原因挂掉,则锁一直无法自动释放。从而导致死锁。
那么怎么解决这个问题呢?
服务某种原因挂掉,导致无法释放锁,这时候我们能想到的就是给这把锁加个时间,在 Redis 中,给 key 设置一个过期时间。
> setnx name 1
(integer) 1
> expire name 5
(integer) 1
这样的话,无论是否异常,我们设置的这个锁都会在 5 秒之后自动释放锁,其他客户端还是可以获取到锁的。
此方式解决了方式 1 死锁的问题,但同时引入了新的死锁问题,因为我们设置过期时间是经过 2 条命令来执行的,可能发生以下的情况:
SETNX 成功以后,因为各种原因(网络、Redis异常、宕机崩溃),都会导致陷入死锁,两条命令不能保证原子操作,就会导致过期时间设置失败的问题。依然会发生死锁。
那么怎么解决这个问题呢?
> set name 1 ex 10 nx
OK
这个方式通过 set 的 EX/NX 选项,将加锁、设置超时两个步骤合并为一个原子操作,从而解决方式 1、2 的问题,但是此方式还是会出现问题,什么问题呢?
如果锁被错误的释放(如超时),或被错误的抢占,或因 Redis 问题等导致锁丢失,无法很快的感知到。
比如 客户端 A 去加锁成功去操作资源,超过锁的过期时间自动释放锁,这时候客户端 B 加锁成功去操作资源,这时候客户端 A 操作资源完成,释放锁,可能释放的是客户端 B 的锁。
如何解决这个问题呢?
客户端在加锁时,设置一个只有自己知道的唯一标识进去。在释放锁时,要先判断这把锁是否自己持有的。
if redis.get("lock") == $uuid:
redis.del("lock")
但是这里释放锁,使用的是 GET + DEL 两条命令,又回出现我们前面所讲的的原子性问题,为保证原子性,需要通过 lua 脚本实现。
if redis.call("GET",KEYS[1]) == ARGV[1]
then
return redis.call("DEL",KEYS[1])
else
return 0
end
此方案更严谨,即使因为某些异常导致锁被错误的抢占,也能部分保证锁的正确释放。并且在释放锁时能检测到锁是否被错误抢占、错误释放,从而进行特殊处理。
项目我们总结一下,基于 Redis 实现的分布式锁,严谨的的流程如下所示:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。