什么是分布式锁
- 分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现
分布式锁的特征
- 互斥性 任意时刻, 只有一个客户端持有锁
- 超时释放 某个客户端持有锁超过一定时限, 自动释放锁, 防止死锁
- 可重入 某个客户端持有锁后, 可再次请求对其加锁
- 高可用, 高性能 加锁和释放开销低, 同时保证高可用
- 安全性 只有持有锁的客户端能够释放锁
实现方案
- 方式一:使用
setnx
命令来抢锁,如果抢到之后,再用expire
命令给锁设置一个过期时间,防止锁忘记了释放。这种方式的缺点是setnx
和expire
两个命令分开了,不是原子操作,如果程序在设置过期时间之前崩溃了,就会导致锁永远不会释放。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16-- 执行一个setnx命令,设置value为任意值
local res, err = red:setnx("lock", "1234")
if not res then
ngx.say("failed to setnx lock: ", err)
return
end
if res == 1 then -- 加锁成功,设置过期时间(秒)
local ok, err = red:expire("lock", 1)
if not ok then
ngx.say("failed to expire lock: ", err)
return
end
ngx.say("setnx and expire result: ", ok)
else -- 加锁失败,锁已经存在
ngx.say("lock already exists.")
end - 方式二:在
setnx
的value
值中存放一个 系统时间+过期时间 的值,如果加锁失败,再拿出value
值校验一下是否已经过期,如果过期了,就用getset
命令重新设置锁并返回旧值,再次校验旧值是否过期。这种方式的缺点是需要客户端的时间同步,而且锁没有保存持有者的唯一标识,可能被其他客户端释放。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35-- 定义一个过期时间(毫秒)
local expire_time = 1000
-- 计算一个过期时间戳(毫秒)
local expires = ngx.now() * 1000 + expire_time
-- 把过期时间戳转换为字符串
local expires_str = tostring(expires)
-- 执行一个setnx命令,设置value为过期时间戳
local res, err = red:setnx("lock", expires_str)
if not res then
ngx.say("failed to setnx lock: ", err)
return
end
if res == 1 then -- 加锁成功
ngx.say("setnx result: ", res)
else -- 加锁失败,获取锁的过期时间
local current_value, err = red:get("lock")
if not current_value then
ngx.say("failed to get lock: ", err)
return
end
if current_value ~= ngx.null and tonumber(current_value) < ngx.now() * 1000 then -- 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
local old_value, err = red:getset("lock", expires_str)
if not old_value then
ngx.say("failed to getset lock: ", err)
return
end
if old_value == current_value then -- 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁成功
ngx.say("getset result: ", old_value)
else -- 其他线程已经抢到了锁,加锁失败
ngx.say("lock is taken by others.")
end
else -- 锁未过期,加锁失败
ngx.say("lock is not expired.")
end
end - 方式三:使用
Lua
脚本来保证原子性,脚本中包含setnx
和expire
两个命令。这种方式的优点是简单高效,缺点是还是没有保存持有者的唯一标识。1
2
3
4
5
6
7
8
9
10-- 定义一个lua脚本,包含setnx和expire两个命令
local script = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" ..
" redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end"
-- 执行eval命令,传入脚本、键名、值和过期时间
local res, err = red:eval(script, 1, "lock", "1234", 1000)
if not res then
ngx.say("failed to eval script: ", err)
return
end
ngx.say("eval result: ", res) - 方式四:使用
set
命令的扩展参数EX
PX
NX
来实现加锁和设置过期时间的原子操作。这种方式的优点是更简洁方便,缺点是还是没有保存持有者的唯一标识。1
2
3
4
5
6
7-- 执行一个set命令,设置value、过期时间和条件
ok, err = red:set("lock", "1234", "PX", 1000, "NX")
if not ok then
ngx.say("failed to set lock: ", err)
return
end
ngx.say("set result: ", ok) - 方式五:在
方式四
的基础上,增加一个持有者的唯一标识(比如UUID
),并在解锁时使用Lua
脚本来判断是否是同一个持有者,如果是就删除锁,否则就返回失败。这种方式的优点是更安全可靠,缺点是需要额外生成和传递唯一标识。
1 | -- 定义一个唯一随机值,可以用uuid或者其他方式生成 |