锁我们可以理解为对某项资源使用权限的管理,通常使用锁来控制共享资源,比如一个进程内多个线程竞争一个资源的使用权限,解决方式其中就含有加锁。
而分布式锁,就是分布式场景下的锁,多台不同的机器上的进程去竞争同一个资源,需要加分布式锁。
1.互斥性:只让一个竞争者持有锁
2.安全性:避免锁因为异常永久不被释放,使用过期时间兜底
3.对称性:同一个锁,必须由同一个竞争者加锁和释放锁
4.可靠性:一定程度的异常处理能力、容灾能力
利用Redis的setnx命令,将key设置为value,并且返回1;如果key存在,不会创建锁,返回0
流程:通过setnx加锁,加锁之后其他服务无法加锁,用完之后再通过delete解锁
如果获取锁的服务挂了,那么锁就一直不被释放,后面的服务无法获取被锁着的资源,所以我们加一个超时时间来兜底。
使用Redis命令,给键加上过期时间即可。
set key value nx ex seconds
ex:增加了过期时间,seconds:设置的过期时间是多久
但是这又有一个新的问题,如果服务A的业务时间比较长,此时锁过期后,服务B获取了锁,然后服务A业务结束后释放了该锁(服务B业务此时还没结束)。
基于这个场景,我们又可以进一步优化分布式锁。
分布式锁需要满足,谁申请的锁谁释放的原则。
同样的,使用Redis命令:set key 业务id nx ex second
在释放锁前,get锁进行判断即可。
另一个方法,也可以启动一个看门狗(go启动一个 gorountine),过期前去刷新过期时间即可。
其实完整的流程是对的,但是现在还有一个问题:原子性
检查锁,再释放锁,这些操作不是原子的;可能锁获取时还是自己的,删除时已经是别人的了(比如你持有的锁过期了,被别人获取到了)。
使用Redis特性,整合原子操作的lua;Redis+Lua,可以专门解决原子性问题。
有了Lua,Redis才能真正的在分布式锁场景下被使用。
到了版本四,我们已经满足了分布式锁的前三个特性:对称性、安全性、互斥性。
之前都是基于单机的,但是如果Redis挂了,那么就不能获取锁了。
解决方法:主从容灾和多级部署
给Redis配置从节点,当主节点挂掉,可以用从节点暂时顶包。
但是主从切换需要人工参与,提高了人力成本。但是Redis已经有了解决方案,使用哨兵模式,节点灵活自动切换,不需要人工参与。
尽管这种方法一定的程度上解决了单点的容灾问题,但是由于同步时延,Slave会损失部分数据,分布式锁可能会失效,可能会导致短暂的多个机器获取到执行权限。
所以有一个更可靠的办法。
对一致性要求更高可以使用多机部署,使用Redis的RedLock。
大概的思路是:通常是N个机器(N一般为奇数),达到一半以上同意加锁才算加锁成功。
加锁流程一般为(以N=5为例):
1.向5个Redis申请加锁
2.超过一半(3个)同意,就可以获取到锁;反之,向每个Redis发送解锁命令
3.由于向5个Redis发送请求,会有一定的时耗,所以锁持有时间应该需要减去请求时间。如果剩余时间为0,那就是获取锁失败
4.使用完锁后,向5个Redis发送解锁请求
这种方案是一般比较成熟的方法了,最后我们再给每个Redis配上哨兵模式就增强了可靠性。
但是这就一定能保证可靠性吗?
NPC问题
1.网络时延(Network Delay):RedLock的锁剩余持有时间,需要减去请求时间,可以一定程度的解决网络延迟问题。
2.进程暂停(Process Pause):如果进程获取锁后发生了GC,但是锁已经过期了,那么就会有两个进程获取到同一个锁。
3.时钟漂移(Clock Drift):如果服务A通过RedLock方案获取了锁,但是机器都发送了时钟漂移,锁瞬间过期了,那么又会有两个进程获取到同一个锁。