下载帮

您现在的位置是:首页 > 数据库 > Redis

Redis

Redis分布式锁怎么玩(上)

2022-06-06 23:50Redis

为什么需要分布式锁

聊到锁其实我们在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中就可以设置锁的过期时间解决

### 加锁分为两步 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需要如何应对呢?

未完待续.....

文章评论