redis实现分布式锁
使用setnx
最简单的方法是使用setnx命令。key是锁的唯一标识,按业务来决定命名。比如想要给一种商品的秒杀活动加锁,可以给key命名为 “lock_sale_商品ID” 。 setnx orderA 1
value设置成什么呢?锁的value值为一个随机生成的UUID。我们可以姑且设置成1。
当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁;当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败。
解锁就是del key即可
setnx的问题?
如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程再也别想进来。所以,setnx的key必须设置一个超时时间,单位为second,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放,避免死锁。
(1) 不是原子操作, 无法保证一定能设置成功过期时间
setnx不支持超时参数,所以需要额外的指令.
expire orderA 30
但是问题来了, expire和setnx是两个指令, 那么如果某个线程通过setnx得到了一把锁, 但是还没来得及设置过期时间, 线程就挂了, 那么这把锁依旧陷入了死锁的状态, 永远解不开.
(2) 锁释放过早的问题
在业务场景中, 你很难估计自动释放锁的时间, 如果某个线程拿到锁后, 正在处理, 但是处理时间超过30s了, 然后锁被自动释放了, 那么分布式锁的意义就失去了
(3) 误删锁的情况
假设某个线程A持有锁, 然后处理时间超过30s了, 锁被自动释放 此时线程B持有锁了, 正在执行, 然后现在A执行完毕了, 去删除锁, 那么就会把B的锁给删除了.
对于误删锁的情况有啥办法吗?
如果考虑在删除下, 先判断一个锁是不是属于自己, 例如在setnx的时候, value值设为自己的线程id(能够标明当前线程值), 那么在del之前, 先获取一下这个值比较一下
但是问题来了, 获取和比较是两个操作, 也受并发的影响, 有可能出现比较的时候属于自己, 但是删除的时候不属于自己了.
通过value设置成时间戳的方式来避免死锁?
既然setnx和expire不属于原子操作, 那么我直接将value值设置为当前系统的时间戳, 那么其它线程在获取锁的时候, 如果发现锁被占用, 然后比较时间戳, 是不是就可以避免死锁的情况了
虽然这种方式能解决设置过期时间非原子性操作的情况, 但是无法解决同时获取锁的情况. 因为当两个线程并发的时候, 还是可能存在同时比较, 然后分别设置自己的时间戳的情况, 这样两个线程都获得锁了.
守护线程法
通过set命令来设置key, 带有nx参数实现和setnx的效果, 同时可以原子性操作设置过期时间
set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在时设置value,成功返回OK,失败返回(nil)
XX:key存在时设置value,成功返回OK,失败返回(nil)
然后创建一个当前线程的守护线程, 守护线程每隔30s, 为锁续费一次, 这样能保证在一个事务没处理完后, 锁不会被释放, 同时如果主线程挂掉的话, 守护线程也会死亡, 持有的锁没人给续费, 最多30s后, 也会自动释放.
其它线程获取锁也通过set key value 30 NX 的返回值来判断能否获得锁.
zookeeper实现分布式锁
zookeeper设计出来, 就是为了分布式锁的, 需要获得锁的线程, 就在zookeeper中创建一个自增的临时节点, 如果创建的节点是最小的节点, 就代表获取到锁了, 执行完后, 断开连接即可, 相应的节点就会消失(因为是临时节点, 当前线程挂掉, 节点也会消失).
并且zookeeper的写入都是在leader节点的, 不会存在数据不一致的问题.