分布式锁是什么
分布式锁是用来解决在分布式场景
中,多服务实例并发争抢共享资源时,且该资源同一时刻只能一个访问,就需要通过加锁来让获得锁的应用执行,获取不到锁的服务不执行或者后续执行。
分布式锁的解决方案
Redis分布式锁
Redis分布式锁依赖setnx
命令来实现。
具体实现思路:
- 服务1尝试拿锁的时候,
setnx key value
返回的是1,表示设置值成功,也就是拿到了锁,顺利执行业务逻辑。 - 后续服务2尝试拿锁的时候,
setnx key value
返回的是0,表示设置值失败,也就是拿锁失败。 - 这样就可以保证只有一个服务执行。
服务1执行完业务逻辑之后呢,应该有一个锁释放
的动作,不然key永远存在,不会再有服务能够拿到锁,也就是出现死锁
。
如何避免死锁
锁释放分为:主动删除和自然消亡。
- 主动删除就是业务逻辑执行完毕,服务代码主动删除,依赖
del key
命令来实现。
需要注意的是,主动删除锁的逻辑最好放到代码块的finally
里,可以防止代码执行异常无法释放锁的情况。
但是如果代码服务突然挂了呢,主动删除是解决不了的。
- 自然消亡就是通过Redis提供的key过期机制,依赖
set key value ex seconds nx
命令来实现。
需要注意的是,过期时间的设置问题。
如果设置过短,可能业务还没有执行完;如果设置过长,可能就会造成卡顿等问题。
既然都有些问题,为了增加健壮性,那就采用两种结合的方式吧:代码主动删除key+redis_key自动过期,双道保险来保证。
那还有问题么?有的。
存在一种可能情况是,本服务key被其他服务释放掉的情况:
服务1处理业务很慢,大于key的过期时间,这时候锁就自然消亡,锁后面可以被服务2拿到,存在服务2执行业务过程中,key被服务1主动删除的情况。
针对这个情况,也好解决,就是set-key的时候,在value里加上当前服务的标识,可以是随机数,在主动删除key的时候,做一个get-key的value校验。
那还有问题么?还有的。
仔细分析流程后我们发现,判断锁是否属于当前服务和释放锁两个步骤并不是原子操作
。
假如在执行删除锁的动作之前,系统卡顿了几秒钟,恰好在这几秒钟内,key自动过期了,
服务2就顺利获取到锁开始执行自己的逻辑了,此时,服务1卡顿恢复了,开始继续执行删除锁的动作,那么此时删除的还是服务2的锁。
如下图所示:
终极解决-Lua脚本
Lua是一门胶水语言,它支持原子性操作,Redis会将整个Lua脚本作为一个整体执行,中间不会被其他请求插入,因此Redis执行Lua脚本是一个原子操作。
在上面的流程中,我们把get-key、判断value是否属于当前服务、del-key这三步写到Lua脚本中,使它们变成一个整体交个Redis执行,改造后流程如下:
Lua脚本示例:
local key = KEYS[1]
local value = KEYS[2]
if redis.call('get',key) == value
then
return redis.call('del',key)
else
return false
end
当然,上面加锁的过程,我们也可以通过Lua脚本实现。
expire的时间适用性
- 提供
时间冗余
,时间设置足够的长,优先保证业务完毕,但是不推荐使用。 - 用Redisson的
Watch Dog
机制解决,简单来说就是,它是一个守护线程,定时去检查key的时效,如果业务还没有执行完&key快要过期了,就进行key的续期。
总结
针对上面redis分布式锁的解决方案,单机Redis节点是没什么问题的;
但是企业开发时,往往都会有'Redis集群',主从同步。主节点挂了,从节点还没有同步完的时候,key丢了,就会有问题。
一般Redis分布式锁就够用了,所以其他的解决方案我们简单分析一下。
Zookeeper实现分布式锁
客户端1和客户端2都创建临时节点 /lock
假设1创建成功,资源操作,锁释放。
没有过期时间一说。只要保证1拿到锁之后,与zk的连接保持不断(定时心跳机制),就一直拿到锁,如果断了,就会自动删除节点,释放锁。
zk不用考虑过期时间,可以实现实现分布式锁,但是单独搞一个zk,我觉得成本太大。
RedLock解决方案
5台独立的实例 具有容错机制
获取流程:
1.客户端获取一个当前时间戳1
2.客户端向5台客户端发起加锁请求,并设置毫秒级的超时时间。如果某一个实例失败,那么就继续下一个加锁。最后>=3个加锁成功,再获取一个时间戳2,如果时间戳2-时间戳1<锁过期时间,就成功,可以执行业务代码。
否则失败,就去解锁。
更多参考:https://blog.csdn.net/lisheng19870305/article/details/122464924