Redis之分布式锁简析

767 阅读4分钟

初初级分布式锁

1.setnx key value // 设置成功返回1,设置失败返回0
2.    do something ... // 进行逻辑操作
3.del key // 删除之前设置的key,即删除分布式锁

初初级分布式锁使用setnx + del完成,先使用setnx设置key,如果设置成功,就开始进行逻辑操作,逻辑操作完成后,使用del删除分布式锁。但如果在使用setnx加锁后,由于某些原因程序终止运行了,那么del解锁命令就永远得不到执行,那么之前加的分布式锁就永远在redis中了,其他业务也就永远无法获得锁。

如上图所示,非正常业务流程中,业务1获得锁后,最后del命令没有执行,导致后续其他业务加锁失败。

为了解决del命令可能未执行导致锁永远无法释放的问题,于是使用到了expire命令,给key加过期时间,防止del命令未运行出现的死锁情况。

初级分布式锁

1.setnx key value // 设置成功返回1,设置失败返回0
2.expire key seconds // 为key设置过期时间
3.    do something ... // 进行逻辑操作
4.del key // 删除之前设置的key,即删除分布式锁

初级分布式锁使用setnx + expire + del完成,首先setnx设置key,如果设置成功则调用expire为该key设置过期时间,然后开始做一些逻辑操作,最后相关逻辑操作完成后使用del删除key来释放分布式锁。
初级分布式锁很清晰的逻辑,但是首先要明白,在setnxexpire两个命令之间也会因为某些原因导致业务中断,程序就没法正常进行下去,也就会导致分布式锁失效。比方说: setnx成功后,开始设置过期时间应该,但是这时候服务器宕机了,导致expire命令未执行,于是,之前设置的key就会永远存在redis中,当服务器恢复正常后,正常业务就再也无法获得锁,因为他发现,这个key一直存在,再使用setnx就一直失败。

具体就如上图所示,虚线是未执行的。由于expire命令未正常运行,这就和第一种“初初级分布式锁”仅用setnx + del情况是一毛一样了,出现了死锁。

中级分布式锁

1.set key value ex seconds nx  // 设置成功返回1,设置失败返回nil
2.    do something ... // 进行逻辑操作
3.del key // 删除之前设置的key,即删除分布式锁

中级分布式锁的基础是redis版本已经升级到2.6.12版本以上,set命令在该版本之前还仅是set key value的操作模式,升级后就加了ex和nx等参数,使用ex和nx参数后等效于 setnx key value; expire key seconds两条命令,且使用set实现ex和nx的功能都是原子性的,不会出现ex和nx断档。

该分布式锁虽然解决了setnxexpire两个命令的断档问题,但是如果do something的逻辑操作执行时间过长会出现什么新的问题?

  • 问题一:业务Ado something时间超出设置的过期时间,如果这时候业务逻辑还没做完,业务B就来获取锁了,然后也会走do something,这时候就会出现业务数据紊乱,也就打破了使用分布式锁的初衷
  • 问题二:业务Ado something时间超出设置的过期时间,业务A的分布式锁被释放,业务B获取了该锁,正在执行,这时候业务Ado something完成了,开始释放锁,结果把已经获取分布式锁的业务B的锁给释放了

如何解决上述问题呢?

方法一:
针对问题二,可以在加锁操作时,给key的value设置一个随机数并记录下来,当do something走完后,要释放锁时,先判断分布式锁的value和当前要删除的key的value,也就是之前设置的随机数是否一致,如果一致才可以删除,如果删除不了,就说明这个key已经过期了,被自动释放了。这个方法仍旧有一个弊端,就是在判断value是否一致时的操作也不是原子型的,假如业务A的锁还没过期,然后最后要开始释放锁,他先判断value是否一致,发现value是一致的,但是因为某些原因导致要过N长时间才能执行del操作,然后业务A的锁到期了自动释放了,而这时候业务B获取了锁,结果业务A的del操作终于苏醒了,于是他开始释放锁,结果把业务B的锁给释放了,针对这个问题,说是可以用lua脚本来解决,因为 Lua 脚本可以保证连续多个指令的原子性执行(但是我目前还不会lua脚本)
方法二:
针对问题一的数据错乱,那目前没招... 要么就是在业务层做控制,比方说在do something工作期间判断锁是否有效,无效的话就回滚当前业务,抛出异常,还比方说自动续期