面试:Redis分布式是锁如何实现的?
大弟:通过Redis的setnx(set if not exist)就可以实现了,setnx(key,value)只有当key不存在时,才会创建这个键,创建成功后就代表拿到了这个锁,执行逻辑代码后,在删除这个键就相当于释放锁。
面试:emm,如果拿到锁之后,在执行逻辑代码期间,异常了,崩溃了,还没释放锁就崩溃了,这不就死锁了么。
大弟:啊啊啊啊,对,该死,居然忘记了,在创建键之后,还要通过setex为这个键加上一个过期时间,这样就算拿到锁的客户端挂掉了,过期时间到了,这个锁也就被释放了。
面试:emm,按你说的setnx创建这个代表锁的键,setex为这个键设置一个过期时间,当setnx执行完,还没来得及执行setex它又挂了怎么办,又死锁了
大弟:(不可能,我不信,绝对不会真实发生。)啊啊啊啊,我又想起来了,这时候要做一个原子的操作,Redis2.6.12版本增加了set命令,可以直接使用set key value ex 10 nx 达到setnx setex的效果,并且这是一个原子操作。
面试:emm,用户A用户B都想操作键KEY,不过用户A先拿到了锁,但是A在执行逻辑代码时,用的时间太久了,已经超时了。这时候这个锁的键已经被删除了。 这时候等待的用户B就获得了这个锁,也开始执行逻辑代码。用户B还在执行期间,A那边执行结束了,A就进行了释放锁的操作,那此时B的锁就会被A释放掉。这个问题怎么解决。
大弟:(把逻辑代码也少点被,尽量别整这老多。)啊啊啊,其实可以解决这种重复释放的问题,A获得的锁时,在将键的值value追加个随机数xxx,这样删除的时候,判断下键对应的值与valuexxx是否相等,相等则释放。不相等,那就说明早就过期了呗。
面试:emmm,你知道一句话talk is cheap,。。。
大弟:懂懂懂,为弟研磨
def lock(conn,name,timeout=10,expire=10):
#生成一个随机数
identifier = str(uuid.uuid4())
#键的key
lockname='lock:'+name
#设置一个获取锁的超时时间
end=time.time()+timeout
while time.time() < end:
#尝试获取锁
if conn.set(lockname,identifier,nx=True,ex=expire):
return identifier
#没拿到,休息会再试。
sleep(0.01)
#超时返回False
return False
def unlock(conn,name,identifier):
#通过piepline减少网络通信次数
pipeline = conn.pipeline()
lockname='lock:'+name
while True:
try:
#监视这个键,
pipeline.watch(lockname)
#如果键值相等,说明当前要释放的锁,并没有过期。
if pipeline.get(lockname) == identifier:
pipeline.multi()
pipeline.delete(lockname)
pipeline.execute()
return True
#说明锁已经过期,此时锁被其他用户获取了
pipeline.unwatch()
break
except redis.exceptions.WatchError:
pass
return False
```
面试:emmm,你在想想分布式锁还有什么问题没有?
大弟:(我靠。。。)emmm,如果是主从服务器环境下,主服务器挂了,还没有将用户A获取锁的这条命令发送给从服务器,而监视主从服务器状态的sentinel进行故障处理,将从服务器升级为主服务器,此时,用户B在获取用户A的锁也是可以获取到的,因为这个新的主服务器压根不知道A获得了锁。
面试:emmm,那这种情况应该如何保证安全?
大弟:(。。想回家)Redlock,这个Redlock需要额外增加多个独立的redis实例,当要获取锁时,向这些独立的redis实力发送请求加锁的命令,当一半以上的redis实例set成功时,就认为获取锁成功。就像选取领头Sentinel似的,超过一半就行了。
面试:emmmm
大弟: 突然好想去厕所,不行了。。。。。
面试:emmm,休息10分钟把,一会聊聊这个选举领头Sentinel
大弟:。。。。。。。。。。。。。。