文章目录

  • 前言
  • 一、最简单的版本: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,收到通知后再次尝试获取锁。

演示获取锁

能正常获取锁,能正常互斥其他线程

redisson为啥不释放锁 redis释放锁必须用lua吗_Redis


重入次数正常累加

redisson为啥不释放锁 redis释放锁必须用lua吗_lua_02

释放锁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

演示释放锁

能正常锁重入次数递减,能正常释放锁

redisson为啥不释放锁 redis释放锁必须用lua吗_lua_03


释放锁后,等待队列能正常监听

redisson为啥不释放锁 redis释放锁必须用lua吗_redis_04


总结

以上就是今天要讲的内容,本文仅仅简单介绍了Redis分布式锁的使用,Lua脚本提供了原子性操作。而Redisson提供了大量能使我们快速便捷使用的分布式锁实现。