分布式锁是我们在分布式场景中经常用到的一种技术,在后端面试中也是出镜率很高,那么我们设计分布式锁的时候应该从那几方面去考虑呢
实现分布式锁需要考虑的点
设置超时时间
设置超时时间的目的是为了避免这个场景:进程A拿了锁,但是在持有锁期间自己因为某些异常挂掉了,这样进程A就永远不会释放掉这个锁了。如果没有设置超时时间的话,这个锁就会死锁,所以会需要设置一个超时时间,来保证当锁的持有者无法释放锁的时候,锁不会成为死锁。
设置守护线程
设置守护线程的目的是为了避免这个场景:进程A拿了锁,也设置了超时时间30s,但是因为某些业务的原因,进程A确实需要持有锁超过30s,那么这个时候如果不加任何干预的话30s后锁自动释放肯定是不符合预期的。
有些同学可能就会问了,那我不是把超时时间设置长一点就好吗?但是一是超时时间设置长了是会有副作用的,那就是锁持有者无法释放锁时锁自动持有的时间会变长;二是即使设置的再长也是有个限制的,无法完全保证不会发生正常使用的锁被错误的自动释放掉。
所以我们需要在持有锁的这个进程中额外增加一个线程,具体做的事情就是当持有锁的时间超过锁过期时间的一半时,自动对超时时间进行一个续期,这样当进程存在的时候,就会一直对超时时间进行续期,不会被意外的自动释放;如果进程挂掉的话,则不会进行续期,超时后锁会被自动释放,也不会产生死锁。
Redis实现分布式锁主要要注意的地方
- 使用SETNX实现排他性锁,SETNX意为set if not exist,即如果这个锁已经被持有了,则无法再次申请
- 使用超时限制特性来避免死锁
- 释放锁的时候需要进行检查来避免误释放别的进程的锁:添加key的时候使用uuid生成一个identifier,作为key的value随key一同写入redis;释放的时候,使用get命令获取到锁的value的时候,检查是不是之前存的那个identifier,如果不是的话就说明锁已经释放了
tooz实现基于redis的分布式锁的实现方法
获取到一个锁的实例以后,如果acquire成功,则将acquired置为True
任意一个锁的实例想要release锁的话,先判断acquired是否为True,如果为True才允许释放,如果为False直接抛出异常“不能释放没有获取的锁”,以此避免错误释放
超时时间过一半以后,实例会自动延长超时时间到原本设置的超时时间,避免因为超时时间的设置把原本需要很长时间处理的任务的锁错误的释放掉了
使用setnx和expire的问题:setnx和expire是两条指令而不是原子指令。
解决办法:使用set扩展参数替代:set key value ex 5 nx