redis锁方案

什么是分布式锁
  • 分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现
分布式锁的特征
  • 互斥性 任意时刻, 只有一个客户端持有锁
  • 超时释放 某个客户端持有锁超过一定时限, 自动释放锁, 防止死锁
  • 可重入 某个客户端持有锁后, 可再次请求对其加锁
  • 高可用, 高性能 加锁和释放开销低, 同时保证高可用
  • 安全性 只有持有锁的客户端能够释放锁
实现方案
  • 方式一:使用setnx命令来抢锁,如果抢到之后,再用expire命令给锁设置一个过期时间,防止锁忘记了释放。这种方式的缺点是setnxexpire两个命令分开了,不是原子操作,如果程序在设置过期时间之前崩溃了,就会导致锁永远不会释放。
    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
  • 方式二:在setnxvalue值中存放一个 系统时间+过期时间 的值,如果加锁失败,再拿出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脚本来保证原子性,脚本中包含setnxexpire两个命令。这种方式的优点是简单高效,缺点是还是没有保存持有者的唯一标识。
    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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 定义一个唯一随机值,可以用uuid或者其他方式生成
local value = "1234"
-- 执行一个set命令,设置value、过期时间和条件
ok, err = red:set("lock", value, "PX", 1000, "NX")
if not ok then
ngx.say("failed to set lock: ", err)
return
end
ngx.say("set result: ", ok)
-- 执行业务逻辑,省略...
-- 执行一个lua脚本,判断锁的值是否和客户端传入的值相等,如果相等才删除锁
local script = "if redis.call('get',KEYS[1]) == ARGV[1] then" ..
" return redis.call('del',KEYS[1]) " ..
"else" ..
" return 0 " ..
"end"
local res, err = red:eval(script, 1, "lock", value)
if not res then
ngx.say("failed to eval script: ", err)
return
end
ngx.say("eval result: ", res)