编程中多线程并发和锁的相关应用,尤其是现在应用都是部署在多台服务器上,分布式锁尤其重要。我们可以用 redis
来非常简单地实现分布式锁。其核心思想就是将对锁的操作转换为 redis
操作时,要保障原子性。
用到的两个关键 redis
操作 SETNX 和 GETSET。
基础设计
首先让我们自己来构思如何用 redis
实现一个分布式锁。
很简单,在 redis
中存放一个键值对,key
标识锁,value
做其他用途。
当一个线程需要同步操作时:
- 首先从
redis
中获取指定key
的值, - 如果返回
null
,说明空闲,当前线程设置key
和value
值;如果获取该key
时如果有值,说明是加锁状态,某个线程正在执行同步操作,当前线程应该挂起等待一段时间再执行。 - 当同步操作完成后再删除此
key
这时就要考虑并发情况下对锁的操作了,比如加锁操作包含两步 redis
操作,获取和设值,如果并发情况下两个线程先后交叉执行这两步操作,就会出问题。
- 线程一获取
key
的值为null
- 线程二获取
key
的值为null
- 线程一设置
value
为 value1,并认为自己获取到了锁。 - 线程二设置
value
为 value2, 并认为自己获取到了锁。
所以我们需要将这两步合为一步,成为原子操作,便可消除这个问题,这就需要上面提到的 SETNX
:
SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
设置成功,返回 1 。设置失败,返回 0 。
很简单,返回 1 取到锁;返回 0 没有取到锁。
异常状况
上面其实已经解决了分布式锁的最基本的问题,包括对锁的操作。下面,我们就要考虑一些特殊情况了,比如某个线程挂掉或网络问题或其他原因导致没有释放锁,这样一来所有线程就陷入了死锁,为了解决这个问题,我们可以为锁设置超时时间,value
值设置为 当前时间戳+过期时长
,当其他线程无法获取锁时,可以查看 value
值是否已过期,如果过期,则可以删除掉 value
,执行上面的争用锁的操作。
- 获取
key
的值value
不为null
,说明有锁 - 查看
value
的值是否过期 - 如果过期,删除
key
值 - 将
key
值设置为自己的value
,认为获取到锁。
这也会出现上面两个线程交叉执行出现的问题,所以我们需要保障 redis 操作的原子性。
我们需要 GETSET
操作:
将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
当 key 存在但不是字符串类型时,返回一个错误。
当我们判断 value
值过期之后,我们直接用 GETSET
设置新值。如果返回值为原 value
,说明我们设置成功,并获取了锁,如果返回值不为原 value
,说明有其他线程抢到了锁。
可重入锁
简单来说,可重入锁就是一个线程获取某个锁之后,可以再次获取该锁。为了实现这一特性,我们可以在 value
值中加入一些标识,比如 UUID,可以让线程确认是不是自己持有的锁。