Redis分布式锁

2021/07/31

Redis 分布式锁

锁指的是对某个资源的限制,分布式锁是相对于单机锁来的,一般来说程序内的加锁都是针对于单个进程的,但是在集群的条件下,业务实例会有多个,进程内的锁就无法适用,就出现了分布式锁概念。

分布式锁指的是:适用于分布式场景下的锁,而不是分布式实现的锁。本质上就是一个独立的工作点,可以被多个实例访问。分布式锁需要做到以下几点:

  • 互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁
  • 高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署
  • 可重入(防止锁超时):如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁
  • 独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了;

Redis 是一种分布式锁方案,但不一定是最好的方案,它有以下几个好处:

  • 核心单进程单线程,获得锁的过程是确定的,一个实例获得,另一个就无法获得
  • 可重入性的实现,直接通过对key设定存活周期
  • 高可用性,可以通过Redis集群的主从复制(但这里是一个坑点,主从复制不是强一致性,可能会丢锁)
  • 独占性,通过对锁key进行随机赋值,只有加锁方才可以解锁(其实不讲规矩的话都可以删除该锁)

但它也有以下的缺点:

  • 高可用性和一致性难以保证,Redis 集群不是强一致性的,容易丢失 key
  • 可重入性和独占性的实现,需要业务代码的强力支撑,如果乱来的话,Redis 无法保证这两点

Redis 锁的使用

SETNX key value

SETNX是『 SET if Not eXists』(如果不存在,则 SET)的简写,设置成功就返回1,否则返回0。

> SETNX lock1 1
(interger) 1

但是这样使用,锁就没法自动释放,假如加锁方服务挂掉了,那么就死锁了。

可以通过先设置 key,然后赋予存活周期,但这是两步操作,不符合原子性,容易出问题,所以该方法一般不用。

SETEX key seconds value

> setex lock1 10 1
OK

这里确实可以解决上述方法的原子性问题,同时分布式锁也可重入,但它有个问题,就是没法保证独占性,任何实例都可以这么操作这个锁。我在某篇文章中看到了以下方案,但不太理解,暂且记录:

  1. setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向
  2. get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3。
  3. 计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime。
  4. 判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
  5. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。

通过业务上的逻辑实现独占性,但无法防止不规矩的代码生效。

对于上述实现,该博主在Redis分布式锁的实现表示否定,上述方案中不确定性多。

PSETEX key milliseconds value

和上述指令一样,只是时间改为了毫秒。

Redis 锁的释放

为了实现独占性,释放锁的过程需要做两件事:

  • 判断释放锁的实例是不是加锁的实例?
  • 删除该锁

但这里又会牵扯到一个原子性的问题,一般用lua脚本解决。

`if redis.call("get",KEYS[1]) == ARGV[1]`
`then`
 `return redis.call("del",KEYS[1])`
`else`
 `return 0`
`end`

eval lua-script 1 lock1 1

参考

Search

    Table of Contents