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从Redis分布式系统中获取锁,成功。
- 客户端1开始执行业务,但操作业务时间过长导致锁过期了,删除键值。
- 客户端2从Redis分布式系统中获取锁,成功。
- 客户端2开始执行业务。
- 客户端1执行完毕,释放锁。
- Redis锁持有时间超过Redis的过期时间这时Redis锁自动释放。
- Redis分布锁没有设置客户端标识,任一客户端都可以解锁。
那么锁过期如何解决呢?Redisson帮我们做到了,Reisson实现了一个叫看门狗的功能,当设置一个键值的过期时间后,会启动一个守护线程,这个守护线程会去检测锁的失效时间,如果锁马上要过期但是业务操作还在进行,那么Redisson将自动对锁进行续期,重新设置过期时间。
释放别人的锁 这个问题产生的原因相对简单,因为所有客户端持有相同标识的锁,所以任一客户端都可以操作,这时我们可以传入一个客户端唯一标识,也可以是UUID作为value值,在解锁时通过Lua脚本进行校验,就可以避免释放其它客户端拥有锁的情况
那么加锁命令就可以变化成如下
### uuid生成客户端唯一标识
set lock $uuid ex 10 nx
释放锁分为两步
- 判断键值lock的value值是否是客户端开始生成的uuid
- 如果是执行删除命令,不是则返回其它标识
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return ""
end
总结 通过上面问题的梳理,我们已经清楚Redis使用简单的分布式锁应该如何操作了,但上面我们谈到的仅仅是单体实例的Redis实现分布式锁,还没有考虑到Redis自身的一些集群环境如主从集群、哨兵集群、切片集群等等这些集群之间的故障转移、持久化会不会影响分布式锁呢?Redis需要如何应对呢?
未完待续…
推荐阅读
- 笔记|简述redis数据结构
- redis|redis底层数据结构(redis底层存储结构、源码分析)
- Redis|11 Redis 节省内存的数据结构
- #|redis底层数据结构详解
- 数据库|Redis(持久化)
- NoSql|Redis - 底层数据结构与持久化简述
- Redis优化
- 数据库|oracle数据库排序后获取第一条数据
- ASP.NET MVC – SQL 数据库简介