分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现 如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此干扰。
redis分布式锁的三要素:
1.加锁
使用setnx命令加锁,key是锁的唯一标识,可以根据业务来命名,value为当前线程的ID或者UUID(后面介绍原因) 比如扣减商品库存,key可是 lock_stock_upc ,value可以为当前线程ID。
setnx(key,value):
2.锁超时
如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程再也别想进来。那么这个锁就永远被无法获取到 所以,我们需要给setnx的key必须设置一个超时时间,以保证在异常情况下即使锁没有被显式释放,这把锁也要在一定时间后自动释放。
3.释放锁
当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行del指令, del(key)释放锁之后,其他线程就可以继续执行setnx命令来获得锁。
分布式锁常见的问题
1.死锁
由于set和expire的非原子性,会导致一个异常情况
所以要保证SETNX和SETEX(设置过期时间)这2个命令一起执行,要么都成功,要么都失败,保证其原子性。
2.误删其他线程的锁
怎么避免这种情况呢?可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁。
至于具体的实现,可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。 这就是前面说的设置value的时候要设置成uuid或者线程ID的原因。
if(threadId .equals(redisClient.get(key))){
del(key)
}
然而由于,if判断和释放锁是两个独立操作,不是原子性,所以采用Lua脚本释放锁。后面介绍实现。
分布式锁设计方案
通过以上可知,要想实现一个可靠的分布式锁,设计锁的时候需要考虑一下要素:
加锁
SET key value NX PX 30000
value是由客户端生成的一个随机字符串,相当于是客户端持有锁的标志
NX表示只有key值不存在的时候才能SET成功,相当于只有第一个请求的客户端才能获得锁
PX 30000表示这个锁有一个30秒的自动过期时间。
解锁
为了防止客户端1获得的锁,被客户端2给释放,采用下面的Lua脚本来释放锁
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
redis分布式锁的不靠谱
假如在redis sentinel集群中,我们具有多台redis,他们之间有着主从的关系,例如一主二从。我们的set命令对应的数据写到主库,然后同步到从库。当我们申请一个锁的时候,对应就是一条命令 setnx mykey myvalue ,在redis sentinel集群中,这条命令先是落到了主库。假设这时主库down了,而这条数据还没来得及同步到从库,sentinel将从库中的一台选举为主库了。这时,我们的新主库中并没有mykey这条数据,若此时另外一个client执行 setnx mykey hisvalue , 也会成功,即也能得到锁。这就意味着,此时有两个client获得了锁。这不是我们希望看到的,虽然这个情况发生的记录很小,只会在主从failover的时候才会发生,大多数情况下、大多数系统都可以容忍,但是不是所有的系统都能容忍这种瑕疵。
RedLock
为了解决故障转移情况下的缺陷,Antirez 发明了 Redlock 算法,使用redlock算法,需要多个redis实例,加锁的时候,它会想多半节点发送 setex mykey myvalue 命令,只要过半节点成功了,那么就算加锁成功了。释放锁的时候需要想所有节点发送del命令。这是一种基于【大多数都同意】的一种机制。我们可以选择已有的开源实现,python有redlock-py,java 中有Redisson redlock。
redlock确实解决了上面所说的“不靠谱的情况”。但是,它解决问题的同时,也带来了代价。你需要多个redis实例,你需要引入新的库 代码也得调整,性能上也会有损。所以,果然是不存在“完美的解决方案”,我们更需要的是能够根据实际的情况和条件把问题解决了就好。