Redis面试|Redis分布式锁怎么玩(上)

Redis分布式锁怎么玩(上) 为什么需要分布式锁 聊到锁其实我们在JAVA中早有接触如JAVA管程原语的实现synchronized,也有基于SDK管程实现的Lock,这些锁可以实现互斥等逻辑,但是这些都是单机锁,就是说有用范围只是一个进程里面,但如果在微服务架构中,出现多个服务同时需要修改一条数据库记录的情况,为了保证操作的顺序性需要引进一个独立管理锁的外部系统,这就是分布式锁出现的场景,分布式锁的具体实现有Redis、Zookeeper甚至Mysql等,这里以Redis为例讲解如何实现分布式锁。
Redis实现分布式锁 setnx简单分布式锁
想要实现分布式锁最重要的点就是需要互斥,正好Redis提供了setnx这个命令,是SET if Not eXists的缩写,只有键值不存在才插入,使用也是非常简单,如下所示

#### 客户端1加锁 返回1加锁成功 127.0.0.1:6379> setnx lock 1 (integer) 1#### 客户端2加锁 返回0加锁失败 127.0.0.1:6379> setnx lock 1 (integer) 0### 解锁,就是删除键值 127.0.0.1:6379> del lock (integer) 1

【Redis面试|Redis分布式锁怎么玩(上)】但是这里存在一个弊端,当持有锁的进程突然宕机或者程序出现异常没有解锁就退出,这样就会导致其它进程无法再获取锁,从而产生死锁。那么如何解决这个问题呢?
死锁解决方案
出现死锁的原因就是持有锁的进程未释放锁一直持有,导致其它进程无法获取,这种情况就可以给锁设置一个有效期,过了有效期将自动删除该键值,也就是释放锁,redis中就可以设置锁的过期时间解决
### 加锁分为两步 setnx + expire 127.0.0.1:6379> setnx name 1 (integer) 1 ### 设置锁的过期时间 单位秒 127.0.0.1:6379> expire name 10 (integer) 1### 查看锁的过期时间 127.0.0.1:6379> ttl name ## 剩余秒数 (integer) 6 127.0.0.1:6379> ttl name (integer) 0 127.0.0.1:6379> ttl name (integer) -2 ### -2表示已过期 127.0.0.1:6379> get name (nil)

死锁问题解决了但是迎来一个新的问题,那便是原子性问题,执行加锁操作如果出现setnx命令执行成功,但是expire命令执行失败,那么同样会有死锁问题的产生,如果想两个命令都需要成功,也就是保证原子性,那么可以采用单命令操作或者Lua脚本,这里采用单命令操作就可以直接实现,Redis在2.6.12版本支持两个命令合并。
### SET key value [EX seconds|PX milliseconds] NX ### EX和PX都表示设置过期时间,只是单位不同 ### 加入NX命令就可以得到setnx的效果 127.0.0.1:6379> set name 1 ex 10 nx OK 127.0.0.1:6379> get name "1" 127.0.0.1:6379> ttl name (integer) 4 127.0.0.1:6379> ttl name (integer) -2

我们在使用时设置set就可以得到一个比较健全的分布式锁了,那是不是就一定安全了呢?显然不是
Redis锁的两大问题
Redis直接采用set命令确实可以实现分布式锁,但是锁安全使用也是关键,我们假设以下场景
  1. 客户端1从Redis分布式系统中获取锁,成功。
  2. 客户端1开始执行业务,但操作业务时间过长导致锁过期了,删除键值。
  3. 客户端2从Redis分布式系统中获取锁,成功。
  4. 客户端2开始执行业务。
  5. 客户端1执行完毕,释放锁。
这里的场景就提到了Redis锁面临的两大问题
  • Redis锁持有时间超过Redis的过期时间这时Redis锁自动释放。
  • Redis分布锁没有设置客户端标识,任一客户端都可以解锁。
锁过期问题 第一个问题,客户端持有锁的时长超过过期时间,这个问题本质上是对业务执行时长评估不准确的情况下造成,那是不是无脑给键值延长过期时间就不会有这个问题呢?这样的办法是典型的治标不治本,因为生产环境可能遇到各种各样的问题如网络请求超时、客户端异常等等情况都有可能导致业务执行时长超过键值过期时长。
那么锁过期如何解决呢?Redisson帮我们做到了,Reisson实现了一个叫看门狗的功能,当设置一个键值的过期时间后,会启动一个守护线程,这个守护线程会去检测锁的失效时间,如果锁马上要过期但是业务操作还在进行,那么Redisson将自动对锁进行续期,重新设置过期时间。
释放别人的锁 这个问题产生的原因相对简单,因为所有客户端持有相同标识的锁,所以任一客户端都可以操作,这时我们可以传入一个客户端唯一标识,也可以是UUID作为value值,在解锁时通过Lua脚本进行校验,就可以避免释放其它客户端拥有锁的情况
那么加锁命令就可以变化成如下
### uuid生成客户端唯一标识 set lock $uuid ex 10 nx

释放锁分为两步
  • 判断键值lock的value值是否是客户端开始生成的uuid
  • 如果是执行删除命令,不是则返回其它标识
这里的两步需要注意的是保证原子性,但是没有单命令操作可以使用,所以这里采用Lua脚本,参考如下
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return "" end

总结 通过上面问题的梳理,我们已经清楚Redis使用简单的分布式锁应该如何操作了,但上面我们谈到的仅仅是单体实例的Redis实现分布式锁,还没有考虑到Redis自身的一些集群环境如主从集群、哨兵集群、切片集群等等这些集群之间的故障转移、持久化会不会影响分布式锁呢?Redis需要如何应对呢?
未完待续…

    推荐阅读