文章目录
- 前言
- 一、最简单的版本:setnx key value
- 获取锁成功
- 获取锁失败
- 释放锁
- 缺点
- 二、升级版本:set key value [ex seconds] [nx]
- 获取锁成功
- 获取锁失败
- 释放锁
- 缺点
- 三、Lua脚本 可重入分布式锁
- 获取锁Lua脚本
- 演示获取锁
- 释放锁Lua脚本
- 演示释放锁
- 总结
前言
使用Redis可以很方便地实现分布式锁。
实现分布式锁不难,难的是要考虑性能及优化加锁解锁机制。
提示:以下是本篇文章正文内容,下面案例可供参考
一、最简单的版本:setnx key value
Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。
基于setnx命令的特性,我们就可以实现一个最简单的分布式锁了。
key = 竞争资源
value =threadId+count
,线程ID+重入次数
客户端需要自行维护 [线程ID+重入次数]
获取锁成功
redis> setnx lock1 threadId_count
(integer) 1
获取锁失败
客户端自行实现自旋
redis> setnx lock1 threadId_count
(integer) 0
释放锁
# 判断是否自己持有的锁
redis> get lock1
"threadId_count"
# 重入次数减1
redis> set lock1 threadId_count
OK
# 直接释放锁
redis> del lock1
(integer) 1
缺点
- 客户端需要自行维护自旋、超时、[线程ID+重入次数]
- 当释放锁Redis宕机时,会出现锁饥饿现象,永远无法获取到锁,至于锁超时。
二、升级版本:set key value [ex seconds] [nx]
将客户端维护锁超时的工作,交给Redis来做。
Redis Setex 命令为指定的 key 设置值及其过期时间。
如果需要同时拥有Setex跟Setnx的特性,可以使用 set命令 + options
key = 竞争资源
value =threadId+count
,线程ID+重入次数
expire = 锁存活时间
客户端需要自行维护 [线程ID+重入次数]
获取锁成功
redis> set lock1 threadId_count ex expire nx
(integer) 1
获取锁失败
redis> set lock1 threadId_count ex expire nx
(integer) 0
释放锁
# 判断是否自己持有的锁
redis> get lock1
"threadId_count"
# 重入次数减1
redis> setex lock1 threadId_count expire
OK
# 直接释放锁
redis> del lock1
(integer) 1
缺点
- 客户端需要自行维护自旋、[线程ID+重入次数]
三、Lua脚本 可重入分布式锁
前面的Redis原生命令实现的方式,需要多次的网络请求,在锁竞争激烈的情况下,对应用和Redis都有不少的压力,对于客户端也需要自行维护原子性、一致性等并发安全问题。
这里可以使用Lua脚本减少对Redis网络请求,并保证一系列操作的原子性。
获取锁Lua脚本
涉及的Redis命令:
- hincrby :将hash中指定域的值增加给定的数字
- pexpire:设置key的有效时间以毫秒为单位
- hexists:判断field是否存在于hash中
- pttl:获取key的有效毫秒数
- subscribe:订阅一个或多个符合给定的Channel
local key = KEYS[1] -- 锁资源
local waitSet = key .. '_waitSet' -- 该锁的等待线程队列的Key
local timeout = ARGV[1] -- 持有锁的时间
local threadId = ARGV[2] -- 线程唯一标识
-- 判断资源是否在锁
if (redis.call('exists', key) == 0) then
-- 锁重入次数=1
redis.call('hincrby', key, threadId, 1)
redis.call('pexpire', key, timeout)
return 'ok'
-- 是否是自己的锁
elseif (redis.call('hexists', key, threadId) == 1) then
-- 锁重入次数+1
redis.call('hincrby', key, threadId, 1)
redis.call('pexpire', key, timeout)
return 'ok'
else
-- 未获取到锁,返回该锁剩余持有时间
return redis.call('pttl', key)
end
获取锁失败后,需要订阅waitSet
Channel,收到通知后再次尝试获取锁。
演示获取锁
能正常获取锁,能正常互斥其他线程
重入次数正常累加
释放锁Lua脚本
涉及Redis命令:
- hincrby :将hash中指定域的值增加给定的数字
- pexpire:设置key的有效时间以毫秒为单位
- hexists:判断field是否存在于hash中
- publish:将信息发送到指定的Channel
local key = KEYS[1] -- 锁资源
local waitSet = key .. '_waitSet' -- 该锁的等待线程队列的Key
local timeout = ARGV[1] -- 持有锁的时间
local threadId = ARGV[2] -- 线程唯一标识
-- 锁不存在or不是自己的锁
if (redis.call('hexists', key, threadId) == 0) then
return nil
end
-- 释放锁,重入次数减1
local counter = redis.call('hincrby', key, threadId, -1)
-- 释放锁后,本线程的其它业务代码仍持有锁
if (counter > 0) then
redis.call('pexpire', key, timeout)
return 0
-- 释放锁后,可以通知其它等待线程唤醒竞争锁
else
redis.call('del', key);
redis.call('publish', waitSet, 'UNLOCK')
return 1
end
演示释放锁
能正常锁重入次数递减,能正常释放锁
释放锁后,等待队列能正常监听
总结
以上就是今天要讲的内容,本文仅仅简单介绍了Redis分布式锁的使用,Lua脚本提供了原子性操作。而Redisson提供了大量能使我们快速便捷使用的分布式锁实现。