Redis 分布式锁的实现理解基本概念
分布式锁实现的三个核心要素:
加锁
最简单的方法是使用 setnx 命令。 key 是锁的唯一标识,按业务来决定命名。比如想要给一种商品的 秒杀活动加锁,可以给 key 命名为 “lock_sale_商品ID” 。
当一个线程执行 setnx 返回 1 ,说明 key 原本不存在,该线程成功得到了锁;当一个线程执行 setnx 返回 0 ,说明 key 已经存在,该线程抢锁失败。
解锁
有加锁就得有解锁。当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简 单方式是执行 del 指令。
释放锁之后,其他线程就可以继续执行 setnx 命令来获得锁。
锁超时
锁超时是什么意思呢?如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资 源将会永远被锁住(死锁),别的线程再也别想进来。所以, setnx 的 key 必须设置一个超时时间, 以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。 setnx 不支持超时参数,所以需要额 外的指令 expire 。
存在的问题
setnx 和 expire 的非原子性
当某线程执行 setnx ,成功得到了锁, setnx 刚执行成功,还未来得及执行 expire 指令,节点 1 挂 掉了。
可以使用 set (key, value, time, NX),这样就可以取代 setnx 指令。
del 导致误删
假如某线程成功得到了锁,并且设置的超时时间是 30 秒。某些原因导致线程 A 执行的很慢很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。随后,线程 A 执行完了任务,线程 A 接 着执行 del 指令来释放锁。但这时候线程 B 还没执行完,线程A实际上 删除的是线程 B 加的锁 。
怎么避免这种情况呢?
可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。至于具体的实现,可以在加锁的 时候把当前的线程 ID 当做 value ,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。
上面这种方式是两个独立的操作,不是原子性的。
我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。
当过去了 29 秒,线程 A 还没执行完,这时候守护线程会执行 expire 指令,为这把锁“续命”20 秒。守 护线程从第 29 秒开始执行,每 20 秒执行一次。
当线程 A 执行完任务,会显式关掉守护线程。
如果节点 1 忽然断电,由于线程 A 和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时 候,没人给它续命,也就自动释放了。